Skip to main content

tycho_simulation/evm/protocol/uniswap_v4/
state.rs

1use std::{any::Any, collections::HashMap, fmt};
2
3use alloy::primitives::{Address, Sign, I256, U256};
4use num_bigint::BigUint;
5use num_traits::{CheckedSub, ToPrimitive, Zero};
6use revm::primitives::I128;
7use tracing::trace;
8use tycho_common::{
9    dto::ProtocolStateDelta,
10    models::token::Token,
11    simulation::{
12        errors::{SimulationError, TransitionError},
13        protocol_sim::{
14            Balances, GetAmountOutResult, PoolSwap, ProtocolSim, QueryPoolSwapParams,
15            SwapConstraint,
16        },
17    },
18    Bytes,
19};
20
21use super::hooks::utils::{has_permission, HookOptions};
22use crate::{
23    evm::protocol::{
24        clmm::clmm_swap_to_price,
25        safe_math::{safe_add_u256, safe_sub_u256},
26        u256_num::u256_to_biguint,
27        uniswap_v4::hooks::{
28            hook_handler::HookHandler,
29            models::{
30                AfterSwapParameters, BalanceDelta, BeforeSwapDelta, BeforeSwapParameters,
31                StateContext, SwapParams,
32            },
33        },
34        utils::{
35            add_fee_markup,
36            uniswap::{
37                i24_be_bytes_to_i32, liquidity_math,
38                lp_fee::{self, is_dynamic},
39                sqrt_price_math::{get_amount0_delta, get_amount1_delta, sqrt_price_q96_to_f64},
40                swap_math,
41                tick_list::{TickInfo, TickList, TickListErrorKind},
42                tick_math::{
43                    get_sqrt_ratio_at_tick, get_tick_at_sqrt_ratio, MAX_SQRT_RATIO, MAX_TICK,
44                    MIN_SQRT_RATIO, MIN_TICK,
45                },
46                StepComputation, SwapResults, SwapState,
47            },
48        },
49        vm::constants::EXTERNAL_ACCOUNT,
50    },
51    impl_non_serializable_protocol,
52};
53
54// Fixed overhead per swap: covers router overhead, executor preamble (decode, sync,
55// unlock/callback pattern), and token transfer-in.
56const SWAP_BASE_GAS: u64 = 185_000;
57// Per loop: PoolManager.swap bitmap lookup + sqrt math + computeSwapStep.
58// V4's singleton PoolManager hits warmer storage than V3 standalone pools: ~3,500/loop.
59const GAS_PER_BITMAP_LOOKUP: u64 = 3_500;
60// Initialized tick crossing: _updateTick() updates feeGrowthOutside0/1 (2 SSTOREs).
61// Warm ≈ 10–17k, cold ≈ 40–52k. We use a blended estimate that
62// weights toward cold costs.
63const GAS_PER_TICK: u64 = 29_000;
64// Settlement overhead within swapExactInputSingle: _settle() + _getFullCredit() + misc.
65const V4_CALLBACK_SETTLEMENT_GAS: u64 = 30_000;
66// PoolManager Hooks.sol wrapper overhead per hook call: ABI encode params,
67// external CALL dispatch, decode return, validate selector, and process
68// the returned BeforeSwapDelta / AfterSwapDelta.
69const PM_PER_HOOK_CALL_OVERHEAD: u64 = 25_000;
70// Conservative max gas budget for a single swap (Ethereum transaction gas limit)
71const MAX_SWAP_GAS: u64 = 16_700_000;
72const MAX_TICKS_CROSSED: u64 = (MAX_SWAP_GAS - SWAP_BASE_GAS) / GAS_PER_TICK;
73
74#[derive(Clone)]
75pub struct UniswapV4State {
76    liquidity: u128,
77    sqrt_price: U256,
78    fees: UniswapV4Fees,
79    tick: i32,
80    ticks: TickList,
81    tick_spacing: i32,
82    pub hook: Option<Box<dyn HookHandler>>,
83}
84
85impl_non_serializable_protocol!(UniswapV4State, "not supported due vm state deps");
86
87impl fmt::Debug for UniswapV4State {
88    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89        f.debug_struct("UniswapV4State")
90            .field("liquidity", &self.liquidity)
91            .field("sqrt_price", &self.sqrt_price)
92            .field("fees", &self.fees)
93            .field("tick", &self.tick)
94            .field("tick_spacing", &self.tick_spacing)
95            .finish_non_exhaustive()
96    }
97}
98
99impl PartialEq for UniswapV4State {
100    fn eq(&self, other: &Self) -> bool {
101        match (&self.hook, &other.hook) {
102            (Some(a), Some(b)) => a.is_equal(&**b),
103            (None, None) => true,
104            _ => false,
105        }
106    }
107}
108
109impl Eq for UniswapV4State {}
110
111#[derive(Clone, Debug, PartialEq, Eq)]
112pub struct UniswapV4Fees {
113    // Protocol fees in the zero for one direction
114    pub zero_for_one: u32,
115    // Protocol fees in the one for zero direction
116    pub one_for_zero: u32,
117    // Liquidity providers fees
118    pub lp_fee: u32,
119}
120
121impl UniswapV4Fees {
122    pub fn new(zero_for_one: u32, one_for_zero: u32, lp_fee: u32) -> Self {
123        Self { zero_for_one, one_for_zero, lp_fee }
124    }
125
126    fn calculate_swap_fees_pips(&self, zero_for_one: bool, lp_fee_override: Option<u32>) -> u32 {
127        let protocol_fee = if zero_for_one { self.zero_for_one } else { self.one_for_zero };
128        let lp_fee = lp_fee_override.unwrap_or_else(|| {
129            // If a protocol has dynamic fees,
130            if is_dynamic(self.lp_fee) {
131                0
132            } else {
133                self.lp_fee
134            }
135        });
136
137        // UniswapV4 formula: protocolFee + lpFee - (protocolFee * lpFee / 1_000_000)
138        // Source: https://raw.githubusercontent.com/Uniswap/v4-core/main/src/libraries/ProtocolFeeLibrary.sol
139        // This accounts for the fact that protocol fee is taken first, then LP fee applies to
140        // remainder
141        protocol_fee + lp_fee - ((protocol_fee as u64 * lp_fee as u64 / 1_000_000) as u32)
142    }
143}
144
145impl UniswapV4State {
146    /// Creates a new `UniswapV4State` with specified values.
147    pub fn new(
148        liquidity: u128,
149        sqrt_price: U256,
150        fees: UniswapV4Fees,
151        tick: i32,
152        tick_spacing: i32,
153        ticks: Vec<TickInfo>,
154    ) -> Result<Self, SimulationError> {
155        let tick_spacing_u16 = tick_spacing.try_into().map_err(|_| {
156            // even though it's given as int24, tick_spacing must be positive, see here:
157            // https://github.com/Uniswap/v4-core/blob/a22414e4d7c0d0b0765827fe0a6c20dfd7f96291/src/libraries/TickMath.sol#L25-L28
158            SimulationError::FatalError(format!(
159                "tick_spacing {} must be positive (int24 -> u16 conversion failed)",
160                tick_spacing
161            ))
162        })?;
163        let tick_list = TickList::from(tick_spacing_u16, ticks)?;
164        Ok(UniswapV4State {
165            liquidity,
166            sqrt_price,
167            fees,
168            tick,
169            ticks: tick_list,
170            tick_spacing,
171            hook: None,
172        })
173    }
174
175    fn swap(
176        &self,
177        zero_for_one: bool,
178        amount_specified: I256,
179        sqrt_price_limit: Option<U256>,
180        lp_fee_override: Option<u32>,
181    ) -> Result<SwapResults, SimulationError> {
182        if amount_specified == I256::ZERO {
183            return Ok(SwapResults {
184                amount_calculated: I256::ZERO,
185                amount_specified: I256::ZERO,
186                amount_remaining: I256::ZERO,
187                sqrt_price: self.sqrt_price,
188                liquidity: self.liquidity,
189                tick: self.tick,
190                gas_used: U256::from(3_000), // baseline gas cost for no-op swap
191            });
192        }
193
194        if self.liquidity == 0 {
195            return Err(SimulationError::RecoverableError("No liquidity".to_string()));
196        }
197        let price_limit = if let Some(limit) = sqrt_price_limit {
198            limit
199        } else if zero_for_one {
200            safe_add_u256(MIN_SQRT_RATIO, U256::from(1u64))?
201        } else {
202            safe_sub_u256(MAX_SQRT_RATIO, U256::from(1u64))?
203        };
204
205        let price_limit_valid = if zero_for_one {
206            price_limit > MIN_SQRT_RATIO && price_limit < self.sqrt_price
207        } else {
208            price_limit < MAX_SQRT_RATIO && price_limit > self.sqrt_price
209        };
210        if !price_limit_valid {
211            return Err(SimulationError::InvalidInput("Price limit out of range".into(), None));
212        }
213
214        let exact_input = amount_specified < I256::ZERO;
215
216        let mut state = SwapState {
217            amount_remaining: amount_specified,
218            amount_calculated: I256::ZERO,
219            sqrt_price: self.sqrt_price,
220            tick: self.tick,
221            liquidity: self.liquidity,
222        };
223        let mut gas_used = U256::from(SWAP_BASE_GAS);
224
225        while state.amount_remaining != I256::ZERO && state.sqrt_price != price_limit {
226            let (mut next_tick, initialized) = match self
227                .ticks
228                .next_initialized_tick_within_one_word(state.tick, zero_for_one)
229            {
230                Ok((tick, init)) => {
231                    gas_used = safe_add_u256(gas_used, U256::from(GAS_PER_BITMAP_LOOKUP))?;
232                    (tick, init)
233                }
234                Err(tick_err) => match tick_err.kind {
235                    TickListErrorKind::TicksExeeded => {
236                        let mut new_state = self.clone();
237                        new_state.liquidity = state.liquidity;
238                        new_state.tick = state.tick;
239                        new_state.sqrt_price = state.sqrt_price;
240                        return Err(SimulationError::InvalidInput(
241                            "Ticks exceeded".into(),
242                            Some(GetAmountOutResult::new(
243                                u256_to_biguint(state.amount_calculated.abs().into_raw()),
244                                u256_to_biguint(gas_used),
245                                Box::new(new_state),
246                            )),
247                        ));
248                    }
249                    _ => return Err(SimulationError::FatalError("Unknown error".to_string())),
250                },
251            };
252
253            next_tick = next_tick.clamp(MIN_TICK, MAX_TICK);
254
255            let sqrt_price_next = get_sqrt_ratio_at_tick(next_tick)?;
256            let fee_pips = self
257                .fees
258                .calculate_swap_fees_pips(zero_for_one, lp_fee_override);
259
260            let sqrt_price_start = state.sqrt_price;
261            let (sqrt_price, amount_in, amount_out, fee_amount) = swap_math::compute_swap_step(
262                state.sqrt_price,
263                UniswapV4State::get_sqrt_ratio_target(sqrt_price_next, price_limit, zero_for_one),
264                state.liquidity,
265                // The core univ4 swap logic assumes that if the amount is > 0 it's exact in, and
266                // if it's < 0 it's exact out. The compute_swap_step assumes the
267                // opposite (it's like that for univ3).
268                -state.amount_remaining,
269                fee_pips,
270            )?;
271            state.sqrt_price = sqrt_price;
272
273            let step = StepComputation {
274                sqrt_price_start,
275                tick_next: next_tick,
276                initialized,
277                sqrt_price_next,
278                amount_in,
279                amount_out,
280                fee_amount,
281            };
282            if exact_input {
283                state.amount_remaining += I256::checked_from_sign_and_abs(
284                    Sign::Positive,
285                    safe_add_u256(step.amount_in, step.fee_amount)?,
286                )
287                .unwrap();
288                state.amount_calculated -=
289                    I256::checked_from_sign_and_abs(Sign::Positive, step.amount_out).unwrap();
290            } else {
291                state.amount_remaining -=
292                    I256::checked_from_sign_and_abs(Sign::Positive, step.amount_out).unwrap();
293                state.amount_calculated += I256::checked_from_sign_and_abs(
294                    Sign::Positive,
295                    safe_add_u256(step.amount_in, step.fee_amount)?,
296                )
297                .unwrap();
298            }
299            if state.sqrt_price == step.sqrt_price_next {
300                if step.initialized {
301                    let liquidity_raw = self
302                        .ticks
303                        .get_tick(step.tick_next)
304                        .unwrap()
305                        .net_liquidity;
306                    let liquidity_net = if zero_for_one { -liquidity_raw } else { liquidity_raw };
307                    state.liquidity =
308                        liquidity_math::add_liquidity_delta(state.liquidity, liquidity_net)?;
309                    gas_used = safe_add_u256(gas_used, U256::from(GAS_PER_TICK))?;
310                }
311                state.tick = if zero_for_one { step.tick_next - 1 } else { step.tick_next };
312            } else if state.sqrt_price != step.sqrt_price_start {
313                state.tick = get_tick_at_sqrt_ratio(state.sqrt_price)?;
314            }
315        }
316
317        Ok(SwapResults {
318            amount_calculated: state.amount_calculated,
319            amount_specified,
320            amount_remaining: state.amount_remaining,
321            sqrt_price: state.sqrt_price,
322            liquidity: state.liquidity,
323            tick: state.tick,
324            gas_used: safe_add_u256(gas_used, U256::from(V4_CALLBACK_SETTLEMENT_GAS))?,
325        })
326    }
327
328    pub fn set_hook_handler(&mut self, handler: Box<dyn HookHandler>) {
329        self.hook = Some(handler);
330    }
331
332    fn get_sqrt_ratio_target(
333        sqrt_price_next: U256,
334        sqrt_price_limit: U256,
335        zero_for_one: bool,
336    ) -> U256 {
337        let cond1 = if zero_for_one {
338            sqrt_price_next < sqrt_price_limit
339        } else {
340            sqrt_price_next > sqrt_price_limit
341        };
342
343        if cond1 {
344            sqrt_price_limit
345        } else {
346            sqrt_price_next
347        }
348    }
349
350    fn find_limits_experimentally(
351        &self,
352        token_in: Bytes,
353        token_out: Bytes,
354    ) -> Result<(BigUint, BigUint), SimulationError> {
355        // Create dummy token objects with proper addresses. This is fine since `get_amount_out`
356        // only uses the token addresses.
357        let token_in_obj =
358            Token::new(&token_in, "TOKEN_IN", 18, 0, &[Some(10_000)], Default::default(), 100);
359        let token_out_obj =
360            Token::new(&token_out, "TOKEN_OUT", 18, 0, &[Some(10_000)], Default::default(), 100);
361
362        self.find_max_amount(&token_in_obj, &token_out_obj)
363    }
364
365    /// Finds max amount by performing exponential search.
366    ///
367    /// Reasoning:
368    /// - get_amount_out(I256::MAX) will almost always fail, so this will waste time checking values
369    ///   unrealistically high.
370    /// - If you were to start binary search from 1 to 10^76, you'd need hundreds of iterations.
371    ///
372    /// More about exponential search: https://en.wikipedia.org/wiki/Exponential_search
373    ///
374    /// # Returns
375    ///
376    /// Returns a tuple containing the max amount in and max amount out respectively.
377    fn find_max_amount(
378        &self,
379        token_in: &Token,
380        token_out: &Token,
381    ) -> Result<(BigUint, BigUint), SimulationError> {
382        let mut low = BigUint::from(1u64);
383
384        // The max you can swap on a USV4 is I256::MAX is 5.7e76, since input amount is I256.
385        // So start with something much smaller to search for a reasonable upper bound.
386        let mut high = BigUint::from(10u64).pow(18); // 1 ether in wei
387        let mut last_successful_amount_in = BigUint::from(1u64);
388        let mut last_successful_amount_out = BigUint::from(0u64);
389
390        // First, find an upper bound where the swap fails using exponential search.
391        // Save and return both the amount in and amount out.
392        while let Ok(result) = self.get_amount_out(high.clone(), token_in, token_out) {
393            // We haven't found the upper bound yet, increase the attempted upper bound
394            // by order of magnitude and store the last success as the lower bound.
395            low = last_successful_amount_in.clone();
396            last_successful_amount_in = high.clone();
397            last_successful_amount_out = result.amount;
398            high *= BigUint::from(10u64);
399
400            // Stop if we're getting too large for I256 (about 10^75)
401            if high > BigUint::from(10u64).pow(75) {
402                return Ok((last_successful_amount_in, last_successful_amount_out));
403            }
404        }
405
406        // Use binary search to narrow down value between low and high
407        while &high - &low > BigUint::from(1u64) {
408            let mid = (&low + &high) / BigUint::from(2u64);
409
410            match self.get_amount_out(mid.clone(), token_in, token_out) {
411                Ok(result) => {
412                    last_successful_amount_in = mid.clone();
413                    last_successful_amount_out = result.amount;
414                    low = mid;
415                }
416                Err(_) => {
417                    high = mid;
418                }
419            }
420        }
421
422        Ok((last_successful_amount_in, last_successful_amount_out))
423    }
424
425    /// Helper method to check if there are no initialized ticks in either direction
426    fn has_no_initialized_ticks(&self) -> bool {
427        !self.ticks.has_initialized_ticks()
428    }
429}
430
431#[typetag::serde]
432impl ProtocolSim for UniswapV4State {
433    // Not possible to implement correctly with the current interface because we need to know the
434    // swap direction.
435    fn fee(&self) -> f64 {
436        todo!()
437    }
438
439    fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
440        if let Some(hook) = &self.hook {
441            match hook.spot_price(base, quote) {
442                Ok(price) => return Ok(price),
443                Err(SimulationError::RecoverableError(_)) => {
444                    // Calculate spot price by swapping two amounts and use the approximation
445                    // to get the derivative, following the pattern from vm/state.rs
446
447                    // Calculate the first sell amount (x1) as a small amount
448                    let x1 = BigUint::from(10u64).pow(base.decimals) / BigUint::from(100u64); // 0.01 token
449
450                    // Calculate the second sell amount (x2) as x1 + 1% of x1
451                    let x2 = &x1 + (&x1 / BigUint::from(100u64));
452
453                    // Perform swaps to get the received amounts
454                    let y1 = self.get_amount_out(x1.clone(), base, quote)?;
455                    let y2 = self.get_amount_out(x2.clone(), base, quote)?;
456
457                    // Calculate the marginal price
458                    let num = y2
459                        .amount
460                        .checked_sub(&y1.amount)
461                        .ok_or_else(|| {
462                            SimulationError::FatalError(
463                                "Cannot calculate spot price: y2 < y1".to_string(),
464                            )
465                        })?;
466                    let den = x2.checked_sub(&x1).ok_or_else(|| {
467                        SimulationError::FatalError(
468                            "Cannot calculate spot price: x2 < x1".to_string(),
469                        )
470                    })?;
471
472                    if den == BigUint::from(0u64) {
473                        return Err(SimulationError::FatalError(
474                            "Cannot calculate spot price: denominator is zero".to_string(),
475                        ));
476                    }
477
478                    // Convert to f64 and adjust for decimals
479                    let num_f64 = num.to_f64().ok_or_else(|| {
480                        SimulationError::FatalError(
481                            "Failed to convert numerator to f64".to_string(),
482                        )
483                    })?;
484                    let den_f64 = den.to_f64().ok_or_else(|| {
485                        SimulationError::FatalError(
486                            "Failed to convert denominator to f64".to_string(),
487                        )
488                    })?;
489
490                    let token_correction = 10f64.powi(base.decimals as i32 - quote.decimals as i32);
491
492                    return Ok(num_f64 / den_f64 * token_correction);
493                }
494                Err(e) => return Err(e),
495            }
496        }
497
498        let zero_for_one = base < quote;
499        let fee_pips = self
500            .fees
501            .calculate_swap_fees_pips(zero_for_one, None);
502        let fee = fee_pips as f64 / 1_000_000.0;
503
504        let price = if zero_for_one {
505            sqrt_price_q96_to_f64(self.sqrt_price, base.decimals, quote.decimals)?
506        } else {
507            1.0f64 / sqrt_price_q96_to_f64(self.sqrt_price, quote.decimals, base.decimals)?
508        };
509
510        Ok(add_fee_markup(price, fee))
511    }
512
513    fn get_amount_out(
514        &self,
515        amount_in: BigUint,
516        token_in: &Token,
517        token_out: &Token,
518    ) -> Result<GetAmountOutResult, SimulationError> {
519        let zero_for_one = token_in < token_out;
520        let amount_specified = I256::checked_from_sign_and_abs(
521            Sign::Negative,
522            U256::from_be_slice(&amount_in.to_bytes_be()),
523        )
524        .ok_or_else(|| {
525            SimulationError::InvalidInput("I256 overflow: amount_in".to_string(), None)
526        })?;
527
528        let mut amount_to_swap = amount_specified;
529        let mut lp_fee_override: Option<u32> = None;
530        let mut before_swap_gas = 0u64;
531        let mut after_swap_gas = 0u64;
532        let mut before_swap_delta = BeforeSwapDelta(I256::ZERO);
533        let mut storage_overwrites = None;
534
535        let token_in_address = Address::from_slice(&token_in.address);
536        let token_out_address = Address::from_slice(&token_out.address);
537
538        let state_context = StateContext {
539            currency_0: if zero_for_one { token_in_address } else { token_out_address },
540            currency_1: if zero_for_one { token_out_address } else { token_in_address },
541            fees: self.fees.clone(),
542            tick_spacing: self.tick_spacing,
543        };
544
545        let swap_params = SwapParams {
546            zero_for_one,
547            amount_specified: amount_to_swap,
548            sqrt_price_limit: self.sqrt_price,
549        };
550
551        // Check if hook is set and has before_swap permissions
552        if let Some(ref hook) = self.hook {
553            if has_permission(hook.address(), HookOptions::BeforeSwap) {
554                let before_swap_params = BeforeSwapParameters {
555                    context: state_context.clone(),
556                    sender: *EXTERNAL_ACCOUNT,
557                    swap_params: swap_params.clone(),
558                    hook_data: Bytes::new(),
559                };
560
561                let before_swap_result = hook
562                    .before_swap(before_swap_params, None, None)
563                    .map_err(|e| {
564                        SimulationError::FatalError(format!(
565                            "BeforeSwap hook simulation failed: {e:?}"
566                        ))
567                    })?;
568
569                before_swap_gas = before_swap_result.gas_estimate;
570                before_swap_delta = before_swap_result.result.amount_delta;
571                storage_overwrites = Some(before_swap_result.result.overwrites);
572
573                // Convert amountDelta to amountToSwap as per Uniswap V4 spec
574                // See: https://github.com/Uniswap/v4-core/blob/main/src/libraries/Hooks.sol#L270
575                if before_swap_delta.as_i256() != I256::ZERO {
576                    amount_to_swap += I256::from(before_swap_delta.get_specified_delta());
577                    if amount_to_swap > I256::ZERO {
578                        return Err(SimulationError::FatalError(
579                            "Hook delta exceeds swap amount".into(),
580                        ));
581                    }
582                }
583
584                // Set LP fee override if provided by hook
585                // The fee returned by beforeSwap may have the override flag (bit 22) set,
586                // which needs to be removed before using the fee value.
587                // See: https://github.com/Uniswap/v4-core/blob/main/src/libraries/LPFeeLibrary.sol
588                let hook_fee = before_swap_result
589                    .result
590                    .fee
591                    .to::<u32>();
592                if hook_fee != 0 {
593                    // Remove the override flag (bit 22) as per LPFeeLibrary.sol
594                    let cleaned_fee = lp_fee::remove_override_flag(hook_fee);
595
596                    // Validate the fee doesn't exceed MAX_LP_FEE (1,000,000 pips = 100%)
597                    if !lp_fee::is_valid(cleaned_fee) {
598                        return Err(SimulationError::FatalError(format!(
599                            "LP fee override {} exceeds maximum {} pips",
600                            cleaned_fee,
601                            lp_fee::MAX_LP_FEE
602                        )));
603                    }
604
605                    lp_fee_override = Some(cleaned_fee);
606                }
607            }
608        }
609
610        // Perform the swap with potential hook modifications
611        let result = self.swap(zero_for_one, amount_to_swap, None, lp_fee_override)?;
612
613        // Create BalanceDelta from swap result using the proper constructor
614        let mut swap_delta = BalanceDelta::from_swap_result(result.amount_calculated, zero_for_one);
615
616        // Get deltas (change in the specified/given and unspecified/computed token balances after
617        // calling before swap)
618        let hook_delta_specified = before_swap_delta.get_specified_delta();
619        let mut hook_delta_unspecified = before_swap_delta.get_unspecified_delta();
620
621        if let Some(ref hook) = self.hook {
622            if has_permission(hook.address(), HookOptions::AfterSwap) {
623                let after_swap_params = AfterSwapParameters {
624                    context: state_context,
625                    sender: *EXTERNAL_ACCOUNT,
626                    swap_params,
627                    delta: swap_delta,
628                    hook_data: Bytes::new(),
629                };
630
631                let after_swap_result = hook
632                    .after_swap(after_swap_params, storage_overwrites, None)
633                    .map_err(|e| {
634                        SimulationError::FatalError(format!(
635                            "AfterSwap hook simulation failed: {e:?}"
636                        ))
637                    })?;
638                after_swap_gas = after_swap_result.gas_estimate;
639                hook_delta_unspecified += after_swap_result.result;
640            }
641        }
642
643        // Replicates the behaviour of the Hooks library wrapper of the afterSwap method:
644        // https://github.com/Uniswap/v4-core/blob/59d3ecf53afa9264a16bba0e38f4c5d2231f80bc/src/libraries/Hooks.sol
645        if (hook_delta_specified != I128::ZERO) || (hook_delta_unspecified != I128::ZERO) {
646            let hook_delta = if (amount_specified < I256::ZERO) == zero_for_one {
647                BalanceDelta::new(hook_delta_specified, hook_delta_unspecified)
648            } else {
649                BalanceDelta::new(hook_delta_unspecified, hook_delta_specified)
650            };
651            // This is a BalanceDelta subtraction
652            swap_delta = swap_delta - hook_delta
653        }
654
655        let amount_out = if (amount_specified < I256::ZERO) == zero_for_one {
656            swap_delta.amount1()
657        } else {
658            swap_delta.amount0()
659        };
660
661        trace!(?amount_in, ?token_in, ?token_out, ?zero_for_one, ?result, "V4 SWAP");
662        let mut new_state = self.clone();
663        new_state.liquidity = result.liquidity;
664        new_state.tick = result.tick;
665        new_state.sqrt_price = result.sqrt_price;
666
667        // Add hook gas costs to baseline swap cost.
668        // before_swap_gas / after_swap_gas capture the hook contract's internal
669        // logic (from VM simulation). PM_PER_HOOK_CALL_OVERHEAD accounts for the
670        // PoolManager's Hooks.sol dispatch wrapper that is not captured by either
671        // the native swap constants or the VM simulation.
672        let mut hook_overhead = before_swap_gas + after_swap_gas;
673        if before_swap_gas > 0 {
674            hook_overhead += PM_PER_HOOK_CALL_OVERHEAD;
675        }
676        if after_swap_gas > 0 {
677            hook_overhead += PM_PER_HOOK_CALL_OVERHEAD;
678        }
679        let total_gas_used = result.gas_used + U256::from(hook_overhead);
680        Ok(GetAmountOutResult::new(
681            u256_to_biguint(U256::from(amount_out.abs())),
682            u256_to_biguint(total_gas_used),
683            Box::new(new_state),
684        ))
685    }
686
687    fn get_limits(
688        &self,
689        token_in: Bytes,
690        token_out: Bytes,
691    ) -> Result<(BigUint, BigUint), SimulationError> {
692        if let Some(hook) = &self.hook {
693            // Check if pool has no liquidity & ticks -> hook manages liquidity
694            if self.liquidity == 0 && self.has_no_initialized_ticks() {
695                // If the hook has a get_amount_ranges entrypoint, call it and return (0, limits[1])
696                match hook.get_amount_ranges(token_in.clone(), token_out.clone()) {
697                    Ok(amount_ranges) => {
698                        return Ok((
699                            u256_to_biguint(amount_ranges.amount_in_range.1),
700                            u256_to_biguint(amount_ranges.amount_out_range.1),
701                        ))
702                    }
703                    // Check if hook get_amount_ranges is not implemented or the limits entrypoint
704                    // is not set for this hook
705                    Err(SimulationError::RecoverableError(msg))
706                        if msg.contains("not implemented") || msg.contains("not set") =>
707                    {
708                        // Hook manages liquidity but doesn't have get_amount_ranges
709                        // Use binary search to find limits by calling swap with increasing amounts
710                        return self.find_limits_experimentally(token_in, token_out);
711                        // Otherwise fall back to default implementation
712                    }
713                    Err(e) => return Err(e),
714                }
715            }
716        }
717
718        // If the pool has no liquidity, return zeros for both limits
719        if self.liquidity == 0 {
720            return Ok((BigUint::zero(), BigUint::zero()));
721        }
722
723        let zero_for_one = token_in < token_out;
724        let mut current_tick = self.tick;
725        let mut current_sqrt_price = self.sqrt_price;
726        let mut current_liquidity = self.liquidity;
727        let mut total_amount_in = U256::ZERO;
728        let mut total_amount_out = U256::ZERO;
729        let mut ticks_crossed: u64 = 0;
730
731        // Iterate through ticks in the direction of the swap
732        // Stops when: no more liquidity, no more ticks, or gas limit would be exceeded
733        while let Ok((tick, initialized)) = self
734            .ticks
735            .next_initialized_tick_within_one_word(current_tick, zero_for_one)
736        {
737            // Cap iteration to prevent exceeding Ethereum's gas limit
738            if ticks_crossed >= MAX_TICKS_CROSSED {
739                break;
740            }
741            ticks_crossed += 1;
742
743            // Clamp the tick value to ensure it's within valid range
744            let next_tick = tick.clamp(MIN_TICK, MAX_TICK);
745
746            // Calculate the sqrt price at the next tick boundary
747            let sqrt_price_next = get_sqrt_ratio_at_tick(next_tick)?;
748
749            // Calculate the amount of tokens swapped when moving from current_sqrt_price to
750            // sqrt_price_next. Direction determines which token is being swapped in vs out
751            let (amount_in, amount_out) = if zero_for_one {
752                let amount0 = get_amount0_delta(
753                    sqrt_price_next,
754                    current_sqrt_price,
755                    current_liquidity,
756                    true,
757                )?;
758                let amount1 = get_amount1_delta(
759                    sqrt_price_next,
760                    current_sqrt_price,
761                    current_liquidity,
762                    false,
763                )?;
764                (amount0, amount1)
765            } else {
766                let amount0 = get_amount0_delta(
767                    sqrt_price_next,
768                    current_sqrt_price,
769                    current_liquidity,
770                    false,
771                )?;
772                let amount1 = get_amount1_delta(
773                    sqrt_price_next,
774                    current_sqrt_price,
775                    current_liquidity,
776                    true,
777                )?;
778                (amount1, amount0)
779            };
780
781            // Accumulate total amounts for this tick range
782            total_amount_in = safe_add_u256(total_amount_in, amount_in)?;
783            total_amount_out = safe_add_u256(total_amount_out, amount_out)?;
784
785            // If this tick is "initialized" (meaning its someone's position boundary), update the
786            // liquidity when crossing it
787            // For zero_for_one, liquidity is removed when crossing a tick
788            // For one_for_zero, liquidity is added when crossing a tick
789            if initialized {
790                let liquidity_raw = self
791                    .ticks
792                    .get_tick(next_tick)
793                    .unwrap()
794                    .net_liquidity;
795                let liquidity_delta = if zero_for_one { -liquidity_raw } else { liquidity_raw };
796
797                // Check if applying this liquidity delta would cause underflow
798                // If so, stop here rather than continuing with invalid state
799                match liquidity_math::add_liquidity_delta(current_liquidity, liquidity_delta) {
800                    Ok(new_liquidity) => {
801                        current_liquidity = new_liquidity;
802                    }
803                    Err(_) => {
804                        // Liquidity would underflow, stop iteration here
805                        // This represents the maximum liquidity we can actually use
806                        break;
807                    }
808                }
809            }
810
811            // Move to the next tick position
812            current_tick = if zero_for_one { next_tick - 1 } else { next_tick };
813            current_sqrt_price = sqrt_price_next;
814
815            // If we've consumed all liquidity, no point continuing the loop
816            if current_liquidity == 0 {
817                break;
818            }
819        }
820
821        Ok((u256_to_biguint(total_amount_in), u256_to_biguint(total_amount_out)))
822    }
823
824    fn delta_transition(
825        &mut self,
826        delta: ProtocolStateDelta,
827        tokens: &HashMap<Bytes, Token>,
828        balances: &Balances,
829    ) -> Result<(), TransitionError> {
830        if let Some(mut hook) = self.hook.clone() {
831            match hook.delta_transition(delta.clone(), tokens, balances) {
832                Ok(()) => self.set_hook_handler(hook),
833                Err(TransitionError::SimulationError(SimulationError::RecoverableError(msg)))
834                    if msg.contains("not implemented") =>
835                {
836                    // Fall back to default implementation
837                }
838                Err(e) => return Err(e),
839            }
840        }
841
842        // Apply attribute changes
843        if let Some(liquidity) = delta
844            .updated_attributes
845            .get("liquidity")
846        {
847            self.liquidity = u128::from(liquidity.clone());
848        }
849        if let Some(sqrt_price) = delta
850            .updated_attributes
851            .get("sqrt_price_x96")
852        {
853            self.sqrt_price = U256::from_be_slice(sqrt_price);
854        }
855        if let Some(tick) = delta.updated_attributes.get("tick") {
856            self.tick = i24_be_bytes_to_i32(tick);
857        }
858        if let Some(lp_fee) = delta.updated_attributes.get("fee") {
859            self.fees.lp_fee = u32::from(lp_fee.clone());
860        }
861        if let Some(zero2one_protocol_fee) = delta
862            .updated_attributes
863            .get("protocol_fees/zero2one")
864        {
865            self.fees.zero_for_one = u32::from(zero2one_protocol_fee.clone());
866        }
867        if let Some(one2zero_protocol_fee) = delta
868            .updated_attributes
869            .get("protocol_fees/one2zero")
870        {
871            self.fees.one_for_zero = u32::from(one2zero_protocol_fee.clone());
872        }
873
874        // apply tick changes
875        for (key, value) in delta.updated_attributes.iter() {
876            // tick liquidity keys are in the format "ticks/{tick_index}/net_liquidity"
877            if key.starts_with("ticks/") {
878                let parts: Vec<&str> = key.split('/').collect();
879                self.ticks
880                    .set_tick_liquidity(
881                        parts[1]
882                            .parse::<i32>()
883                            .map_err(|err| TransitionError::DecodeError(err.to_string()))?,
884                        i128::from(value.clone()),
885                    )
886                    .map_err(|err| TransitionError::DecodeError(err.to_string()))?;
887            }
888        }
889        // delete ticks - ignores deletes for attributes other than tick liquidity
890        for key in delta.deleted_attributes.iter() {
891            // tick liquidity keys are in the format "ticks/{tick_index}/net_liquidity"
892            if key.starts_with("ticks/") {
893                let parts: Vec<&str> = key.split('/').collect();
894                self.ticks
895                    .set_tick_liquidity(
896                        parts[1]
897                            .parse::<i32>()
898                            .map_err(|err| TransitionError::DecodeError(err.to_string()))?,
899                        0,
900                    )
901                    .map_err(|err| TransitionError::DecodeError(err.to_string()))?;
902            }
903        }
904
905        Ok(())
906    }
907
908    /// See [`ProtocolSim::query_pool_swap`] for the trait documentation.
909    ///
910    /// This method uses Uniswap V4 internal swap logic by swapping an infinite amount of token_in
911    /// until the target price is reached. Takes into account V4-specific features like protocol
912    /// fees and dynamic LP fees.
913    ///
914    /// Note: This implementation does not invoke hooks, as it is a query-only operation meant to
915    /// determine available liquidity at a given price without executing an actual swap.
916    fn query_pool_swap(&self, params: &QueryPoolSwapParams) -> Result<PoolSwap, SimulationError> {
917        if self.liquidity == 0 {
918            return Err(SimulationError::FatalError("No liquidity".to_string()));
919        }
920
921        // Calculate total fee (protocol + LP fee) for V4
922        let zero_for_one = params.token_in().address < params.token_out().address;
923        let fee_pips = self
924            .fees
925            .calculate_swap_fees_pips(zero_for_one, None);
926
927        match params.swap_constraint() {
928            SwapConstraint::TradeLimitPrice { .. } => Err(SimulationError::InvalidInput(
929                "Uniswap V4 does not support TradeLimitPrice constraint in query_pool_swap"
930                    .to_string(),
931                None,
932            )),
933            SwapConstraint::PoolTargetPrice {
934                target,
935                tolerance: _,
936                min_amount_in: _,
937                max_amount_in: _,
938            } => {
939                if self.liquidity == 0 {
940                    return Err(SimulationError::FatalError("No liquidity".to_string()));
941                }
942
943                let (amount_in, amount_out, swap_result) = clmm_swap_to_price(
944                    self.sqrt_price,
945                    &params.token_in().address,
946                    &params.token_out().address,
947                    target,
948                    fee_pips,
949                    Sign::Negative, // V4 uses negative for exact input
950                    |zero_for_one, amount_specified, sqrt_price_limit| {
951                        self.swap(zero_for_one, amount_specified, Some(sqrt_price_limit), None)
952                    },
953                )?;
954
955                let mut new_state = self.clone();
956                new_state.liquidity = swap_result.liquidity;
957                new_state.tick = swap_result.tick;
958                new_state.sqrt_price = swap_result.sqrt_price;
959
960                Ok(PoolSwap::new(amount_in, amount_out, Box::new(new_state), None))
961            }
962        }
963    }
964
965    fn clone_box(&self) -> Box<dyn ProtocolSim> {
966        Box::new(self.clone())
967    }
968
969    fn as_any(&self) -> &dyn Any {
970        self
971    }
972
973    fn as_any_mut(&mut self) -> &mut dyn Any {
974        self
975    }
976
977    fn eq(&self, other: &dyn ProtocolSim) -> bool {
978        if let Some(other_state) = other
979            .as_any()
980            .downcast_ref::<UniswapV4State>()
981        {
982            self.liquidity == other_state.liquidity &&
983                self.sqrt_price == other_state.sqrt_price &&
984                self.fees == other_state.fees &&
985                self.tick == other_state.tick &&
986                self.ticks == other_state.ticks
987        } else {
988            false
989        }
990    }
991}
992
993#[cfg(test)]
994mod tests {
995    use std::{collections::HashSet, fs, path::Path, str::FromStr};
996
997    use alloy::primitives::aliases::U24;
998    use num_traits::FromPrimitive;
999    use rstest::rstest;
1000    use serde_json::Value;
1001    use tycho_client::feed::{synchronizer::ComponentWithState, BlockHeader};
1002    use tycho_common::{models::Chain, simulation::protocol_sim::Price};
1003
1004    use super::*;
1005    use crate::{
1006        evm::{
1007            engine_db::{
1008                create_engine,
1009                simulation_db::SimulationDB,
1010                utils::{get_client, get_runtime},
1011            },
1012            protocol::{
1013                uniswap_v4::hooks::{
1014                    angstrom::hook_handler::{AngstromFees, AngstromHookHandler},
1015                    generic_vm_hook_handler::GenericVMHookHandler,
1016                },
1017                utils::uniswap::{lp_fee, sqrt_price_math::get_sqrt_price_q96},
1018            },
1019        },
1020        protocol::models::{DecoderContext, TryFromWithBlock},
1021    };
1022
1023    // Helper methods to create commonly used tokens
1024    fn usdc() -> Token {
1025        Token::new(
1026            &Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
1027            "USDC",
1028            6,
1029            0,
1030            &[Some(10_000)],
1031            Default::default(),
1032            100,
1033        )
1034    }
1035
1036    fn weth() -> Token {
1037        Token::new(
1038            &Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(),
1039            "WETH",
1040            18,
1041            0,
1042            &[Some(10_000)],
1043            Default::default(),
1044            100,
1045        )
1046    }
1047
1048    fn eth() -> Token {
1049        Token::new(
1050            &Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(),
1051            "ETH",
1052            18,
1053            0,
1054            &[Some(10_000)],
1055            Default::default(),
1056            100,
1057        )
1058    }
1059
1060    fn token_x() -> Token {
1061        Token::new(
1062            &Bytes::from_str("0x0000000000000000000000000000000000000001").unwrap(),
1063            "X",
1064            18,
1065            0,
1066            &[Some(10_000)],
1067            Default::default(),
1068            100,
1069        )
1070    }
1071
1072    fn token_y() -> Token {
1073        Token::new(
1074            &Bytes::from_str("0x0000000000000000000000000000000000000002").unwrap(),
1075            "Y",
1076            18,
1077            0,
1078            &[Some(10_000)],
1079            Default::default(),
1080            100,
1081        )
1082    }
1083
1084    #[test]
1085    fn test_delta_transition() {
1086        let mut pool = UniswapV4State::new(
1087            1000,
1088            U256::from_str("1000").unwrap(),
1089            UniswapV4Fees { zero_for_one: 100, one_for_zero: 90, lp_fee: 700 },
1090            100,
1091            60,
1092            vec![TickInfo::new(120, 10000).unwrap(), TickInfo::new(180, -10000).unwrap()],
1093        )
1094        .unwrap();
1095
1096        let attributes: HashMap<String, Bytes> = [
1097            ("liquidity".to_string(), Bytes::from(2000_u64.to_be_bytes().to_vec())),
1098            ("sqrt_price_x96".to_string(), Bytes::from(1001_u64.to_be_bytes().to_vec())),
1099            ("tick".to_string(), Bytes::from(120_i32.to_be_bytes().to_vec())),
1100            ("protocol_fees/zero2one".to_string(), Bytes::from(50_u32.to_be_bytes().to_vec())),
1101            ("protocol_fees/one2zero".to_string(), Bytes::from(75_u32.to_be_bytes().to_vec())),
1102            ("fee".to_string(), Bytes::from(100_u32.to_be_bytes().to_vec())),
1103            ("ticks/-120/net_liquidity".to_string(), Bytes::from(10200_u64.to_be_bytes().to_vec())),
1104            ("ticks/120/net_liquidity".to_string(), Bytes::from(9800_u64.to_be_bytes().to_vec())),
1105            ("block_number".to_string(), Bytes::from(2000_u64.to_be_bytes().to_vec())),
1106            ("block_timestamp".to_string(), Bytes::from(1758201935_u64.to_be_bytes().to_vec())),
1107        ]
1108        .into_iter()
1109        .collect();
1110
1111        let delta = ProtocolStateDelta {
1112            component_id: "State1".to_owned(),
1113            updated_attributes: attributes,
1114            deleted_attributes: HashSet::new(),
1115        };
1116
1117        pool.delta_transition(delta, &HashMap::new(), &Balances::default())
1118            .unwrap();
1119
1120        assert_eq!(pool.liquidity, 2000);
1121        assert_eq!(pool.sqrt_price, U256::from(1001));
1122        assert_eq!(pool.tick, 120);
1123        assert_eq!(pool.fees.zero_for_one, 50);
1124        assert_eq!(pool.fees.one_for_zero, 75);
1125        assert_eq!(pool.fees.lp_fee, 100);
1126        assert_eq!(
1127            pool.ticks
1128                .get_tick(-120)
1129                .unwrap()
1130                .net_liquidity,
1131            10200
1132        );
1133        assert_eq!(
1134            pool.ticks
1135                .get_tick(120)
1136                .unwrap()
1137                .net_liquidity,
1138            9800
1139        );
1140    }
1141
1142    #[tokio::test]
1143    /// Compares a quote from the UniswapV4 Quoter contract on Sepolia with a simulation.
1144    async fn test_swap_sim() {
1145        use tycho_client::feed::dto;
1146        let project_root = env!("CARGO_MANIFEST_DIR");
1147        let asset_path = Path::new(project_root)
1148            .join("tests/assets/decoder/uniswap_v4_snapshot_sepolia_block_7239119.json");
1149        let json_data = fs::read_to_string(asset_path).expect("Failed to read test asset");
1150        let data: Value = serde_json::from_str(&json_data).expect("Failed to parse JSON");
1151        let state: ComponentWithState = serde_json::from_value::<dto::ComponentWithState>(data)
1152            .expect("Expected json to match ComponentWithState structure")
1153            .into();
1154
1155        let block = BlockHeader {
1156            number: 7239119,
1157            hash: Bytes::from_str(
1158                "0x28d41d40f2ac275a4f5f621a636b9016b527d11d37d610a45ac3a821346ebf8c",
1159            )
1160            .expect("Invalid block hash"),
1161            parent_hash: Bytes::from(vec![0; 32]),
1162            ..Default::default()
1163        };
1164
1165        let t0 = Token::new(
1166            &Bytes::from_str("0x647e32181a64f4ffd4f0b0b4b052ec05b277729c").unwrap(),
1167            "T0",
1168            18,
1169            0,
1170            &[Some(10_000)],
1171            Chain::Ethereum,
1172            100,
1173        );
1174        let t1 = Token::new(
1175            &Bytes::from_str("0xe390a1c311b26f14ed0d55d3b0261c2320d15ca5").unwrap(),
1176            "T1",
1177            18,
1178            0,
1179            &[Some(10_000)],
1180            Chain::Ethereum,
1181            100,
1182        );
1183
1184        let all_tokens = [t0.clone(), t1.clone()]
1185            .iter()
1186            .map(|t| (t.address.clone(), t.clone()))
1187            .collect();
1188
1189        let usv4_state = UniswapV4State::try_from_with_header(
1190            state,
1191            block,
1192            &Default::default(),
1193            &all_tokens,
1194            &DecoderContext::new(),
1195        )
1196        .await
1197        .unwrap();
1198
1199        let res = usv4_state
1200            .get_amount_out(BigUint::from_u64(1000000000000000000).unwrap(), &t0, &t1)
1201            .unwrap();
1202
1203        let expected_amount = BigUint::from(9999909699895_u64);
1204        assert_eq!(res.amount, expected_amount);
1205    }
1206
1207    #[tokio::test]
1208    async fn test_get_limits() {
1209        use tycho_client::feed::dto;
1210        let block = BlockHeader {
1211            number: 22689129,
1212            hash: Bytes::from_str(
1213                "0x7763ea30d11aef68da729b65250c09a88ad00458c041064aad8c9a9dbf17adde",
1214            )
1215            .expect("Invalid block hash"),
1216            parent_hash: Bytes::from(vec![0; 32]),
1217            ..Default::default()
1218        };
1219
1220        let project_root = env!("CARGO_MANIFEST_DIR");
1221        let asset_path =
1222            Path::new(project_root).join("tests/assets/decoder/uniswap_v4_snapshot.json");
1223        let json_data = fs::read_to_string(asset_path).expect("Failed to read test asset");
1224        let data: Value = serde_json::from_str(&json_data).expect("Failed to parse JSON");
1225        let state: ComponentWithState = serde_json::from_value::<dto::ComponentWithState>(data)
1226            .expect("Expected json to match ComponentWithState structure")
1227            .into();
1228
1229        let t0 = Token::new(
1230            &Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap(),
1231            "WBTC",
1232            8,
1233            0,
1234            &[Some(10_000)],
1235            Chain::Ethereum,
1236            100,
1237        );
1238        let t1 = Token::new(
1239            &Bytes::from_str("0xdac17f958d2ee523a2206206994597c13d831ec7").unwrap(),
1240            "USDT",
1241            6,
1242            0,
1243            &[Some(10_000)],
1244            Chain::Ethereum,
1245            100,
1246        );
1247
1248        let all_tokens = [t0.clone(), t1.clone()]
1249            .iter()
1250            .map(|t| (t.address.clone(), t.clone()))
1251            .collect();
1252
1253        let usv4_state = UniswapV4State::try_from_with_header(
1254            state,
1255            block,
1256            &Default::default(),
1257            &all_tokens,
1258            &DecoderContext::new(),
1259        )
1260        .await
1261        .unwrap();
1262
1263        let res = usv4_state
1264            .get_limits(t0.address.clone(), t1.address.clone())
1265            .unwrap();
1266
1267        assert_eq!(&res.0, &BigUint::from_u128(71698353688830259750744466706).unwrap());
1268
1269        let out = usv4_state
1270            .get_amount_out(res.0, &t0, &t1)
1271            .expect("swap for limit in didn't work");
1272
1273        assert_eq!(&res.1, &out.amount);
1274    }
1275    #[test]
1276    fn test_get_amount_out_no_hook() {
1277        // Test using transaction 0x78ea4bbb7d4405000f33fdf6f3fa08b5e557d50e5e7f826a79766d50bd643b6f
1278
1279        // Pool ID: 0x00b9edc1583bf6ef09ff3a09f6c23ecb57fd7d0bb75625717ec81eed181e22d7
1280        // Information taken from Tenderly simulation / event emitted on Etherscan
1281        let usv4_state = UniswapV4State::new(
1282            541501951282951892,
1283            U256::from_str("5362798333066270795901222").unwrap(), // Sqrt price
1284            UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 100 },
1285            -192022,
1286            1,
1287            // Ticks taken from indexer logs
1288            vec![
1289                TickInfo {
1290                    index: -887272,
1291                    net_liquidity: 460382969070005,
1292                    sqrt_price: U256::from(4295128739_u64),
1293                },
1294                TickInfo {
1295                    index: -207244,
1296                    net_liquidity: 561268407024557,
1297                    sqrt_price: U256::from_str("2505291706254206075074035").unwrap(),
1298                },
1299                TickInfo {
1300                    index: -196411,
1301                    net_liquidity: 825711941800452,
1302                    sqrt_price: U256::from_str("4306080513146952705853399").unwrap(),
1303                },
1304                TickInfo {
1305                    index: -196257,
1306                    net_liquidity: 64844666874010,
1307                    sqrt_price: U256::from_str("4339363644587371378270009").unwrap(),
1308                },
1309                TickInfo {
1310                    index: -195611,
1311                    net_liquidity: 2344045150766798,
1312                    sqrt_price: U256::from_str("4481806029599743916020126").unwrap(),
1313                },
1314                TickInfo {
1315                    index: -194715,
1316                    net_liquidity: 391037380558274654,
1317                    sqrt_price: U256::from_str("4687145946111116896040494").unwrap(),
1318                },
1319                TickInfo {
1320                    index: -194599,
1321                    net_liquidity: 89032603464508,
1322                    sqrt_price: U256::from_str("4714409015946702405379370").unwrap(),
1323                },
1324                TickInfo {
1325                    index: -194389,
1326                    net_liquidity: 66635600426483168,
1327                    sqrt_price: U256::from_str("4764168603367683402636621").unwrap(),
1328                },
1329                TickInfo {
1330                    index: -194160,
1331                    net_liquidity: 6123093436523361,
1332                    sqrt_price: U256::from_str("4819029067726467394386780").unwrap(),
1333                },
1334                TickInfo {
1335                    index: -194025,
1336                    net_liquidity: 79940813798964,
1337                    sqrt_price: U256::from_str("4851665907541490407930032").unwrap(),
1338                },
1339                TickInfo {
1340                    index: -193922,
1341                    net_liquidity: 415630967437234,
1342                    sqrt_price: U256::from_str("4876715181040466809166531").unwrap(),
1343                },
1344                TickInfo {
1345                    index: -193876,
1346                    net_liquidity: 9664144015186047,
1347                    sqrt_price: U256::from_str("4887943972687250473582419").unwrap(),
1348                },
1349                TickInfo {
1350                    index: -193818,
1351                    net_liquidity: 435344726052344,
1352                    sqrt_price: U256::from_str("4902138873132735049121973").unwrap(),
1353                },
1354                TickInfo {
1355                    index: -193804,
1356                    net_liquidity: 221726179374067,
1357                    sqrt_price: U256::from_str("4905571399964683340605904").unwrap(),
1358                },
1359                TickInfo {
1360                    index: -193719,
1361                    net_liquidity: 101340835774487,
1362                    sqrt_price: U256::from_str("4926463397882393957462188").unwrap(),
1363                },
1364                TickInfo {
1365                    index: -193690,
1366                    net_liquidity: 193367475630077,
1367                    sqrt_price: U256::from_str("4933611593595025190448924").unwrap(),
1368                },
1369                TickInfo {
1370                    index: -193643,
1371                    net_liquidity: 357016631583746,
1372                    sqrt_price: U256::from_str("4945218633428068823432932").unwrap(),
1373                },
1374                TickInfo {
1375                    index: -193520,
1376                    net_liquidity: 917243184365178,
1377                    sqrt_price: U256::from_str("4975723910367862081017120").unwrap(),
1378                },
1379                TickInfo {
1380                    index: -193440,
1381                    net_liquidity: 114125890211958292,
1382                    sqrt_price: U256::from_str("4995665665861492533686137").unwrap(),
1383                },
1384                TickInfo {
1385                    index: -193380,
1386                    net_liquidity: -65980729148766579,
1387                    sqrt_price: U256::from_str("5010674414300823856025303").unwrap(),
1388                },
1389                TickInfo {
1390                    index: -192891,
1391                    net_liquidity: 1687883551433195,
1392                    sqrt_price: U256::from_str("5134689105039642314202223").unwrap(),
1393                },
1394                TickInfo {
1395                    index: -192573,
1396                    net_liquidity: 11108903221360975,
1397                    sqrt_price: U256::from_str("5216979018647067786855495").unwrap(),
1398                },
1399                TickInfo {
1400                    index: -192448,
1401                    net_liquidity: 32888457482352,
1402                    sqrt_price: U256::from_str("5249685603828944002327927").unwrap(),
1403                },
1404                TickInfo {
1405                    index: -191525,
1406                    net_liquidity: -221726179374067,
1407                    sqrt_price: U256::from_str("5497623359964843320146512").unwrap(),
1408                },
1409                TickInfo {
1410                    index: -191447,
1411                    net_liquidity: -32888457482352,
1412                    sqrt_price: U256::from_str("5519104878745833608097296").unwrap(),
1413                },
1414                TickInfo {
1415                    index: -191444,
1416                    net_liquidity: -114125890211958292,
1417                    sqrt_price: U256::from_str("5519932765173943847315221").unwrap(),
1418                },
1419                TickInfo {
1420                    index: -191417,
1421                    net_liquidity: -101340835774487,
1422                    sqrt_price: U256::from_str("5527389333636021285046380").unwrap(),
1423                },
1424                TickInfo {
1425                    index: -191384,
1426                    net_liquidity: -9664144015186047,
1427                    sqrt_price: U256::from_str("5536516597603056457376182").unwrap(),
1428                },
1429                TickInfo {
1430                    index: -191148,
1431                    net_liquidity: -561268407024557,
1432                    sqrt_price: U256::from_str("5602231161238705865493165").unwrap(),
1433                },
1434                TickInfo {
1435                    index: -191147,
1436                    net_liquidity: -1687883551433195,
1437                    sqrt_price: U256::from_str("5602511265794328966803451").unwrap(),
1438                },
1439                TickInfo {
1440                    index: -191091,
1441                    net_liquidity: -89032603464508,
1442                    sqrt_price: U256::from_str("5618219493196441347292357").unwrap(),
1443                },
1444                TickInfo {
1445                    index: -190950,
1446                    net_liquidity: -189177935487638,
1447                    sqrt_price: U256::from_str("5657965894785859782969011").unwrap(),
1448                },
1449                TickInfo {
1450                    index: -190756,
1451                    net_liquidity: -6123093436523361,
1452                    sqrt_price: U256::from_str("5713112435031881967192022").unwrap(),
1453                },
1454                TickInfo {
1455                    index: -190548,
1456                    net_liquidity: -193367475630077,
1457                    sqrt_price: U256::from_str("5772835841671084402427710").unwrap(),
1458                },
1459                TickInfo {
1460                    index: -190430,
1461                    net_liquidity: -11108903221360975,
1462                    sqrt_price: U256::from_str("5806994534290341208820930").unwrap(),
1463                },
1464                TickInfo {
1465                    index: -190195,
1466                    net_liquidity: -391583014714302569,
1467                    sqrt_price: U256::from_str("5875625707132601785181387").unwrap(),
1468                },
1469                TickInfo {
1470                    index: -190043,
1471                    net_liquidity: -357016631583746,
1472                    sqrt_price: U256::from_str("5920448331650864936739481").unwrap(),
1473                },
1474                TickInfo {
1475                    index: -189779,
1476                    net_liquidity: -917243184365178,
1477                    sqrt_price: U256::from_str("5999112356918485175181346").unwrap(),
1478                },
1479                TickInfo {
1480                    index: -189663,
1481                    net_liquidity: -2344045150766798,
1482                    sqrt_price: U256::from_str("6034006559279282606084981").unwrap(),
1483                },
1484                TickInfo {
1485                    index: -189620,
1486                    net_liquidity: -435344726052344,
1487                    sqrt_price: U256::from_str("6046992979471024289177519").unwrap(),
1488                },
1489                TickInfo {
1490                    index: -189409,
1491                    net_liquidity: -825711941800452,
1492                    sqrt_price: U256::from_str("6111123241285165242130911").unwrap(),
1493                },
1494                TickInfo {
1495                    index: -189325,
1496                    net_liquidity: -3947182209207,
1497                    sqrt_price: U256::from_str("6136842645893819031257990").unwrap(),
1498                },
1499                TickInfo {
1500                    index: -189324,
1501                    net_liquidity: -415630967437234,
1502                    sqrt_price: U256::from_str("6137149480355443943537284").unwrap(),
1503                },
1504                TickInfo {
1505                    index: -115136,
1506                    net_liquidity: 462452451821,
1507                    sqrt_price: U256::from_str("250529060232794967902094762").unwrap(),
1508                },
1509                TickInfo {
1510                    index: -92109,
1511                    net_liquidity: -462452451821,
1512                    sqrt_price: U256::from_str("792242363124136400178523925").unwrap(),
1513                },
1514                TickInfo {
1515                    index: 887272,
1516                    net_liquidity: -521280453734808,
1517                    sqrt_price: U256::from_str("1461446703485210103287273052203988822378723970342")
1518                        .unwrap(),
1519                },
1520            ],
1521        )
1522        .unwrap();
1523
1524        let t0 = usdc();
1525        let t1 = eth();
1526
1527        let out = usv4_state
1528            .get_amount_out(BigUint::from_u64(2000000).unwrap(), &t0, &t1)
1529            .unwrap();
1530
1531        assert_eq!(out.amount, BigUint::from_str("436478419853848").unwrap())
1532    }
1533
1534    #[test]
1535    fn test_get_amount_out_euler_hook() {
1536        // Test using transaction 0xb372306a81c6e840f4ec55f006da6b0b097f435802a2e6fd216998dd12fb4aca
1537        //
1538        // Output of beforeSwap:
1539        // "output":{
1540        //      "amountToSwap":"0"
1541        //      "hookReturn":"2520471492123673565794154180707800634502860978735"
1542        //      "lpFeeOverride":"0"
1543        // }
1544        //
1545        // Output of entire swap, including hooks:
1546        // "swapDelta":"-2520471491783391198873215717244426027071092767279"
1547        //
1548        // Get amount out:
1549        // "amountOut":"2681115183499232721"
1550
1551        let block = BlockHeader {
1552            number: 22689128,
1553            hash: Bytes::from_str(
1554                "0xfbfa716523d25d6d5248c18d001ca02b1caf10cabd1ab7321465e2262c41157b",
1555            )
1556            .expect("Invalid block hash"),
1557            timestamp: 1749739055,
1558            ..Default::default()
1559        };
1560
1561        // Pool ID: 0xdd8dd509e58ec98631b800dd6ba86ee569c517ffbd615853ed5ab815bbc48ccb
1562        // Information taken from Tenderly simulation
1563        let mut usv4_state = UniswapV4State::new(
1564            0,
1565            U256::from_str("4295128740").unwrap(),
1566            UniswapV4Fees { zero_for_one: 100, one_for_zero: 90, lp_fee: 500 },
1567            0,
1568            1,
1569            vec![],
1570        )
1571        .unwrap();
1572
1573        let hook_address: Address = Address::from_str("0x69058613588536167ba0aa94f0cc1fe420ef28a8")
1574            .expect("Invalid hook address");
1575
1576        let db = SimulationDB::new(
1577            get_client(None).expect("Failed to create client"),
1578            get_runtime().expect("Failed to get runtime"),
1579            Some(block.clone()),
1580        );
1581        let engine = create_engine(db, true).expect("Failed to create simulation engine");
1582        let pool_manager = Address::from_str("0x000000000004444c5dc75cb358380d2e3de08a90")
1583            .expect("Invalid pool manager address");
1584
1585        let hook_handler = GenericVMHookHandler::new(
1586            hook_address,
1587            engine,
1588            pool_manager,
1589            HashMap::new(),
1590            HashMap::new(),
1591            None,
1592            true, // Euler hook
1593        )
1594        .unwrap();
1595
1596        let t0 = usdc();
1597        let t1 = weth();
1598
1599        usv4_state.set_hook_handler(Box::new(hook_handler));
1600        let out = usv4_state
1601            .get_amount_out(BigUint::from_u64(7407000000).unwrap(), &t0, &t1)
1602            .unwrap();
1603
1604        assert_eq!(out.amount, BigUint::from_str("2681115183499232721").unwrap())
1605    }
1606
1607    #[test]
1608    fn test_get_amount_out_angstrom_hook() {
1609        // Test using transaction 0x671b8e1d0966cee520dc2bb9628de8e22a17b036e70077504796d0a476932d21
1610        let mut usv4_state = UniswapV4State::new(
1611            // Liquidity and tick taken from tycho indexer for same block as transaction
1612            66319800403673162,
1613            U256::from_str("1314588940601923011323000261788004").unwrap(),
1614            // 8388608 (i.e. 0x800000) signifies a dynamic fee.
1615            UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 8388608 },
1616            194343,
1617            10,
1618            vec![
1619                TickInfo::new(-887270, 198117767801).unwrap(),
1620                TickInfo::new(191990, 24561988698695).unwrap(),
1621                TickInfo::new(192280, 2839631428751224).unwrap(),
1622                TickInfo::new(193130, 318786492813931).unwrap(),
1623                TickInfo::new(194010, 26209207141081).unwrap(),
1624                TickInfo::new(194210, -26209207141081).unwrap(),
1625                TickInfo::new(194220, 63136622375641511).unwrap(),
1626                TickInfo::new(194420, -63136622375641511).unwrap(),
1627                TickInfo::new(195130, -318786492813931).unwrap(),
1628                TickInfo::new(196330, -2839631428751224).unwrap(),
1629                TickInfo::new(197100, -24561988698695).unwrap(),
1630                TickInfo::new(887270, -198117767801).unwrap(),
1631            ],
1632        )
1633        .unwrap();
1634
1635        let fees = AngstromFees {
1636            // To get these values, enable storage access logs on tenderly,
1637            // and look at the hex value retrieved right after calling afterSwap
1638            //
1639            // The value (hex: 0x70000152) contains two packed uint24 values:
1640            // Lower 24 bits (unlockedFee):         0x152   = 338
1641            // Upper 24 bits (protocolUnlockedFee): 0x70    = 112
1642            unlock: U24::from(338),
1643            protocol_unlock: U24::from(112),
1644        };
1645        let hook_handler = AngstromHookHandler::new(
1646            Address::from_str("0x0000000aa232009084bd71a5797d089aa4edfad4").unwrap(),
1647            Address::from_str("0x000000000004444c5dc75cb358380d2e3de08a90").unwrap(),
1648            fees,
1649            false,
1650        );
1651
1652        let t0 = usdc();
1653        let t1 = weth();
1654
1655        usv4_state.set_hook_handler(Box::new(hook_handler));
1656        let out = usv4_state
1657            .get_amount_out(
1658                BigUint::from_u64(
1659                    6645198144, // usdc
1660                )
1661                .unwrap(),
1662                &t0, // usdc IN
1663                &t1, // weth OUT
1664            )
1665            .unwrap();
1666
1667        assert_eq!(out.amount, BigUint::from_str("1825627051870330472").unwrap())
1668    }
1669
1670    #[test]
1671    fn test_spot_price_with_recoverable_error() {
1672        // Test that spot_price correctly falls back to swap-based calculation
1673        // when a RecoverableError (other than "not implemented") is returned
1674
1675        let usv4_state = UniswapV4State::new(
1676            1000000000000000000u128,                                  // 1e18 liquidity
1677            U256::from_str("79228162514264337593543950336").unwrap(), // 1:1 price
1678            UniswapV4Fees { zero_for_one: 100, one_for_zero: 100, lp_fee: 100 },
1679            0,
1680            60,
1681            vec![
1682                TickInfo::new(-600, 500000000000000000i128).unwrap(),
1683                TickInfo::new(600, -500000000000000000i128).unwrap(),
1684            ],
1685        )
1686        .unwrap();
1687
1688        // Test spot price calculation without a hook (should use default implementation)
1689        let spot_price_result = usv4_state.spot_price(&usdc(), &weth());
1690        assert!(spot_price_result.is_ok());
1691
1692        // The price should be approximately 1.0 (since we set sqrt_price for 1:1)
1693        // Adjusting for decimals difference (USDC has 6, WETH has 18)
1694        let price = spot_price_result.unwrap();
1695        assert!(price > 0.0);
1696    }
1697
1698    #[test]
1699    fn test_get_limits_with_hook_managed_liquidity_no_ranges_entrypoint() {
1700        // This test demonstrates the experimental limit finding logic for hooks that:
1701        // 1. Manage liquidity (pool has no liquidity & no ticks)
1702        // 2. Don't have get_amount_ranges entrypoint
1703
1704        let block = BlockHeader {
1705            number: 22689128,
1706            hash: Bytes::from_str(
1707                "0xfbfa716523d25d6d5248c18d001ca02b1caf10cabd1ab7321465e2262c41157b",
1708            )
1709            .expect("Invalid block hash"),
1710            timestamp: 1749739055,
1711            ..Default::default()
1712        };
1713
1714        let hook_address: Address = Address::from_str("0x69058613588536167ba0aa94f0cc1fe420ef28a8")
1715            .expect("Invalid hook address");
1716
1717        let db = SimulationDB::new(
1718            get_client(None).expect("Failed to create client"),
1719            get_runtime().expect("Failed to get runtime"),
1720            Some(block.clone()),
1721        );
1722        let engine = create_engine(db, true).expect("Failed to create simulation engine");
1723        let pool_manager = Address::from_str("0x000000000004444c5dc75cb358380d2e3de08a90")
1724            .expect("Invalid pool manager address");
1725
1726        // Create a GenericVMHookHandler without limits_entrypoint
1727        // This will trigger the "not set" error path and use experimental limit finding
1728        let hook_handler = GenericVMHookHandler::new(
1729            hook_address,
1730            engine,
1731            pool_manager,
1732            HashMap::new(),
1733            HashMap::new(),
1734            None,
1735            true, // Euler hook
1736        )
1737        .unwrap();
1738
1739        // Create a UniswapV4State with NO liquidity and NO ticks (hook manages all liquidity)
1740        let mut usv4_state = UniswapV4State::new(
1741            0, // no liquidity - hook provides it
1742            U256::from_str("4295128740").unwrap(),
1743            UniswapV4Fees { zero_for_one: 100, one_for_zero: 90, lp_fee: 500 },
1744            0,      // current tick
1745            1,      // tick spacing
1746            vec![], // no ticks - hook manages liquidity
1747        )
1748        .unwrap();
1749
1750        usv4_state.set_hook_handler(Box::new(hook_handler));
1751
1752        let token_in = usdc().address;
1753        let token_out = weth().address;
1754
1755        let (amount_in_limit, amount_out_limit) = usv4_state
1756            .get_limits(token_in, token_out)
1757            .expect("Should find limits through experimental swapping");
1758
1759        // Assuming pool supply doesn't change drastically at time of this test
1760        // At least 1 million USDC, not more than 100 million USDC
1761        assert!(amount_in_limit > BigUint::from(10u64).pow(12));
1762        assert!(amount_in_limit < BigUint::from(10u64).pow(14));
1763
1764        // At least 100 ETH, not more than 10 000 ETH
1765        assert!(amount_out_limit > BigUint::from(10u64).pow(20));
1766        assert!(amount_out_limit < BigUint::from(10u64).pow(22));
1767    }
1768
1769    #[rstest]
1770    #[case::high_liquidity(u128::MAX / 2)] // Very large liquidity
1771    #[case::medium_liquidity(10000000000000000000u128)] // Moderate liquidity: 10e18
1772    #[case::minimal_liquidity(1000u128)] // Very small liquidity
1773    fn test_find_max_amount(#[case] liquidity: u128) {
1774        // Use fixed configuration for all test cases
1775        let fees = UniswapV4Fees { zero_for_one: 100, one_for_zero: 100, lp_fee: 100 };
1776        let tick_spacing = 60;
1777        let ticks = vec![
1778            TickInfo::new(-600, (liquidity / 4) as i128).unwrap(),
1779            TickInfo::new(600, -((liquidity / 4) as i128)).unwrap(),
1780        ];
1781
1782        let usv4_state = UniswapV4State::new(
1783            liquidity,
1784            U256::from_str("79228162514264337593543950336").unwrap(),
1785            fees,
1786            0,
1787            tick_spacing,
1788            ticks,
1789        )
1790        .unwrap();
1791
1792        let token_in = usdc();
1793        let token_out = weth();
1794
1795        let (max_amount_in, _max_amount_out) = usv4_state
1796            .find_max_amount(&token_in, &token_out)
1797            .unwrap();
1798
1799        let success = usv4_state
1800            .get_amount_out(max_amount_in.clone(), &token_in, &token_out)
1801            .is_ok();
1802        assert!(success, "Should be able to swap the exact max amount.");
1803
1804        let one_more = &max_amount_in + BigUint::from(1u64);
1805        let should_fail = usv4_state
1806            .get_amount_out(one_more, &token_in, &token_out)
1807            .is_err();
1808        assert!(should_fail, "Swapping max_amount + 1 should fail.");
1809    }
1810
1811    #[test]
1812    fn test_calculate_swap_fees_with_override() {
1813        // Test that calculate_swap_fees_pips works correctly with overridden fees
1814        let fees = UniswapV4Fees::new(100, 90, 500);
1815
1816        // Without override, should use UniswapV4 formula: protocol + lp - (protocol * lp /
1817        // 1_000_000)
1818        let total_zero_for_one = fees.calculate_swap_fees_pips(true, None);
1819        // 100 + 500 - (100 * 500 / 1_000_000) = 600 - 0 = 600 (rounded down)
1820        assert_eq!(total_zero_for_one, 600);
1821
1822        // With override, should use override fee + protocol fee with same formula
1823        let total_with_override = fees.calculate_swap_fees_pips(true, Some(1000));
1824        // 100 + 1000 - (100 * 1000 / 1_000_000) = 1100 - 0 = 1100 (rounded down)
1825        assert_eq!(total_with_override, 1100);
1826    }
1827
1828    #[test]
1829    fn test_max_combined_fees_stays_valid() {
1830        // Test that even with max protocol + max LP fees, we stay under compute_swap_step limit
1831        let fees = UniswapV4Fees::new(1000, 1000, 1000);
1832        let total = fees.calculate_swap_fees_pips(true, Some(lp_fee::MAX_LP_FEE));
1833
1834        // Using UniswapV4 formula: 1000 + 1000000 - (1000 * 1000000 / 1_000_000)
1835        // = 1001000 - 1000 = 1000000
1836        assert_eq!(total, 1_000_000);
1837    }
1838
1839    #[test]
1840    fn test_get_limits_graceful_underflow() {
1841        // Verifies graceful handling of liquidity underflow in get_limits for V4
1842        let usv4_state = UniswapV4State::new(
1843            1000000,
1844            U256::from_str("79228162514264337593543950336").unwrap(), // 1:1 price
1845            UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 3000 },
1846            0,
1847            60,
1848            vec![
1849                // A tick with net_liquidity > current_liquidity
1850                // When zero_for_one=true, this gets negated and would cause underflow
1851                TickInfo {
1852                    index: -60,
1853                    net_liquidity: 2000000, // 2x current liquidity
1854                    sqrt_price: U256::from_str("79051508376726796163471739988").unwrap(),
1855                },
1856            ],
1857        )
1858        .unwrap();
1859
1860        let usdc = usdc();
1861        let weth = weth();
1862
1863        let (limit_in, limit_out) = usv4_state
1864            .get_limits(usdc.address.clone(), weth.address.clone())
1865            .unwrap();
1866
1867        // Should return some conservative limits
1868        assert!(limit_in > BigUint::zero());
1869        assert!(limit_out > BigUint::zero());
1870    }
1871
1872    // Tests based on Uniswap V4's ProtocolFeeLibrary.t.sol
1873    // See: https://github.com/Uniswap/v4-core/blob/main/test/libraries/ProtocolFeeLibrary.t.sol
1874
1875    /// Maximum protocol fee in pips (1000 = 0.1%)
1876    const MAX_PROTOCOL_FEE: u32 = 1000;
1877
1878    #[rstest]
1879    #[case::max_protocol_and_max_lp(MAX_PROTOCOL_FEE, lp_fee::MAX_LP_FEE, lp_fee::MAX_LP_FEE)]
1880    #[case::max_protocol_with_3000_lp(MAX_PROTOCOL_FEE, 3000, 3997)]
1881    #[case::max_protocol_with_zero_lp(MAX_PROTOCOL_FEE, 0, MAX_PROTOCOL_FEE)]
1882    #[case::zero_protocol_zero_lp(0, 0, 0)]
1883    #[case::zero_protocol_with_1000_lp(0, 1000, 1000)]
1884    fn test_calculate_swap_fees_uniswap_test_cases(
1885        #[case] protocol_fee: u32,
1886        #[case] lp_fee: u32,
1887        #[case] expected: u32,
1888    ) {
1889        let fees = UniswapV4Fees::new(protocol_fee, protocol_fee, lp_fee);
1890        let result = fees.calculate_swap_fees_pips(true, None);
1891        assert_eq!(result, expected);
1892    }
1893
1894    #[test]
1895    fn test_calculate_swap_fees_with_dynamic_fee() {
1896        // Test that dynamic fees default to 0 when no override is provided
1897        let fees = UniswapV4Fees::new(100, 90, lp_fee::DYNAMIC_FEE_FLAG);
1898
1899        // Without override, dynamic fee should be treated as 0
1900        let total_zero_for_one = fees.calculate_swap_fees_pips(true, None);
1901        // 100 + 0 - (100 * 0 / 1_000_000) = 100
1902        assert_eq!(total_zero_for_one, 100);
1903
1904        // With override, should use the override value
1905        let total_with_override = fees.calculate_swap_fees_pips(true, Some(500));
1906        // 100 + 500 - (100 * 500 / 1_000_000) = 600 - 0 = 600
1907        assert_eq!(total_with_override, 600);
1908    }
1909
1910    #[test]
1911    fn test_calculate_swap_fees_direction_matters() {
1912        // Test that zero_for_one direction affects which protocol fee is used
1913        let fees = UniswapV4Fees::new(100, 200, 500);
1914
1915        let zero_for_one_fee = fees.calculate_swap_fees_pips(true, None);
1916        // 100 + 500 - (100 * 500 / 1_000_000) = 600 - 0 = 600
1917        assert_eq!(zero_for_one_fee, 600);
1918
1919        let one_for_zero_fee = fees.calculate_swap_fees_pips(false, None);
1920        // 200 + 500 - (200 * 500 / 1_000_000) = 700 - 0 = 700
1921        assert_eq!(one_for_zero_fee, 700);
1922    }
1923
1924    #[rstest]
1925    #[case::high_lp_fee(1000, 500_000, 500_500)] // 1000 + 500k - 500 = 500.5k
1926    #[case::mid_fees(500, 500_000, 500_250)] // 500 + 500k - 250 = 500.25k
1927    #[case::low_fees(100, 100_000, 100_090)] // 100 + 100k - 10 = 100.09k
1928    fn test_calculate_swap_fees_formula_precision(
1929        #[case] protocol_fee: u32,
1930        #[case] lp_fee: u32,
1931        #[case] expected: u32,
1932    ) {
1933        // Test cases where the subtraction term (protocol * lp / 1M) significantly affects the
1934        // result
1935        let fees = UniswapV4Fees::new(protocol_fee, protocol_fee, lp_fee);
1936        let result = fees.calculate_swap_fees_pips(true, None);
1937        assert_eq!(result, expected, "Failed for protocol={}, lp={}", protocol_fee, lp_fee);
1938    }
1939
1940    #[test]
1941    fn test_calculate_swap_fees_override_takes_precedence() {
1942        // Test that lp_fee_override completely replaces stored lp_fee
1943        let fees = UniswapV4Fees::new(100, 100, 3000);
1944
1945        // With override, stored lp_fee should be ignored
1946        let result = fees.calculate_swap_fees_pips(true, Some(5000));
1947        // 100 + 5000 - (100 * 5000 / 1_000_000) = 5100 - 0 = 5100
1948        assert_eq!(result, 5100);
1949
1950        // Without override, should use stored lp_fee
1951        let result_no_override = fees.calculate_swap_fees_pips(true, None);
1952        // 100 + 3000 - (100 * 3000 / 1_000_000) = 3100 - 0 = 3100
1953        assert_eq!(result_no_override, 3100);
1954    }
1955
1956    #[test]
1957    fn test_calculate_swap_fees_zero_protocol_fee() {
1958        // When protocol fee is 0, formula simplifies to just lpFee
1959        let fees = UniswapV4Fees::new(0, 0, 3000);
1960        let result = fees.calculate_swap_fees_pips(true, None);
1961        // 0 + 3000 - (0 * 3000 / 1_000_000) = 3000
1962        assert_eq!(result, 3000);
1963    }
1964
1965    #[test]
1966    fn test_calculate_swap_fees_zero_lp_fee() {
1967        // When lp fee is 0, formula simplifies to just protocolFee
1968        let fees = UniswapV4Fees::new(500, 500, 0);
1969        let result = fees.calculate_swap_fees_pips(true, None);
1970        // 500 + 0 - (500 * 0 / 1_000_000) = 500
1971        assert_eq!(result, 500);
1972    }
1973
1974    // Helper to create a basic test pool for swap_to_price tests
1975    fn create_basic_v4_test_pool() -> UniswapV4State {
1976        let liquidity = 100_000_000_000_000_000_000u128; // 100e18
1977        let sqrt_price = get_sqrt_price_q96(U256::from(20_000_000u64), U256::from(10_000_000u64))
1978            .expect("Failed to calculate sqrt price");
1979        let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
1980
1981        let ticks = vec![TickInfo::new(0, 0).unwrap(), TickInfo::new(46080, 0).unwrap()];
1982
1983        UniswapV4State::new(
1984            liquidity,
1985            sqrt_price,
1986            UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 3000 }, // 0.3% fee
1987            tick,
1988            60, // tick spacing
1989            ticks,
1990        )
1991        .expect("Failed to create pool")
1992    }
1993
1994    fn create_tick_boundary_v4_test_pool() -> UniswapV4State {
1995        let sqrt_price = get_sqrt_ratio_at_tick(0).expect("Failed to calculate sqrt price");
1996        let ticks = vec![TickInfo::new(-120, 0).unwrap(), TickInfo::new(120, 0).unwrap()];
1997
1998        UniswapV4State::new(
1999            100_000_000_000_000_000_000u128,
2000            sqrt_price,
2001            UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 3000 },
2002            0,
2003            60,
2004            ticks,
2005        )
2006        .expect("Failed to create pool")
2007    }
2008
2009    #[test]
2010    fn test_partial_step_updates_tick_when_price_moves_without_crossing_initialized_tick() {
2011        let pool = create_tick_boundary_v4_test_pool();
2012        let amount = -I256::from_raw(U256::from(100_000_000_000_000_000u64));
2013
2014        let result = pool
2015            .swap(true, amount, None, None)
2016            .expect("swap should stay within the current liquidity range");
2017        let expected_tick =
2018            get_tick_at_sqrt_ratio(result.sqrt_price).expect("new sqrt price should map to a tick");
2019
2020        assert_ne!(result.sqrt_price, pool.sqrt_price);
2021        assert_ne!(result.sqrt_price, get_sqrt_ratio_at_tick(-120).unwrap());
2022        assert_ne!(expected_tick, pool.tick);
2023        assert_eq!(result.tick, expected_tick);
2024    }
2025
2026    #[test]
2027    fn test_swap_keeps_boundary_tick_when_price_does_not_move() {
2028        let mut pool = create_tick_boundary_v4_test_pool();
2029        pool.tick = -1;
2030        let amount = -I256::from_raw(U256::from(1u64));
2031
2032        let result = pool
2033            .swap(true, amount, None, None)
2034            .expect("swap should consume the input as fee without moving price");
2035
2036        assert_eq!(result.sqrt_price, pool.sqrt_price);
2037        assert_eq!(get_tick_at_sqrt_ratio(result.sqrt_price).unwrap(), 0);
2038        assert_eq!(result.tick, pool.tick);
2039    }
2040
2041    #[test]
2042    fn test_swap_to_price_price_too_high() {
2043        let pool = create_basic_v4_test_pool();
2044
2045        let token_x = token_x();
2046        let token_y = token_y();
2047
2048        // Price far above pool price - should return zero
2049        let target_price = Price::new(BigUint::from(10_000_000u64), BigUint::from(1_000_000u64));
2050
2051        let result = pool.query_pool_swap(&QueryPoolSwapParams::new(
2052            token_x,
2053            token_y,
2054            SwapConstraint::PoolTargetPrice {
2055                target: target_price,
2056                tolerance: 0f64,
2057                min_amount_in: None,
2058                max_amount_in: None,
2059            },
2060        ));
2061        assert!(result.is_err(), "Should return error when target price is unreachable");
2062    }
2063
2064    #[test]
2065    fn test_swap_to_price_no_liquidity() {
2066        // Test that swap_to_price returns zero for pool with no liquidity
2067        let pool = UniswapV4State::new(
2068            0, // No liquidity
2069            U256::from_str("79228162514264337593543950336").unwrap(),
2070            UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 3000 },
2071            0,
2072            60,
2073            vec![],
2074        )
2075        .unwrap();
2076
2077        let token_x = token_x();
2078        let token_y = token_y();
2079
2080        let target_price = Price::new(BigUint::from(2_000_000u64), BigUint::from(1_000_000u64));
2081
2082        let pool_swap = pool.query_pool_swap(&QueryPoolSwapParams::new(
2083            token_x,
2084            token_y,
2085            SwapConstraint::PoolTargetPrice {
2086                target: target_price,
2087                tolerance: 0f64,
2088                min_amount_in: None,
2089                max_amount_in: None,
2090            },
2091        ));
2092
2093        assert!(pool_swap.is_err());
2094    }
2095
2096    #[test]
2097    fn test_swap_to_price_with_protocol_fees() {
2098        let liquidity = 100_000_000_000_000_000_000u128;
2099        let sqrt_price = get_sqrt_price_q96(U256::from(20_000_000u64), U256::from(10_000_000u64))
2100            .expect("Failed to calculate sqrt price");
2101        let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
2102
2103        let ticks = vec![TickInfo::new(0, 0).unwrap(), TickInfo::new(46080, 0).unwrap()];
2104
2105        // Create pool with different protocol fees for each direction
2106        let pool = UniswapV4State::new(
2107            liquidity,
2108            sqrt_price,
2109            UniswapV4Fees {
2110                zero_for_one: 1000, // 0.1% protocol fee for zero_for_one
2111                one_for_zero: 200,  // 0.02% protocol fee for one_for_zero
2112                lp_fee: 3000,       // 0.3% LP fee
2113            },
2114            tick,
2115            60,
2116            ticks,
2117        )
2118        .expect("Failed to create pool");
2119
2120        let token_x = token_x();
2121        let token_y = token_y();
2122
2123        // Pool at 2.0 Y/X = 0.5 X/Y, swap_to_price moves price DOWN to target
2124
2125        // Test zero_for_one direction (X -> Y, uses zero_for_one fee)
2126        let target_price = Price::new(BigUint::from(2_000_000u64), BigUint::from(1_010_000u64));
2127        let pool_swap_forward = pool
2128            .query_pool_swap(&QueryPoolSwapParams::new(
2129                token_x.clone(),
2130                token_y.clone(),
2131                SwapConstraint::PoolTargetPrice {
2132                    target: target_price,
2133                    tolerance: 0f64,
2134                    min_amount_in: None,
2135                    max_amount_in: None,
2136                },
2137            ))
2138            .expect("swap_to_price failed");
2139
2140        // Test one_for_zero direction (Y -> X, uses one_for_zero fee)
2141        let target_price_reverse =
2142            Price::new(BigUint::from(1_010_000u64), BigUint::from(2_040_000u64));
2143        let pool_swap_backward = pool
2144            .query_pool_swap(&QueryPoolSwapParams::new(
2145                token_y,
2146                token_x,
2147                SwapConstraint::PoolTargetPrice {
2148                    target: target_price_reverse,
2149                    tolerance: 0f64,
2150                    min_amount_in: None,
2151                    max_amount_in: None,
2152                },
2153            ))
2154            .expect("swap_to_price failed");
2155
2156        assert!(
2157            pool_swap_backward.amount_out().clone() > BigUint::ZERO,
2158            "One for zero swap should return non-zero output"
2159        );
2160
2161        // Higher fees require more volume to reach the same price target
2162        // trade_zfo has 0.1% protocol fee, trade_ofz has 0.02% protocol fee
2163        assert!(
2164            pool_swap_forward.amount_out() < pool_swap_backward.amount_in(),
2165            "Backward fees should be lower therefore backward swap should be bigger"
2166        );
2167        assert!(
2168            pool_swap_forward.amount_in() < pool_swap_backward.amount_out(),
2169            "Backward fees should be lower therefore backward swap should be bigger"
2170        );
2171    }
2172
2173    #[test]
2174    fn test_swap_to_price_different_targets() {
2175        // Test with various target prices using working format
2176        let pool = create_basic_v4_test_pool();
2177
2178        let token_x = token_x();
2179        let token_y = token_y();
2180
2181        // Pool at 2.0 Y/X (20M/10M)
2182        // Test 1: Target close to spot (1.98 Y/X)
2183        let target_price = Price::new(BigUint::from(2_000_000u64), BigUint::from(1_010_000u64));
2184        let pool_swap_close = pool
2185            .query_pool_swap(&QueryPoolSwapParams::new(
2186                token_x.clone(),
2187                token_y.clone(),
2188                SwapConstraint::PoolTargetPrice {
2189                    target: target_price,
2190                    tolerance: 0f64,
2191                    min_amount_in: None,
2192                    max_amount_in: None,
2193                },
2194            ))
2195            .expect("swap_to_price failed");
2196        assert!(
2197            *pool_swap_close.amount_out() > BigUint::ZERO,
2198            "Expected non-zero for 1.98 Y/X target"
2199        );
2200
2201        // Test 2: Target further from spot (1.90 Y/X)
2202        let target_price = Price::new(BigUint::from(1_900_000u64), BigUint::from(1_000_000u64));
2203        let pool_swap_below = pool
2204            .query_pool_swap(&QueryPoolSwapParams::new(
2205                token_x.clone(),
2206                token_y.clone(),
2207                SwapConstraint::PoolTargetPrice {
2208                    target: target_price,
2209                    tolerance: 0f64,
2210                    min_amount_in: None,
2211                    max_amount_in: None,
2212                },
2213            ))
2214            .expect("swap_to_price failed");
2215        assert!(
2216            pool_swap_below.amount_out().clone() > BigUint::ZERO,
2217            "Expected non-zero for 1.90 Y/X target"
2218        );
2219
2220        // Test 3: Target far from spot (1.5 Y/X)
2221        let target_price = Price::new(BigUint::from(1_500_000u64), BigUint::from(1_000_000u64));
2222        let pool_swap_far = pool
2223            .query_pool_swap(&QueryPoolSwapParams::new(
2224                token_x,
2225                token_y,
2226                SwapConstraint::PoolTargetPrice {
2227                    target: target_price,
2228                    tolerance: 0f64,
2229                    min_amount_in: None,
2230                    max_amount_in: None,
2231                },
2232            ))
2233            .expect("swap_to_price failed");
2234        assert!(
2235            pool_swap_far.amount_out().clone() > BigUint::ZERO,
2236            "Expected non-zero for 1.5 Y/X target"
2237        );
2238
2239        // Verify that further targets require more volume
2240        assert!(
2241            pool_swap_close.amount_out().clone() < pool_swap_below.amount_out().clone(),
2242            "Closer target (1.98 Y/X) should require less volume than medium target (1.90 Y/X). \
2243             Got close: {}, medium: {}",
2244            pool_swap_close.amount_out().clone(),
2245            pool_swap_below.amount_out().clone()
2246        );
2247        assert!(
2248            pool_swap_below.amount_out().clone() < pool_swap_far.amount_out().clone(),
2249            "Medium target (1.90 Y/X) should require less volume than far target (1.5 Y/X). \
2250             Got medium: {}, far: {}",
2251            pool_swap_below.amount_out().clone(),
2252            pool_swap_far.amount_out().clone()
2253        );
2254    }
2255
2256    #[test]
2257    fn test_swap_to_price_around_spot_price() {
2258        let liquidity = 10_000_000_000_000_000u128;
2259        let sqrt_price =
2260            get_sqrt_price_q96(U256::from(2_000_000_000u64), U256::from(1_000_000_000u64))
2261                .expect("Failed to calculate sqrt price");
2262        let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
2263
2264        let ticks = vec![TickInfo::new(0, 0).unwrap(), TickInfo::new(46080, 0).unwrap()];
2265
2266        // Use FeeAmount::Low equivalent (500 pips = 0.05%)
2267        let pool = UniswapV4State::new(
2268            liquidity,
2269            sqrt_price,
2270            UniswapV4Fees {
2271                zero_for_one: 0,
2272                one_for_zero: 0,
2273                lp_fee: 500, // 0.05% to match V3 FeeAmount::Low
2274            },
2275            tick,
2276            60,
2277            ticks,
2278        )
2279        .expect("Failed to create pool");
2280
2281        let token_x = token_x();
2282        let token_y = token_y();
2283
2284        // Test 1: Price just above spot price, too little to cover fees
2285        let target_price = Price::new(BigUint::from(1_999_750u64), BigUint::from(1_000_250u64));
2286
2287        let result = pool.query_pool_swap(&QueryPoolSwapParams::new(
2288            token_x.clone(),
2289            token_y.clone(),
2290            SwapConstraint::PoolTargetPrice {
2291                target: target_price,
2292                tolerance: 0f64,
2293                min_amount_in: None,
2294                max_amount_in: None,
2295            },
2296        ));
2297        assert!(result.is_err(), "Should return error when target price is unreachable");
2298
2299        // Test 2: Price far enough from spot prices to enable trading despite fees (0.1% lower)
2300        let target_price = Price::new(BigUint::from(1_999_000u64), BigUint::from(1_001_000u64));
2301
2302        let pool_swap = pool
2303            .query_pool_swap(&QueryPoolSwapParams::new(
2304                token_x,
2305                token_y,
2306                SwapConstraint::PoolTargetPrice {
2307                    target: target_price,
2308                    tolerance: 0f64,
2309                    min_amount_in: None,
2310                    max_amount_in: None,
2311                },
2312            ))
2313            .expect("swap_to_price failed");
2314
2315        // Should match V3 output exactly with same fees
2316        let expected_amount_out =
2317            BigUint::from_str("7062236922008").expect("Failed to parse expected value");
2318        assert_eq!(
2319            pool_swap.amount_out().clone(),
2320            expected_amount_out,
2321            "V4 should match V3 output with same fees (0.05%)"
2322        );
2323    }
2324
2325    #[test]
2326    fn test_swap_to_price_matches_get_amount_out() {
2327        let pool = create_basic_v4_test_pool();
2328
2329        let token_x = token_x();
2330        let token_y = token_y();
2331
2332        // Get the trade from swap_to_price
2333        let target_price = Price::new(BigUint::from(2_000_000u64), BigUint::from(1_010_000u64));
2334        let pool_swap = pool
2335            .query_pool_swap(&QueryPoolSwapParams::new(
2336                token_x.clone(),
2337                token_y.clone(),
2338                SwapConstraint::PoolTargetPrice {
2339                    target: target_price,
2340                    tolerance: 0f64,
2341                    min_amount_in: None,
2342                    max_amount_in: None,
2343                },
2344            ))
2345            .expect("swap_to_price failed");
2346        assert!(*pool_swap.amount_in() > BigUint::ZERO, "Amount in should be positive");
2347
2348        // Use the amount_in from swap_to_price with get_amount_out
2349        let result = pool
2350            .get_amount_out(pool_swap.amount_in().clone(), &token_x, &token_y)
2351            .expect("get_amount_out failed");
2352
2353        // The amount_out from get_amount_out should be close to swap_to_price's amount_out
2354        // Allow for small rounding differences
2355        assert!(result.amount > BigUint::ZERO);
2356        assert!(result.amount >= *pool_swap.amount_out());
2357    }
2358
2359    #[test]
2360    fn test_swap_to_price_basic() {
2361        let liquidity = 100_000_000_000_000_000_000u128;
2362        let sqrt_price = get_sqrt_price_q96(U256::from(20_000_000u64), U256::from(10_000_000u64))
2363            .expect("Failed to calculate sqrt price");
2364        let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
2365
2366        let ticks = vec![TickInfo::new(0, 0).unwrap(), TickInfo::new(46080, 0).unwrap()];
2367
2368        let pool = UniswapV4State::new(
2369            liquidity,
2370            sqrt_price,
2371            UniswapV4Fees {
2372                zero_for_one: 0,
2373                one_for_zero: 0,
2374                lp_fee: 3000, // 0.3% LP fee
2375            },
2376            tick,
2377            60,
2378            ticks,
2379        )
2380        .expect("Failed to create pool");
2381
2382        let token_x = token_x();
2383        let token_y = token_y();
2384
2385        // Target price: 2_000_000/1_010_000 ≈ 1.98 Y/X
2386        let target_price = Price::new(BigUint::from(2_000_000u64), BigUint::from(1_010_000u64));
2387
2388        let pool_swap = pool
2389            .query_pool_swap(&QueryPoolSwapParams::new(
2390                token_x,
2391                token_y,
2392                SwapConstraint::PoolTargetPrice {
2393                    target: target_price,
2394                    tolerance: 0f64,
2395                    min_amount_in: None,
2396                    max_amount_in: None,
2397                },
2398            ))
2399            .expect("swap_to_price failed");
2400
2401        // Should match V3's output exactly with same fees (0.3%)
2402        let expected_amount_in = BigUint::from_str("246739021727519745").unwrap();
2403        let expected_amount_out = BigUint::from_str("490291909043340795").unwrap();
2404
2405        assert_eq!(
2406            *pool_swap.amount_in(),
2407            expected_amount_in,
2408            "amount_in should match expected value"
2409        );
2410        assert_eq!(
2411            *pool_swap.amount_out(),
2412            expected_amount_out,
2413            "amount_out should match expected value"
2414        );
2415    }
2416
2417    #[test]
2418    fn test_swap_price_limit_out_of_range_returns_error() {
2419        let pool = create_basic_v4_test_pool();
2420        let amount = -I256::from_raw(U256::from(1000u64)); // V4 uses negative for exact input
2421
2422        // zero_for_one: price_limit equal to sqrt_price is invalid (must be strictly less)
2423        let result = pool.swap(true, amount, Some(pool.sqrt_price), None);
2424        assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
2425
2426        // zero_for_one: price_limit at MIN_SQRT_RATIO is invalid (must be strictly greater)
2427        let result = pool.swap(true, amount, Some(MIN_SQRT_RATIO), None);
2428        assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
2429
2430        // one_for_zero: price_limit equal to sqrt_price is invalid (must be strictly greater)
2431        let result = pool.swap(false, amount, Some(pool.sqrt_price), None);
2432        assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
2433
2434        // one_for_zero: price_limit at MAX_SQRT_RATIO is invalid (must be strictly less)
2435        let result = pool.swap(false, amount, Some(MAX_SQRT_RATIO), None);
2436        assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
2437    }
2438
2439    #[test]
2440    fn test_swap_at_extreme_price_returns_error() {
2441        // Simulates the depth calculation scenario: pool sqrt_price is at MIN_SQRT_RATIO + 1,
2442        // so the default price limit for zero_for_one equals sqrt_price and fails validation.
2443        let sqrt_price = MIN_SQRT_RATIO + U256::from(1u64);
2444        let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
2445        // tick_spacing 60; ticks must be aligned
2446        let aligned_tick = (MIN_TICK / 60) * 60 + 60; // first multiple of 60 above MIN_TICK
2447        let ticks = vec![
2448            TickInfo::new(aligned_tick, 0).unwrap(),
2449            TickInfo::new(aligned_tick + 60, 0).unwrap(),
2450        ];
2451        let pool = UniswapV4State::new(
2452            100_000_000_000_000_000_000u128,
2453            sqrt_price,
2454            UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 3000 },
2455            tick,
2456            60,
2457            ticks,
2458        )
2459        .unwrap();
2460
2461        let amount = -I256::from_raw(U256::from(1000u64));
2462        // Default price limit for zero_for_one is MIN_SQRT_RATIO + 1 == sqrt_price, so invalid
2463        let result = pool.swap(true, amount, None, None);
2464        assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
2465    }
2466}