use std::{collections::HashMap, sync::Arc};
use chrono::NaiveDateTime;
use num_bigint::BigUint;
use tokio::sync::RwLock;
use tycho_simulation::{
tycho_core::{
dto::ProtocolStateDelta,
models::{protocol::ProtocolComponent, token::Token, Address, Chain},
simulation::{
errors::{SimulationError, TransitionError},
protocol_sim::{
Balances, GetAmountOutResult, PoolSwap, ProtocolSim, QueryPoolSwapParams,
SwapConstraint,
},
},
Bytes,
},
tycho_ethereum::gas::{BlockGasPrice, GasPrice},
};
use crate::{
algorithm::most_liquid::DepthAndPrice,
feed::market_data::SharedMarketData,
graph::{petgraph::PetgraphStableDiGraphManager, GraphManager},
types::{quote::OrderSide, BlockInfo, Order},
};
pub const ONE_ETH: u128 = 1_000_000_000_000_000_000;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct MockProtocolSim {
pub spot_price: f64,
pub gas: u64,
pub liquidity: u128,
pub fee: f64,
#[serde(default)]
pub token_decimals: HashMap<Bytes, u32>,
}
impl MockProtocolSim {
pub fn new(spot_price: f64) -> Self {
Self { spot_price, ..Default::default() }
}
pub fn with_spot_price(mut self, spot_price: f64) -> Self {
self.spot_price = spot_price;
self
}
pub fn with_gas(mut self, gas: u64) -> Self {
self.gas = gas;
self
}
pub fn with_liquidity(mut self, liquidity: u128) -> Self {
self.liquidity = liquidity;
self
}
pub fn with_fee(mut self, fee: f64) -> Self {
self.fee = fee;
self
}
pub fn with_tokens(mut self, tokens: &[Token]) -> Self {
for token in tokens {
self.token_decimals
.insert(token.address.clone(), token.decimals);
}
self
}
}
impl Default for MockProtocolSim {
fn default() -> Self {
Self {
spot_price: 2.0,
gas: 50_000,
liquidity: u128::MAX,
fee: 0.0,
token_decimals: HashMap::new(),
}
}
}
#[typetag::serde]
impl ProtocolSim for MockProtocolSim {
fn fee(&self) -> f64 {
self.fee
}
fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
let post_fee_spot_price = self.spot_price / (1.0 - self.fee);
if base.address < quote.address {
Ok(post_fee_spot_price)
} else {
Ok(1.0 / post_fee_spot_price)
}
}
fn get_amount_out(
&self,
amount_in: BigUint,
token_in: &Token,
token_out: &Token,
) -> Result<GetAmountOutResult, SimulationError> {
const PRECISION: u64 = 1_000_000_000_000;
let effective_price = if token_in.address < token_out.address {
self.spot_price * (1.0 - self.fee)
} else {
(1.0 - self.fee) / self.spot_price
};
let price_scaled = (effective_price * PRECISION as f64) as u128;
let amount_out = (&amount_in * price_scaled) / PRECISION;
let decimal_diff = token_out.decimals as i32 - token_in.decimals as i32;
let amount_out = if decimal_diff >= 0 {
amount_out * BigUint::from(10u64).pow(decimal_diff as u32)
} else {
amount_out / BigUint::from(10u64).pow((-decimal_diff) as u32)
};
if amount_out > BigUint::from(self.liquidity) {
return Err(SimulationError::InvalidInput(
"amount exceeds available liquidity".to_string(),
None,
));
}
let new_state = Box::new(MockProtocolSim {
spot_price: self.spot_price + 1.0,
gas: self.gas,
liquidity: self.liquidity,
fee: self.fee,
token_decimals: self.token_decimals.clone(),
});
Ok(GetAmountOutResult::new(amount_out, BigUint::from(self.gas), new_state))
}
fn get_limits(
&self,
sell_token: Bytes,
buy_token: Bytes,
) -> Result<(BigUint, BigUint), SimulationError> {
let buy_limit = BigUint::from(self.liquidity);
const PRECISION: u64 = 1_000_000_000_000;
let effective_price = if sell_token < buy_token {
self.spot_price * (1.0 - self.fee)
} else {
(1.0 - self.fee) / self.spot_price
};
let price_scaled = (effective_price * PRECISION as f64) as u128;
let sell_limit = (&buy_limit * PRECISION) / price_scaled;
let sell_limit = if let (Some(&sell_dec), Some(&buy_dec)) =
(self.token_decimals.get(&sell_token), self.token_decimals.get(&buy_token))
{
let decimal_diff = sell_dec as i32 - buy_dec as i32;
if decimal_diff >= 0 {
sell_limit * BigUint::from(10u64).pow(decimal_diff as u32)
} else {
sell_limit / BigUint::from(10u64).pow((-decimal_diff) as u32)
}
} else {
sell_limit
};
Ok((sell_limit, buy_limit))
}
fn query_pool_swap(&self, params: &QueryPoolSwapParams) -> Result<PoolSwap, SimulationError> {
let token_in = params.token_in();
let token_out = params.token_out();
match params.swap_constraint() {
SwapConstraint::TradeLimitPrice { .. } => {
let (sell_limit, _) =
self.get_limits(token_in.address.clone(), token_out.address.clone())?;
let result = self.get_amount_out(sell_limit.clone(), token_in, token_out)?;
Ok(PoolSwap::new(sell_limit, result.amount, result.new_state, None))
}
_ => Err(SimulationError::InvalidInput(
"MockProtocolSim only supports TradeLimitPrice".to_string(),
None,
)),
}
}
fn delta_transition(
&mut self,
_delta: ProtocolStateDelta,
_tokens: &HashMap<Bytes, Token>,
_balances: &Balances,
) -> Result<(), TransitionError> {
unimplemented!("delta_transition not implemented in MockProtocolSim")
}
fn clone_box(&self) -> Box<dyn ProtocolSim> {
Box::new(self.clone())
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn eq(&self, other: &dyn ProtocolSim) -> bool {
other
.as_any()
.downcast_ref::<Self>()
.map(|o| (o.spot_price - self.spot_price).abs() < f64::EPSILON)
.unwrap_or(false)
}
}
pub fn addr(b: u8) -> Address {
Address::from([b; 20])
}
pub fn token(addr_b: u8, symbol: &str) -> Token {
token_with_decimals(addr_b, symbol, 18)
}
pub fn token_with_decimals(addr_b: u8, symbol: &str, decimals: u32) -> Token {
Token {
address: addr(addr_b),
symbol: symbol.to_string(),
decimals,
tax: Default::default(),
gas: vec![],
chain: Chain::Ethereum,
quality: 100,
}
}
pub fn component(id: &str, tokens: &[Token]) -> ProtocolComponent {
ProtocolComponent::new(
id,
"uniswap_v2",
"swap",
Chain::Ethereum,
tokens
.iter()
.map(|t| t.address.clone())
.collect(),
vec![],
HashMap::new(),
Default::default(),
Default::default(),
NaiveDateTime::default(),
)
}
pub fn order(token_in: &Token, token_out: &Token, amount: u128, side: OrderSide) -> Order {
Order::new(
token_in.address.clone(),
token_out.address.clone(),
BigUint::from(amount),
side,
Address::default(),
)
.with_id("test-order".to_string())
}
pub fn setup_market(
pools: Vec<(&str, &Token, &Token, MockProtocolSim)>,
) -> (Arc<RwLock<SharedMarketData>>, PetgraphStableDiGraphManager<DepthAndPrice>) {
let mut market = SharedMarketData::new();
let mut component_weights = HashMap::new();
market.update_gas_price(BlockGasPrice {
block_number: 1,
block_hash: Default::default(),
block_timestamp: 0,
pricing: GasPrice::Legacy { gas_price: BigUint::from(100u64) },
});
market.update_last_updated(BlockInfo::new(1, "0x00".into(), 0));
for (pool_id, token_in, token_out, state) in pools {
let tokens = vec![token_in.clone(), token_out.clone()];
let comp = component(pool_id, &tokens);
let weight_to = DepthAndPrice::from_protocol_sim(&state, token_in, token_out).unwrap();
let weight_from = DepthAndPrice::from_protocol_sim(&state, token_out, token_in).unwrap();
market.upsert_components(std::iter::once(comp));
market.update_states([(pool_id.to_string(), Box::new(state) as Box<dyn ProtocolSim>)]);
market.upsert_tokens(tokens);
component_weights.insert(pool_id, (token_in, token_out, weight_to, weight_from));
}
let mut graph_manager = PetgraphStableDiGraphManager::default();
graph_manager.initialize_graph(&market.component_topology());
for (pool_id, (token_in, token_out, weight_to, weight_from)) in component_weights {
graph_manager
.set_edge_weight(
&pool_id.to_string(),
&token_in.address,
&token_out.address,
weight_to,
false,
)
.unwrap();
graph_manager
.set_edge_weight(
&pool_id.to_string(),
&token_out.address,
&token_in.address,
weight_from,
false,
)
.unwrap();
}
(Arc::new(RwLock::new(market)), graph_manager)
}
pub fn market_read(
lock: &Arc<RwLock<SharedMarketData>>,
) -> tokio::sync::RwLockReadGuard<'_, SharedMarketData> {
lock.try_read()
.expect("lock should not be contested in test")
}
pub mod fixtures {
use super::*;
pub fn addrs() -> (Address, Address, Address, Address) {
(addr(0x0A), addr(0x0B), addr(0x0C), addr(0x0D))
}
pub(crate) fn linear_graph() -> PetgraphStableDiGraphManager<DepthAndPrice> {
let (a, b, c, d) = addrs();
let mut m = PetgraphStableDiGraphManager::<DepthAndPrice>::new();
let mut t = HashMap::new();
t.insert("ab".into(), vec![a.clone(), b.clone()]);
t.insert("bc".into(), vec![b.clone(), c.clone()]);
t.insert("cd".into(), vec![c, d]);
m.initialize_graph(&t);
m
}
pub(crate) fn parallel_graph() -> PetgraphStableDiGraphManager<DepthAndPrice> {
let (a, b, c, _) = addrs();
let mut m = PetgraphStableDiGraphManager::<DepthAndPrice>::new();
let mut t = HashMap::new();
t.insert("ab1".into(), vec![a.clone(), b.clone()]);
t.insert("ab2".into(), vec![a.clone(), b.clone()]);
t.insert("ab3".into(), vec![a, b.clone()]);
t.insert("bc1".into(), vec![b.clone(), c.clone()]);
t.insert("bc2".into(), vec![b, c]);
m.initialize_graph(&t);
m
}
pub(crate) fn diamond_graph() -> PetgraphStableDiGraphManager<DepthAndPrice> {
let (a, b, c, d) = addrs();
let mut m = PetgraphStableDiGraphManager::<DepthAndPrice>::new();
let mut t = HashMap::new();
t.insert("ab".into(), vec![a.clone(), b.clone()]);
t.insert("ac".into(), vec![a, c.clone()]);
t.insert("bd".into(), vec![b, d.clone()]);
t.insert("cd".into(), vec![c, d]);
m.initialize_graph(&t);
m
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
fn ordered_tokens() -> (Token, Token) {
let token_low = token(0x01, "LOW");
let token_high = token(0x02, "HIGH");
assert!(token_low.address < token_high.address);
(token_low, token_high)
}
fn ordered_tokens_with_decimals(dec_low: u32, dec_high: u32) -> (Token, Token) {
let token_low = token_with_decimals(0x01, "LOW", dec_low);
let token_high = token_with_decimals(0x02, "HIGH", dec_high);
assert!(token_low.address < token_high.address);
(token_low, token_high)
}
#[test]
fn default_values_are_as_expected() {
let sim = MockProtocolSim::default();
assert_eq!(sim.spot_price, 2.0);
assert_eq!(sim.gas, 50_000);
assert_eq!(sim.liquidity, u128::MAX);
assert_eq!(sim.fee, 0.0);
}
#[test]
fn new_sets_spot_price_with_defaults() {
let sim = MockProtocolSim::new(10.0);
assert_eq!(sim.spot_price, 10.0);
assert_eq!(sim.gas, 50_000); assert_eq!(sim.liquidity, u128::MAX); assert_eq!(sim.fee, 0.0); }
#[test]
fn builder_methods_chain_correctly() {
let sim = MockProtocolSim::new(1.0)
.with_spot_price(5.0)
.with_gas(100_000)
.with_liquidity(1_000_000)
.with_fee(0.003);
assert_eq!(sim.spot_price, 5.0);
assert_eq!(sim.gas, 100_000);
assert_eq!(sim.liquidity, 1_000_000);
assert_eq!(sim.fee, 0.003);
}
#[test]
fn fee_returns_configured_value() {
let sim = MockProtocolSim::default().with_fee(0.003);
assert_eq!(sim.fee(), 0.003);
}
#[test]
fn fee_returns_zero_by_default() {
let sim = MockProtocolSim::default();
assert_eq!(sim.fee(), 0.0);
}
#[test]
fn spot_price_asymmetric_based_on_token_order() {
let (token_low, token_high) = ordered_tokens();
let sim = MockProtocolSim::new(4.0);
let price_low_to_high = sim
.spot_price(&token_low, &token_high)
.unwrap();
assert_eq!(price_low_to_high, 4.0);
let price_high_to_low = sim
.spot_price(&token_high, &token_low)
.unwrap();
assert_eq!(price_high_to_low, 0.25); }
#[test]
fn spot_price_accounts_for_fee() {
let (token_low, token_high) = ordered_tokens();
let sim = MockProtocolSim::new(2.0).with_fee(0.5);
let price_low_to_high = sim
.spot_price(&token_low, &token_high)
.unwrap();
assert_eq!(price_low_to_high, 4.0);
let price_high_to_low = sim
.spot_price(&token_high, &token_low)
.unwrap();
assert_eq!(price_high_to_low, 0.25);
}
#[test]
fn get_amount_out_multiplies_by_spot_price_low_to_high() {
let (token_low, token_high) = ordered_tokens();
let sim = MockProtocolSim::new(3.0);
let result = sim
.get_amount_out(BigUint::from(1000u64), &token_low, &token_high)
.unwrap();
assert_eq!(result.amount, BigUint::from(3000u64));
}
#[test]
fn get_amount_out_divides_by_spot_price_high_to_low() {
let (token_low, token_high) = ordered_tokens();
let sim = MockProtocolSim::new(4.0);
let result = sim
.get_amount_out(BigUint::from(1000u64), &token_high, &token_low)
.unwrap();
assert_eq!(result.amount, BigUint::from(250u64));
}
#[test]
fn get_amount_out_applies_fee() {
let (token_low, token_high) = ordered_tokens();
let sim = MockProtocolSim::new(2.0).with_fee(0.1);
let result = sim
.get_amount_out(BigUint::from(1000u64), &token_low, &token_high)
.unwrap();
assert_eq!(result.amount, BigUint::from(1800u64));
}
#[test]
fn get_amount_out_returns_configured_gas() {
let (token_low, token_high) = ordered_tokens();
let sim = MockProtocolSim::default().with_gas(75_000);
let result = sim
.get_amount_out(BigUint::from(1000u64), &token_low, &token_high)
.unwrap();
assert_eq!(result.gas, BigUint::from(75_000u64));
}
#[test]
fn get_amount_out_returns_new_state_with_incremented_spot_price() {
let (token_low, token_high) = ordered_tokens();
let sim = MockProtocolSim::new(5.0)
.with_gas(60_000)
.with_fee(0.01);
let result = sim
.get_amount_out(BigUint::from(1000u64), &token_low, &token_high)
.unwrap();
let new_state = result
.new_state
.as_any()
.downcast_ref::<MockProtocolSim>()
.unwrap();
assert_eq!(new_state.spot_price, 6.0); assert_eq!(new_state.gas, 60_000); assert_eq!(new_state.fee, 0.01); }
#[test]
fn get_amount_out_fails_when_exceeding_liquidity() {
let (token_low, token_high) = ordered_tokens();
let sim = MockProtocolSim::default().with_liquidity(1000);
let result = sim.get_amount_out(BigUint::from(1001u64), &token_low, &token_high);
assert!(result.is_err());
match result {
Err(SimulationError::InvalidInput(msg, _)) => {
assert!(msg.contains("liquidity"));
}
_ => panic!("Expected InvalidInput error"),
}
}
#[test]
fn get_limits_returns_liquidity_as_buy_limit() {
let sim = MockProtocolSim::new(2.0).with_liquidity(10_000);
let (_, buy_limit) = sim
.get_limits(Bytes::default(), Bytes::default())
.unwrap();
assert_eq!(buy_limit, BigUint::from(10_000u64));
}
#[test]
fn get_limits_calculates_sell_limit_from_buy_limit_and_spot_price() {
let sim = MockProtocolSim::new(4.0).with_liquidity(8_000);
let (sell_limit, buy_limit) = sim
.get_limits(addr(1), addr(2))
.unwrap();
assert_eq!(buy_limit, BigUint::from(8_000u64));
assert_eq!(sell_limit, BigUint::from(2_000u64)); }
#[test]
fn get_limits_accounts_for_fee_in_sell_limit() {
let sim = MockProtocolSim::new(2.0)
.with_liquidity(1_000)
.with_fee(0.5);
let (sell_limit, buy_limit) = sim
.get_limits(addr(1), addr(2))
.unwrap();
assert_eq!(buy_limit, BigUint::from(1_000u64));
assert_eq!(sell_limit, BigUint::from(1_000u64));
}
#[test]
fn clone_box_creates_independent_copy() {
let sim = MockProtocolSim::new(7.0)
.with_gas(80_000)
.with_fee(0.02);
let cloned: Box<dyn ProtocolSim> = sim.clone_box();
let cloned_mock = cloned
.as_any()
.downcast_ref::<MockProtocolSim>()
.unwrap();
assert_eq!(cloned_mock.spot_price, 7.0);
assert_eq!(cloned_mock.gas, 80_000);
assert_eq!(cloned_mock.fee, 0.02);
}
#[test]
fn eq_compares_spot_price_only() {
let sim1 = MockProtocolSim::new(5.0).with_gas(100);
let sim2 = MockProtocolSim::new(5.0).with_gas(200); let sim3 = MockProtocolSim::new(6.0).with_gas(100);
assert!(sim1.eq(&sim2)); assert!(!sim1.eq(&sim3)); }
#[rstest]
#[case::high_to_low_decimals(18, 6, 2000.0, 1_000_000_000_000_000_000u128, 2_000_000_000u128)]
#[case::low_to_high_decimals(6, 18, 2000.0, 1_000_000u128, 2_000_000_000_000_000_000_000u128)]
#[case::same_decimals(
18,
18,
2000.0,
1_000_000_000_000_000_000u128,
2_000_000_000_000_000_000_000u128
)]
fn get_amount_out_scales_by_decimals(
#[case] dec_low: u32,
#[case] dec_high: u32,
#[case] spot_price: f64,
#[case] amount_in: u128,
#[case] expected_out: u128,
) {
let (token_low, token_high) = ordered_tokens_with_decimals(dec_low, dec_high);
let sim = MockProtocolSim::new(spot_price);
let result = sim
.get_amount_out(BigUint::from(amount_in), &token_low, &token_high)
.unwrap();
assert_eq!(result.amount, BigUint::from(expected_out));
}
#[rstest]
#[case::high_to_low_decimals(18, 6, 4.0, 4_000_000u128, 1_000_000_000_000_000_000u128)]
#[case::low_to_high_decimals(6, 18, 4.0, 4_000_000_000_000_000_000u128, 1_000_000u128)]
#[case::same_decimals(
18,
18,
4.0,
4_000_000_000_000_000_000u128,
1_000_000_000_000_000_000u128
)]
fn get_amount_out_reverse_direction_scales_by_decimals(
#[case] dec_low: u32,
#[case] dec_high: u32,
#[case] spot_price: f64,
#[case] amount_in: u128,
#[case] expected_out: u128,
) {
let (token_low, token_high) = ordered_tokens_with_decimals(dec_low, dec_high);
let sim = MockProtocolSim::new(spot_price);
let result = sim
.get_amount_out(BigUint::from(amount_in), &token_high, &token_low)
.unwrap();
assert_eq!(result.amount, BigUint::from(expected_out));
}
#[rstest]
#[case::high_sell_low_buy(18, 6, 4.0, 8_000)]
#[case::low_sell_high_buy(6, 18, 4.0, 8_000)]
fn get_limits_scales_sell_limit_by_decimals(
#[case] sell_dec: u32,
#[case] buy_dec: u32,
#[case] spot_price: f64,
#[case] liquidity: u128,
) {
let sell_token = token_with_decimals(0x01, "SELL", sell_dec);
let buy_token = token_with_decimals(0x02, "BUY", buy_dec);
let sim = MockProtocolSim::new(spot_price)
.with_liquidity(liquidity)
.with_tokens(&[sell_token.clone(), buy_token.clone()]);
let (sell_limit, buy_limit) = sim
.get_limits(sell_token.address, buy_token.address)
.unwrap();
assert_eq!(buy_limit, BigUint::from(liquidity));
let base_sell_limit = BigUint::from((liquidity as f64 / spot_price) as u128);
let decimal_diff = sell_dec as i32 - buy_dec as i32;
let expected_sell_limit = if decimal_diff >= 0 {
base_sell_limit * BigUint::from(10u64).pow(decimal_diff as u32)
} else {
base_sell_limit / BigUint::from(10u64).pow((-decimal_diff) as u32)
};
assert_eq!(sell_limit, expected_sell_limit);
}
#[test]
fn get_limits_without_token_decimals_assumes_equal_decimals() {
let sim = MockProtocolSim::new(4.0).with_liquidity(8_000);
let (sell_limit, _) = sim
.get_limits(addr(1), addr(2))
.unwrap();
assert_eq!(sell_limit, BigUint::from(2_000u64));
}
#[test]
fn get_limits_uses_reciprocal_price_for_reverse_direction() {
let sim = MockProtocolSim::new(4.0).with_liquidity(8_000);
let (sell_limit, buy_limit) = sim
.get_limits(addr(2), addr(1))
.unwrap();
assert_eq!(buy_limit, BigUint::from(8_000u64));
assert_eq!(sell_limit, BigUint::from(32_000u64)); }
#[rstest]
#[case::same_decimals(18, 18, 1000, 2000)]
#[case::high_to_low(18, 6, 1000, 2_000_000)]
#[case::low_to_high(6, 18, 2_000_000, 1000)]
fn mock_matches_univ2_for_small_swaps(
#[case] dec_in: u32,
#[case] dec_out: u32,
#[case] human_reserve_in: u64,
#[case] human_reserve_out: u64,
) {
use alloy::primitives::U256;
use tycho_simulation::evm::protocol::uniswap_v2::state::UniswapV2State;
let token_in = token_with_decimals(0x01, "IN", dec_in);
let token_out = token_with_decimals(0x02, "OUT", dec_out);
let reserve_in = U256::from(human_reserve_in) * U256::from(10u64).pow(U256::from(dec_in));
let reserve_out =
U256::from(human_reserve_out) * U256::from(10u64).pow(U256::from(dec_out));
let univ2 = UniswapV2State::new(reserve_in, reserve_out);
let univ2_spot = univ2
.spot_price(&token_in, &token_out)
.expect("UniV2 spot_price should succeed");
let mock_spot_price = human_reserve_out as f64 / human_reserve_in as f64;
let liquidity = human_reserve_out as u128 * 10u128.pow(dec_out);
let sim = MockProtocolSim::new(mock_spot_price)
.with_liquidity(liquidity)
.with_tokens(&[token_in.clone(), token_out.clone()]);
let mock_spot = sim
.spot_price(&token_in, &token_out)
.expect("mock spot_price should succeed");
let spot_price_ratio = mock_spot / univ2_spot;
assert!(
(0.99..=1.01).contains(&spot_price_ratio),
"spot_price mismatch: mock={mock_spot}, univ2={univ2_spot}"
);
let small_amount_in = BigUint::from(human_reserve_in) * BigUint::from(10u64).pow(dec_in) /
BigUint::from(1000u64);
let univ2_result = univ2
.get_amount_out(small_amount_in.clone(), &token_in, &token_out)
.expect("UniV2 get_amount_out should succeed");
let mock_result = sim
.get_amount_out(small_amount_in, &token_in, &token_out)
.expect("mock get_amount_out should succeed");
let univ2_out: f64 = univ2_result
.amount
.to_string()
.parse()
.unwrap();
let mock_out: f64 = mock_result
.amount
.to_string()
.parse()
.unwrap();
let amount_out_ratio = mock_out / univ2_out;
assert!(
(0.99..=1.01).contains(&amount_out_ratio),
"get_amount_out mismatch: mock={mock_out}, univ2={univ2_out}"
);
let univ2_limits = univ2
.get_limits(token_in.address.clone(), token_out.address.clone())
.expect("UniV2 get_limits should succeed");
let mock_limits = sim
.get_limits(token_in.address.clone(), token_out.address.clone())
.expect("mock get_limits should succeed");
let univ2_sell: f64 = univ2_limits
.0
.to_string()
.parse()
.unwrap();
let mock_sell: f64 = mock_limits
.0
.to_string()
.parse()
.unwrap();
if univ2_sell > 0.0 && mock_sell > 0.0 {
let sell_limit_ratio = mock_sell / univ2_sell;
assert!(
(0.1..=10.0).contains(&sell_limit_ratio),
"get_limits sell_limit order of magnitude mismatch: mock={mock_sell}, \
univ2={univ2_sell}"
);
}
}
}