From 80add90e269abe51f753c36b24f06b22d47b73f9 Mon Sep 17 00:00:00 2001 From: Moody Salem Date: Mon, 9 Jun 2025 15:14:54 +0100 Subject: [PATCH 1/4] MEV resist pool --- src/quoting/base_pool.rs | 4 + src/quoting/full_range_pool.rs | 12 ++- src/quoting/mev_resist_pool.rs | 174 +++++++++++++++++++++++++++++++++ src/quoting/mod.rs | 1 + src/quoting/oracle_pool.rs | 4 + src/quoting/twamm_pool.rs | 4 + src/quoting/types.rs | 6 +- 7 files changed, 201 insertions(+), 4 deletions(-) create mode 100644 src/quoting/mev_resist_pool.rs diff --git a/src/quoting/base_pool.rs b/src/quoting/base_pool.rs index 7e567cf..32da682 100644 --- a/src/quoting/base_pool.rs +++ b/src/quoting/base_pool.rs @@ -651,6 +651,10 @@ impl Pool for BasePool { fn min_tick_with_liquidity(&self) -> Option { self.sorted_ticks.first().map(|t| t.index) } + + fn is_path_dependent(&self) -> bool { + false + } } #[cfg(test)] diff --git a/src/quoting/full_range_pool.rs b/src/quoting/full_range_pool.rs index 7951d95..6a375b4 100644 --- a/src/quoting/full_range_pool.rs +++ b/src/quoting/full_range_pool.rs @@ -53,6 +53,7 @@ pub struct FullRangePool { pub enum FullRangePoolError { /// Token0 must be less than token1. TokenOrderInvalid, + SqrtRatioInvalid, } impl FullRangePool { @@ -61,13 +62,14 @@ impl FullRangePool { return Err(FullRangePoolError::TokenOrderInvalid); } - // Ensure sqrt_ratio is within valid bounds - let sqrt_ratio = state.sqrt_ratio.clamp(MIN_SQRT_RATIO, MAX_SQRT_RATIO); + if state.sqrt_ratio < MIN_SQRT_RATIO || state.sqrt_ratio > MAX_SQRT_RATIO { + return Err(FullRangePoolError::SqrtRatioInvalid); + } Ok(Self { key, state: FullRangePoolState { - sqrt_ratio, + sqrt_ratio: state.sqrt_ratio, liquidity: state.liquidity, }, }) @@ -220,6 +222,10 @@ impl Pool for FullRangePool { None } } + + fn is_path_dependent(&self) -> bool { + false + } } #[cfg(test)] diff --git a/src/quoting/mev_resist_pool.rs b/src/quoting/mev_resist_pool.rs new file mode 100644 index 0000000..48857b7 --- /dev/null +++ b/src/quoting/mev_resist_pool.rs @@ -0,0 +1,174 @@ +use crate::math::tick::FULL_RANGE_TICK_SPACING; +use crate::quoting::base_pool::{BasePool, BasePoolQuoteError, BasePoolResources, BasePoolState}; +use crate::quoting::types::{BlockTimestamp, NodeKey, Pool, Quote, QuoteParams}; +use core::ops::{Add, AddAssign}; + +// Resources consumed during any swap execution in a full range pool. +#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)] +pub struct MEVResistPoolResources { + pub no_override_cross_one_spacing: u32, + pub base_pool_resources: BasePoolResources, +} + +impl AddAssign for MEVResistPoolResources { + fn add_assign(&mut self, rhs: Self) { + self.no_override_cross_one_spacing += rhs.no_override_cross_one_spacing; + self.base_pool_resources += rhs.base_pool_resources; + } +} + +impl Add for MEVResistPoolResources { + type Output = Self; + + fn add(mut self, rhs: Self) -> Self::Output { + self += rhs; + self + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct MEVResistPool { + base_pool: BasePool, + last_update_time: u32, + tick: i32, +} + +#[derive(Clone, Debug, PartialEq, Eq, Copy)] +pub struct MEVResistPoolState { + last_update_time: u32, + tick: i32, + has_paid_additional_fees: bool, + base_pool_state: BasePoolState, +} + +/// Errors that can occur when constructing a MEVResistPool. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum MEVResistPoolError { + FeeMustBeGreaterThanZero, + CannotBeFullRange, + MissingExtension, + InvalidCurrentTick, +} + +impl MEVResistPool { + // An MEV resist pool just wraps a base pool with some additional logic + pub fn new( + base_pool: BasePool, + last_update_time: u32, + tick: i32, + ) -> Result { + let key = base_pool.get_key(); + if key.config.fee == 0 { + return Err(MEVResistPoolError::FeeMustBeGreaterThanZero); + } + if key.config.tick_spacing == FULL_RANGE_TICK_SPACING { + return Err(MEVResistPoolError::CannotBeFullRange); + } + if key.config.extension.is_zero() { + return Err(MEVResistPoolError::MissingExtension); + } + + // validates that the current tick is between the active tick and the active tick index + 1 + if let Some(i) = base_pool.get_state().active_tick_index { + let sorted_ticks = base_pool.get_sorted_ticks(); + if let Some(t) = sorted_ticks.get(i) { + if t.index > tick { + return Err(MEVResistPoolError::InvalidCurrentTick); + } + } + if let Some(t) = sorted_ticks.get(i + 1) { + if t.index <= tick { + return Err(MEVResistPoolError::InvalidCurrentTick); + } + } + } else { + if let Some(t) = base_pool.get_sorted_ticks().first() { + if t.index <= tick { + return Err(MEVResistPoolError::InvalidCurrentTick); + } + } + } + + Ok(Self { + base_pool: base_pool, + last_update_time, + tick, + }) + } +} + +impl Pool for MEVResistPool { + type Resources = MEVResistPoolResources; + type State = MEVResistPoolState; + type QuoteError = BasePoolQuoteError; + type Meta = BlockTimestamp; + + fn get_key(&self) -> &NodeKey { + self.base_pool.get_key() + } + + fn get_state(&self) -> Self::State { + MEVResistPoolState { + base_pool_state: self.base_pool.get_state(), + last_update_time: self.last_update_time, + tick: self.tick, + has_paid_additional_fees: false, + } + } + + fn quote( + &self, + params: QuoteParams, + ) -> Result, Self::QuoteError> { + match self.base_pool.quote(QuoteParams { + token_amount: params.token_amount, + sqrt_ratio_limit: params.sqrt_ratio_limit, + override_state: params.override_state.map(|o| o.base_pool_state), + meta: (), + }) { + Ok(quote) => { + let current_time = (params.meta & 0xFFFFFFFF) as u32; + + return Ok(Quote { + // todo: discount the calculated amount + calculated_amount: quote.calculated_amount, + consumed_amount: quote.consumed_amount, + execution_resources: MEVResistPoolResources { + no_override_cross_one_spacing: 1, + base_pool_resources: quote.execution_resources, + }, + fees_paid: quote.fees_paid, + is_price_increasing: quote.is_price_increasing, + state_after: MEVResistPoolState { + last_update_time: current_time, + // todo: compute the tick + tick: self.tick, + has_paid_additional_fees: current_time + != params + .override_state + .map_or(self.last_update_time, |os| os.last_update_time), + base_pool_state: quote.state_after, + }, + }); + } + Err(err) => return Err(err), + } + } + + fn has_liquidity(&self) -> bool { + self.base_pool.has_liquidity() + } + + fn max_tick_with_liquidity(&self) -> Option { + self.base_pool.max_tick_with_liquidity() + } + + fn min_tick_with_liquidity(&self) -> Option { + self.base_pool.min_tick_with_liquidity() + } + + fn is_path_dependent(&self) -> bool { + true + } +} diff --git a/src/quoting/mod.rs b/src/quoting/mod.rs index a4cc4db..0088847 100644 --- a/src/quoting/mod.rs +++ b/src/quoting/mod.rs @@ -1,6 +1,7 @@ pub mod base_pool; pub mod constants; pub mod full_range_pool; +pub mod mev_resist_pool; pub mod oracle_pool; pub mod twamm_pool; pub mod types; diff --git a/src/quoting/oracle_pool.rs b/src/quoting/oracle_pool.rs index 5ddf90e..43244e3 100644 --- a/src/quoting/oracle_pool.rs +++ b/src/quoting/oracle_pool.rs @@ -139,6 +139,10 @@ impl Pool for OraclePool { fn min_tick_with_liquidity(&self) -> Option { self.full_range_pool.min_tick_with_liquidity() } + + fn is_path_dependent(&self) -> bool { + false + } } #[cfg(test)] diff --git a/src/quoting/twamm_pool.rs b/src/quoting/twamm_pool.rs index 9158fd5..2c23cc1 100644 --- a/src/quoting/twamm_pool.rs +++ b/src/quoting/twamm_pool.rs @@ -387,6 +387,10 @@ impl Pool for TwammPool { fn min_tick_with_liquidity(&self) -> Option { self.full_range_pool.min_tick_with_liquidity() } + + fn is_path_dependent(&self) -> bool { + false + } } #[cfg(test)] diff --git a/src/quoting/types.rs b/src/quoting/types.rs index 065812e..56f01b7 100644 --- a/src/quoting/types.rs +++ b/src/quoting/types.rs @@ -39,7 +39,8 @@ pub mod serde_u256 { where D: serde::Deserializer<'de>, { - let hex_str: alloc::borrow::Cow<'static, str> = serde::Deserialize::deserialize(deserializer)?; + let hex_str: alloc::borrow::Cow<'static, str> = + serde::Deserialize::deserialize(deserializer)?; U256::from_str_radix(&hex_str, 16).map_err(serde::de::Error::custom) } } @@ -122,6 +123,9 @@ pub trait Pool: Send + Sync + Debug + Clone + PartialEq + Eq { fn max_tick_with_liquidity(&self) -> Option; // Returns the smallest tick with non-zero liquidity in the pool fn min_tick_with_liquidity(&self) -> Option; + + // Returns false if a swap of x followed by a swap of y will have the same output as a swap of x + y + fn is_path_dependent(&self) -> bool; } #[cfg(test)] From 9242c7658cbd2a2ea661149a8987946cebe4a673 Mon Sep 17 00:00:00 2001 From: Moody Salem Date: Mon, 9 Jun 2025 17:28:50 +0100 Subject: [PATCH 2/4] more implementation details --- src/math/tick.rs | 132 +++++++++++++++++++++++---------- src/math/uint.rs | 7 ++ src/quoting/mev_resist_pool.rs | 49 ++++++++---- src/quoting/util.rs | 9 +-- 4 files changed, 136 insertions(+), 61 deletions(-) diff --git a/src/math/tick.rs b/src/math/tick.rs index ef5e45a..e27282a 100644 --- a/src/math/tick.rs +++ b/src/math/tick.rs @@ -1,4 +1,4 @@ -use crate::math::uint::U256; +use crate::{math::uint::u256_to_float_base_x128, math::uint::U256}; const ONE_X128: U256 = U256([0, 0, 1, 0]); @@ -76,50 +76,104 @@ pub fn to_sqrt_ratio(tick: i32) -> Option { Some(ratio) } +const SQRT_TICK_SIZE: f64 = + 1.00000049999987500006249996093752734372949220361326815796989439990616646_f64; + +pub fn approximate_sqrt_ratio_to_tick(sqrt_ratio: U256) -> i32 { + u256_to_float_base_x128(sqrt_ratio) + .log(SQRT_TICK_SIZE) + .round() as i32 +} + #[cfg(test)] mod tests { - use super::{to_sqrt_ratio, MAX_SQRT_RATIO, MAX_TICK, MIN_SQRT_RATIO, MIN_TICK}; - use crate::math::uint::U256; - - #[test] - fn test_tick_examples() { - assert_eq!( - to_sqrt_ratio(1000000).unwrap(), - U256::from_str_radix("561030636129153856579134353873645338624", 10).unwrap(), - ); - assert_eq!( - to_sqrt_ratio(10000000).unwrap(), - U256::from_str_radix("50502254805927926084423855178401471004672", 10).unwrap(), - ); - assert_eq!( - to_sqrt_ratio(-1000000).unwrap(), - U256::from_str_radix("206391740095027370700312310528859963392", 10).unwrap(), - ); - assert_eq!( - to_sqrt_ratio(-10000000).unwrap(), - U256::from_str_radix("2292810285051363400276741630355046400", 10).unwrap(), - ); - } + mod to_sqrt_ratio { + use super::super::{to_sqrt_ratio, MAX_SQRT_RATIO, MAX_TICK, MIN_SQRT_RATIO, MIN_TICK}; + use crate::math::uint::U256; + + #[test] + fn test_tick_examples() { + assert_eq!( + to_sqrt_ratio(1000000).unwrap(), + U256::from_str_radix("561030636129153856579134353873645338624", 10).unwrap(), + ); + assert_eq!( + to_sqrt_ratio(10000000).unwrap(), + U256::from_str_radix("50502254805927926084423855178401471004672", 10).unwrap(), + ); + assert_eq!( + to_sqrt_ratio(-1000000).unwrap(), + U256::from_str_radix("206391740095027370700312310528859963392", 10).unwrap(), + ); + assert_eq!( + to_sqrt_ratio(-10000000).unwrap(), + U256::from_str_radix("2292810285051363400276741630355046400", 10).unwrap(), + ); + } - #[test] - fn test_tick_too_small() { - assert!(to_sqrt_ratio(MIN_TICK - 1).is_none()); - assert!(to_sqrt_ratio(i32::MIN).is_none()); - } + #[test] + fn test_tick_too_small() { + assert!(to_sqrt_ratio(MIN_TICK - 1).is_none()); + assert!(to_sqrt_ratio(i32::MIN).is_none()); + } - #[test] - fn test_min_tick() { - assert_eq!(to_sqrt_ratio(MIN_TICK).unwrap(), MIN_SQRT_RATIO,); - } + #[test] + fn test_min_tick() { + assert_eq!(to_sqrt_ratio(MIN_TICK).unwrap(), MIN_SQRT_RATIO,); + } - #[test] - fn test_max_tick() { - assert_eq!(to_sqrt_ratio(MAX_TICK).unwrap(), MAX_SQRT_RATIO,); + #[test] + fn test_max_tick() { + assert_eq!(to_sqrt_ratio(MAX_TICK).unwrap(), MAX_SQRT_RATIO,); + } + + #[test] + fn test_tick_too_large() { + assert!(to_sqrt_ratio(MAX_TICK + 1).is_none()); + assert!(to_sqrt_ratio(i32::MAX).is_none()); + } } - #[test] - fn test_tick_too_large() { - assert!(to_sqrt_ratio(MAX_TICK + 1).is_none()); - assert!(to_sqrt_ratio(i32::MAX).is_none()); + mod approximate_sqrt_ratio_to_tick { + use crate::math::tick::{MAX_TICK, MIN_TICK}; + + use super::super::{approximate_sqrt_ratio_to_tick, to_sqrt_ratio}; + + #[test] + fn test_tick_examples() { + assert_eq!(approximate_sqrt_ratio_to_tick(to_sqrt_ratio(0).unwrap()), 0,); + assert_eq!( + approximate_sqrt_ratio_to_tick(to_sqrt_ratio(1000000).unwrap()), + 1000000, + ); + assert_eq!( + approximate_sqrt_ratio_to_tick(to_sqrt_ratio(10000000).unwrap()), + 10000000 + ); + assert_eq!( + approximate_sqrt_ratio_to_tick(to_sqrt_ratio(-1000000).unwrap()), + -1000000, + ); + assert_eq!( + approximate_sqrt_ratio_to_tick(to_sqrt_ratio(-10000000).unwrap()), + -10000000, + ); + } + + #[test] + fn test_min_tick() { + assert_eq!( + approximate_sqrt_ratio_to_tick(to_sqrt_ratio(MIN_TICK).unwrap()), + MIN_TICK, + ); + } + + #[test] + fn test_max_tick() { + assert_eq!( + approximate_sqrt_ratio_to_tick(to_sqrt_ratio(MAX_TICK).unwrap()), + MAX_TICK, + ); + } } } diff --git a/src/math/uint.rs b/src/math/uint.rs index 051b867..66c6602 100644 --- a/src/math/uint.rs +++ b/src/math/uint.rs @@ -1,3 +1,10 @@ uint::construct_uint! { pub struct U256(4); } + +pub fn u256_to_float_base_x128(x128: U256) -> f64 { + x128.0[0] as f64 / 340282366920938463463374607431768211456f64 + + (x128.0[1] as f64 / 18446744073709551616f64) + + x128.0[2] as f64 + + (x128.0[3] as f64 * 18446744073709551616f64) +} diff --git a/src/quoting/mev_resist_pool.rs b/src/quoting/mev_resist_pool.rs index 48857b7..88aae5a 100644 --- a/src/quoting/mev_resist_pool.rs +++ b/src/quoting/mev_resist_pool.rs @@ -1,4 +1,4 @@ -use crate::math::tick::FULL_RANGE_TICK_SPACING; +use crate::math::tick::{approximate_sqrt_ratio_to_tick, FULL_RANGE_TICK_SPACING}; use crate::quoting::base_pool::{BasePool, BasePoolQuoteError, BasePoolResources, BasePoolState}; use crate::quoting::types::{BlockTimestamp, NodeKey, Pool, Quote, QuoteParams}; use core::ops::{Add, AddAssign}; @@ -6,13 +6,13 @@ use core::ops::{Add, AddAssign}; // Resources consumed during any swap execution in a full range pool. #[derive(Clone, Copy, Default, Debug, PartialEq, Eq)] pub struct MEVResistPoolResources { - pub no_override_cross_one_spacing: u32, + pub state_update_count: u32, pub base_pool_resources: BasePoolResources, } impl AddAssign for MEVResistPoolResources { fn add_assign(&mut self, rhs: Self) { - self.no_override_cross_one_spacing += rhs.no_override_cross_one_spacing; + self.state_update_count += rhs.state_update_count; self.base_pool_resources += rhs.base_pool_resources; } } @@ -37,8 +37,6 @@ pub struct MEVResistPool { #[derive(Clone, Debug, PartialEq, Eq, Copy)] pub struct MEVResistPoolState { last_update_time: u32, - tick: i32, - has_paid_additional_fees: bool, base_pool_state: BasePoolState, } @@ -112,8 +110,6 @@ impl Pool for MEVResistPool { MEVResistPoolState { base_pool_state: self.base_pool.get_state(), last_update_time: self.last_update_time, - tick: self.tick, - has_paid_additional_fees: false, } } @@ -130,24 +126,49 @@ impl Pool for MEVResistPool { Ok(quote) => { let current_time = (params.meta & 0xFFFFFFFF) as u32; + let tick_after_swap = approximate_sqrt_ratio_to_tick(quote.state_after.sqrt_ratio); + + let approximate_fee_multiplier = ((tick_after_swap - self.tick).abs() as u32) + / self.base_pool.get_key().config.tick_spacing; + + let pool_time = params + .override_state + .map_or(self.last_update_time, |mrps| mrps.last_update_time); + + // if the time is updated, fees are accumulated to the current liquidity providers + // this is at least 2 additional SSTOREs + let state_update_count = if pool_time != current_time { 1 } else { 0 }; + + if approximate_fee_multiplier == 0 { + // nothing to do here + return Ok(Quote { + calculated_amount: quote.calculated_amount, + consumed_amount: quote.consumed_amount, + execution_resources: MEVResistPoolResources { + state_update_count: state_update_count, + base_pool_resources: quote.execution_resources, + }, + fees_paid: quote.fees_paid, + is_price_increasing: quote.is_price_increasing, + state_after: MEVResistPoolState { + last_update_time: current_time, + base_pool_state: quote.state_after, + }, + }); + } + return Ok(Quote { // todo: discount the calculated amount calculated_amount: quote.calculated_amount, consumed_amount: quote.consumed_amount, execution_resources: MEVResistPoolResources { - no_override_cross_one_spacing: 1, + state_update_count: state_update_count, base_pool_resources: quote.execution_resources, }, fees_paid: quote.fees_paid, is_price_increasing: quote.is_price_increasing, state_after: MEVResistPoolState { last_update_time: current_time, - // todo: compute the tick - tick: self.tick, - has_paid_additional_fees: current_time - != params - .override_state - .map_or(self.last_update_time, |os| os.last_update_time), base_pool_state: quote.state_after, }, }); diff --git a/src/quoting/util.rs b/src/quoting/util.rs index a769534..e236422 100644 --- a/src/quoting/util.rs +++ b/src/quoting/util.rs @@ -1,5 +1,5 @@ use crate::math::tick::{MAX_TICK, MIN_TICK}; -use crate::math::uint::U256; +use crate::math::uint::{u256_to_float_base_x128, U256}; use crate::quoting::base_pool::MAX_TICK_SPACING; use crate::quoting::types::Tick; use alloc::vec::Vec; @@ -29,13 +29,6 @@ pub fn find_nearest_initialized_tick_index(sorted_ticks: &[Tick], tick: i32) -> const LOG_BASE_SQRT_TICK_SIZE: f64 = 4.9999975000016666654166676666658333340476184226196031741031750577196410537756684185262518589393595459766211405607685305832e-7; -pub fn u256_to_float_base_x128(x128: U256) -> f64 { - x128.0[0] as f64 / 340282366920938463463374607431768211456f64 - + (x128.0[1] as f64 / 18446744073709551616f64) - + x128.0[2] as f64 - + (x128.0[3] as f64 * 18446744073709551616f64) -} - pub fn approximate_number_of_tick_spacings_crossed( starting_sqrt_ratio: U256, ending_sqrt_ratio: U256, From fd13261fba93453330e61ae03c7fc2ab4609c44d Mon Sep 17 00:00:00 2001 From: Moody Salem Date: Mon, 9 Jun 2025 18:03:36 +0100 Subject: [PATCH 3/4] closer to the new impl --- src/quoting/mev_resist_pool.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/quoting/mev_resist_pool.rs b/src/quoting/mev_resist_pool.rs index 88aae5a..6098f5a 100644 --- a/src/quoting/mev_resist_pool.rs +++ b/src/quoting/mev_resist_pool.rs @@ -128,8 +128,13 @@ impl Pool for MEVResistPool { let tick_after_swap = approximate_sqrt_ratio_to_tick(quote.state_after.sqrt_ratio); - let approximate_fee_multiplier = ((tick_after_swap - self.tick).abs() as u32) - / self.base_pool.get_key().config.tick_spacing; + let approximate_fee_multiplier = ((tick_after_swap - self.tick).abs() as f64) + / (self.base_pool.get_key().config.tick_spacing as f64); + + let fixed_point_additional_fee: u64 = + ((approximate_fee_multiplier * self.get_key().config.fee as f64).round() + as u128) + .min(u64::MAX as u128) as u64; let pool_time = params .override_state @@ -139,7 +144,7 @@ impl Pool for MEVResistPool { // this is at least 2 additional SSTOREs let state_update_count = if pool_time != current_time { 1 } else { 0 }; - if approximate_fee_multiplier == 0 { + if fixed_point_additional_fee == 0 { // nothing to do here return Ok(Quote { calculated_amount: quote.calculated_amount, From a8c7618e3b81559f17714b1bdd946b9ed3aa4b99 Mon Sep 17 00:00:00 2001 From: Moody Salem Date: Mon, 9 Jun 2025 22:34:55 +0100 Subject: [PATCH 4/4] working quoting algorithm --- src/quoting/mev_resist_pool.rs | 189 +++++++++++++++++++++++++++++++-- 1 file changed, 181 insertions(+), 8 deletions(-) diff --git a/src/quoting/mev_resist_pool.rs b/src/quoting/mev_resist_pool.rs index 6098f5a..49169a3 100644 --- a/src/quoting/mev_resist_pool.rs +++ b/src/quoting/mev_resist_pool.rs @@ -1,3 +1,4 @@ +use crate::math::swap::{amount_before_fee, compute_fee}; use crate::math::tick::{approximate_sqrt_ratio_to_tick, FULL_RANGE_TICK_SPACING}; use crate::quoting::base_pool::{BasePool, BasePoolQuoteError, BasePoolResources, BasePoolState}; use crate::quoting::types::{BlockTimestamp, NodeKey, Pool, Quote, QuoteParams}; @@ -128,12 +129,12 @@ impl Pool for MEVResistPool { let tick_after_swap = approximate_sqrt_ratio_to_tick(quote.state_after.sqrt_ratio); + let pool_config = self.base_pool.get_key().config; let approximate_fee_multiplier = ((tick_after_swap - self.tick).abs() as f64) - / (self.base_pool.get_key().config.tick_spacing as f64); + / (pool_config.tick_spacing as f64); let fixed_point_additional_fee: u64 = - ((approximate_fee_multiplier * self.get_key().config.fee as f64).round() - as u128) + ((approximate_fee_multiplier * pool_config.fee as f64).round() as u128) .min(u64::MAX as u128) as u64; let pool_time = params @@ -162,9 +163,30 @@ impl Pool for MEVResistPool { }); } - return Ok(Quote { - // todo: discount the calculated amount - calculated_amount: quote.calculated_amount, + let mut calculated_amount = quote.calculated_amount; + + if params.token_amount.amount >= 0 { + // exact input, remove the additional fee from the output + calculated_amount -= + compute_fee(calculated_amount as u128, fixed_point_additional_fee) as i128; + } else { + let input_amount_fee: u128 = + compute_fee(calculated_amount as u128, pool_config.fee); + let input_amount = (calculated_amount as u128) - input_amount_fee; + + if let Some(bf) = amount_before_fee(input_amount, fixed_point_additional_fee) { + let fee = bf - input_amount; + // exact output, compute the additional fee for the output + calculated_amount += fee as i128; + } else { + return Err(BasePoolQuoteError::FailedComputeSwapStep( + crate::math::swap::ComputeStepError::AmountBeforeFeeOverflow, + )); + } + } + + Ok(Quote { + calculated_amount: calculated_amount, consumed_amount: quote.consumed_amount, execution_resources: MEVResistPoolResources { state_update_count: state_update_count, @@ -176,9 +198,9 @@ impl Pool for MEVResistPool { last_update_time: current_time, base_pool_state: quote.state_after, }, - }); + }) } - Err(err) => return Err(err), + Err(err) => Err(err), } } @@ -198,3 +220,154 @@ impl Pool for MEVResistPool { true } } + +mod tests { + use alloc::vec; + + use crate::{ + math::{tick::to_sqrt_ratio, uint::U256}, + quoting::{ + base_pool::{BasePool, BasePoolState}, + mev_resist_pool::MEVResistPool, + types::{Config, NodeKey, Pool, QuoteParams, Tick, TokenAmount}, + }, + }; + + #[test] + fn test_swap_input_amount_token0() { + let liquidity: i128 = 28_898_102; + let pool = MEVResistPool::new( + BasePool::new( + NodeKey { + token0: U256::one(), + token1: U256::one() + U256::one(), + config: Config { + fee: ((1_u128 << 64) / 100) as u64, + tick_spacing: 20_000, + extension: U256::one(), + }, + }, + BasePoolState { + active_tick_index: Some(0), + liquidity: liquidity as u128, + sqrt_ratio: to_sqrt_ratio(700_000).unwrap(), + }, + vec![ + Tick { + index: 600_000, + liquidity_delta: liquidity, + }, + Tick { + index: 800_000, + liquidity_delta: -liquidity, + }, + ], + ) + .unwrap(), + 1, + 700_000, + ) + .unwrap(); + + let result = pool + .quote(QuoteParams { + meta: 1, + override_state: None, + sqrt_ratio_limit: None, + token_amount: TokenAmount { + amount: 100_000, + token: U256::one(), + }, + }) + .unwrap(); + + assert_eq!( + (result.consumed_amount, result.calculated_amount), + (100_000, 197_432) + ); + assert_eq!(result.state_after.last_update_time, 1); + + // two swaps + let mut result = pool + .quote(QuoteParams { + meta: 1, + override_state: None, + sqrt_ratio_limit: None, + token_amount: TokenAmount { + amount: 300_000, + token: U256::one(), + }, + }) + .unwrap(); + result = pool + .quote(QuoteParams { + meta: 1, + override_state: Some(result.state_after), + sqrt_ratio_limit: None, + token_amount: TokenAmount { + amount: 300_000, + token: U256::one(), + }, + }) + .unwrap(); + assert_eq!( + (result.consumed_amount, result.calculated_amount), + (300_000, 556_308) + ); + } + + #[test] + fn test_swap_output_amount_token0() { + let liquidity: i128 = 28_898_102; + let pool = MEVResistPool::new( + BasePool::new( + NodeKey { + token0: U256::one(), + token1: U256::one() + U256::one(), + config: Config { + fee: ((1_u128 << 64) / 100) as u64, + tick_spacing: 20_000, + extension: U256::one(), + }, + }, + BasePoolState { + active_tick_index: Some(0), + liquidity: liquidity as u128, + sqrt_ratio: to_sqrt_ratio(700_000).unwrap(), + }, + vec![ + Tick { + index: 600_000, + liquidity_delta: liquidity, + }, + Tick { + index: 800_000, + liquidity_delta: -liquidity, + }, + ], + ) + .unwrap(), + 1, + 700_000, + ) + .unwrap(); + + let result = pool + .quote(QuoteParams { + meta: 1, + override_state: None, + sqrt_ratio_limit: None, + token_amount: TokenAmount { + amount: -100_000, + token: U256::one(), + }, + }) + .unwrap(); + + assert_eq!( + (result.consumed_amount, result.calculated_amount), + (-100_000, 205_416) + ); + assert_eq!(result.state_after.last_update_time, 1); + } +}