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