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