From 9a0f79569b854236829a8cf5d1ac96e5b94dd1e5 Mon Sep 17 00:00:00 2001 From: Louise Poole Date: Fri, 20 Dec 2024 17:51:52 +0200 Subject: [PATCH 1/3] feat(uniswap_v4): implement delta transition --- src/evm/protocol/uniswap_v4/state.rs | 96 ++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 4 deletions(-) diff --git a/src/evm/protocol/uniswap_v4/state.rs b/src/evm/protocol/uniswap_v4/state.rs index 5ee2ab36..5b26d11a 100644 --- a/src/evm/protocol/uniswap_v4/state.rs +++ b/src/evm/protocol/uniswap_v4/state.rs @@ -10,7 +10,7 @@ use crate::{ safe_math::{safe_add_u256, safe_sub_u256}, u256_num::u256_to_biguint, utils::uniswap::{ - liquidity_math, + i24_be_bytes_to_i32, liquidity_math, sqrt_price_math::sqrt_price_q96_to_f64, swap_math, tick_list::{TickInfo, TickList, TickListErrorKind}, @@ -280,13 +280,101 @@ impl ProtocolSim for UniswapV4State { )) } - #[allow(unused_variables)] //TODO: remove when implemented fn delta_transition( &mut self, delta: ProtocolStateDelta, - tokens: &HashMap, + _tokens: &HashMap, ) -> Result<(), TransitionError> { - todo!() + // Apply attribute changes + if let Some(liquidity) = delta + .updated_attributes + .get("liquidity") + { + // This is a hotfix because if the liquidity has never been updated after creation, it's + // currently encoded as H256::zero(), therefore, we can't decode this as u128. + // We can remove this once it has been fixed on the tycho side. + let liq_16_bytes = if liquidity.len() == 32 { + // Make sure it only happens for 0 values, otherwise error. + if liquidity == &Bytes::zero(32) { + Bytes::from([0; 16]) + } else { + return Err(TransitionError::DecodeError(format!( + "Liquidity bytes too long for {}, expected 16", + liquidity + ))); + } + } else { + liquidity.clone() + }; + + self.liquidity = u128::from(liq_16_bytes); + } + if let Some(sqrt_price) = delta + .updated_attributes + .get("sqrt_price_x96") + { + self.sqrt_price = U256::from_be_slice(sqrt_price); + } + if let Some(tick) = delta.updated_attributes.get("tick") { + // This is a hotfix because if the tick has never been updated after creation, it's + // currently encoded as H256::zero(), therefore, we can't decode this as i32. + // We can remove this once it has been fixed on the tycho side. + let ticks_4_bytes = if tick.len() == 32 { + // Make sure it only happens for 0 values, otherwise error. + if tick == &Bytes::zero(32) { + Bytes::from([0; 4]) + } else { + return Err(TransitionError::DecodeError(format!( + "Tick bytes too long for {}, expected 4", + tick + ))); + } + } else { + tick.clone() + }; + self.tick = i24_be_bytes_to_i32(&ticks_4_bytes); + } + if let Some(zero2one_protocol_fee) = delta + .updated_attributes + .get("protocol_fees/zero2one") + { + self.fees.zero_for_one = u32::from(zero2one_protocol_fee.clone()); + } + if let Some(one2zero_protocol_fee) = delta + .updated_attributes + .get("protocol_fees/one2zero") + { + self.fees.one_for_zero = u32::from(one2zero_protocol_fee.clone()); + } + + // apply tick changes + for (key, value) in delta.updated_attributes.iter() { + // tick liquidity keys are in the format "tick/{tick_index}/net_liquidity" + if key.starts_with("ticks/") { + let parts: Vec<&str> = key.split('/').collect(); + self.ticks.set_tick_liquidity( + parts[1] + .parse::() + .map_err(|err| TransitionError::DecodeError(err.to_string()))?, + i128::from(value.clone()), + ) + } + } + // delete ticks - ignores deletes for attributes other than tick liquidity + for key in delta.deleted_attributes.iter() { + // tick liquidity keys are in the format "tick/{tick_index}/net_liquidity" + if key.starts_with("tick/") { + let parts: Vec<&str> = key.split('/').collect(); + self.ticks.set_tick_liquidity( + parts[1] + .parse::() + .map_err(|err| TransitionError::DecodeError(err.to_string()))?, + 0, + ) + } + } + + Ok(()) } fn clone_box(&self) -> Box { From fa7ff078e97a324349fff284542ac823c90c647d Mon Sep 17 00:00:00 2001 From: Louise Poole Date: Fri, 20 Dec 2024 18:41:25 +0200 Subject: [PATCH 2/3] test: test uniswap v4 delta transition --- src/evm/protocol/uniswap_v4/state.rs | 65 ++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/src/evm/protocol/uniswap_v4/state.rs b/src/evm/protocol/uniswap_v4/state.rs index 5b26d11a..a4e620e1 100644 --- a/src/evm/protocol/uniswap_v4/state.rs +++ b/src/evm/protocol/uniswap_v4/state.rs @@ -404,3 +404,68 @@ impl ProtocolSim for UniswapV4State { } } } + +#[cfg(test)] +mod tests { + use std::{ + collections::{HashMap, HashSet}, + str::FromStr, + }; + + use tycho_core::hex_bytes::Bytes; + + use super::*; + + #[test] + fn test_delta_transition() { + let mut pool = UniswapV4State::new( + 1000, + U256::from_str("1000").unwrap(), + UniswapV4Fees { zero_for_one: 100, one_for_zero: 90, lp_fee: 700 }, + 100, + 60, + vec![TickInfo::new(120, 10000), TickInfo::new(180, -10000)], + ); + + let attributes: HashMap = [ + ("liquidity".to_string(), Bytes::from(2000_u64.to_be_bytes().to_vec())), + ("sqrt_price_x96".to_string(), Bytes::from(1001_u64.to_be_bytes().to_vec())), + ("tick".to_string(), Bytes::from(120_i32.to_be_bytes().to_vec())), + ("protocol_fees/zero2one".to_string(), Bytes::from(50_u32.to_be_bytes().to_vec())), + ("protocol_fees/one2zero".to_string(), Bytes::from(75_u32.to_be_bytes().to_vec())), + ("ticks/-120/net_liquidity".to_string(), Bytes::from(10200_u64.to_be_bytes().to_vec())), + ("ticks/120/net_liquidity".to_string(), Bytes::from(9800_u64.to_be_bytes().to_vec())), + ] + .into_iter() + .collect(); + + let delta = ProtocolStateDelta { + component_id: "State1".to_owned(), + updated_attributes: attributes, + deleted_attributes: HashSet::new(), + }; + + pool.delta_transition(delta, &HashMap::new()) + .unwrap(); + + assert_eq!(pool.liquidity, 2000); + assert_eq!(pool.sqrt_price, U256::from(1001)); + assert_eq!(pool.tick, 120); + assert_eq!(pool.fees.zero_for_one, 50); + assert_eq!(pool.fees.one_for_zero, 75); + assert_eq!( + pool.ticks + .get_tick(-120) + .unwrap() + .net_liquidity, + 10200 + ); + assert_eq!( + pool.ticks + .get_tick(120) + .unwrap() + .net_liquidity, + 9800 + ); + } +} From 96c56e4bf3ca8fc6da4a2ad6fdf74089fdb65f04 Mon Sep 17 00:00:00 2001 From: zizou <111426680+flopell@users.noreply.github.com> Date: Mon, 23 Dec 2024 10:39:53 +0100 Subject: [PATCH 3/3] refactor: small fixes and code improvements --- src/evm/protocol/uniswap_v4/state.rs | 42 +++++----------------------- 1 file changed, 7 insertions(+), 35 deletions(-) diff --git a/src/evm/protocol/uniswap_v4/state.rs b/src/evm/protocol/uniswap_v4/state.rs index a4e620e1..cf3bd95c 100644 --- a/src/evm/protocol/uniswap_v4/state.rs +++ b/src/evm/protocol/uniswap_v4/state.rs @@ -290,24 +290,7 @@ impl ProtocolSim for UniswapV4State { .updated_attributes .get("liquidity") { - // This is a hotfix because if the liquidity has never been updated after creation, it's - // currently encoded as H256::zero(), therefore, we can't decode this as u128. - // We can remove this once it has been fixed on the tycho side. - let liq_16_bytes = if liquidity.len() == 32 { - // Make sure it only happens for 0 values, otherwise error. - if liquidity == &Bytes::zero(32) { - Bytes::from([0; 16]) - } else { - return Err(TransitionError::DecodeError(format!( - "Liquidity bytes too long for {}, expected 16", - liquidity - ))); - } - } else { - liquidity.clone() - }; - - self.liquidity = u128::from(liq_16_bytes); + self.liquidity = u128::from(liquidity.clone()); } if let Some(sqrt_price) = delta .updated_attributes @@ -316,23 +299,10 @@ impl ProtocolSim for UniswapV4State { self.sqrt_price = U256::from_be_slice(sqrt_price); } if let Some(tick) = delta.updated_attributes.get("tick") { - // This is a hotfix because if the tick has never been updated after creation, it's - // currently encoded as H256::zero(), therefore, we can't decode this as i32. - // We can remove this once it has been fixed on the tycho side. - let ticks_4_bytes = if tick.len() == 32 { - // Make sure it only happens for 0 values, otherwise error. - if tick == &Bytes::zero(32) { - Bytes::from([0; 4]) - } else { - return Err(TransitionError::DecodeError(format!( - "Tick bytes too long for {}, expected 4", - tick - ))); - } - } else { - tick.clone() - }; - self.tick = i24_be_bytes_to_i32(&ticks_4_bytes); + self.tick = i24_be_bytes_to_i32(tick); + } + if let Some(lp_fee) = delta.updated_attributes.get("fee") { + self.fees.lp_fee = u32::from(lp_fee.clone()); } if let Some(zero2one_protocol_fee) = delta .updated_attributes @@ -433,6 +403,7 @@ mod tests { ("tick".to_string(), Bytes::from(120_i32.to_be_bytes().to_vec())), ("protocol_fees/zero2one".to_string(), Bytes::from(50_u32.to_be_bytes().to_vec())), ("protocol_fees/one2zero".to_string(), Bytes::from(75_u32.to_be_bytes().to_vec())), + ("fee".to_string(), Bytes::from(100_u32.to_be_bytes().to_vec())), ("ticks/-120/net_liquidity".to_string(), Bytes::from(10200_u64.to_be_bytes().to_vec())), ("ticks/120/net_liquidity".to_string(), Bytes::from(9800_u64.to_be_bytes().to_vec())), ] @@ -453,6 +424,7 @@ mod tests { assert_eq!(pool.tick, 120); assert_eq!(pool.fees.zero_for_one, 50); assert_eq!(pool.fees.one_for_zero, 75); + assert_eq!(pool.fees.lp_fee, 100); assert_eq!( pool.ticks .get_tick(-120)