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