tycho_simulation/evm/protocol/fluid/
v1.rs

1/// FluidV1 simulation logic.
2///
3/// This implementation is a port from the [Kyberswap reference implementation](https://github.com/KyberNetwork/kyberswap-dex-lib/blob/main/pkg/liquidity-source/fluid/dex-t1/pool_simulator.go)
4/// functions and errors are ported equivalently and then used to implement the ProtocolSim
5/// interface.
6///
7/// ## Differences
8/// - Native ETH: Tycho uses a zero-byte address while Fluid uses 0xeee... address
9/// - Limits: Tycho uses binary search to find limits that will actually execute
10/// - State: Tycho uses the local VM to retrieve and update the state of each pool
11use std::{
12    any::Any,
13    collections::HashMap,
14    time::{SystemTime, UNIX_EPOCH},
15};
16
17use alloy::primitives::U256;
18use num_bigint::{BigUint, ToBigUint};
19use num_traits::Euclid;
20use thiserror::Error;
21use tracing::trace;
22use tycho_common::{
23    dto::ProtocolStateDelta,
24    models::token::Token,
25    simulation::{
26        errors::{SimulationError, TransitionError},
27        protocol_sim::{Balances, GetAmountOutResult, ProtocolSim},
28    },
29    Bytes,
30};
31
32use crate::evm::{
33    engine_db::{create_engine, SHARED_TYCHO_DB},
34    protocol::{
35        fluid::{v1::constant::RESERVES_RESOLVER, vm},
36        u256_num::{biguint_to_u256, u256_to_biguint, u256_to_f64},
37        utils::add_fee_markup,
38    },
39};
40
41mod constant {
42    use alloy::{hex, primitives::U256};
43
44    pub const MAX_PRICE_DIFF: U256 = U256::from_limbs([5, 0, 0, 0]); // 5
45    pub const MIN_SWAP_LIQUIDITY: U256 = U256::from_limbs([8500, 0, 0, 0]); // 8500
46    pub const SIX_DECIMALS: U256 = U256::from_limbs([1000000, 0, 0, 0]); // 1e6
47    pub const TWO_DECIMALS: U256 = U256::from_limbs([100, 0, 0, 0]); // 1e2
48    pub const B_I1E18: U256 = U256::from_limbs([0x0DE0B6B3A7640000, 0, 0, 0]); // 1e18
49    pub const B_I1E27: U256 = U256::from_limbs([0x9fd0803ce8000000, 0x33b2e3c, 0, 0]); // 1e27
50    pub const DEX_AMOUNT_DECIMALS: i64 = 12;
51    pub const FEE_PERCENT_PRECISION: U256 = U256::from_limbs([10000, 0, 0, 0]);
52    pub const ZERO_ADDRESS: &[u8] = &hex!("0x0000000000000000000000000000000000000000");
53    pub const NATIVE_ADDRESS: &[u8] = &hex!("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE");
54    pub const RESERVES_RESOLVER: &[u8] = &hex!("0xc93876c0eed99645dd53937b25433e311881a27c");
55}
56
57#[derive(Debug, Clone, PartialEq)]
58pub struct FluidV1 {
59    pool_address: Bytes,
60    token0: Token,
61    token1: Token,
62    collateral_reserves: CollateralReserves,
63    debt_reserves: DebtReserves,
64    dex_limits: DexLimits,
65    center_price: U256,
66    fee: U256,
67    sync_time: u64,
68    pool_reserve0: U256,
69    pool_reserve1: U256,
70}
71
72#[derive(Debug, Clone, PartialEq)]
73pub(super) struct CollateralReserves {
74    pub(super) token0_real_reserves: U256,
75    pub(super) token1_real_reserves: U256,
76    pub(super) token0_imaginary_reserves: U256,
77    pub(super) token1_imaginary_reserves: U256,
78}
79
80#[derive(Debug, Clone, PartialEq)]
81pub(super) struct DebtReserves {
82    pub(super) token0_real_reserves: U256,
83    pub(super) token1_real_reserves: U256,
84    pub(super) token0_imaginary_reserves: U256,
85    pub(super) token1_imaginary_reserves: U256,
86}
87
88#[derive(Debug, Clone, PartialEq)]
89pub(super) struct DexLimits {
90    pub(super) borrowable_token0: TokenLimit,
91    pub(super) borrowable_token1: TokenLimit,
92    pub(super) withdrawable_token0: TokenLimit,
93    pub(super) withdrawable_token1: TokenLimit,
94}
95
96#[derive(Debug, Clone, PartialEq)]
97pub(super) struct TokenLimit {
98    pub(super) available: U256,
99    pub(super) expands_to: U256,
100    pub(super) expand_duration: U256,
101}
102
103#[derive(Debug, Error)]
104enum SwapError {
105    #[error("Insufficient reserve: tokenOut amount exceeds reserve")]
106    InsufficientReserve,
107    #[error("Insufficient reserve: tokenOut amount exceeds borrowable limit")]
108    InsufficientBorrowable,
109    #[error("Insufficient reserve: tokenOut amount exceeds withdrawable limit")]
110    InsufficientWithdrawable,
111    #[error("Insufficient reserve: tokenOut amount exceeds max price limit")]
112    InsufficientMaxPrice,
113    #[error("Invalid reserves ratio")]
114    VerifyReservesRatiosInvalid,
115    #[error("No pools are enabled")]
116    NoPoolsEnabled,
117    #[error("InvalidAmountIn: Amount too low")]
118    InvalidAmountIn,
119}
120
121impl From<SwapError> for SimulationError {
122    fn from(value: SwapError) -> Self {
123        Self::FatalError(value.to_string())
124    }
125}
126impl FluidV1 {
127    #[allow(clippy::too_many_arguments)]
128    pub(super) fn new(
129        pool_address: &Bytes,
130        token0: &Token,
131        token1: &Token,
132        collateral_reserves: CollateralReserves,
133        debt_reserves: DebtReserves,
134        dex_limits: DexLimits,
135        center_price: U256,
136        fee: U256,
137        sync_time: u64,
138    ) -> Self {
139        let pool_reserve0 = get_max_reserves(
140            token0.decimals as u8,
141            &dex_limits.withdrawable_token0,
142            &dex_limits.borrowable_token0,
143            &collateral_reserves.token0_real_reserves,
144            &debt_reserves.token0_real_reserves,
145        );
146        let pool_reserve1 = get_max_reserves(
147            token1.decimals as u8,
148            &dex_limits.withdrawable_token1,
149            &dex_limits.borrowable_token1,
150            &collateral_reserves.token1_real_reserves,
151            &debt_reserves.token1_real_reserves,
152        );
153
154        // potentially flip token0 and token1 since ETH address is different from our eth marker
155        // address
156        let (token0_normalized, token1_normalized) =
157            if FluidV1::normalize_native_address(&token0.address) <
158                FluidV1::normalize_native_address(&token1.address)
159            {
160                (token0.clone(), token1.clone())
161            } else {
162                (token1.clone(), token0.clone())
163            };
164        Self {
165            pool_address: pool_address.clone(),
166            token0: token0_normalized,
167            token1: token1_normalized,
168            collateral_reserves,
169            debt_reserves,
170            dex_limits,
171            center_price,
172            fee,
173            sync_time,
174            pool_reserve0,
175            pool_reserve1,
176        }
177    }
178
179    fn normalize_native_address(address: &Bytes) -> &[u8] {
180        if address == constant::ZERO_ADDRESS {
181            constant::NATIVE_ADDRESS
182        } else {
183            address
184        }
185    }
186}
187
188impl ProtocolSim for FluidV1 {
189    fn fee(&self) -> f64 {
190        let fee = u256_to_f64(self.fee).expect("Fluid fee values are safe to convert");
191        let precision =
192            u256_to_f64(constant::FEE_PERCENT_PRECISION).expect("FEE_PERCENT_PRECISION is safe");
193        // Fee is in basis points: fee / FEE_PERCENT_PRECISION / 100
194        // e.g., fee=68 means 68/10000/100 = 0.000068 = 0.0068%
195        fee / precision / 100.0
196    }
197
198    fn spot_price(&self, base: &Token, _quote: &Token) -> Result<f64, SimulationError> {
199        let price_f64 = if !self
200            .collateral_reserves
201            .token0_imaginary_reserves
202            .is_zero()
203        {
204            u256_to_f64(
205                self.collateral_reserves
206                    .token1_imaginary_reserves,
207            )? / u256_to_f64(
208                self.collateral_reserves
209                    .token0_imaginary_reserves,
210            )?
211        } else {
212            u256_to_f64(
213                self.debt_reserves
214                    .token1_imaginary_reserves,
215            )? / u256_to_f64(
216                self.debt_reserves
217                    .token0_imaginary_reserves,
218            )?
219        };
220        let oriented_price_f64 =
221            if base.address == self.token0.address { price_f64 } else { 1.0 / price_f64 };
222
223        Ok(add_fee_markup(oriented_price_f64, self.fee()))
224    }
225
226    fn get_amount_out(
227        &self,
228        amount_in: BigUint,
229        token_in: &Token,
230        token_out: &Token,
231    ) -> Result<GetAmountOutResult, SimulationError> {
232        if amount_in == BigUint::from(0u32) {
233            return Ok(GetAmountOutResult {
234                amount: BigUint::from(0u32),
235                gas: BigUint::from(155433u32),
236                new_state: Box::new(self.clone()),
237            });
238        }
239        let zero2one = self.token0.address == token_in.address;
240
241        let (token_in_decimals, token_out_decimals) = (token_in.decimals, token_out.decimals);
242
243        let amount_in = biguint_to_u256(&amount_in);
244        let fee = amount_in * self.fee / constant::SIX_DECIMALS;
245
246        let amount_in_after_fee = amount_in - fee;
247        let amount_in_adjusted = to_adjusted_amount(amount_in_after_fee, token_in_decimals as i64);
248
249        if amount_in_adjusted < constant::SIX_DECIMALS ||
250            amount_in_after_fee < constant::TWO_DECIMALS
251        {
252            return Err(SwapError::InvalidAmountIn.into())
253        }
254        let mut new_col_reserves = self.collateral_reserves.clone();
255        let mut new_debt_reserves = self.debt_reserves.clone();
256        let mut new_limits = self.dex_limits.clone();
257
258        let amount_out = swap_in_adjusted(
259            zero2one,
260            amount_in_adjusted,
261            &mut new_col_reserves,
262            &mut new_debt_reserves,
263            token_out_decimals as i64,
264            &mut new_limits,
265            self.center_price,
266            self.sync_time,
267        )?;
268
269        let reserve = if zero2one { self.pool_reserve1 } else { self.pool_reserve0 };
270        if amount_out > reserve {
271            return Err(SwapError::InsufficientReserve.into())
272        }
273
274        let result = GetAmountOutResult::new(
275            u256_to_biguint(amount_out),
276            155433.to_biguint().expect("infallible"),
277            Box::new(Self {
278                pool_address: self.pool_address.clone(),
279                token0: self.token0.clone(),
280                token1: self.token1.clone(),
281                collateral_reserves: new_col_reserves,
282                debt_reserves: new_debt_reserves,
283                dex_limits: new_limits,
284                center_price: self.center_price,
285                fee: self.fee,
286                sync_time: self.sync_time,
287                pool_reserve0: self.pool_reserve0,
288                pool_reserve1: self.pool_reserve1,
289            }),
290        );
291        Ok(result)
292    }
293
294    fn get_limits(
295        &self,
296        sell_token: Bytes,
297        buy_token: Bytes,
298    ) -> Result<(BigUint, BigUint), SimulationError> {
299        let zero2one = sell_token == self.token0.address;
300
301        let (upper_bound_out, out_decimals, in_decimals) = if zero2one {
302            (
303                to_adjusted_amount(
304                    self.dex_limits
305                        .withdrawable_token0
306                        .available +
307                        self.dex_limits
308                            .borrowable_token0
309                            .available,
310                    self.token0.decimals as i64,
311                ),
312                self.token1.decimals,
313                self.token0.decimals,
314            )
315        } else {
316            (
317                to_adjusted_amount(
318                    self.dex_limits
319                        .withdrawable_token1
320                        .available +
321                        self.dex_limits
322                            .borrowable_token1
323                            .available,
324                    self.token1.decimals as i64,
325                ),
326                self.token0.decimals,
327                self.token1.decimals,
328            )
329        };
330        if upper_bound_out == U256::ZERO {
331            trace!("Upper bound is zero for {}", self.pool_address);
332            return Ok((BigUint::ZERO, BigUint::ZERO));
333        }
334        let delta = U256::from(10).pow(U256::from(2));
335        let (max_valid, res) = find_max_valid_u256(upper_bound_out, delta, |amount| {
336            let mut col_clone = self.collateral_reserves.clone();
337            let mut debt_clone = self.debt_reserves.clone();
338            let mut limits_clone = self.dex_limits.clone();
339            swap_in_adjusted(
340                zero2one,
341                amount,
342                &mut col_clone,
343                &mut debt_clone,
344                out_decimals as i64,
345                &mut limits_clone,
346                self.center_price,
347                self.sync_time,
348            )
349        });
350        Ok((
351            u256_to_biguint(from_adjusted_amount(max_valid, in_decimals as i64)),
352            u256_to_biguint(res.unwrap_or_else(|| {
353                trace!(
354                    "All evaluations errored during limit search for {} -> {}",
355                    sell_token,
356                    buy_token
357                );
358                U256::ZERO
359            })),
360        ))
361    }
362
363    fn delta_transition(
364        &mut self,
365        _delta: ProtocolStateDelta,
366        _tokens: &HashMap<Bytes, Token>,
367        _balances: &Balances,
368    ) -> Result<(), TransitionError<String>> {
369        let engine = create_engine(SHARED_TYCHO_DB.clone(), false).expect("Infallible");
370
371        let state = vm::decode_from_vm(
372            &self.pool_address,
373            &self.token0,
374            &self.token1,
375            RESERVES_RESOLVER,
376            engine,
377        )?;
378
379        trace!(?state, "Calling delta transition for {}", &self.pool_address);
380
381        self.collateral_reserves = state.collateral_reserves;
382        self.debt_reserves = state.debt_reserves;
383        self.dex_limits = state.dex_limits;
384        self.center_price = state.center_price;
385        self.fee = state.fee;
386        self.sync_time = state.sync_time;
387
388        self.pool_reserve0 = get_max_reserves(
389            self.token0.decimals as u8,
390            &self.dex_limits.withdrawable_token0,
391            &self.dex_limits.borrowable_token0,
392            &self
393                .collateral_reserves
394                .token0_real_reserves,
395            &self.debt_reserves.token0_real_reserves,
396        );
397        self.pool_reserve1 = get_max_reserves(
398            self.token1.decimals as u8,
399            &self.dex_limits.withdrawable_token1,
400            &self.dex_limits.borrowable_token1,
401            &self
402                .collateral_reserves
403                .token1_real_reserves,
404            &self.debt_reserves.token1_real_reserves,
405        );
406        Ok(())
407    }
408
409    fn clone_box(&self) -> Box<dyn ProtocolSim> {
410        Box::new(self.clone())
411    }
412
413    fn as_any(&self) -> &dyn Any {
414        self
415    }
416
417    fn as_any_mut(&mut self) -> &mut dyn Any {
418        self
419    }
420
421    fn eq(&self, other: &dyn ProtocolSim) -> bool {
422        if let Some(other_state) = other.as_any().downcast_ref::<Self>() {
423            self == other_state
424        } else {
425            false
426        }
427    }
428}
429
430/// Generic binary search for the largest `U256` input that doesn't return an error.
431///
432/// # Parameters
433/// - `upper_bound`: The maximum value to test.
434/// - `delta`: Stop searching when `high - low < delta`.
435/// - `f`: A closure that takes a `U256` input and returns `Result<T, E>`.
436///
437/// # Returns
438/// The largest input value for which `f(input)` succeeded.
439pub fn find_max_valid_u256<T, E, F>(upper_bound: U256, delta: U256, mut f: F) -> (U256, Option<T>)
440where
441    F: FnMut(U256) -> Result<T, E>,
442    E: std::fmt::Debug,
443{
444    let mut low = U256::ZERO;
445    let mut high = upper_bound;
446    let mut best = U256::ZERO;
447    let mut best_result: Option<T> = None;
448
449    while high > low + delta {
450        let mid = (low + high) / U256::from(2);
451
452        match f(mid) {
453            Ok(result) => {
454                best = mid;
455                best_result = Some(result);
456                low = mid;
457            }
458            Err(_) => {
459                high = mid;
460            }
461        }
462    }
463
464    (best, best_result)
465}
466
467#[allow(clippy::too_many_arguments)]
468fn swap_in_adjusted(
469    swap0_to_1: bool,
470    amount_to_swap: U256,
471    col_reserves: &mut CollateralReserves,
472    debt_reserves: &mut DebtReserves,
473    out_decimals: i64,
474    current_limits: &mut DexLimits,
475    center_price: U256,
476    sync_time: u64,
477) -> Result<U256, SwapError> {
478    let (
479        col_reserve_in,
480        col_reserve_out,
481        col_i_reserve_in,
482        col_i_reserve_out,
483        debt_reserve_in,
484        debt_reserve_out,
485        debt_i_reserve_in,
486        debt_i_reserve_out,
487        borrowable,
488        withdrawable,
489    ) = if swap0_to_1 {
490        (
491            col_reserves.token0_real_reserves,
492            col_reserves.token1_real_reserves,
493            col_reserves.token0_imaginary_reserves,
494            col_reserves.token1_imaginary_reserves,
495            debt_reserves.token0_real_reserves,
496            debt_reserves.token1_real_reserves,
497            debt_reserves.token0_imaginary_reserves,
498            debt_reserves.token1_imaginary_reserves,
499            get_expanded_limit(sync_time, &current_limits.borrowable_token1),
500            get_expanded_limit(sync_time, &current_limits.withdrawable_token1),
501        )
502    } else {
503        (
504            col_reserves.token1_real_reserves,
505            col_reserves.token0_real_reserves,
506            col_reserves.token1_imaginary_reserves,
507            col_reserves.token0_imaginary_reserves,
508            debt_reserves.token1_real_reserves,
509            debt_reserves.token0_real_reserves,
510            debt_reserves.token1_imaginary_reserves,
511            debt_reserves.token0_imaginary_reserves,
512            get_expanded_limit(sync_time, &current_limits.borrowable_token0),
513            get_expanded_limit(sync_time, &current_limits.withdrawable_token0),
514        )
515    };
516
517    // Adjust borrowable and withdrawable amounts to match output decimals
518    let borrowable = to_adjusted_amount(borrowable, out_decimals);
519    let withdrawable = to_adjusted_amount(withdrawable, out_decimals);
520
521    // Check if all reserves are greater than 0
522    let col_pool_enabled = col_reserves.token0_real_reserves > U256::ZERO &&
523        col_reserves.token1_real_reserves > U256::ZERO &&
524        col_reserves.token0_imaginary_reserves > U256::ZERO &&
525        col_reserves.token1_imaginary_reserves > U256::ZERO;
526
527    let debt_pool_enabled = debt_reserves.token0_real_reserves > U256::ZERO &&
528        debt_reserves.token1_real_reserves > U256::ZERO &&
529        debt_reserves.token0_imaginary_reserves > U256::ZERO &&
530        debt_reserves.token1_imaginary_reserves > U256::ZERO;
531
532    if !col_pool_enabled && !debt_pool_enabled {
533        return Err(SwapError::NoPoolsEnabled);
534    }
535
536    let a = if col_pool_enabled && debt_pool_enabled {
537        swap_routing_in(
538            amount_to_swap,
539            col_i_reserve_out,
540            col_i_reserve_in,
541            debt_i_reserve_out,
542            debt_i_reserve_in,
543        )
544    } else if debt_pool_enabled {
545        U256::MAX // Route from debt pool
546    } else if col_pool_enabled {
547        amount_to_swap + U256::ONE // Route from collateral pool
548    } else {
549        return Err(SwapError::NoPoolsEnabled);
550    };
551
552    let (amount_in_collateral, amount_out_collateral, amount_in_debt, amount_out_debt) = if a ==
553        U256::ZERO ||
554        a == U256::MAX
555    {
556        // Entire trade routes through debt pool
557        let amount_out_debt = get_amount_out(amount_to_swap, debt_i_reserve_in, debt_i_reserve_out);
558        (U256::ZERO, U256::ZERO, amount_to_swap, amount_out_debt)
559    } else if a >= amount_to_swap {
560        // Entire trade routes through collateral pool
561        let amount_out_collateral =
562            get_amount_out(amount_to_swap, col_i_reserve_in, col_i_reserve_out);
563        (amount_to_swap, amount_out_collateral, U256::ZERO, U256::ZERO)
564    } else {
565        // Trade routes through both pools
566        let amount_in_debt = amount_to_swap - a;
567        let amount_out_debt = get_amount_out(amount_in_debt, debt_i_reserve_in, debt_i_reserve_out);
568        let amount_out_collateral = get_amount_out(a, col_i_reserve_in, col_i_reserve_out);
569        (a, amount_out_collateral, amount_in_debt, amount_out_debt)
570    };
571
572    if amount_out_debt > debt_reserve_out {
573        return Err(SwapError::InsufficientReserve);
574    }
575
576    if amount_out_collateral > col_reserve_out {
577        return Err(SwapError::InsufficientReserve);
578    }
579
580    if amount_out_debt > borrowable {
581        return Err(SwapError::InsufficientBorrowable);
582    }
583
584    if amount_out_collateral > withdrawable {
585        return Err(SwapError::InsufficientWithdrawable);
586    }
587
588    if amount_in_collateral > U256::ZERO {
589        let reserves_ratio_valid = if swap0_to_1 {
590            verify_token1_reserves(
591                col_reserve_in + amount_in_collateral,
592                col_reserve_out - amount_out_collateral,
593                center_price,
594            )
595        } else {
596            verify_token0_reserves(
597                col_reserve_out - amount_out_collateral,
598                col_reserve_in + amount_in_collateral,
599                center_price,
600            )
601        };
602        if !reserves_ratio_valid {
603            return Err(SwapError::VerifyReservesRatiosInvalid);
604        }
605    }
606
607    if amount_in_debt > U256::ZERO {
608        let reserves_ratio_valid = if swap0_to_1 {
609            verify_token1_reserves(
610                debt_reserve_in + amount_in_debt,
611                debt_reserve_out - amount_out_debt,
612                center_price,
613            )
614        } else {
615            verify_token0_reserves(
616                debt_reserve_out - amount_out_debt,
617                debt_reserve_in + amount_in_debt,
618                center_price,
619            )
620        };
621        if !reserves_ratio_valid {
622            return Err(SwapError::VerifyReservesRatiosInvalid);
623        }
624    }
625
626    let (old_price, new_price) = if amount_in_collateral > amount_in_debt {
627        if swap0_to_1 {
628            (
629                col_i_reserve_out * constant::B_I1E27 / col_i_reserve_in,
630                (col_i_reserve_out - amount_out_collateral) * constant::B_I1E27 /
631                    (col_i_reserve_in + amount_in_collateral),
632            )
633        } else {
634            (
635                col_i_reserve_in * constant::B_I1E27 / col_i_reserve_out,
636                (col_i_reserve_in + amount_in_collateral) * constant::B_I1E27 /
637                    (col_i_reserve_out - amount_out_collateral),
638            )
639        }
640    } else if swap0_to_1 {
641        (
642            debt_i_reserve_out * constant::B_I1E27 / debt_i_reserve_in,
643            (debt_i_reserve_out - amount_out_debt) * constant::B_I1E27 /
644                (debt_i_reserve_in + amount_in_debt),
645        )
646    } else {
647        (
648            debt_i_reserve_in * constant::B_I1E27 / debt_i_reserve_out,
649            (debt_i_reserve_in + amount_in_debt) * constant::B_I1E27 /
650                (debt_i_reserve_out - amount_out_debt),
651        )
652    };
653
654    let price_diff = old_price.abs_diff(new_price);
655    let max_price_diff = old_price * constant::MAX_PRICE_DIFF / constant::TWO_DECIMALS;
656
657    if price_diff > max_price_diff {
658        return Err(SwapError::InsufficientMaxPrice);
659    }
660
661    if amount_in_collateral > U256::ZERO {
662        update_collateral_reserves_and_limits(
663            swap0_to_1,
664            amount_in_collateral,
665            amount_out_collateral,
666            col_reserves,
667            current_limits,
668            out_decimals,
669        );
670    }
671
672    if amount_in_debt > U256::ZERO {
673        update_debt_reserves_and_limits(
674            swap0_to_1,
675            amount_in_debt,
676            amount_out_debt,
677            debt_reserves,
678            current_limits,
679            out_decimals,
680        );
681    }
682
683    Ok(from_adjusted_amount(amount_out_collateral + amount_out_debt, out_decimals))
684}
685
686#[allow(clippy::too_many_arguments, dead_code)]
687fn swap_out_adjusted(
688    swap0_to_1: bool,
689    amount_to_receive: U256,
690    col_reserves: &mut CollateralReserves,
691    debt_reserves: &mut DebtReserves,
692    in_decimals: i64,
693    out_decimals: i64,
694    current_limits: &mut DexLimits,
695    center_price: U256,
696    sync_time: u64,
697) -> Result<U256, SwapError> {
698    let (
699        col_reserve_in,
700        col_reserve_out,
701        col_i_reserve_in,
702        col_i_reserve_out,
703        debt_reserve_in,
704        debt_reserve_out,
705        debt_i_reserve_in,
706        debt_i_reserve_out,
707        borrowable,
708        withdrawable,
709    ) = if swap0_to_1 {
710        (
711            col_reserves.token0_real_reserves,
712            col_reserves.token1_real_reserves,
713            col_reserves.token0_imaginary_reserves,
714            col_reserves.token1_imaginary_reserves,
715            debt_reserves.token0_real_reserves,
716            debt_reserves.token1_real_reserves,
717            debt_reserves.token0_imaginary_reserves,
718            debt_reserves.token1_imaginary_reserves,
719            get_expanded_limit(sync_time, &current_limits.borrowable_token1),
720            get_expanded_limit(sync_time, &current_limits.withdrawable_token1),
721        )
722    } else {
723        (
724            col_reserves.token1_real_reserves,
725            col_reserves.token0_real_reserves,
726            col_reserves.token1_imaginary_reserves,
727            col_reserves.token0_imaginary_reserves,
728            debt_reserves.token1_real_reserves,
729            debt_reserves.token0_real_reserves,
730            debt_reserves.token1_imaginary_reserves,
731            debt_reserves.token0_imaginary_reserves,
732            get_expanded_limit(sync_time, &current_limits.borrowable_token0),
733            get_expanded_limit(sync_time, &current_limits.withdrawable_token0),
734        )
735    };
736
737    let borrowable = to_adjusted_amount(borrowable, out_decimals);
738    let withdrawable = to_adjusted_amount(withdrawable, out_decimals);
739
740    let col_pool_enabled = col_reserves.token0_real_reserves > U256::ZERO &&
741        col_reserves.token1_real_reserves > U256::ZERO &&
742        col_reserves.token0_imaginary_reserves > U256::ZERO &&
743        col_reserves.token1_imaginary_reserves > U256::ZERO;
744
745    let debt_pool_enabled = debt_reserves.token0_real_reserves > U256::ZERO &&
746        debt_reserves.token1_real_reserves > U256::ZERO &&
747        debt_reserves.token0_imaginary_reserves > U256::ZERO &&
748        debt_reserves.token1_imaginary_reserves > U256::ZERO;
749
750    if !col_pool_enabled && !debt_pool_enabled {
751        return Err(SwapError::NoPoolsEnabled);
752    }
753
754    let a = if col_pool_enabled && debt_pool_enabled {
755        swap_routing_out(
756            amount_to_receive,
757            col_i_reserve_out,
758            col_i_reserve_in,
759            debt_i_reserve_out,
760            debt_i_reserve_in,
761        )
762    } else if debt_pool_enabled {
763        U256::MAX
764    } else if col_pool_enabled {
765        amount_to_receive + U256::ONE
766    } else {
767        return Err(SwapError::NoPoolsEnabled);
768    };
769
770    let mut trigger_update_debt_reserves = false;
771    let mut trigger_update_col_reserves = false;
772
773    let (amount_in_collateral, amount_out_collateral, amount_in_debt, amount_out_debt) =
774        if a == U256::ZERO || a == U256::MAX {
775            let amount_in_debt =
776                get_amount_in(amount_to_receive, debt_i_reserve_in, debt_i_reserve_out);
777            if amount_to_receive > debt_reserve_out {
778                return Err(SwapError::InsufficientReserve);
779            }
780
781            trigger_update_debt_reserves = true;
782            (U256::ZERO, U256::ZERO, amount_in_debt, amount_to_receive)
783        } else if a >= amount_to_receive {
784            let amount_in_collateral =
785                get_amount_in(amount_to_receive, col_i_reserve_in, col_i_reserve_out);
786
787            if amount_to_receive > col_reserve_out {
788                return Err(SwapError::InsufficientReserve);
789            }
790
791            trigger_update_col_reserves = true;
792            (amount_in_collateral, amount_to_receive, U256::ZERO, U256::ZERO)
793        } else {
794            let amount_out_collateral = a;
795            let amount_in_collateral =
796                get_amount_in(amount_out_collateral, col_i_reserve_in, col_i_reserve_out);
797            let amount_out_debt = amount_to_receive - amount_out_collateral;
798            let amount_in_debt =
799                get_amount_in(amount_out_debt, debt_i_reserve_in, debt_i_reserve_out);
800
801            if amount_out_debt > debt_reserve_out || amount_out_collateral > col_reserve_out {
802                return Err(SwapError::InsufficientReserve);
803            }
804
805            (amount_in_collateral, amount_out_collateral, amount_in_debt, amount_out_debt)
806        };
807
808    if amount_in_debt > borrowable {
809        return Err(SwapError::InsufficientBorrowable);
810    }
811
812    if amount_in_collateral > withdrawable {
813        return Err(SwapError::InsufficientWithdrawable);
814    }
815
816    if amount_in_collateral > U256::ZERO {
817        let reserves_ratio_valid = if swap0_to_1 {
818            verify_token1_reserves(
819                col_reserve_in + amount_in_collateral,
820                col_reserve_out - amount_out_collateral,
821                center_price,
822            )
823        } else {
824            verify_token0_reserves(
825                col_reserve_out - amount_out_collateral,
826                col_reserve_in + amount_in_collateral,
827                center_price,
828            )
829        };
830        if !reserves_ratio_valid {
831            return Err(SwapError::VerifyReservesRatiosInvalid);
832        }
833    }
834
835    if amount_in_debt > U256::ZERO {
836        let reserves_ratio_valid = if swap0_to_1 {
837            verify_token1_reserves(
838                debt_reserve_in + amount_in_debt,
839                debt_reserve_out - amount_out_debt,
840                center_price,
841            )
842        } else {
843            verify_token0_reserves(
844                debt_reserve_out - amount_out_debt,
845                debt_reserve_in + amount_in_debt,
846                center_price,
847            )
848        };
849        if !reserves_ratio_valid {
850            return Err(SwapError::VerifyReservesRatiosInvalid);
851        }
852    }
853
854    let (old_price, new_price) = if amount_in_collateral > amount_in_debt {
855        if swap0_to_1 {
856            (
857                col_i_reserve_out * constant::B_I1E27 / col_i_reserve_in,
858                (col_i_reserve_out - amount_out_collateral) * constant::B_I1E27 /
859                    (col_i_reserve_in + amount_in_collateral),
860            )
861        } else {
862            (
863                col_i_reserve_in * constant::B_I1E27 / col_i_reserve_out,
864                (col_i_reserve_in + amount_in_collateral) * constant::B_I1E27 /
865                    (col_i_reserve_out - amount_out_collateral),
866            )
867        }
868    } else if swap0_to_1 {
869        (
870            debt_i_reserve_out * constant::B_I1E27 / debt_i_reserve_in,
871            (debt_i_reserve_out - amount_out_debt) * constant::B_I1E27 /
872                (debt_i_reserve_in + amount_in_debt),
873        )
874    } else {
875        (
876            debt_i_reserve_in * constant::B_I1E27 / debt_i_reserve_out,
877            (debt_i_reserve_in + amount_in_debt) * constant::B_I1E27 /
878                (debt_i_reserve_out - amount_out_debt),
879        )
880    };
881
882    let price_diff = old_price.abs_diff(new_price);
883    let max_price_diff = old_price * constant::MAX_PRICE_DIFF / constant::TWO_DECIMALS;
884
885    if price_diff > max_price_diff {
886        return Err(SwapError::InsufficientMaxPrice);
887    }
888
889    if trigger_update_col_reserves {
890        update_collateral_reserves_and_limits(
891            swap0_to_1,
892            amount_in_collateral,
893            amount_out_collateral,
894            col_reserves,
895            current_limits,
896            out_decimals,
897        );
898    }
899
900    if trigger_update_debt_reserves {
901        update_debt_reserves_and_limits(
902            swap0_to_1,
903            amount_in_debt,
904            amount_out_debt,
905            debt_reserves,
906            current_limits,
907            out_decimals,
908        );
909    }
910
911    Ok(from_adjusted_amount(amount_in_collateral + amount_in_debt, in_decimals))
912}
913
914/// Calculates how much of a swap should go through the collateral pool.
915///
916/// # Parameters
917/// - `t`: Total amount in.
918/// - `x`: Imaginary reserves of token out of collateral.
919/// - `y`: Imaginary reserves of token in of collateral.
920/// - `x2`: Imaginary reserves of token out of debt.
921/// - `y2`: Imaginary reserves of token in of debt.
922///
923/// # Returns
924/// - `a`: How much of the swap should go through the collateral pool. The remaining amount will go
925///   through the debt pool.
926///
927/// # Notes
928/// - If `a < 0`, the entire trade routes through the debt pool and debt pool arbitrages with
929///   collateral pool.
930/// - If `a > t`, the entire trade routes through the collateral pool and collateral pool arbitrages
931///   with debt pool.
932/// - If `a > 0 && a < t`, the swap will route through both pools.
933fn swap_routing_in(t: U256, x: U256, y: U256, x2: U256, y2: U256) -> U256 {
934    let xy_root = (x * y * constant::B_I1E18).root(2);
935    let x2y2_root = (x2 * y2 * constant::B_I1E18).root(2);
936
937    let numerator = y2 * xy_root + t * xy_root - y * x2y2_root;
938    let denominator = xy_root + x2y2_root;
939    numerator / denominator
940}
941
942/// Calculates how much of a swap should go through the collateral pool for an output amount.
943///
944/// # Notes
945/// - If `a < 0` → entire trade goes through debt pool.
946/// - If `a > t` → entire trade goes through collateral pool.
947/// - If `0 < a < t` → swap routes through both pools.
948#[allow(dead_code)]
949fn swap_routing_out(t: U256, x: U256, y: U256, x2: U256, y2: U256) -> U256 {
950    let xy_root = (x * y * constant::B_I1E18).root(2);
951    let x2y2_root = (x2 * y2 * constant::B_I1E18).root(2);
952
953    let numerator = t * xy_root + y * x2y2_root - y2 * xy_root;
954    let denominator = xy_root + x2y2_root;
955
956    numerator / denominator
957}
958
959fn get_amount_out(amount_in: U256, i_reserve_in: U256, i_reserve_out: U256) -> U256 {
960    amount_in * i_reserve_out / (i_reserve_in + amount_in)
961}
962
963/// Given an output amount of asset and reserves, returns the input amount of the other asset.
964///
965/// Formula: (amount_out * iReserveIn) / (iReserveOut - amount_out)
966#[allow(dead_code)]
967fn get_amount_in(amount_out: U256, i_reserve_in: U256, i_reserve_out: U256) -> U256 {
968    amount_out * i_reserve_in / (i_reserve_out - amount_out)
969}
970
971fn to_adjusted_amount(amount: U256, decimals: i64) -> U256 {
972    let diff = decimals - constant::DEX_AMOUNT_DECIMALS;
973    if diff == 0 {
974        amount
975    } else if diff > 0 {
976        amount / ten_pow(diff)
977    } else {
978        amount * ten_pow(-diff)
979    }
980}
981
982/// Converts an adjusted amount to the original precision by compensating for decimal differences.
983///
984/// # Arguments
985/// * `adjusted_amount` - The amount adjusted to DexAmountsDecimals.
986/// * `decimals` - The original token decimals.
987/// * `dex_amounts_decimals` - The reference decimals used by DEX amounts.
988///
989/// # Returns
990/// * The amount scaled back to the original decimals.
991fn from_adjusted_amount(adjusted_amount: U256, decimals: i64) -> U256 {
992    let diff = decimals - constant::DEX_AMOUNT_DECIMALS;
993
994    if diff == 0 {
995        adjusted_amount
996    } else if diff < 0 {
997        // Divide by 10^(-diff)
998        let divisor = ten_pow(-diff);
999        adjusted_amount / divisor
1000    } else {
1001        // Multiply by 10^(diff)
1002        let multiplier = ten_pow(diff);
1003        adjusted_amount * multiplier
1004    }
1005}
1006
1007fn ten_pow(v: i64) -> U256 {
1008    U256::from(10u64).pow(U256::from((v) as u64))
1009}
1010
1011/// Checks if token0 reserves are sufficient compared to token1 reserves.
1012///
1013/// This prevents reserve imbalance and ensures price calculations remain stable and precise.
1014///
1015/// # Arguments
1016/// * `token0_reserves` - Reserves of token0.
1017/// * `token1_reserves` - Reserves of token1.
1018/// * `price` - Current price used in the reserve validation.
1019///
1020/// # Returns
1021/// Returns `false` if token0 reserves are too low, `true` otherwise.
1022///
1023/// # Formula
1024/// ```text
1025/// token0_reserves >= (token1_reserves * 1e27) / (price * MIN_SWAP_LIQUIDITY)
1026/// ```
1027fn verify_token0_reserves(token0_reserves: U256, token1_reserves: U256, price: U256) -> bool {
1028    let numerator = token1_reserves.saturating_mul(constant::B_I1E27);
1029    let denominator = price.saturating_mul(constant::MIN_SWAP_LIQUIDITY);
1030    token0_reserves >=
1031        numerator
1032            .checked_div(denominator)
1033            .unwrap_or(U256::ZERO)
1034}
1035
1036/// Checks if token1 reserves are sufficient compared to token0 reserves.
1037///
1038/// This prevents reserve imbalance and ensures price calculations remain stable and precise.
1039///
1040/// # Arguments
1041/// * `token0_reserves` - Reserves of token0.
1042/// * `token1_reserves` - Reserves of token1.
1043/// * `price` - Current price used in the reserve validation.
1044///
1045/// # Returns
1046/// `false` if token1 reserves are too low, `true` otherwise.
1047///
1048/// # Formula
1049/// ```text
1050/// token1_reserves >= (token0_reserves * price) / (1e27 * MIN_SWAP_LIQUIDITY)
1051/// ```
1052fn verify_token1_reserves(token0_reserves: U256, token1_reserves: U256, price: U256) -> bool {
1053    let numerator = token0_reserves.saturating_mul(price);
1054    let denominator = constant::B_I1E27.saturating_mul(constant::MIN_SWAP_LIQUIDITY);
1055    token1_reserves >= numerator.div_euclid(&denominator)
1056}
1057
1058/// Calculates the currently available swappable amount for a token limit,
1059/// considering how much it has expanded since the last synchronization.
1060///
1061/// This models gradual limit recovery over time.
1062///
1063/// # Arguments
1064/// * `sync_time` — UNIX timestamp (in seconds) of the last synchronization.
1065/// * `limit` — The token limit definition.
1066///
1067/// # Returns
1068/// Returns the currently effective limit as a `U256`.
1069fn get_expanded_limit(sync_time: u64, limit: &TokenLimit) -> U256 {
1070    let current_time = SystemTime::now()
1071        .duration_since(UNIX_EPOCH)
1072        .expect("system time before UNIX_EPOCH")
1073        .as_secs();
1074
1075    let elapsed_time = current_time.saturating_sub(sync_time);
1076    let elapsed = U256::from(elapsed_time);
1077
1078    if elapsed_time < 10 {
1079        // If almost no time has elapsed, return available amount
1080        return limit.available;
1081    }
1082
1083    if elapsed >= limit.expand_duration {
1084        // If full duration has passed, return max amount
1085        return limit.expands_to;
1086    }
1087
1088    // Linear interpolation:
1089    // expanded = available + (expands_to - available) * elapsed / expand_duration
1090    let delta = limit
1091        .expands_to
1092        .saturating_sub(limit.available);
1093    limit
1094        .available
1095        .saturating_add(delta.saturating_mul(elapsed) / limit.expand_duration)
1096}
1097
1098/// Returns updated copies of `CollateralReserves` and `DexLimits` based on swap direction.
1099///
1100/// # Note
1101/// Updates reserves and limits in-place.
1102fn update_collateral_reserves_and_limits(
1103    swap0_to_1: bool,
1104    amount_in: U256,
1105    amount_out: U256,
1106    col_reserves: &mut CollateralReserves,
1107    limits: &mut DexLimits,
1108    out_decimals: i64,
1109) {
1110    let unadjusted_amount_out = from_adjusted_amount(amount_out, out_decimals);
1111
1112    if swap0_to_1 {
1113        // token0 → token1 swap
1114        col_reserves.token0_real_reserves = col_reserves
1115            .token0_real_reserves
1116            .saturating_add(amount_in);
1117        col_reserves.token0_imaginary_reserves = col_reserves
1118            .token0_imaginary_reserves
1119            .saturating_add(amount_in);
1120        col_reserves.token1_real_reserves = col_reserves
1121            .token1_real_reserves
1122            .saturating_sub(amount_out);
1123        col_reserves.token1_imaginary_reserves = col_reserves
1124            .token1_imaginary_reserves
1125            .saturating_sub(amount_out);
1126
1127        limits.withdrawable_token1.available = limits
1128            .withdrawable_token1
1129            .available
1130            .saturating_sub(unadjusted_amount_out);
1131        limits.withdrawable_token1.expands_to = limits
1132            .withdrawable_token1
1133            .expands_to
1134            .saturating_sub(unadjusted_amount_out);
1135    } else {
1136        // token1 → token0 swap
1137        col_reserves.token0_real_reserves = col_reserves
1138            .token0_real_reserves
1139            .saturating_sub(amount_out);
1140        col_reserves.token0_imaginary_reserves = col_reserves
1141            .token0_imaginary_reserves
1142            .saturating_sub(amount_out);
1143        col_reserves.token1_real_reserves = col_reserves
1144            .token1_real_reserves
1145            .saturating_add(amount_in);
1146        col_reserves.token1_imaginary_reserves = col_reserves
1147            .token1_imaginary_reserves
1148            .saturating_add(amount_in);
1149
1150        limits.withdrawable_token0.available = limits
1151            .withdrawable_token0
1152            .available
1153            .saturating_sub(unadjusted_amount_out);
1154        limits.withdrawable_token0.expands_to = limits
1155            .withdrawable_token0
1156            .expands_to
1157            .saturating_sub(unadjusted_amount_out);
1158    }
1159}
1160
1161fn update_debt_reserves_and_limits(
1162    swap0_to1: bool,
1163    amount_in: U256,
1164    amount_out: U256,
1165    debt_reserves: &mut DebtReserves,
1166    limits: &mut DexLimits,
1167    out_decimals: i64,
1168) {
1169    let unadjusted_amount_out = from_adjusted_amount(amount_out, out_decimals);
1170
1171    if swap0_to1 {
1172        debt_reserves.token0_real_reserves += amount_in;
1173        debt_reserves.token0_imaginary_reserves += amount_in;
1174        debt_reserves.token1_real_reserves -= amount_out;
1175        debt_reserves.token1_imaginary_reserves -= amount_out;
1176
1177        // Comment Ref #4327563287
1178        // if expandTo for borrowable and withdrawable match, that means they are a hard limit like
1179        // liquidity layer balance or utilization limit. In that case, the available swap
1180        // amount should increase by `amountIn` but it's not guaranteed because the actual
1181        // borrow limit / withdrawal limit could be the limiting factor now, which could be even
1182        // only +1 bigger. So not updating in amount to avoid any revert. The same applies on all
1183        // other similar cases in the code below. Note a swap would anyway trigger an event,
1184        // so the proper limits will be fetched shortly after the swap.
1185        limits.borrowable_token1.available -= unadjusted_amount_out;
1186        limits.borrowable_token1.expands_to -= unadjusted_amount_out;
1187    } else {
1188        debt_reserves.token0_real_reserves -= amount_out;
1189        debt_reserves.token0_imaginary_reserves -= amount_out;
1190        debt_reserves.token1_real_reserves += amount_in;
1191        debt_reserves.token1_imaginary_reserves += amount_in;
1192
1193        limits.borrowable_token0.available -= unadjusted_amount_out;
1194        limits.borrowable_token0.expands_to -= unadjusted_amount_out;
1195    }
1196}
1197
1198fn get_max_reserves(
1199    decimals: u8,
1200    withdrawable_limit: &TokenLimit,
1201    borrowable_limit: &TokenLimit,
1202    real_col_reserves: &U256,
1203    real_debt_reserves: &U256,
1204) -> U256 {
1205    // Step 1: Determine maxLimitReserves
1206    let mut max_limit_reserves = borrowable_limit.expands_to;
1207
1208    if borrowable_limit.expands_to != withdrawable_limit.expands_to {
1209        max_limit_reserves += withdrawable_limit.expands_to;
1210    }
1211
1212    // Step 2: Calculate maxRealReserves
1213    let mut max_real_reserves = *real_col_reserves + *real_debt_reserves;
1214
1215    if decimals > constant::DEX_AMOUNT_DECIMALS as u8 {
1216        let diff = decimals as i64 - constant::DEX_AMOUNT_DECIMALS;
1217        max_real_reserves *= ten_pow(diff);
1218    } else if decimals < constant::DEX_AMOUNT_DECIMALS as u8 {
1219        let diff = constant::DEX_AMOUNT_DECIMALS - decimals as i64;
1220        max_real_reserves /= ten_pow(diff);
1221    }
1222
1223    // Step 3: Return the smaller of the two
1224    if max_real_reserves < max_limit_reserves {
1225        max_real_reserves
1226    } else {
1227        max_limit_reserves
1228    }
1229}
1230
1231#[cfg(test)]
1232mod test {
1233    use std::str::FromStr;
1234
1235    use alloy::primitives::I256;
1236    use anyhow::bail;
1237    use num_traits::Num;
1238    use tycho_common::models::Chain;
1239
1240    use super::*;
1241
1242    fn setup_fluid_pool(center_price: U256) -> (Token, Token, FluidV1) {
1243        let wsteth = Token::new(
1244            &Bytes::from_str("0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0").unwrap(),
1245            "wsteth",
1246            18,
1247            0,
1248            &[Some(20000)],
1249            Chain::Ethereum,
1250            100,
1251        );
1252        let eth = Token::new(
1253            &Bytes::from_str("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE").unwrap(),
1254            "ETH",
1255            18,
1256            0,
1257            &[Some(2000)],
1258            Chain::Ethereum,
1259            100,
1260        );
1261
1262        let pool = FluidV1::new(
1263            &Bytes::from_str("0x0B1a513ee24972DAEf112bC777a5610d4325C9e7").unwrap(),
1264            &wsteth,
1265            &eth,
1266            CollateralReserves {
1267                token0_real_reserves: U256::from_str("2169934539358").unwrap(),
1268                token1_real_reserves: U256::from_str("19563846299171").unwrap(),
1269                token0_imaginary_reserves: U256::from_str("62490032619260838").unwrap(),
1270                token1_imaginary_reserves: U256::from_str("73741038977020279").unwrap(),
1271            },
1272            DebtReserves {
1273                token0_real_reserves: U256::from_str("2169108220421").unwrap(),
1274                token1_real_reserves: U256::from_str("19572550738602").unwrap(),
1275                token0_imaginary_reserves: U256::from_str("62511862774117387").unwrap(),
1276                token1_imaginary_reserves: U256::from_str("73766803277429176").unwrap(),
1277            },
1278            limits_wide(),
1279            center_price,
1280            U256::from_str("100").unwrap(),
1281            SystemTime::now()
1282                .duration_since(UNIX_EPOCH)
1283                .unwrap()
1284                .as_secs() -
1285                10,
1286        );
1287        (wsteth, eth, pool)
1288    }
1289
1290    fn limits_wide() -> DexLimits {
1291        let limit_wide = U256::from_str("34242332879776515083099999").unwrap();
1292        DexLimits {
1293            withdrawable_token0: TokenLimit {
1294                available: limit_wide,
1295                expands_to: limit_wide,
1296                expand_duration: U256::ZERO,
1297            },
1298            withdrawable_token1: TokenLimit {
1299                available: limit_wide,
1300                expands_to: limit_wide,
1301                expand_duration: U256::from(22),
1302            },
1303            borrowable_token0: TokenLimit {
1304                available: limit_wide,
1305                expands_to: limit_wide,
1306                expand_duration: U256::ZERO,
1307            },
1308            borrowable_token1: TokenLimit {
1309                available: limit_wide,
1310                expands_to: limit_wide,
1311                expand_duration: U256::from(22),
1312            },
1313        }
1314    }
1315
1316    fn limits_tight() -> DexLimits {
1317        let limit_expand_tight = U256::from_str("711907234052361388866").unwrap();
1318
1319        DexLimits {
1320            withdrawable_token0: TokenLimit {
1321                available: U256::from_str("456740438880263").unwrap(),
1322                expands_to: limit_expand_tight,
1323                expand_duration: U256::from(600),
1324            },
1325            withdrawable_token1: TokenLimit {
1326                available: U256::from_str("825179383432029").unwrap(),
1327                expands_to: limit_expand_tight,
1328                expand_duration: U256::from(600),
1329            },
1330            borrowable_token0: TokenLimit {
1331                available: U256::from_str("941825058374170").unwrap(),
1332                expands_to: limit_expand_tight,
1333                expand_duration: U256::from(600),
1334            },
1335            borrowable_token1: TokenLimit {
1336                available: U256::from_str("941825058374170").unwrap(),
1337                expands_to: limit_expand_tight,
1338                expand_duration: U256::from(600),
1339            },
1340        }
1341    }
1342    fn new_col_reserves_one() -> CollateralReserves {
1343        CollateralReserves {
1344            token0_real_reserves: U256::from_str("20000000006000000").unwrap(),
1345            token1_real_reserves: U256::from_str("20000000000500000").unwrap(),
1346            token0_imaginary_reserves: U256::from_str("389736659726997981").unwrap(),
1347            token1_imaginary_reserves: U256::from_str("389736659619871949").unwrap(),
1348        }
1349    }
1350
1351    fn new_col_reserves_empty() -> CollateralReserves {
1352        CollateralReserves {
1353            token0_real_reserves: U256::ZERO,
1354            token1_real_reserves: U256::ZERO,
1355            token0_imaginary_reserves: U256::ZERO,
1356            token1_imaginary_reserves: U256::ZERO,
1357        }
1358    }
1359
1360    fn new_debt_reserves_empty() -> DebtReserves {
1361        DebtReserves {
1362            token0_real_reserves: U256::ZERO,
1363            token1_real_reserves: U256::ZERO,
1364            token0_imaginary_reserves: U256::ZERO,
1365            token1_imaginary_reserves: U256::ZERO,
1366        }
1367    }
1368
1369    fn new_debt_reserves_one() -> DebtReserves {
1370        DebtReserves {
1371            token0_real_reserves: U256::from_str("9486832995556050").unwrap(),
1372            token1_real_reserves: U256::from_str("9486832993079885").unwrap(),
1373            token0_imaginary_reserves: U256::from_str("184868330099560759").unwrap(),
1374            token1_imaginary_reserves: U256::from_str("184868330048879109").unwrap(),
1375        }
1376    }
1377
1378    pub fn get_approx_center_price_in(
1379        amount_to_swap: U256,
1380        swap0_to_1: bool,
1381        col_reserves: &CollateralReserves,
1382        debt_reserves: &DebtReserves,
1383    ) -> Result<U256, anyhow::Error> {
1384        let col_pool_enabled = !col_reserves
1385            .token0_real_reserves
1386            .is_zero() &&
1387            !col_reserves
1388                .token1_real_reserves
1389                .is_zero() &&
1390            !col_reserves
1391                .token0_imaginary_reserves
1392                .is_zero() &&
1393            !col_reserves
1394                .token1_imaginary_reserves
1395                .is_zero();
1396
1397        let debt_pool_enabled = !debt_reserves
1398            .token0_real_reserves
1399            .is_zero() &&
1400            !debt_reserves
1401                .token1_real_reserves
1402                .is_zero() &&
1403            !debt_reserves
1404                .token0_imaginary_reserves
1405                .is_zero() &&
1406            !debt_reserves
1407                .token1_imaginary_reserves
1408                .is_zero();
1409
1410        let (col_i_reserve_in, col_i_reserve_out, debt_i_reserve_in, debt_i_reserve_out) =
1411            if swap0_to_1 {
1412                (
1413                    col_reserves.token0_imaginary_reserves,
1414                    col_reserves.token1_imaginary_reserves,
1415                    debt_reserves.token0_imaginary_reserves,
1416                    debt_reserves.token1_imaginary_reserves,
1417                )
1418            } else {
1419                (
1420                    col_reserves.token1_imaginary_reserves,
1421                    col_reserves.token0_imaginary_reserves,
1422                    debt_reserves.token1_imaginary_reserves,
1423                    debt_reserves.token0_imaginary_reserves,
1424                )
1425            };
1426
1427        let a = if col_pool_enabled && debt_pool_enabled {
1428            swap_routing_in(
1429                amount_to_swap,
1430                col_i_reserve_out,
1431                col_i_reserve_in,
1432                debt_i_reserve_out,
1433                debt_i_reserve_in,
1434            )
1435        } else if debt_pool_enabled {
1436            U256::MAX // equivalent to -1 in Go logic for error handling
1437        } else if col_pool_enabled {
1438            amount_to_swap
1439                .checked_add(U256::from(1))
1440                .unwrap()
1441        } else {
1442            bail!("No pools are enabled");
1443        };
1444
1445        let (amount_in_collateral, amount_in_debt) = if a == U256::MAX || a == U256::ZERO {
1446            (U256::ZERO, amount_to_swap)
1447        } else if a >= amount_to_swap {
1448            (amount_to_swap, U256::ZERO)
1449        } else {
1450            (a, amount_to_swap - a)
1451        };
1452
1453        let price = if amount_in_collateral > amount_in_debt {
1454            if swap0_to_1 {
1455                col_i_reserve_out
1456                    .checked_mul(constant::B_I1E27)
1457                    .unwrap() /
1458                    col_i_reserve_in
1459            } else {
1460                col_i_reserve_in
1461                    .checked_mul(constant::B_I1E27)
1462                    .unwrap() /
1463                    col_i_reserve_out
1464            }
1465        } else if swap0_to_1 {
1466            debt_i_reserve_out
1467                .checked_mul(constant::B_I1E27)
1468                .unwrap() /
1469                debt_i_reserve_in
1470        } else {
1471            debt_i_reserve_in
1472                .checked_mul(constant::B_I1E27)
1473                .unwrap() /
1474                debt_i_reserve_out
1475        };
1476
1477        Ok(price)
1478    }
1479
1480    pub fn get_approx_center_price_out(
1481        amount_out: U256,
1482        swap0_to_1: bool,
1483        col_reserves: &CollateralReserves,
1484        debt_reserves: &DebtReserves,
1485    ) -> Result<U256, SwapError> {
1486        let col_pool_enabled = col_reserves.token0_real_reserves > U256::ZERO &&
1487            col_reserves.token1_real_reserves > U256::ZERO &&
1488            col_reserves.token0_imaginary_reserves > U256::ZERO &&
1489            col_reserves.token1_imaginary_reserves > U256::ZERO;
1490
1491        let debt_pool_enabled = debt_reserves.token0_real_reserves > U256::ZERO &&
1492            debt_reserves.token1_real_reserves > U256::ZERO &&
1493            debt_reserves.token0_imaginary_reserves > U256::ZERO &&
1494            debt_reserves.token1_imaginary_reserves > U256::ZERO;
1495
1496        let (col_i_reserve_in, col_i_reserve_out, debt_i_reserve_in, debt_i_reserve_out) =
1497            if swap0_to_1 {
1498                (
1499                    col_reserves.token0_imaginary_reserves,
1500                    col_reserves.token1_imaginary_reserves,
1501                    debt_reserves.token0_imaginary_reserves,
1502                    debt_reserves.token1_imaginary_reserves,
1503                )
1504            } else {
1505                (
1506                    col_reserves.token1_imaginary_reserves,
1507                    col_reserves.token0_imaginary_reserves,
1508                    debt_reserves.token1_imaginary_reserves,
1509                    debt_reserves.token0_imaginary_reserves,
1510                )
1511            };
1512
1513        let a = if col_pool_enabled && debt_pool_enabled {
1514            swap_routing_out(
1515                amount_out,
1516                col_i_reserve_in,
1517                col_i_reserve_out,
1518                debt_i_reserve_in,
1519                debt_i_reserve_out,
1520            )
1521        } else if debt_pool_enabled {
1522            U256::MAX // Special case: Route entirely from debt pool
1523        } else if col_pool_enabled {
1524            amount_out + U256::ONE // Special case: Route entirely from collateral pool
1525        } else {
1526            return Err(SwapError::NoPoolsEnabled);
1527        };
1528
1529        let mut amount_in_collateral = U256::ZERO;
1530        let mut amount_in_debt = U256::ZERO;
1531
1532        if a <= U256::ZERO {
1533            amount_in_debt = get_amount_in(amount_out, debt_i_reserve_in, debt_i_reserve_out);
1534        } else if a >= amount_out {
1535            amount_in_collateral = get_amount_in(amount_out, col_i_reserve_in, col_i_reserve_out);
1536        } else {
1537            amount_in_collateral = get_amount_in(a, col_i_reserve_in, col_i_reserve_out);
1538            amount_in_debt = get_amount_in(amount_out - a, debt_i_reserve_in, debt_i_reserve_out);
1539        }
1540
1541        let price = if amount_in_collateral > amount_in_debt {
1542            if swap0_to_1 {
1543                col_i_reserve_out * constant::B_I1E27 / col_i_reserve_in
1544            } else {
1545                col_i_reserve_in * constant::B_I1E27 / col_i_reserve_out
1546            }
1547        } else if swap0_to_1 {
1548            debt_i_reserve_out * constant::B_I1E27 / debt_i_reserve_in
1549        } else {
1550            debt_i_reserve_in * constant::B_I1E27 / debt_i_reserve_out
1551        };
1552
1553        Ok(price)
1554    }
1555
1556    #[test]
1557    fn test_calc_amount_out_zero2one() {
1558        let (wsteth, eth, pool) = setup_fluid_pool(U256::ONE);
1559        let cases = [
1560            ("1000000000000000000", "1179917402128000000"),
1561            ("500000000000000000", "589961060629000000"),
1562        ];
1563        for (amount_in_str, exp_out_str) in cases.into_iter() {
1564            let exp_out = BigUint::from_str_radix(exp_out_str, 10).unwrap();
1565            let res = pool
1566                .get_amount_out(BigUint::from_str_radix(amount_in_str, 10).unwrap(), &wsteth, &eth)
1567                .unwrap();
1568
1569            assert_eq!(res.amount, exp_out);
1570        }
1571    }
1572
1573    #[test]
1574    fn test_calc_amount_out_one2zero() {
1575        let center_price = U256::from_str("1200000000000000000000000000").unwrap();
1576        let (wsteth, eth, pool) = setup_fluid_pool(center_price);
1577        let cases = [("800000000000000000", "677868867152000000")];
1578        for (amount_in_str, exp_out_str) in cases.into_iter() {
1579            let exp_out = BigUint::from_str_radix(exp_out_str, 10).unwrap();
1580            let res = pool
1581                .get_amount_out(BigUint::from_str_radix(amount_in_str, 10).unwrap(), &eth, &wsteth)
1582                .unwrap();
1583
1584            assert_eq!(res.amount, exp_out);
1585        }
1586    }
1587
1588    #[test]
1589    fn test_amount_out_exceeds_reserve() {
1590        let (wsteth, eth, mut pool) = setup_fluid_pool(U256::ONE);
1591        // set custom reserves to trigger the error
1592        pool.pool_reserve0 = U256::from_str("18760613183894").unwrap();
1593        pool.pool_reserve1 = U256::from_str("22123580158026").unwrap();
1594        let amount_in = BigUint::from_str_radix("30000000000000000000", 10).unwrap(); // 300 wstETH
1595        let result = pool.get_amount_out(amount_in, &wsteth, &eth);
1596
1597        assert!(result.is_err(), "Expected an error for exceeding reserves");
1598        assert_eq!(
1599            result.unwrap_err().to_string(),
1600            SimulationError::from(SwapError::InsufficientReserve).to_string()
1601        );
1602    }
1603
1604    #[test]
1605    fn test_swap_in() {
1606        let sync_time = SystemTime::now()
1607            .duration_since(UNIX_EPOCH)
1608            .unwrap()
1609            .as_secs();
1610
1611        assert_swap_in_result(
1612            true,
1613            U256::from(1_000_000_000_000_000u128), // 1e15
1614            new_col_reserves_one(),
1615            new_debt_reserves_one(),
1616            "998262697204710000000",
1617            12,
1618            18,
1619            limits_wide(),
1620            sync_time - 10,
1621        );
1622
1623        assert_swap_in_result(
1624            true,
1625            U256::from(1_000_000_000_000_000u128),
1626            new_col_reserves_empty(),
1627            new_debt_reserves_one(),
1628            "994619847016724000000",
1629            12,
1630            18,
1631            limits_wide(),
1632            sync_time - 10,
1633        );
1634
1635        assert_swap_in_result(
1636            true,
1637            U256::from(1_000_000_000_000_000u128),
1638            new_col_reserves_one(),
1639            new_debt_reserves_empty(),
1640            "997440731289905000000",
1641            12,
1642            18,
1643            limits_wide(),
1644            sync_time - 10,
1645        );
1646
1647        assert_swap_in_result(
1648            false,
1649            U256::from(1_000_000_000_000_000u128),
1650            new_col_reserves_one(),
1651            new_debt_reserves_one(),
1652            "998262697752553000000",
1653            12,
1654            18,
1655            limits_wide(),
1656            sync_time - 10,
1657        );
1658
1659        assert_swap_in_result(
1660            false,
1661            U256::from(1_000_000_000_000_000u128),
1662            new_col_reserves_empty(),
1663            new_debt_reserves_one(),
1664            "994619847560607000000",
1665            12,
1666            18,
1667            limits_wide(),
1668            sync_time - 10,
1669        );
1670
1671        assert_swap_in_result(
1672            false,
1673            U256::from(1_000_000_000_000_000u128),
1674            new_col_reserves_one(),
1675            new_debt_reserves_empty(),
1676            "997440731837532000000",
1677            12,
1678            18,
1679            limits_wide(),
1680            sync_time - 10,
1681        );
1682    }
1683
1684    /// Asserts that a swap produces the expected output amount.
1685    ///
1686    /// # Arguments
1687    /// - `swap0_to_1`: Direction of the swap.
1688    /// - `amount_in`: Total amount in.
1689    /// - `col_reserves`: Collateral reserves.
1690    /// - `debt_reserves`: Debt reserves.
1691    /// - `expected_amount_out`: Expected output amount as a string.
1692    /// - `in_decimals`: Decimals for the input token.
1693    /// - `out_decimals`: Decimals for the output token.
1694    /// - `limits`: Dex limits.
1695    /// - `sync_time`: Timestamp for syncing.
1696    #[allow(clippy::too_many_arguments)]
1697    fn assert_swap_in_result(
1698        swap0_to_1: bool,
1699        amount_in: U256,
1700        mut col_reserves: CollateralReserves,
1701        mut debt_reserves: DebtReserves,
1702        expected_amount_out: &str,
1703        in_decimals: i64,
1704        out_decimals: i64,
1705        mut limits: DexLimits,
1706        sync_time: u64,
1707    ) {
1708        let price =
1709            get_approx_center_price_in(amount_in, swap0_to_1, &col_reserves, &debt_reserves)
1710                .expect("Failed to get approx center price");
1711
1712        let adjusted_amount_in = to_adjusted_amount(amount_in, in_decimals);
1713        let out_amt = swap_in_adjusted(
1714            swap0_to_1,
1715            adjusted_amount_in,
1716            &mut col_reserves,
1717            &mut debt_reserves,
1718            out_decimals,
1719            &mut limits,
1720            price,
1721            sync_time,
1722        )
1723        .expect("Failed to calculate swap in adjusted");
1724
1725        assert_eq!(expected_amount_out, out_amt.to_string(), "Amount out mismatch");
1726    }
1727
1728    #[allow(clippy::too_many_arguments)]
1729    fn assert_swap_out_result(
1730        swap0_to_1: bool,
1731        amount_out: U256,
1732        mut col_reserves: CollateralReserves,
1733        mut debt_reserves: DebtReserves,
1734        expected_amount_in: &str,
1735        in_decimals: i64,
1736        out_decimals: i64,
1737        mut limits: DexLimits,
1738        sync_time: i64,
1739    ) {
1740        let price =
1741            get_approx_center_price_out(amount_out, swap0_to_1, &col_reserves, &debt_reserves)
1742                .expect("failed to get approx center price");
1743
1744        let in_amt = swap_out_adjusted(
1745            swap0_to_1,
1746            to_adjusted_amount(amount_out, out_decimals),
1747            &mut col_reserves,
1748            &mut debt_reserves,
1749            in_decimals,
1750            out_decimals,
1751            &mut limits,
1752            price,
1753            sync_time as u64,
1754        )
1755        .expect("swap_out_adjusted failed");
1756
1757        assert_eq!(expected_amount_in, from_adjusted_amount(in_amt, in_decimals).to_string());
1758    }
1759
1760    #[test]
1761    fn test_swap_in_limits() {
1762        let sync_time = SystemTime::now()
1763            .duration_since(UNIX_EPOCH)
1764            .unwrap()
1765            .as_secs();
1766
1767        // when limits hit
1768        let price = get_approx_center_price_in(
1769            U256::from(1_000_000_000_000_000u128),
1770            true,
1771            &new_col_reserves_one(),
1772            &new_debt_reserves_one(),
1773        )
1774        .unwrap();
1775
1776        let res = swap_in_adjusted(
1777            true,
1778            U256::from(1_000_000_000_000_000u128),
1779            &mut new_col_reserves_one(),
1780            &mut new_debt_reserves_one(),
1781            18,
1782            &mut limits_tight(),
1783            price,
1784            sync_time - 10,
1785        );
1786
1787        assert_eq!(res.unwrap_err().to_string(), SwapError::InsufficientBorrowable.to_string());
1788
1789        // when expanded
1790        let price = get_approx_center_price_out(
1791            U256::from(1_000_000_000_000_000u128),
1792            true,
1793            &new_col_reserves_one(),
1794            &new_debt_reserves_one(),
1795        )
1796        .unwrap();
1797
1798        let out_amt = swap_in_adjusted(
1799            true,
1800            U256::from(1_000_000_000_000_000u128),
1801            &mut new_col_reserves_one(),
1802            &mut new_debt_reserves_one(),
1803            18,
1804            &mut limits_tight(),
1805            price,
1806            sync_time - 6000,
1807        )
1808        .unwrap();
1809
1810        assert_eq!(out_amt.to_string(), "998262697204710000000");
1811
1812        // when price diff hit
1813        let price = get_approx_center_price_out(
1814            U256::from(30_000_000_000_000_000u128),
1815            true,
1816            &new_col_reserves_one(),
1817            &new_debt_reserves_one(),
1818        )
1819        .unwrap();
1820
1821        let res = swap_in_adjusted(
1822            true,
1823            U256::from(30_000_000_000_000_000u128),
1824            &mut new_col_reserves_one(),
1825            &mut new_debt_reserves_one(),
1826            18,
1827            &mut limits_wide(),
1828            price,
1829            sync_time - 10,
1830        );
1831
1832        assert_eq!(res.unwrap_err().to_string(), SwapError::InsufficientMaxPrice.to_string());
1833
1834        // when reserves limit is hit
1835        let price = get_approx_center_price_out(
1836            U256::from(50_000_000_000_000_000u128),
1837            true,
1838            &new_col_reserves_one(),
1839            &new_debt_reserves_one(),
1840        )
1841        .unwrap();
1842
1843        let res = swap_in_adjusted(
1844            true,
1845            U256::from(50_000_000_000_000_000u128),
1846            &mut new_col_reserves_one(),
1847            &mut new_debt_reserves_one(),
1848            18,
1849            &mut limits_wide(),
1850            price,
1851            sync_time - 10,
1852        );
1853
1854        assert_eq!(res.unwrap_err().to_string(), SwapError::InsufficientReserve.to_string());
1855    }
1856
1857    #[test]
1858    fn test_swap_in_adjusted_compare_estimate_in() {
1859        let now = SystemTime::now()
1860            .duration_since(UNIX_EPOCH)
1861            .unwrap()
1862            .as_secs();
1863        let expected_amount_out = U256::from_str("1180035404724000000").unwrap();
1864        let mut col_reserves = CollateralReserves {
1865            token0_real_reserves: U256::from_str("2169934539358").unwrap(),
1866            token1_real_reserves: U256::from_str("19563846299171").unwrap(),
1867            token0_imaginary_reserves: U256::from_str("62490032619260838").unwrap(),
1868            token1_imaginary_reserves: U256::from_str("73741038977020279").unwrap(),
1869        };
1870        let mut debt_reserves = DebtReserves {
1871            token0_real_reserves: U256::from_str("2169108220421").unwrap(),
1872            token1_real_reserves: U256::from_str("19572550738602").unwrap(),
1873            token0_imaginary_reserves: U256::from_str("62511862774117387").unwrap(),
1874            token1_imaginary_reserves: U256::from_str("73766803277429176").unwrap(),
1875        };
1876        let amount_in = U256::from(1000000000000u128); // 1e12
1877        let price = get_approx_center_price_in(amount_in, true, &col_reserves, &debt_reserves)
1878            .expect("Failed to get approximate center price");
1879
1880        let out_amt = swap_in_adjusted(
1881            true,
1882            amount_in,
1883            &mut col_reserves,
1884            &mut debt_reserves,
1885            18,
1886            &mut limits_wide(),
1887            price,
1888            now - 10,
1889        )
1890        .expect("Failed to swap in adjusted");
1891
1892        assert_eq!(expected_amount_out, out_amt);
1893    }
1894
1895    #[test]
1896    fn test_swap_in_debt_empty() {
1897        let now = SystemTime::now()
1898            .duration_since(UNIX_EPOCH)
1899            .unwrap()
1900            .as_secs();
1901
1902        assert_swap_in_result(
1903            true,
1904            U256::from_str("1000000000000000").unwrap(),
1905            new_col_reserves_empty(),
1906            new_debt_reserves_one(),
1907            "994619847016724",
1908            12,
1909            12,
1910            limits_wide(),
1911            now - 10,
1912        );
1913
1914        assert_swap_in_result(
1915            false,
1916            U256::from_str("1000000000000000").unwrap(),
1917            new_col_reserves_empty(),
1918            new_debt_reserves_one(),
1919            "994619847560607",
1920            12,
1921            12,
1922            limits_wide(),
1923            now - 10,
1924        )
1925    }
1926
1927    #[test]
1928    fn test_swap_in_col_empty() {
1929        let now = SystemTime::now()
1930            .duration_since(UNIX_EPOCH)
1931            .unwrap()
1932            .as_secs();
1933
1934        assert_swap_in_result(
1935            true,
1936            U256::from_str("1000000000000000").unwrap(),
1937            new_col_reserves_one(),
1938            new_debt_reserves_empty(),
1939            "997440731289905",
1940            12,
1941            12,
1942            limits_wide(),
1943            now - 10,
1944        );
1945
1946        assert_swap_in_result(
1947            false,
1948            U256::from_str("1000000000000000").unwrap(),
1949            new_col_reserves_one(),
1950            new_debt_reserves_empty(),
1951            "997440731837532",
1952            12,
1953            12,
1954            limits_wide(),
1955            now - 10,
1956        )
1957    }
1958
1959    #[test]
1960    fn test_swap_out() {
1961        let sync_time = (SystemTime::now()
1962            .duration_since(UNIX_EPOCH)
1963            .unwrap()
1964            .as_secs() as i64) -
1965            10;
1966
1967        assert_swap_out_result(
1968            true,
1969            U256::from(1_000_000_000_000_000u64),
1970            new_col_reserves_one(),
1971            new_debt_reserves_one(),
1972            "1001743360284199",
1973            12,
1974            12,
1975            limits_wide(),
1976            sync_time,
1977        );
1978
1979        assert_swap_out_result(
1980            true,
1981            U256::from(1_000_000_000_000_000u64),
1982            new_col_reserves_empty(),
1983            new_debt_reserves_one(),
1984            "1005438674786548",
1985            12,
1986            12,
1987            limits_wide(),
1988            sync_time,
1989        );
1990
1991        assert_swap_out_result(
1992            true,
1993            U256::from(1_000_000_000_000_000u64),
1994            new_col_reserves_one(),
1995            new_debt_reserves_empty(),
1996            "1002572435818386",
1997            12,
1998            12,
1999            limits_wide(),
2000            sync_time,
2001        );
2002
2003        assert_swap_out_result(
2004            false,
2005            U256::from(1_000_000_000_000_000u64),
2006            new_col_reserves_one(),
2007            new_debt_reserves_one(),
2008            "1001743359733488",
2009            12,
2010            12,
2011            limits_wide(),
2012            sync_time,
2013        );
2014
2015        assert_swap_out_result(
2016            false,
2017            U256::from(1_000_000_000_000_000u64),
2018            new_col_reserves_empty(),
2019            new_debt_reserves_one(),
2020            "1005438674233767",
2021            12,
2022            12,
2023            limits_wide(),
2024            sync_time,
2025        );
2026
2027        assert_swap_out_result(
2028            false,
2029            U256::from(1_000_000_000_000_000u64),
2030            new_col_reserves_one(),
2031            new_debt_reserves_empty(),
2032            "1002572435266527",
2033            12,
2034            12,
2035            limits_wide(),
2036            sync_time,
2037        );
2038    }
2039
2040    #[test]
2041    fn test_swap_out_limits() {
2042        let sync_time_recent = (SystemTime::now()
2043            .duration_since(UNIX_EPOCH)
2044            .unwrap()
2045            .as_secs()) -
2046            10;
2047
2048        let sync_time_expanded = sync_time_recent - 5990; // ~6000 seconds earlier
2049
2050        // --- when limits hit ---
2051        let price = get_approx_center_price_out(
2052            U256::from(1_000_000_000_000_000u64),
2053            true,
2054            &new_col_reserves_one(),
2055            &new_debt_reserves_one(),
2056        )
2057        .unwrap();
2058
2059        let result = swap_out_adjusted(
2060            true,
2061            U256::from(1_000_000_000_000_000u64),
2062            &mut new_col_reserves_one(),
2063            &mut new_debt_reserves_one(),
2064            12,
2065            18,
2066            &mut limits_tight(),
2067            price,
2068            sync_time_recent,
2069        );
2070
2071        assert!(matches!(result, Err(SwapError::InsufficientBorrowable)));
2072
2073        // --- when expanded ---
2074        let price = get_approx_center_price_out(
2075            U256::from(1_000_000_000_000_000u64),
2076            true,
2077            &new_col_reserves_one(),
2078            &new_debt_reserves_one(),
2079        )
2080        .unwrap();
2081
2082        let result = swap_out_adjusted(
2083            true,
2084            U256::from(1_000_000_000_000_000u64),
2085            &mut new_col_reserves_one(),
2086            &mut new_debt_reserves_one(),
2087            12,
2088            18,
2089            &mut limits_tight(),
2090            price,
2091            sync_time_expanded,
2092        )
2093        .unwrap();
2094
2095        assert_eq!(from_adjusted_amount(result, 12).to_string(), "1001743360284199");
2096
2097        // --- when price diff hit ---
2098        let price = get_approx_center_price_out(
2099            U256::from(20_000_000_000_000_000u64),
2100            true,
2101            &new_col_reserves_one(),
2102            &new_debt_reserves_one(),
2103        )
2104        .unwrap();
2105
2106        let result = swap_out_adjusted(
2107            true,
2108            U256::from(20_000_000_000_000_000u64),
2109            &mut new_col_reserves_one(),
2110            &mut new_debt_reserves_one(),
2111            12,
2112            18,
2113            &mut limits_wide(),
2114            price,
2115            sync_time_recent,
2116        );
2117
2118        assert!(matches!(result, Err(SwapError::InsufficientMaxPrice)));
2119
2120        // --- when reserves limit is hit ---
2121        let price = get_approx_center_price_out(
2122            U256::from(30_000_000_000_000_000u64),
2123            true,
2124            &new_col_reserves_one(),
2125            &new_debt_reserves_one(),
2126        )
2127        .unwrap();
2128
2129        let result = swap_out_adjusted(
2130            true,
2131            U256::from(30_000_000_000_000_000u64),
2132            &mut new_col_reserves_one(),
2133            &mut new_debt_reserves_one(),
2134            12,
2135            18,
2136            &mut limits_wide(),
2137            price,
2138            sync_time_recent,
2139        );
2140
2141        assert!(matches!(result, Err(SwapError::InsufficientReserve)));
2142    }
2143
2144    #[test]
2145    fn test_swap_out_empty_debt() {
2146        let sync_time = (SystemTime::now()
2147            .duration_since(UNIX_EPOCH)
2148            .unwrap()
2149            .as_secs() as i64) -
2150            10;
2151
2152        // swap0To1 = true
2153        assert_swap_out_result(
2154            true,
2155            U256::from(994_619_847_016_724u64),
2156            new_col_reserves_empty(),
2157            new_debt_reserves_one(),
2158            "999999999999999",
2159            12,
2160            12,
2161            limits_wide(),
2162            sync_time,
2163        );
2164
2165        // swap0To1 = false
2166        assert_swap_out_result(
2167            false,
2168            U256::from(994_619_847_560_607u64),
2169            new_col_reserves_empty(),
2170            new_debt_reserves_one(),
2171            "999999999999999",
2172            12,
2173            12,
2174            limits_wide(),
2175            sync_time,
2176        );
2177    }
2178
2179    #[test]
2180    fn test_swap_out_empty_collateral() {
2181        let sync_time = (SystemTime::now()
2182            .duration_since(UNIX_EPOCH)
2183            .unwrap()
2184            .as_secs() as i64) -
2185            10;
2186
2187        // swap0To1 = true
2188        assert_swap_out_result(
2189            true,
2190            U256::from(997_440_731_289_905u64),
2191            new_col_reserves_one(),
2192            new_debt_reserves_empty(),
2193            "999999999999999",
2194            12,
2195            12,
2196            limits_wide(),
2197            sync_time,
2198        );
2199
2200        // swap0To1 = false
2201        assert_swap_out_result(
2202            false,
2203            U256::from(997_440_731_837_532u64),
2204            new_col_reserves_one(),
2205            new_debt_reserves_empty(),
2206            "999999999999999",
2207            12,
2208            12,
2209            limits_wide(),
2210            sync_time,
2211        );
2212    }
2213
2214    pub fn new_verify_ratio_col_reserves() -> CollateralReserves {
2215        CollateralReserves {
2216            token0_real_reserves: U256::from(2_000_000u64) * U256::from(10u64).pow(U256::from(12)),
2217            token1_real_reserves: U256::from(15_000u64) * U256::from(10u64).pow(U256::from(12)),
2218            token0_imaginary_reserves: U256::ZERO,
2219            token1_imaginary_reserves: U256::ZERO,
2220        }
2221    }
2222
2223    pub fn new_verify_ratio_debt_reserves() -> DebtReserves {
2224        DebtReserves {
2225            token0_real_reserves: U256::from(2_000_000u64) * U256::from(10u64).pow(U256::from(12)),
2226            token1_real_reserves: U256::from(15_000u64) * U256::from(10u64).pow(U256::from(12)),
2227            token0_imaginary_reserves: U256::ZERO,
2228            token1_imaginary_reserves: U256::ZERO,
2229        }
2230    }
2231
2232    /// Calculate reserves outside a price range
2233    pub fn calculate_reserves_outside_range(
2234        geometric_mean_price: U256,
2235        price_at_range: U256,
2236        reserve_x: U256,
2237        reserve_y: U256,
2238    ) -> (I256, I256) {
2239        let geometric_mean_price = I256::from(geometric_mean_price);
2240        let price_at_range = I256::from(price_at_range);
2241        let reserve_x = I256::from(reserve_x);
2242        let reserve_y = I256::from(reserve_y);
2243
2244        let one_e27 = I256::from(constant::B_I1E27);
2245        let two = I256::try_from(2i8).unwrap();
2246
2247        // part1 = priceAtRange - geometricMeanPrice
2248        let part1 = price_at_range
2249            .checked_sub(geometric_mean_price)
2250            .expect("priceAtRange must be >= geometricMeanPrice");
2251
2252        // part2 = (geometricMeanPrice * reserveX + reserveY * 1e27) / (2 * part1)
2253        let part2 = geometric_mean_price
2254            .checked_mul(reserve_x)
2255            .unwrap()
2256            .checked_add(reserve_y.checked_mul(one_e27).unwrap())
2257            .unwrap()
2258            .checked_div(two.checked_mul(part1).unwrap())
2259            .unwrap();
2260
2261        // part3 = reserveX * reserveY
2262        let mut part3 = reserve_x
2263            .checked_mul(reserve_y)
2264            .unwrap();
2265
2266        let one_e50 = I256::try_from(10)
2267            .unwrap()
2268            .pow(U256::from(50));
2269
2270        // Handle overflow
2271        if part3 < one_e50 {
2272            part3 = part3
2273                .checked_mul(one_e27)
2274                .unwrap()
2275                .checked_div(part1)
2276                .unwrap();
2277        } else {
2278            part3 = part3
2279                .checked_div(part1)
2280                .unwrap()
2281                .checked_mul(one_e27)
2282                .unwrap();
2283        }
2284
2285        // reserveXOutside = part2 + sqrt(part3 + part2^2)
2286        let part2_squared = part2.checked_mul(part2).unwrap();
2287        let inside_sqrt = part3
2288            .checked_add(part2_squared)
2289            .unwrap();
2290        let sqrt_value = I256::from(
2291            U256::try_from(inside_sqrt)
2292                .unwrap()
2293                .root(2),
2294        );
2295
2296        let reserve_x_outside = part2.checked_add(sqrt_value).unwrap();
2297
2298        // reserveYOutside = (reserveXOutside * geometricMeanPrice) / 1e27
2299        let reserve_y_outside = reserve_x_outside
2300            .checked_mul(geometric_mean_price)
2301            .unwrap()
2302            .checked_div(one_e27)
2303            .unwrap();
2304
2305        (reserve_x_outside, reserve_y_outside)
2306    }
2307
2308    #[test]
2309    fn test_swap_in_verify_reserves_in_range() {
2310        let decimals: i64 = 6;
2311        let mut col_reserves = new_verify_ratio_col_reserves();
2312        let mut debt_reserves = new_verify_ratio_debt_reserves();
2313
2314        let mut price = U256::from_str("1000001000000000000000000000").unwrap();
2315
2316        // Calculate imaginary reserves for colReserves
2317        let (reserve_x_outside, reserve_y_outside) = calculate_reserves_outside_range(
2318            constant::B_I1E27,
2319            price,
2320            col_reserves.token0_real_reserves,
2321            col_reserves.token1_real_reserves,
2322        );
2323
2324        col_reserves.token0_imaginary_reserves =
2325            U256::from(reserve_x_outside + I256::from(col_reserves.token0_real_reserves));
2326        col_reserves.token1_imaginary_reserves = U256::from(
2327            I256::from(reserve_y_outside) + I256::from(col_reserves.token1_real_reserves),
2328        );
2329
2330        // Calculate imaginary reserves for debtReserves
2331        let (reserve_x_outside, reserve_y_outside) = calculate_reserves_outside_range(
2332            constant::B_I1E27,
2333            price,
2334            debt_reserves.token0_real_reserves,
2335            debt_reserves.token1_real_reserves,
2336        );
2337
2338        debt_reserves.token0_imaginary_reserves =
2339            U256::from(reserve_x_outside + I256::from(debt_reserves.token0_real_reserves));
2340        debt_reserves.token1_imaginary_reserves = U256::from(
2341            I256::from(reserve_y_outside) + I256::from(debt_reserves.token1_real_reserves),
2342        );
2343
2344        let sync_time = SystemTime::now()
2345            .duration_since(UNIX_EPOCH)
2346            .unwrap()
2347            .as_secs() -
2348            10;
2349
2350        // --- Case: Swap amount triggers revert (14_905)
2351        let swap_amount = U256::from(14_905) * U256::from(10).pow(U256::from(12)); // decimals factor
2352        price = get_approx_center_price_in(
2353            swap_amount,
2354            true,
2355            &col_reserves,
2356            &new_debt_reserves_empty(),
2357        )
2358        .unwrap();
2359        let result = swap_in_adjusted(
2360            true,
2361            swap_amount,
2362            &mut col_reserves,
2363            &mut new_debt_reserves_empty(),
2364            decimals,
2365            &mut limits_wide(),
2366            price,
2367            sync_time,
2368        );
2369        assert!(
2370            result.is_err(),
2371            "FAIL: reserves ratio revert NOT hit for col reserves when swap amount 14_905"
2372        );
2373
2374        price = get_approx_center_price_in(
2375            swap_amount,
2376            true,
2377            &new_col_reserves_empty(),
2378            &debt_reserves,
2379        )
2380        .unwrap();
2381        let result = swap_in_adjusted(
2382            true,
2383            swap_amount,
2384            &mut new_col_reserves_empty(),
2385            &mut debt_reserves,
2386            decimals,
2387            &mut limits_wide(),
2388            price,
2389            sync_time,
2390        );
2391        assert!(
2392            result.is_err(),
2393            "FAIL: reserves ratio revert NOT hit for debt reserves when swap amount 14_905"
2394        );
2395
2396        // --- Refresh reserves
2397        col_reserves = new_verify_ratio_col_reserves();
2398        debt_reserves = new_verify_ratio_debt_reserves();
2399
2400        let (reserve_x_outside, reserve_y_outside) = calculate_reserves_outside_range(
2401            constant::B_I1E27,
2402            price,
2403            col_reserves.token0_real_reserves,
2404            col_reserves.token1_real_reserves,
2405        );
2406
2407        col_reserves.token0_imaginary_reserves =
2408            U256::from(reserve_x_outside + I256::from(col_reserves.token0_real_reserves));
2409        col_reserves.token1_imaginary_reserves = U256::from(
2410            I256::from(reserve_y_outside) + I256::from(col_reserves.token1_real_reserves),
2411        );
2412
2413        let (reserve_x_outside, reserve_y_outside) = calculate_reserves_outside_range(
2414            constant::B_I1E27,
2415            // The test relies on this price value, obtained by the previous failing calls setup
2416            //  note that this value is < B_I1E27 so the returned reserves here will be negative
2417            //  it's unclear if this is expected by the Kyberswap implementation but it seems
2418            //  more like the value 14_895 was found with this unwanted side effect in place.
2419            price,
2420            debt_reserves.token0_real_reserves,
2421            debt_reserves.token1_real_reserves,
2422        );
2423        debt_reserves.token0_imaginary_reserves =
2424            U256::from(reserve_x_outside + I256::from(debt_reserves.token0_real_reserves));
2425        debt_reserves.token1_imaginary_reserves = U256::from(
2426            I256::from(reserve_y_outside) + I256::from(debt_reserves.token1_real_reserves),
2427        );
2428
2429        // --- Case: Swap amount should succeed (14_895)
2430        let swap_amount = U256::from(14_895) * U256::from(10).pow(U256::from(12));
2431
2432        price = get_approx_center_price_in(
2433            swap_amount,
2434            true,
2435            &col_reserves,
2436            &new_debt_reserves_empty(),
2437        )
2438        .unwrap();
2439        let result = swap_in_adjusted(
2440            true,
2441            swap_amount,
2442            &mut col_reserves,
2443            &mut new_debt_reserves_empty(),
2444            decimals,
2445            &mut limits_wide(),
2446            price,
2447            sync_time,
2448        );
2449        assert!(
2450            result.is_ok(),
2451            "FAIL: reserves ratio revert hit for col reserves when swap amount 14_895"
2452        );
2453
2454        price = get_approx_center_price_in(
2455            swap_amount,
2456            true,
2457            &new_col_reserves_empty(),
2458            &debt_reserves,
2459        )
2460        .unwrap();
2461        let result = swap_in_adjusted(
2462            true,
2463            swap_amount,
2464            &mut new_col_reserves_empty(),
2465            &mut debt_reserves,
2466            decimals,
2467            &mut limits_wide(),
2468            price,
2469            sync_time,
2470        );
2471        assert!(
2472            result.is_ok(),
2473            "FAIL: reserves ratio revert hit for debt reserves when swap amount 14_895"
2474        );
2475    }
2476
2477    pub fn new_verify_ratio_col_reserves_swap_out() -> CollateralReserves {
2478        CollateralReserves {
2479            token0_real_reserves: U256::from(15_000u64) * U256::from(10u64).pow(U256::from(12)), /* 15_000 * 1e12 */
2480            token1_real_reserves: U256::from(2_000_000u64) * U256::from(10u64).pow(U256::from(12)), /* 2_000_000 * 1e12 */
2481            token0_imaginary_reserves: U256::ZERO,
2482            token1_imaginary_reserves: U256::ZERO,
2483        }
2484    }
2485
2486    pub fn new_verify_ratio_debt_reserves_swap_out() -> DebtReserves {
2487        DebtReserves {
2488            token0_real_reserves: U256::from(15_000u64) * U256::from(10u64).pow(U256::from(12)),
2489            token1_real_reserves: U256::from(2_000_000u64) * U256::from(10u64).pow(U256::from(12)),
2490            token0_imaginary_reserves: U256::ZERO,
2491            token1_imaginary_reserves: U256::ZERO,
2492        }
2493    }
2494
2495    #[test]
2496    fn test_swap_out_verify_reserves_in_range() {
2497        let decimals: i64 = 6;
2498        let sync_time = SystemTime::now()
2499            .duration_since(UNIX_EPOCH)
2500            .unwrap()
2501            .as_secs() -
2502            10;
2503
2504        let mut col_reserves = new_verify_ratio_col_reserves_swap_out();
2505        let mut debt_reserves = new_verify_ratio_debt_reserves_swap_out();
2506
2507        // price = 1.000001 * 1e27
2508        let price = U256::from_str("1000001000000000000000000000").unwrap();
2509
2510        // First reserves calculation
2511        let (reserve_x_outside, reserve_y_outside) = calculate_reserves_outside_range(
2512            constant::B_I1E27,
2513            price,
2514            col_reserves.token0_real_reserves,
2515            col_reserves.token1_real_reserves,
2516        );
2517        col_reserves.token0_imaginary_reserves =
2518            U256::from(reserve_x_outside + I256::from(col_reserves.token0_real_reserves));
2519        col_reserves.token1_imaginary_reserves =
2520            U256::from(reserve_y_outside + I256::from(col_reserves.token1_real_reserves));
2521
2522        let (reserve_x_outside, reserve_y_outside) = calculate_reserves_outside_range(
2523            constant::B_I1E27,
2524            price,
2525            debt_reserves.token0_real_reserves,
2526            debt_reserves.token1_real_reserves,
2527        );
2528        debt_reserves.token0_imaginary_reserves =
2529            U256::from(reserve_x_outside + I256::from(debt_reserves.token0_real_reserves));
2530        debt_reserves.token1_imaginary_reserves =
2531            U256::from(reserve_y_outside + I256::from(debt_reserves.token1_real_reserves));
2532
2533        // Swap amount where revert should hit
2534        let swap_amount = U256::from(14_766u64) * U256::from(10u64).pow(U256::from(12));
2535
2536        let price = get_approx_center_price_out(
2537            swap_amount,
2538            false,
2539            &col_reserves,
2540            &new_debt_reserves_empty(),
2541        )
2542        .unwrap();
2543        let result = swap_out_adjusted(
2544            false,
2545            swap_amount,
2546            &mut col_reserves,
2547            &mut new_debt_reserves_empty(),
2548            decimals,
2549            decimals,
2550            &mut limits_wide(),
2551            price,
2552            sync_time,
2553        );
2554        assert!(result.is_err(), "FAIL: reserves ratio verification revert NOT hit for col reserves when swap amount 14_766");
2555
2556        let price = get_approx_center_price_out(
2557            swap_amount,
2558            false,
2559            &new_col_reserves_empty(),
2560            &debt_reserves,
2561        )
2562        .unwrap();
2563        let result = swap_out_adjusted(
2564            false,
2565            swap_amount,
2566            &mut new_col_reserves_empty(),
2567            &mut debt_reserves,
2568            decimals,
2569            decimals,
2570            &mut limits_wide(),
2571            price,
2572            sync_time,
2573        );
2574        assert!(result.is_err(), "FAIL: reserves ratio verification revert NOT hit for debt reserves when swap amount 14_766");
2575
2576        // Refresh reserves
2577        col_reserves = new_verify_ratio_col_reserves_swap_out();
2578        debt_reserves = new_verify_ratio_debt_reserves_swap_out();
2579
2580        let (reserve_x_outside, reserve_y_outside) = calculate_reserves_outside_range(
2581            constant::B_I1E27,
2582            price,
2583            col_reserves.token0_real_reserves,
2584            col_reserves.token1_real_reserves,
2585        );
2586        col_reserves.token0_imaginary_reserves =
2587            U256::from(reserve_x_outside + I256::from(col_reserves.token0_real_reserves));
2588        col_reserves.token1_imaginary_reserves =
2589            U256::from(reserve_y_outside + I256::from(col_reserves.token1_real_reserves));
2590
2591        let (reserve_x_outside, reserve_y_outside) = calculate_reserves_outside_range(
2592            constant::B_I1E27,
2593            price,
2594            debt_reserves.token0_real_reserves,
2595            debt_reserves.token1_real_reserves,
2596        );
2597        debt_reserves.token0_imaginary_reserves =
2598            U256::from(reserve_x_outside + I256::from(debt_reserves.token0_real_reserves));
2599        debt_reserves.token1_imaginary_reserves =
2600            U256::from(reserve_y_outside + I256::from(debt_reserves.token1_real_reserves));
2601
2602        // Swap amount where revert should NOT hit
2603        let swap_amount = U256::from(14_762u64) * U256::from(10u64).pow(U256::from(12));
2604
2605        let price = get_approx_center_price_out(
2606            swap_amount,
2607            false,
2608            &col_reserves,
2609            &new_debt_reserves_empty(),
2610        )
2611        .unwrap();
2612        let result = swap_out_adjusted(
2613            false,
2614            swap_amount,
2615            &mut col_reserves,
2616            &mut new_debt_reserves_empty(),
2617            decimals,
2618            decimals,
2619            &mut limits_wide(),
2620            price,
2621            sync_time,
2622        );
2623        assert!(
2624            result.is_ok(),
2625            "FAIL: reserves ratio verification revert hit for col reserves when swap amount 14_762"
2626        );
2627
2628        let price = get_approx_center_price_out(
2629            swap_amount,
2630            false,
2631            &new_col_reserves_empty(),
2632            &debt_reserves,
2633        )
2634        .unwrap();
2635        let result = swap_out_adjusted(
2636            false,
2637            swap_amount,
2638            &mut new_col_reserves_empty(),
2639            &mut debt_reserves,
2640            decimals,
2641            decimals,
2642            &mut limits_wide(),
2643            price,
2644            sync_time,
2645        );
2646        assert!(result.is_ok(), "FAIL: reserves ratio verification revert hit for debt reserves when swap amount 14_762");
2647    }
2648
2649    // Use this command to retrieve state for fluid pools:
2650    // ```bash
2651    // cast call 0xC93876C0EEd99645DD53937b25433e311881A27C \
2652    //  'getPoolReservesAdjusted(address)(address,address,address,uint256,uint256,(uint256,uint256,uint256),(uint256,uint256,uint256,uint256,uint256,uint256),((uint256,uint256,uint256),(uint256,uint256,uint256),(uint256,uint256,uint256),(uint256,uint256,uint256)))' \
2653    //  '0x0B1a513ee24972DAEf112bC777a5610d4325C9e7'
2654    // ```
2655    //
2656    // Use this command to get onchain estimates:
2657    //
2658    // ```bash
2659    // cast call -b 23526115 \
2660    //  0xC93876C0EEd99645DD53937b25433e311881A27C \
2661    //  'estimateSwapIn(address,bool,uint,uint)(uint)' \
2662    //  0x0B1a513ee24972DAEf112bC777a5610d4325C9e7 true 100000000000000 0
2663    // ```
2664
2665    fn hard_limit(l: u128) -> TokenLimit {
2666        TokenLimit {
2667            available: U256::from(l),
2668            expands_to: U256::from(l),
2669            expand_duration: U256::ZERO,
2670        }
2671    }
2672
2673    fn wsteth_eth_pool_23526115() -> (Token, Token, FluidV1) {
2674        let wsteth = Token::new(
2675            &Bytes::from_str("0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0").unwrap(),
2676            "wsteth",
2677            18,
2678            0,
2679            &[Some(20000)],
2680            Chain::Ethereum,
2681            100,
2682        );
2683        let eth = Token::new(
2684            &Bytes::from_str("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE").unwrap(),
2685            "ETH",
2686            18,
2687            0,
2688            &[Some(2000)],
2689            Chain::Ethereum,
2690            100,
2691        );
2692        let pool = FluidV1::new(
2693            &Bytes::from_str("0x0B1a513ee24972DAEf112bC777a5610d4325C9e7").unwrap(),
2694            &wsteth,
2695            &eth,
2696            CollateralReserves {
2697                token0_real_reserves: U256::from(4431191840536456u128),
2698                token1_real_reserves: U256::from(13105569017021951u128),
2699                token0_imaginary_reserves: U256::from(20263646714209556492u128),
2700                token1_imaginary_reserves: U256::from(24624319733997222300u128),
2701            },
2702            DebtReserves {
2703                token0_real_reserves: U256::from(3958052320699256u128),
2704                token1_real_reserves: U256::from(11706224851989005u128),
2705                token0_imaginary_reserves: U256::from(18100000404581051720u128),
2706                token1_imaginary_reserves: U256::from(21995063545785045888u128),
2707            },
2708            DexLimits {
2709                borrowable_token0: hard_limit(4431191840536456767040),
2710                borrowable_token1: hard_limit(6552784508510975527319),
2711                withdrawable_token0: hard_limit(4819160955805377144139),
2712                withdrawable_token1: hard_limit(6126272539623278413525),
2713            },
2714            U256::from_str("1215727283480584508000000000").unwrap(),
2715            U256::from(68),
2716            1759795200,
2717        );
2718        (wsteth, eth, pool)
2719    }
2720
2721    #[test]
2722    fn test_spot_price() {
2723        let (wsteth, eth, pool) = wsteth_eth_pool_23526115();
2724        // derived via numerical estimates from onchain quotes
2725        let exp_spot0 = 1.21519682; // 1.21511419 adjusted by 0.0068% fee
2726        let exp_spot1 = 0.82291191; // 0.82228559 adjusted by 0.0068% fee
2727
2728        let spot0 = pool.spot_price(&wsteth, &eth).unwrap();
2729        let spot1 = pool.spot_price(&eth, &wsteth).unwrap();
2730
2731        let rel_err0 = (spot0 - exp_spot0).abs() / exp_spot0;
2732        let rel_err1 = (spot1 - exp_spot1).abs() / exp_spot1;
2733
2734        assert!(
2735            rel_err0 < 1e-4,
2736            "spot0 mismatch: got {spot0}, expected {exp_spot0}, relative error: {rel_err0}"
2737        );
2738        assert!(
2739            rel_err1 < 1e-4,
2740            "spot1 mismatch: got {spot1}, expected {exp_spot1}, relative error: {rel_err1}"
2741        );
2742    }
2743
2744    #[test]
2745    fn test_get_amount_out_zero2one() {
2746        let (wsteth, eth, pool) = wsteth_eth_pool_23526115();
2747        let amount_in = BigUint::from_str_radix("100000000000000", 10).unwrap();
2748        // onchain we get 121511419000000
2749        let exp_amount_out = BigUint::from_str_radix("121511421000000", 10).unwrap();
2750
2751        let res = pool
2752            .get_amount_out(amount_in, &wsteth, &eth)
2753            .unwrap();
2754
2755        assert_eq!(res.amount, exp_amount_out);
2756    }
2757
2758    #[test]
2759    fn test_get_amount_out_one2zero() {
2760        let (wsteth, eth, pool) = wsteth_eth_pool_23526115();
2761        let amount_in = BigUint::from_str_radix("100000000000000", 10).unwrap();
2762        // onchain we get 82285596000000
2763        let exp_amount_out = BigUint::from_str_radix("82285598000000", 10).unwrap();
2764
2765        let res = pool
2766            .get_amount_out(amount_in, &eth, &wsteth)
2767            .unwrap();
2768        assert_eq!(res.amount, exp_amount_out);
2769    }
2770
2771    #[test]
2772    fn get_limits_zero2one() {
2773        let (wsteth, eth, pool) = wsteth_eth_pool_23526115();
2774
2775        let (max_amount_in, _) = pool
2776            .get_limits(wsteth.address.clone(), eth.address.clone())
2777            .unwrap();
2778        let max_amount_onchain_test =
2779            // 10.2k wsteth
2780            BigUint::from_str_radix("10200000000000000000000", 10).unwrap();
2781
2782        let _ = pool
2783            .get_amount_out(max_amount_in.clone(), &wsteth, &eth)
2784            .unwrap();
2785        assert!(max_amount_in < max_amount_onchain_test);
2786    }
2787
2788    #[test]
2789    fn get_limits_one2zero() {
2790        let (wsteth, eth, pool) = wsteth_eth_pool_23526115();
2791
2792        let (max_amount_in, _) = pool
2793            .get_limits(eth.address.clone(), wsteth.address.clone())
2794            .unwrap();
2795        let max_amount_onchain_test =
2796            // 10.2k wsteth
2797            BigUint::from_str_radix("10192694739404003000000", 10).unwrap();
2798
2799        let _ = pool
2800            .get_amount_out(max_amount_in.clone(), &eth, &wsteth)
2801            .unwrap();
2802
2803        assert!(max_amount_in < max_amount_onchain_test);
2804    }
2805}