Skip to main content

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