From 17cd245f5daabdc41877156c2ccc8c1dfd316538 Mon Sep 17 00:00:00 2001 From: Shuhui Luo <107524008+shuhuiluo@users.noreply.github.com> Date: Fri, 4 Oct 2024 20:30:48 -0400 Subject: [PATCH 1/4] chore: update dependencies and bump version to 1.2.0 Upgraded `alloy`, `once_cell`, `regex`, and `uniswap-lens` to their latest versions. Updated the package version from 1.1.0 to 1.2.0 to reflect these dependency changes. --- Cargo.toml | 10 +++++----- README.md | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 16cd0ee..165a155 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uniswap-v3-sdk" -version = "1.1.0" +version = "1.2.0" edition = "2021" authors = ["Shuhui Luo "] description = "Uniswap V3 SDK for Rust" @@ -14,7 +14,7 @@ exclude = [".github", ".gitignore", "rustfmt.toml"] all-features = true [dependencies] -alloy = { version = "0.3", optional = true, features = ["contract"] } +alloy = { version = "0.4", optional = true, features = ["contract"] } alloy-primitives = "0.8" alloy-sol-types = "0.8" anyhow = { version = "1.0", optional = true } @@ -23,12 +23,12 @@ bigdecimal = "0.4.5" num-bigint = "0.4" num-integer = "0.1" num-traits = "0.2" -once_cell = "1.19" -regex = { version = "1.10", optional = true } +once_cell = "1.20" +regex = { version = "1.11", optional = true } rustc-hash = "2.0" serde_json = { version = "1.0", optional = true } thiserror = { version = "1.0", optional = true } -uniswap-lens = { version = "0.3", optional = true } +uniswap-lens = { version = "0.4", optional = true } uniswap-sdk-core = "2.3.0" [features] diff --git a/README.md b/README.md index ec7d007..3e1fd97 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ It is feature-complete with unit tests matching the TypeScript SDK. Add the following to your `Cargo.toml` file: ```toml -uniswap-v3-sdk = { version = "1.1.0", features = ["extensions", "std"] } +uniswap-v3-sdk = { version = "1.2.0", features = ["extensions", "std"] } ``` ### Usage From 5ffc140d1bca9a6d8d92bab6517dc6de172ba46e Mon Sep 17 00:00:00 2001 From: Shuhui Luo <107524008+shuhuiluo@users.noreply.github.com> Date: Fri, 4 Oct 2024 20:41:36 -0400 Subject: [PATCH 2/4] feat: add new fee tiers to `FeeAmount` enum Introduced additional fee tiers: `LOW_200`, `LOW_300`, and `LOW_400` in the `FeeAmount` enum. Adjusted tick spacing and conversion methods to support the new fee tiers. This enhancement allows for more granular fee configurations within the application. --- src/constants.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/constants.rs b/src/constants.rs index ef0cf15..5409caf 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -15,6 +15,9 @@ pub const POOL_INIT_CODE_HASH: B256 = #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum FeeAmount { LOWEST = 100, + LOW_200 = 200, + LOW_300 = 300, + LOW_400 = 400, LOW = 500, MEDIUM = 3000, HIGH = 10000, @@ -27,6 +30,9 @@ impl FeeAmount { pub const fn tick_spacing(&self) -> I24 { match self { Self::LOWEST => I24::ONE, + Self::LOW_200 => I24::from_limbs([4]), + Self::LOW_300 => I24::from_limbs([6]), + Self::LOW_400 => I24::from_limbs([8]), Self::LOW => I24::from_limbs([10]), Self::MEDIUM => I24::from_limbs([60]), Self::HIGH => I24::from_limbs([200]), @@ -39,6 +45,9 @@ impl From for FeeAmount { fn from(fee: u32) -> Self { match fee { 100 => Self::LOWEST, + 200 => Self::LOW_200, + 300 => Self::LOW_300, + 400 => Self::LOW_400, 500 => Self::LOW, 3000 => Self::MEDIUM, 10000 => Self::HIGH, @@ -52,6 +61,9 @@ impl From for FeeAmount { fn from(tick_spacing: i32) -> Self { match tick_spacing { 1 => Self::LOWEST, + 4 => Self::LOW_200, + 6 => Self::LOW_300, + 8 => Self::LOW_400, 10 => Self::LOW, 60 => Self::MEDIUM, 200 => Self::HIGH, From f7709bf962992b4ed2da357b5026e32e638f0575 Mon Sep 17 00:00:00 2001 From: Shuhui Luo <107524008+shuhuiluo@users.noreply.github.com> Date: Sun, 6 Oct 2024 05:54:29 -0400 Subject: [PATCH 3/4] feat: support computing v3 pool addresses on zksync Added logic to compute pool addresses differently for ZKSync using `compute_zksync_create2_address` function. Updated function signatures and dependencies to accommodate optional `ChainId` parameter. --- Cargo.toml | 2 +- src/constants.rs | 4 ++-- src/entities/pool.rs | 1 + src/extensions/pool.rs | 2 +- src/utils/compute_pool_address.rs | 32 +++++++++++++++++++++++++------ 5 files changed, 31 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 165a155..f4b8685 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ rustc-hash = "2.0" serde_json = { version = "1.0", optional = true } thiserror = { version = "1.0", optional = true } uniswap-lens = { version = "0.4", optional = true } -uniswap-sdk-core = "2.3.0" +uniswap-sdk-core = "2.4.0" [features] default = [] diff --git a/src/constants.rs b/src/constants.rs index 5409caf..b484ce4 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,3 +1,5 @@ +#![allow(non_camel_case_types)] + use alloy_primitives::{ address, aliases::{I24, U24}, @@ -6,8 +8,6 @@ use alloy_primitives::{ pub const FACTORY_ADDRESS: Address = address!("1F98431c8aD98523631AE4a59f267346ea31F984"); -pub const ADDRESS_ZERO: Address = Address::ZERO; - pub const POOL_INIT_CODE_HASH: B256 = b256!("e34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54"); diff --git a/src/entities/pool.rs b/src/entities/pool.rs index b8ec3d9..6c641c1 100644 --- a/src/entities/pool.rs +++ b/src/entities/pool.rs @@ -145,6 +145,7 @@ impl Pool { token_b.address(), fee, init_code_hash_manual_override, + Some(token_a.chain_id()), ) } } diff --git a/src/extensions/pool.rs b/src/extensions/pool.rs index 12a279f..40ddb11 100644 --- a/src/extensions/pool.rs +++ b/src/extensions/pool.rs @@ -31,7 +31,7 @@ where P: Provider, { IUniswapV3PoolInstance::new( - compute_pool_address(factory, token_a, token_b, fee, None), + compute_pool_address(factory, token_a, token_b, fee, None, None), provider, ) } diff --git a/src/utils/compute_pool_address.rs b/src/utils/compute_pool_address.rs index 5840f98..0cea070 100644 --- a/src/utils/compute_pool_address.rs +++ b/src/utils/compute_pool_address.rs @@ -1,6 +1,9 @@ use crate::constants::{FeeAmount, POOL_INIT_CODE_HASH}; -use alloy_primitives::{keccak256, Address, B256}; +use alloy_primitives::{b256, keccak256, Address, B256}; use alloy_sol_types::SolValue; +use uniswap_sdk_core::prelude::{ + compute_zksync_create2_address::compute_zksync_create2_address, ChainId, +}; /// Computes a pool address /// @@ -32,6 +35,7 @@ use alloy_sol_types::SolValue; /// DAI_ADDRESS, /// FeeAmount::LOW, /// None, +/// None, /// ); /// assert_eq!(result, address!("90B1b09A9715CaDbFD9331b3A7652B24BfBEfD32")); /// assert_eq!( @@ -42,6 +46,7 @@ use alloy_sol_types::SolValue; /// USDC_ADDRESS, /// FeeAmount::LOW, /// None, +/// None /// ) /// ); /// ``` @@ -53,6 +58,7 @@ pub fn compute_pool_address( token_b: Address, fee: FeeAmount, init_code_hash_manual_override: Option, + chain_id: Option, ) -> Address { assert_ne!(token_a, token_b, "ADDRESSES"); let (token_0, token_1) = if token_a < token_b { @@ -60,9 +66,23 @@ pub fn compute_pool_address( } else { (token_b, token_a) }; - let pool_key = (token_0, token_1, fee as i32); - factory.create2( - keccak256(pool_key.abi_encode()), - init_code_hash_manual_override.unwrap_or(POOL_INIT_CODE_HASH), - ) + let salt = keccak256((token_0, token_1, fee as i32).abi_encode()); + const ZKSYNC_CHAIN_ID: u64 = ChainId::ZKSYNC as u64; + + // ZKSync uses a different create2 address computation + // Most likely all ZKEVM chains will use the different computation from standard create2 + match chain_id { + Some(ZKSYNC_CHAIN_ID) => compute_zksync_create2_address( + factory, + init_code_hash_manual_override.unwrap_or(b256!( + "010013f177ea1fcbc4520f9a3ca7cd2d1d77959e05aa66484027cb38e712aeed" + )), + salt, + None, + ), + _ => factory.create2( + salt, + init_code_hash_manual_override.unwrap_or(POOL_INIT_CODE_HASH), + ), + } } From 76673164ae321a4a2a817c25d7be440a15886a91 Mon Sep 17 00:00:00 2001 From: Shuhui Luo <107524008+shuhuiluo@users.noreply.github.com> Date: Mon, 7 Oct 2024 03:23:16 -0400 Subject: [PATCH 4/4] refactor: extract swap logic to new function Refactored swap logic from `Pool` into a new function `v3_swap` in `swap_math.rs`. This improves modularity and makes the code more maintainable. All relevant references and usages have been updated accordingly. --- benches/swap_math.rs | 8 +-- src/entities/pool.rs | 140 ++++---------------------------------- src/utils/mod.rs | 2 +- src/utils/swap_math.rs | 148 +++++++++++++++++++++++++++++++++++++++-- 4 files changed, 161 insertions(+), 137 deletions(-) diff --git a/benches/swap_math.rs b/benches/swap_math.rs index 9ce7cba..6bb8c9f 100644 --- a/benches/swap_math.rs +++ b/benches/swap_math.rs @@ -1,6 +1,6 @@ #![allow(dead_code)] -use alloy_primitives::{keccak256, I256, U160, U256}; +use alloy_primitives::{aliases::U24, keccak256, I256, U160, U256}; use alloy_sol_types::SolValue; use criterion::{criterion_group, criterion_main, Criterion}; use uniswap_v3_math::swap_math; @@ -15,7 +15,7 @@ fn pseudo_random_128(seed: u64) -> u128 { u128::from_be_bytes(s.to_be_bytes::<32>()[..16].try_into().unwrap()) } -fn generate_inputs() -> Vec<(U160, U160, u128, I256, u32)> { +fn generate_inputs() -> Vec<(U160, U160, u128, I256, U24)> { (0u64..100) .map(|i| { ( @@ -23,7 +23,7 @@ fn generate_inputs() -> Vec<(U160, U160, u128, I256, u32)> { U160::saturating_from(pseudo_random(i.pow(2))), pseudo_random_128(i.pow(3)), I256::from_raw(pseudo_random(i.pow(4))), - i as u32, + U24::from(i), ) }) .collect() @@ -81,7 +81,7 @@ fn compute_swap_step_benchmark_ref(c: &mut Criterion) { *sqrt_ratio_target_x96, *liquidity, *amount_remaining, - *fee_pips, + fee_pips.into_limbs()[0] as u32, ); } }) diff --git a/src/entities/pool.rs b/src/entities/pool.rs index 6c641c1..7e71447 100644 --- a/src/entities/pool.rs +++ b/src/entities/pool.rs @@ -1,5 +1,5 @@ use crate::prelude::{Error, *}; -use alloy_primitives::{ChainId, B256, I256, U160, U256}; +use alloy_primitives::{ChainId, B256, I256, U160}; use core::fmt; use once_cell::sync::Lazy; use uniswap_sdk_core::prelude::*; @@ -53,25 +53,6 @@ where } } -struct SwapState { - amount_specified_remaining: I256, - amount_calculated: I256, - sqrt_price_x96: U160, - tick: I, - liquidity: u128, -} - -#[derive(Clone, Copy, Default)] -struct StepComputations { - sqrt_price_start_x96: U160, - tick_next: I, - initialized: bool, - sqrt_price_next_x96: U160, - amount_in: U256, - amount_out: U256, - fee_amount: U256, -} - impl Pool { /// Construct a pool /// @@ -276,114 +257,17 @@ impl Pool { amount_specified: I256, sqrt_price_limit_x96: Option, ) -> Result, Error> { - let sqrt_price_limit_x96 = sqrt_price_limit_x96.unwrap_or_else(|| { - if zero_for_one { - MIN_SQRT_RATIO + ONE - } else { - MAX_SQRT_RATIO - ONE - } - }); - - if zero_for_one { - assert!(sqrt_price_limit_x96 > MIN_SQRT_RATIO, "RATIO_MIN"); - assert!(sqrt_price_limit_x96 < self.sqrt_ratio_x96, "RATIO_CURRENT"); - } else { - assert!(sqrt_price_limit_x96 < MAX_SQRT_RATIO, "RATIO_MAX"); - assert!(sqrt_price_limit_x96 > self.sqrt_ratio_x96, "RATIO_CURRENT"); - } - - let exact_input = amount_specified >= I256::ZERO; - - // keep track of swap state - let mut state = SwapState { - amount_specified_remaining: amount_specified, - amount_calculated: I256::ZERO, - sqrt_price_x96: self.sqrt_ratio_x96, - tick: self.tick_current, - liquidity: self.liquidity, - }; - - // start swap while loop - while !state.amount_specified_remaining.is_zero() - && state.sqrt_price_x96 != sqrt_price_limit_x96 - { - let mut step = StepComputations { - sqrt_price_start_x96: state.sqrt_price_x96, - ..Default::default() - }; - - // because each iteration of the while loop rounds, we can't optimize this code - // (relative to the smart contract) by simply traversing to the next available tick, we - // instead need to exactly replicate - (step.tick_next, step.initialized) = self - .tick_data_provider - .next_initialized_tick_within_one_word( - state.tick, - zero_for_one, - self.tick_spacing(), - )?; - - step.tick_next = TP::Index::from_i24(step.tick_next.to_i24().clamp(MIN_TICK, MAX_TICK)); - step.sqrt_price_next_x96 = get_sqrt_ratio_at_tick(step.tick_next.to_i24())?; - - ( - state.sqrt_price_x96, - step.amount_in, - step.amount_out, - step.fee_amount, - ) = compute_swap_step( - state.sqrt_price_x96, - if zero_for_one { - step.sqrt_price_next_x96.max(sqrt_price_limit_x96) - } else { - step.sqrt_price_next_x96.min(sqrt_price_limit_x96) - }, - state.liquidity, - state.amount_specified_remaining, - self.fee as u32, - )?; - - if exact_input { - state.amount_specified_remaining = I256::from_raw( - state.amount_specified_remaining.into_raw() - step.amount_in - step.fee_amount, - ); - state.amount_calculated = - I256::from_raw(state.amount_calculated.into_raw() - step.amount_out); - } else { - state.amount_specified_remaining = - I256::from_raw(state.amount_specified_remaining.into_raw() + step.amount_out); - state.amount_calculated = I256::from_raw( - state.amount_calculated.into_raw() + step.amount_in + step.fee_amount, - ); - } - - if state.sqrt_price_x96 == step.sqrt_price_next_x96 { - // if the tick is initialized, run the tick transition - if step.initialized { - let mut liquidity_net = self - .tick_data_provider - .get_tick(step.tick_next)? - .liquidity_net; - // if we're moving leftward, we interpret liquidityNet as the opposite sign - // safe because liquidityNet cannot be type(int128).min - if zero_for_one { - liquidity_net = -liquidity_net; - } - state.liquidity = add_delta(state.liquidity, liquidity_net)?; - } - state.tick = if zero_for_one { - step.tick_next - TP::Index::ONE - } else { - step.tick_next - }; - } else if state.sqrt_price_x96 != step.sqrt_price_start_x96 { - // recompute unless we're on a lower tick boundary (i.e. already transitioned - // ticks), and haven't moved - state.tick = TP::Index::from_i24(state.sqrt_price_x96.get_tick_at_sqrt_ratio()?); - } - } - - Ok(state) + v3_swap( + self.fee.into(), + self.sqrt_ratio_x96, + self.tick_current, + self.liquidity, + self.tick_spacing(), + &self.tick_data_provider, + zero_for_one, + amount_specified, + sqrt_price_limit_x96, + ) } } diff --git a/src/utils/mod.rs b/src/utils/mod.rs index ee537ed..6fd69b8 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -27,7 +27,7 @@ pub use max_liquidity_for_amounts::*; pub use nearest_usable_tick::nearest_usable_tick; pub use price_tick_conversions::*; pub use sqrt_price_math::*; -pub use swap_math::compute_swap_step; +pub use swap_math::*; pub use tick_list::TickList; pub use tick_math::*; pub use types::*; diff --git a/src/utils/swap_math.rs b/src/utils/swap_math.rs index 49f1f54..33d2654 100644 --- a/src/utils/swap_math.rs +++ b/src/utils/swap_math.rs @@ -1,5 +1,25 @@ use crate::prelude::*; -use alloy_primitives::{Uint, I256, U256}; +use alloy_primitives::{aliases::U24, Uint, I256, U160, U256}; + +#[derive(Clone, Copy, Debug, Default)] +pub struct SwapState { + pub amount_specified_remaining: I256, + pub amount_calculated: I256, + pub sqrt_price_x96: U160, + pub tick_current: I, + pub liquidity: u128, +} + +#[derive(Clone, Copy, Debug, Default)] +struct StepComputations { + sqrt_price_start_x96: U160, + tick_next: I, + initialized: bool, + sqrt_price_next_x96: U160, + amount_in: U256, + amount_out: U256, + fee_amount: U256, +} /// Computes the result of swapping some amount in, or amount out, given the parameters of the swap /// @@ -30,10 +50,10 @@ pub fn compute_swap_step( sqrt_ratio_target_x96: Uint, liquidity: u128, amount_remaining: I256, - fee_pips: u32, + fee_pips: U24, ) -> Result<(Uint, U256, U256, U256), Error> { const MAX_FEE: U256 = U256::from_limbs([1000000, 0, 0, 0]); - let fee_pips = U256::from_limbs([fee_pips as u64, 0, 0, 0]); + let fee_pips = U256::from(fee_pips); let fee_complement = MAX_FEE - fee_pips; let zero_for_one = sqrt_ratio_current_x96 >= sqrt_ratio_target_x96; let exact_in = amount_remaining >= I256::ZERO; @@ -133,6 +153,126 @@ pub fn compute_swap_step( Ok((sqrt_ratio_next_x96, amount_in, amount_out, fee_amount)) } +#[inline] +#[allow(clippy::too_many_arguments)] +pub fn v3_swap( + fee: U24, + sqrt_price_x96: U160, + tick_current: TP::Index, + liquidity: u128, + tick_spacing: TP::Index, + tick_data_provider: &TP, + zero_for_one: bool, + amount_specified: I256, + sqrt_price_limit_x96: Option, +) -> Result, Error> { + let sqrt_price_limit_x96 = sqrt_price_limit_x96.unwrap_or_else(|| { + if zero_for_one { + MIN_SQRT_RATIO + ONE + } else { + MAX_SQRT_RATIO - ONE + } + }); + + if zero_for_one { + assert!(sqrt_price_limit_x96 > MIN_SQRT_RATIO, "RATIO_MIN"); + assert!(sqrt_price_limit_x96 < sqrt_price_x96, "RATIO_CURRENT"); + } else { + assert!(sqrt_price_limit_x96 < MAX_SQRT_RATIO, "RATIO_MAX"); + assert!(sqrt_price_limit_x96 > sqrt_price_x96, "RATIO_CURRENT"); + } + + let exact_input = amount_specified >= I256::ZERO; + + // keep track of swap state + let mut state = SwapState { + amount_specified_remaining: amount_specified, + amount_calculated: I256::ZERO, + sqrt_price_x96, + tick_current, + liquidity, + }; + + // start swap while loop + while !state.amount_specified_remaining.is_zero() + && state.sqrt_price_x96 != sqrt_price_limit_x96 + { + let mut step = StepComputations { + sqrt_price_start_x96: state.sqrt_price_x96, + ..Default::default() + }; + + // because each iteration of the while loop rounds, we can't optimize this code + // (relative to the smart contract) by simply traversing to the next available tick, we + // instead need to exactly replicate + (step.tick_next, step.initialized) = tick_data_provider + .next_initialized_tick_within_one_word( + state.tick_current, + zero_for_one, + tick_spacing, + )?; + + step.tick_next = TP::Index::from_i24(step.tick_next.to_i24().clamp(MIN_TICK, MAX_TICK)); + step.sqrt_price_next_x96 = get_sqrt_ratio_at_tick(step.tick_next.to_i24())?; + + ( + state.sqrt_price_x96, + step.amount_in, + step.amount_out, + step.fee_amount, + ) = compute_swap_step( + state.sqrt_price_x96, + if zero_for_one { + step.sqrt_price_next_x96.max(sqrt_price_limit_x96) + } else { + step.sqrt_price_next_x96.min(sqrt_price_limit_x96) + }, + state.liquidity, + state.amount_specified_remaining, + fee, + )?; + + if exact_input { + state.amount_specified_remaining = I256::from_raw( + state.amount_specified_remaining.into_raw() - step.amount_in - step.fee_amount, + ); + state.amount_calculated = + I256::from_raw(state.amount_calculated.into_raw() - step.amount_out); + } else { + state.amount_specified_remaining = + I256::from_raw(state.amount_specified_remaining.into_raw() + step.amount_out); + state.amount_calculated = I256::from_raw( + state.amount_calculated.into_raw() + step.amount_in + step.fee_amount, + ); + } + + if state.sqrt_price_x96 == step.sqrt_price_next_x96 { + // if the tick is initialized, run the tick transition + if step.initialized { + let mut liquidity_net = tick_data_provider.get_tick(step.tick_next)?.liquidity_net; + // if we're moving leftward, we interpret liquidityNet as the opposite sign + // safe because liquidityNet cannot be type(int128).min + if zero_for_one { + liquidity_net = -liquidity_net; + } + state.liquidity = add_delta(state.liquidity, liquidity_net)?; + } + state.tick_current = if zero_for_one { + step.tick_next - TP::Index::ONE + } else { + step.tick_next + }; + } else if state.sqrt_price_x96 != step.sqrt_price_start_x96 { + // recompute unless we're on a lower tick boundary (i.e. already transitioned + // ticks), and haven't moved + state.tick_current = + TP::Index::from_i24(state.sqrt_price_x96.get_tick_at_sqrt_ratio()?); + } + } + + Ok(state) +} + #[cfg(test)] mod tests { use super::*; @@ -151,7 +291,7 @@ mod tests { U160::from_limbs([7829751401545787782, 4282102344, 0]), 94868, amount_specified_remaining, - FeeAmount::MEDIUM as u32, + FeeAmount::MEDIUM.into(), ) .unwrap(); assert_eq!(