diff --git a/Cargo.lock b/Cargo.lock index 2a1de22..ed5b20b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -583,18 +583,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.15" +version = "4.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c12ed66a79a555082f595f7eb980d08669de95009dd4b3d61168c573ebe38fc9" +checksum = "58e54881c004cec7895b0068a0a954cd5d62da01aef83fa35b1e594497bf5445" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.4.15" +version = "4.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f4645eab3431e5a8403a96bea02506a8b35d28cd0f0330977dd5d22f9c84f43" +checksum = "59cb82d7f531603d2fd1f507441cdd35184fa81beff7bd489570de7f773460bb" dependencies = [ "anstyle", "clap_lex", @@ -1934,9 +1934,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.66" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" +checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" dependencies = [ "wasm-bindgen", ] @@ -1971,9 +1971,9 @@ dependencies = [ [[package]] name = "keccak" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" dependencies = [ "cpufeatures", ] @@ -3004,9 +3004,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.28" +version = "0.38.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" +checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" dependencies = [ "bitflags 2.4.1", "errno", @@ -3906,18 +3906,20 @@ dependencies = [ [[package]] name = "uniswap-v3-sdk" -version = "0.10.1" +version = "0.11.0" dependencies = [ "alloy-primitives", "alloy-sol-types", "anyhow", "aperture-lens", + "bigdecimal", "criterion", "ethers", "num-bigint", "num-integer", "num-traits", "once_cell", + "regex", "ruint", "thiserror", "tokio", @@ -4030,9 +4032,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.89" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" +checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -4040,9 +4042,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.89" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" +checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" dependencies = [ "bumpalo", "log", @@ -4055,9 +4057,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.39" +version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12" +checksum = "bde2032aeb86bdfaecc8b261eef3cba735cc426c1f3a3416d1e0791be95fc461" dependencies = [ "cfg-if", "js-sys", @@ -4067,9 +4069,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.89" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" +checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4077,9 +4079,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.89" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" +checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" dependencies = [ "proc-macro2", "quote", @@ -4090,15 +4092,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.89" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" +checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" [[package]] name = "web-sys" -version = "0.3.66" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" +checksum = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index 5c0d844..f8ce9de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uniswap-v3-sdk" -version = "0.10.1" +version = "0.11.0" edition = "2021" authors = ["Shuhui Luo "] description = "Uniswap V3 SDK for Rust" @@ -14,22 +14,24 @@ exclude = [".github", ".gitignore", "rustfmt.toml"] all-features = true [dependencies] -alloy-primitives = "0.6.0" -alloy-sol-types = "0.6.0" +alloy-primitives = "0.6" +alloy-sol-types = "0.6" anyhow = "1.0" -aperture-lens = { version = "0.4.0", optional = true } +aperture-lens = { version = "0.4", optional = true } +bigdecimal = "0.4.2" ethers = { version = "2.0", optional = true } num-bigint = "0.4.4" num-integer = "0.1.45" num-traits = "0.2.17" -once_cell = "1.19.0" -ruint = "1.11.1" -thiserror = "1.0.53" +once_cell = "1.19" +regex = { version = "1.10", optional = true } +ruint = "1.11" +thiserror = "1.0" uniswap-sdk-core = "0.10.0" uniswap_v3_math = "0.4.1" [features] -extensions = ["aperture-lens", "ethers"] +extensions = ["aperture-lens", "ethers", "regex"] [dev-dependencies] criterion = "0.5.1" diff --git a/README.md b/README.md index c06655c..6112b69 100644 --- a/README.md +++ b/README.md @@ -16,14 +16,14 @@ WIP. - Reimplementation of the math libraries in [Uniswap V3 Math In Rust](https://github.com/0xKitsune/uniswap-v3-math) based on optimizations presented in [Uni V3 Lib](https://github.com/Aperture-Finance/uni-v3-lib) - Extensive unit tests and benchmarks -- An `extensions` module for additional functionality related to Uniswap V3 +- An `extensions` feature for additional functionality related to Uniswap V3 ## Getting started Add the following to your `Cargo.toml` file: ```toml -uniswap-v3-sdk = "0.8.0" +uniswap-v3-sdk = { version = "0.11.0", features = ["extensions"] } ``` ### Usage @@ -46,8 +46,12 @@ Tests are run with `cargo test`. To test a specific module, use `cargo test --te ### Linting -Linting is done with `clippy` and `rustfmt`. To run the linter, -use `cargo clippy --all-targets --all-features -- -D warnings` and `cargo fmt --all -- --check`. +Linting is done with `clippy` and `rustfmt`. To run the linter, use: + +```shell +cargo clippy --all-targets --all-features -- -D warnings +cargo fmt --all -- --check +``` ### Benchmarking @@ -65,3 +69,4 @@ This project is inspired by and adapted from the following projects: - [Uniswap SDK Core Rust](https://github.com/malik672/uniswap-sdk-core-rust) - [Uniswap V3 Math In Rust](https://github.com/0xKitsune/uniswap-v3-math) - [Uni V3 Lib](https://github.com/Aperture-Finance/uni-v3-lib) +- [uniswap-v3-automation-sdk](https://github.com/Aperture-Finance/uniswap-v3-automation-sdk) diff --git a/src/extensions/ephemeral_tick_data_provider.rs b/src/extensions/ephemeral_tick_data_provider.rs index f823587..96485d6 100644 --- a/src/extensions/ephemeral_tick_data_provider.rs +++ b/src/extensions/ephemeral_tick_data_provider.rs @@ -1,3 +1,6 @@ +//! ## Ephemeral Tick Data Provider +//! A data provider that fetches ticks using an [ephemeral contract](https://github.com/Aperture-Finance/Aperture-Lens/blob/904101e4daed59e02fd4b758b98b0749e70b583b/contracts/EphemeralGetPopulatedTicksInRange.sol) in a single `eth_call`. + use crate::prelude::*; use alloy_primitives::Address; use anyhow::Result; @@ -5,7 +8,7 @@ use aperture_lens::prelude::get_populated_ticks_in_range; use ethers::prelude::{BlockId, ContractError, Middleware}; use std::sync::Arc; -/// A data provider for ticks that fetches ticks using an ephemeral contract in a single `eth_call`. +/// A data provider that fetches ticks using an ephemeral contract in a single `eth_call`. #[derive(Clone)] pub struct EphemeralTickDataProvider { pub pool: Address, diff --git a/src/extensions/mod.rs b/src/extensions/mod.rs index d3af07b..e2b7c99 100644 --- a/src/extensions/mod.rs +++ b/src/extensions/mod.rs @@ -1,3 +1,15 @@ +//! Extensions to the core library. + mod ephemeral_tick_data_provider; +mod price_tick_conversions; pub use ephemeral_tick_data_provider::EphemeralTickDataProvider; +pub use price_tick_conversions::*; + +use crate::prelude::*; +use alloy_primitives::U256; +use bigdecimal::BigDecimal; + +pub fn u256_to_big_decimal(x: U256) -> BigDecimal { + BigDecimal::from(u256_to_big_int(x)) +} diff --git a/src/extensions/price_tick_conversions.rs b/src/extensions/price_tick_conversions.rs new file mode 100644 index 0000000..59cf91b --- /dev/null +++ b/src/extensions/price_tick_conversions.rs @@ -0,0 +1,348 @@ +//! ## Price and tick conversions +//! Utility functions for converting between [`i32`] ticks, [`BigDecimal`] prices, and SDK Core [`Price`] prices. +//! Ported from [uniswap-v3-automation-sdk](https://github.com/Aperture-Finance/uniswap-v3-automation-sdk/blob/8bc54456753f454848d25029631f4e64ff573e12/price.ts). + +use crate::prelude::*; +use alloy_primitives::U256; +use anyhow::{bail, Result}; +use once_cell::sync::Lazy; +use regex::Regex; +use uniswap_sdk_core::prelude::*; + +pub static MIN_PRICE: Lazy = Lazy::new(|| { + Fraction::new( + u256_to_big_int(MIN_SQRT_RATIO).pow(2), + u256_to_big_int(Q192), + ) +}); +pub static MAX_PRICE: Lazy = Lazy::new(|| { + Fraction::new( + u256_to_big_int(MAX_SQRT_RATIO).pow(2) - u256_to_big_int(ONE), + u256_to_big_int(Q192), + ) +}); + +/// Parses the specified price string for the price of `base_token` denominated in `quote_token`. +/// +/// ## Arguments +/// +/// * `base_token`: The base token. +/// * `quote_token`: The quote token. +/// * `price`: The amount of `quote_token` that is worth the same as 1 `base_token`. +/// +/// ## Returns +/// +/// The parsed price as an instance of [`Price`] in [`uniswap_sdk_core`]. +/// +/// ## Examples +/// +/// ``` +/// use uniswap_sdk_core::{prelude::Token, token}; +/// use uniswap_v3_sdk::prelude::parse_price; +/// +/// let price = parse_price( +/// token!(1, "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", 8, "WBTC"), +/// token!(1, "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", 18, "WETH"), +/// "10.23", +/// ).unwrap(); +/// ``` +pub fn parse_price( + base_token: TBase, + quote_token: TQuote, + price: &str, +) -> Result> +where + TBase: CurrencyTrait, + TQuote: CurrencyTrait, +{ + // Check whether `price` is a valid string of decimal number. + // This regex matches any number of digits optionally followed by '.' which is then followed by at least one digit. + let re = Regex::new(r"^\d*\.?\d+$").unwrap(); + if !re.is_match(price) { + bail!("Invalid price string"); + } + + let (whole, fraction) = match price.split_once('.') { + Some((whole, fraction)) => (whole, fraction), + None => (price, ""), + }; + let decimals = fraction.len(); + let without_decimals = BigInt::from_str(&format!("{}{}", whole, fraction))?; + let numerator = without_decimals * BigInt::from(10).pow(quote_token.decimals() as u32); + let denominator = BigInt::from(10).pow(decimals as u32 + base_token.decimals() as u32); + Ok(Price::new(base_token, quote_token, denominator, numerator)) +} + +/// Given a sqrt ratio, returns the price of the base token in terms of the quote token. +/// +/// ## Arguments +/// +/// * `sqrt_ratio_x96`: The sqrt ratio of the base token in terms of the quote token as a Q64.96 [`U256`]. +/// * `base_token`: The base token. +/// * `quote_token`: The quote token. +/// +/// ## Returns +/// +/// The price of the base token in terms of the quote token as an instance of [`Price`] in [`uniswap_sdk_core`]. +/// +/// ## Examples +/// +/// ``` +/// use uniswap_sdk_core::{prelude::Token, token}; +/// use uniswap_v3_sdk::prelude::*; +/// +/// let token0 = token!(1, "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", 8, "WBTC"); +/// let token1 = token!(1, "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", 18, "WETH"); +/// let min_price = tick_to_price( +/// token0.clone(), +/// token1.clone(), +/// MIN_TICK, +/// ).unwrap(); +/// assert_eq!(sqrt_ratio_x96_to_price(MIN_SQRT_RATIO, token0, token1).unwrap(), min_price); +/// ``` +pub fn sqrt_ratio_x96_to_price( + sqrt_ratio_x96: U256, + base_token: Token, + quote_token: Token, +) -> Result> { + let ratio_x192 = u256_to_big_uint(sqrt_ratio_x96).pow(2); + let q192 = u256_to_big_uint(Q192); + Ok(if base_token.sorts_before("e_token)? { + Price::new(base_token, quote_token, q192, ratio_x192) + } else { + Price::new(base_token, quote_token, ratio_x192, q192) + }) +} + +/// Same as [`price_to_closest_tick`] but returns [`MIN_TICK`] or [`MAX_TICK`] if the price is outside Uniswap's range. +pub fn price_to_closest_tick_safe(price: &Price) -> Result { + let sorted = price + .meta + .base_currency + .sorts_before(&price.meta.quote_currency)?; + if price.as_fraction() < *MIN_PRICE { + Ok(if sorted { MIN_TICK } else { MAX_TICK }) + } else if price.as_fraction() > *MAX_PRICE { + Ok(if sorted { MAX_TICK } else { MIN_TICK }) + } else { + price_to_closest_tick(price) + } +} + +/// Finds the closest usable tick for the specified price and pool fee tier. +/// +/// ## Arguments +/// +/// * `price`: The price of two tokens in the liquidity pool. Either token0 or token1 may be the base token. +/// * `fee`: The liquidity pool fee tier. +/// +/// ## Returns +/// +/// The closest usable tick. +/// +/// ## Examples +/// +/// ``` +/// use uniswap_sdk_core::{prelude::*, token}; +/// use uniswap_v3_sdk::prelude::*; +/// +/// let token0 = token!(1, "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", 8, "WBTC"); +/// let token1 = token!(1, "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", 18, "WETH"); +/// let fee = FeeAmount::MEDIUM; +/// let min_price = Price::new(token0.clone(), token1.clone(), MIN_PRICE.denominator(), 1); +/// let max_price = Price::new(token0.clone(), token1.clone(), MAX_PRICE.denominator(), MAX_PRICE.numerator()); +/// +/// assert_eq!(price_to_closest_usable_tick(&min_price, fee).unwrap(), nearest_usable_tick(MIN_TICK, fee.tick_spacing())); +/// assert_eq!(price_to_closest_usable_tick(&min_price.invert(), fee).unwrap(), nearest_usable_tick(MIN_TICK, fee.tick_spacing())); +/// assert_eq!(price_to_closest_usable_tick(&max_price.invert(), fee).unwrap(), nearest_usable_tick(MAX_TICK, fee.tick_spacing())); +/// ``` +pub fn price_to_closest_usable_tick(price: &Price, fee: FeeAmount) -> Result { + Ok(nearest_usable_tick( + price_to_closest_tick_safe(price)?, + fee.tick_spacing(), + )) +} + +/// Given a tick, returns the price of token0 in terms of token1 as a [`BigDecimal`]. +/// +/// ## Arguments +/// +/// * `tick`: The tick for which to return the price. +/// +/// ## Examples +/// +/// ``` +/// use bigdecimal::BigDecimal; +/// use num_traits::{FromPrimitive, Pow, ToPrimitive}; +/// use uniswap_v3_sdk::prelude::*; +/// +/// assert_eq!(tick_to_big_price(100).unwrap().to_f32().unwrap(), 1.0001f64.pow(100i32).to_f32().unwrap()); +/// ``` +pub fn tick_to_big_price(tick: i32) -> Result { + let sqrt_ratio_x96 = get_sqrt_ratio_at_tick(tick)?; + Ok(BigDecimal::from(u256_to_big_int(sqrt_ratio_x96).pow(2)) / u256_to_big_decimal(Q192)) +} + +/// Convert a [`FractionBase`] object to a [`BigDecimal`]. +pub fn fraction_to_big_decimal(price: &impl FractionBase) -> BigDecimal { + price.to_decimal() +} + +/// Given a price ratio of token1/token0, calculate the sqrt ratio of token1/token0. +/// +/// ## Arguments +/// +/// * `price`: The price ratio of token1/token0, as a [`BigDecimal`]. +/// +/// ## Returns +/// +/// The sqrt ratio of token1/token0, as a [`U256`]. +/// +/// ## Examples +/// +/// ``` +/// use bigdecimal::BigDecimal; +/// use uniswap_v3_sdk::prelude::*; +/// +/// let price: BigDecimal = tick_to_big_price(MAX_TICK).unwrap(); +/// assert_eq!(price_to_sqrt_ratio_x96(&price), MAX_SQRT_RATIO); +/// ``` +pub fn price_to_sqrt_ratio_x96(price: &BigDecimal) -> U256 { + if price < &BigDecimal::zero() { + panic!("Invalid price: must be non-negative"); + } + let price_x192 = price * u256_to_big_decimal(Q192); + let sqrt_ratio_x96 = price_x192.to_bigint().unwrap().sqrt(); + if sqrt_ratio_x96 < u256_to_big_int(MIN_SQRT_RATIO) { + MIN_SQRT_RATIO + } else if sqrt_ratio_x96 > u256_to_big_int(MAX_SQRT_RATIO) { + MAX_SQRT_RATIO + } else { + big_int_to_u256(sqrt_ratio_x96) + } +} + +/// For a given tick range from `tick_lower` to `tick_upper`, and a given proportion of the position value that is held in +/// token0, calculate the price of token0 denominated in token1. +/// +/// ## Arguments +/// +/// * `token0_ratio`: The proportion of the position value that is held in token0, as a [`BigDecimal`] between 0 and 1, inclusive. +/// * `tick_lower`: The lower tick of the range. +/// * `tick_upper`: The upper tick of the range. +/// +/// ## Returns +/// +/// The price of token0 denominated in token1 for the specified tick range and token0 value proportion. +/// +pub fn token0_ratio_to_price( + token0_ratio: BigDecimal, + tick_lower: i32, + tick_upper: i32, +) -> Result { + if tick_upper <= tick_lower { + bail!("Invalid tick range: tickUpper must be greater than tickLower"); + } + if token0_ratio < BigDecimal::zero() || token0_ratio > BigDecimal::from(1) { + bail!("Invalid token0ValueProportion: must be a value between 0 and 1, inclusive"); + } + if token0_ratio.is_zero() { + return tick_to_big_price(tick_upper); + } + if token0_ratio == BigDecimal::from(1) { + return tick_to_big_price(tick_lower); + } + let sqrt_ratio_lower_x96 = get_sqrt_ratio_at_tick(tick_lower)?; + let sqrt_ratio_upper_x96 = get_sqrt_ratio_at_tick(tick_upper)?; + let l = u256_to_big_decimal(sqrt_ratio_lower_x96) / u256_to_big_decimal(Q96); + let u = u256_to_big_decimal(sqrt_ratio_upper_x96) / u256_to_big_decimal(Q96); + let r = token0_ratio; + let a = &r - BigDecimal::from(1); + let b = &u * (BigDecimal::from(1) - BigDecimal::from(2) * &r); + let c = r * l * u; + let numerator = &b + (b.square() - BigDecimal::from(4) * &a * c).sqrt().unwrap(); + let denominator = BigDecimal::from(-2) * a; + Ok((numerator / denominator).square()) +} + +/// Given a price ratio of token1/token0, calculate the proportion of the position value that is held in token0 for a +/// given tick range. Inverse of [`token0_ratio_to_price`]. +/// +/// ## Arguments +/// +/// * `price`: The price ratio of token1/token0, as a [`BigDecimal`]. +/// * `tick_lower`: The lower tick of the range. +/// * `tick_upper`: The upper tick of the range. +/// +/// ## Returns +/// +/// The proportion of the position value that is held in token0, as a [`BigDecimal`] between 0 and 1, inclusive. +/// +pub fn token0_price_to_ratio( + price: BigDecimal, + tick_lower: i32, + tick_upper: i32, +) -> Result { + if tick_upper <= tick_lower { + bail!("Invalid tick range: tickUpper must be greater than tickLower"); + } + let sqrt_price_x96 = price_to_sqrt_ratio_x96(&price); + let tick = get_tick_at_sqrt_ratio(sqrt_price_x96)?; + // only token0 + if tick < tick_lower { + Ok(BigDecimal::from(1)) + } + // only token1 + else if tick >= tick_upper { + Ok(BigDecimal::zero()) + } else { + let liquidity = 2u128 << 96; + let amount0 = get_amount_0_delta( + sqrt_price_x96, + get_sqrt_ratio_at_tick(tick_upper)?, + liquidity, + false, + )?; + let amount1 = get_amount_1_delta( + get_sqrt_ratio_at_tick(tick_lower)?, + sqrt_price_x96, + liquidity, + false, + )?; + let value0 = u256_to_big_decimal(amount0) * price; + Ok(&value0 / (&value0 + u256_to_big_decimal(amount1))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_token0_ratio_to_price_conversion() { + let tick_lower = 253320; + let tick_upper = 264600; + assert_eq!( + token0_ratio_to_price(BigDecimal::from(0), tick_lower, tick_upper).unwrap(), + tick_to_big_price(tick_upper).unwrap() + ); + assert_eq!( + token0_ratio_to_price(BigDecimal::from(1), tick_lower, tick_upper).unwrap(), + tick_to_big_price(tick_lower).unwrap() + ); + let price = + token0_ratio_to_price(BigDecimal::from_str("0.3").unwrap(), tick_lower, tick_upper) + .unwrap(); + assert_eq!( + price.with_scale_round(30, RoundingMode::HalfUp).to_string(), + "226996287752.678057810335753063814266625941" + ); + let token0_ratio = token0_price_to_ratio(price, tick_lower, tick_upper).unwrap(); + assert_eq!( + token0_ratio + .with_scale_round(30, RoundingMode::HalfUp) + .to_string(), + "0.299999999999999999999998780740" + ); + } +} diff --git a/src/utils/price_tick_conversions.rs b/src/utils/price_tick_conversions.rs index 322460e..96d8c28 100644 --- a/src/utils/price_tick_conversions.rs +++ b/src/utils/price_tick_conversions.rs @@ -1,3 +1,6 @@ +//! ## Price and tick conversions +//! Utility functions for converting between [`i32`] ticks and SDK Core [`Price`] prices. + use crate::prelude::*; use anyhow::Result; use uniswap_sdk_core::prelude::*; @@ -5,7 +8,7 @@ use uniswap_sdk_core::prelude::*; /// Returns a price object corresponding to the input tick and the base/quote token. /// Inputs must be tokens because the address order is used to interpret the price represented by the tick. /// -/// # Arguments +/// ## Arguments /// /// * `base_token`: the base token of the price /// * `quote_token`: the quote token of the price @@ -17,8 +20,7 @@ pub fn tick_to_price( tick: i32, ) -> Result> { let sqrt_ratio_x96 = get_sqrt_ratio_at_tick(tick)?; - let sqrt_ratio_x96 = u256_to_big_uint(sqrt_ratio_x96); - let ratio_x192 = &sqrt_ratio_x96 * &sqrt_ratio_x96; + let ratio_x192 = u256_to_big_uint(sqrt_ratio_x96).pow(2); let q192 = u256_to_big_uint(Q192); Ok(if base_token.sorts_before("e_token)? { Price::new(base_token, quote_token, q192, ratio_x192) @@ -29,12 +31,12 @@ pub fn tick_to_price( /// Returns the first tick for which the given price is greater than or equal to the tick price /// -/// # Arguments +/// ## Arguments /// /// * `price`: for which to return the closest tick that represents a price less than or equal to /// the input price, i.e. the price of the returned tick is less than or equal to the input price /// -pub fn price_to_closest_tick(price: Price) -> Result { +pub fn price_to_closest_tick(price: &Price) -> Result { let sorted = price .meta .base_currency @@ -51,12 +53,12 @@ pub fn price_to_closest_tick(price: Price) -> Result { tick + 1, )?; Ok(if sorted { - if price >= next_tick_price { + if price >= &next_tick_price { tick + 1 } else { tick } - } else if price <= next_tick_price { + } else if price <= &next_tick_price { tick + 1 } else { tick @@ -210,7 +212,7 @@ mod tests { #[test] fn price_to_closest_tick_test_1() { assert_eq!( - price_to_closest_tick(Price::new(TOKEN1.clone(), TOKEN0.clone(), 1, 1800)).unwrap(), + price_to_closest_tick(&Price::new(TOKEN1.clone(), TOKEN0.clone(), 1, 1800)).unwrap(), -74960 ); } @@ -218,7 +220,7 @@ mod tests { #[test] fn price_to_closest_tick_test_2() { assert_eq!( - price_to_closest_tick(Price::new(TOKEN0.clone(), TOKEN1.clone(), 1800, 1)).unwrap(), + price_to_closest_tick(&Price::new(TOKEN0.clone(), TOKEN1.clone(), 1800, 1)).unwrap(), -74960 ); } @@ -226,7 +228,7 @@ mod tests { #[test] fn price_to_closest_tick_test_3() { assert_eq!( - price_to_closest_tick(Price::new( + price_to_closest_tick(&Price::new( TOKEN0.clone(), TOKEN2_6DECIMALS.clone(), BigInt::from(100) * BigInt::from(10).pow(18), @@ -240,7 +242,7 @@ mod tests { #[test] fn price_to_closest_tick_test_4() { assert_eq!( - price_to_closest_tick(Price::new( + price_to_closest_tick(&Price::new( TOKEN2_6DECIMALS.clone(), TOKEN0.clone(), BigInt::from(101) * BigInt::from(10).pow(6), @@ -254,7 +256,7 @@ mod tests { #[test] fn price_to_closest_tick_test_5() { assert_eq!( - price_to_closest_tick(tick_to_price(TOKEN1.clone(), TOKEN0.clone(), -74960).unwrap()) + price_to_closest_tick(&tick_to_price(TOKEN1.clone(), TOKEN0.clone(), -74960).unwrap()) .unwrap(), -74960 ); @@ -263,7 +265,7 @@ mod tests { #[test] fn price_to_closest_tick_test_6() { assert_eq!( - price_to_closest_tick(tick_to_price(TOKEN1.clone(), TOKEN0.clone(), 74960).unwrap()) + price_to_closest_tick(&tick_to_price(TOKEN1.clone(), TOKEN0.clone(), 74960).unwrap()) .unwrap(), 74960 ); @@ -272,7 +274,7 @@ mod tests { #[test] fn price_to_closest_tick_test_7() { assert_eq!( - price_to_closest_tick(tick_to_price(TOKEN0.clone(), TOKEN1.clone(), -74960).unwrap()) + price_to_closest_tick(&tick_to_price(TOKEN0.clone(), TOKEN1.clone(), -74960).unwrap()) .unwrap(), -74960 ); @@ -281,7 +283,7 @@ mod tests { #[test] fn price_to_closest_tick_test_8() { assert_eq!( - price_to_closest_tick(tick_to_price(TOKEN0.clone(), TOKEN1.clone(), 74960).unwrap()) + price_to_closest_tick(&tick_to_price(TOKEN0.clone(), TOKEN1.clone(), 74960).unwrap()) .unwrap(), 74960 ); @@ -291,7 +293,7 @@ mod tests { fn price_to_closest_tick_test_9() { assert_eq!( price_to_closest_tick( - tick_to_price(TOKEN0.clone(), TOKEN2_6DECIMALS.clone(), -276225).unwrap(), + &tick_to_price(TOKEN0.clone(), TOKEN2_6DECIMALS.clone(), -276225).unwrap(), ) .unwrap(), -276225 @@ -302,7 +304,7 @@ mod tests { fn price_to_closest_tick_test_10() { assert_eq!( price_to_closest_tick( - tick_to_price(TOKEN2_6DECIMALS.clone(), TOKEN0.clone(), -276225).unwrap(), + &tick_to_price(TOKEN2_6DECIMALS.clone(), TOKEN0.clone(), -276225).unwrap(), ) .unwrap(), -276225