Skip to main content

tycho_simulation/evm/protocol/uniswap_v4/
state.rs

1use std::{any::Any, collections::HashMap, fmt, sync::Arc};
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::{
24        protocol::{
25            clmm::clmm_swap_to_price,
26            safe_math::{safe_add_u256, safe_sub_u256},
27            u256_num::{u256_to_biguint, u256_to_f64},
28            uniswap_v4::hooks::{
29                hook_handler::HookHandler,
30                models::{
31                    AfterSwapParameters, BalanceDelta, BeforeSwapDelta, BeforeSwapParameters,
32                    StateContext, SwapParams,
33                },
34            },
35            utils::{
36                add_fee_markup,
37                uniswap::{
38                    i24_be_bytes_to_i32, liquidity_math,
39                    lp_fee::{self, is_dynamic},
40                    sqrt_price_math::{
41                        get_amount0_delta, get_amount1_delta, sqrt_price_q96_to_f64,
42                    },
43                    swap_math,
44                    tick_list::{TickInfo, TickList, TickListErrorKind},
45                    tick_math::{
46                        get_sqrt_ratio_at_tick, get_tick_at_sqrt_ratio, MAX_SQRT_RATIO, MAX_TICK,
47                        MIN_SQRT_RATIO, MIN_TICK,
48                    },
49                    StepComputation, SwapResults, SwapState,
50                },
51            },
52            vm::constants::EXTERNAL_ACCOUNT,
53        },
54        simulation::PendingOverrides,
55    },
56    impl_non_serializable_protocol,
57};
58
59// Fixed overhead per swap: covers router overhead, executor preamble (decode, sync,
60// unlock/callback pattern), and token transfer-in.
61const SWAP_BASE_GAS: u64 = 185_000;
62// Per loop: PoolManager.swap bitmap lookup + sqrt math + computeSwapStep.
63// V4's singleton PoolManager hits warmer storage than V3 standalone pools: ~3,500/loop.
64const GAS_PER_BITMAP_LOOKUP: u64 = 3_500;
65// Initialized tick crossing: _updateTick() updates feeGrowthOutside0/1 (2 SSTOREs).
66// Warm ≈ 10–17k, cold ≈ 40–52k. We use a blended estimate that
67// weights toward cold costs.
68const GAS_PER_TICK: u64 = 29_000;
69// Settlement overhead within swapExactInputSingle: _settle() + _getFullCredit() + misc.
70const V4_CALLBACK_SETTLEMENT_GAS: u64 = 30_000;
71// PoolManager Hooks.sol wrapper overhead per hook call: ABI encode params,
72// external CALL dispatch, decode return, validate selector, and process
73// the returned BeforeSwapDelta / AfterSwapDelta.
74const PM_PER_HOOK_CALL_OVERHEAD: u64 = 25_000;
75// Conservative max gas budget for a single swap (Ethereum transaction gas limit)
76const MAX_SWAP_GAS: u64 = 16_700_000;
77const MAX_TICKS_CROSSED: u64 = (MAX_SWAP_GAS - SWAP_BASE_GAS) / GAS_PER_TICK;
78// Decimal exponent of the output amount a hook is probed with to read off its fee rate.
79// Hooks floor each term of their fee, so the rate is only exact in the limit of a large amount;
80// 1e30 makes the rounding smaller than an f64 can hold and still leaves room under U256::MAX for
81// the hook's own intermediate products.
82const HOOK_FEE_PROBE_EXP: u64 = 30;
83
84#[derive(Clone)]
85pub struct UniswapV4State {
86    liquidity: u128,
87    sqrt_price: U256,
88    fees: UniswapV4Fees,
89    tick: i32,
90    ticks: TickList,
91    tick_spacing: i32,
92    pub hook: Option<Box<dyn HookHandler>>,
93    /// Storage and block environment a pending quote runs the hook under. `None` on confirmed
94    /// state; set only on the clones `apply_deltas_ephemeral` hands out.
95    pending_overrides: Option<Arc<PendingOverrides>>,
96}
97
98impl_non_serializable_protocol!(UniswapV4State, "not supported due vm state deps");
99
100impl fmt::Debug for UniswapV4State {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        f.debug_struct("UniswapV4State")
103            .field("liquidity", &self.liquidity)
104            .field("sqrt_price", &self.sqrt_price)
105            .field("fees", &self.fees)
106            .field("tick", &self.tick)
107            .field("tick_spacing", &self.tick_spacing)
108            .field("pending_overrides", &self.pending_overrides.is_some())
109            .finish_non_exhaustive()
110    }
111}
112
113impl PartialEq for UniswapV4State {
114    fn eq(&self, other: &Self) -> bool {
115        match (&self.hook, &other.hook) {
116            (Some(a), Some(b)) => a.is_equal(&**b),
117            (None, None) => true,
118            _ => false,
119        }
120    }
121}
122
123impl Eq for UniswapV4State {}
124
125#[derive(Clone, Debug, PartialEq, Eq)]
126pub struct UniswapV4Fees {
127    // Protocol fees in the zero for one direction
128    pub zero_for_one: u32,
129    // Protocol fees in the one for zero direction
130    pub one_for_zero: u32,
131    // Liquidity providers fees
132    pub lp_fee: u32,
133}
134
135impl UniswapV4Fees {
136    pub fn new(zero_for_one: u32, one_for_zero: u32, lp_fee: u32) -> Self {
137        Self { zero_for_one, one_for_zero, lp_fee }
138    }
139
140    fn calculate_swap_fees_pips(&self, zero_for_one: bool, lp_fee_override: Option<u32>) -> u32 {
141        let protocol_fee = if zero_for_one { self.zero_for_one } else { self.one_for_zero };
142        let lp_fee = lp_fee_override.unwrap_or_else(|| {
143            // If a protocol has dynamic fees,
144            if is_dynamic(self.lp_fee) {
145                0
146            } else {
147                self.lp_fee
148            }
149        });
150
151        // UniswapV4 formula: protocolFee + lpFee - (protocolFee * lpFee / 1_000_000)
152        // Source: https://raw.githubusercontent.com/Uniswap/v4-core/main/src/libraries/ProtocolFeeLibrary.sol
153        // This accounts for the fact that protocol fee is taken first, then LP fee applies to
154        // remainder
155        protocol_fee + lp_fee - ((protocol_fee as u64 * lp_fee as u64 / 1_000_000) as u32)
156    }
157}
158
159impl UniswapV4State {
160    /// Creates a new `UniswapV4State` with specified values.
161    pub fn new(
162        liquidity: u128,
163        sqrt_price: U256,
164        fees: UniswapV4Fees,
165        tick: i32,
166        tick_spacing: i32,
167        ticks: Vec<TickInfo>,
168    ) -> Result<Self, SimulationError> {
169        let tick_spacing_u16 = tick_spacing.try_into().map_err(|_| {
170            // even though it's given as int24, tick_spacing must be positive, see here:
171            // https://github.com/Uniswap/v4-core/blob/a22414e4d7c0d0b0765827fe0a6c20dfd7f96291/src/libraries/TickMath.sol#L25-L28
172            SimulationError::FatalError(format!(
173                "tick_spacing {} must be positive (int24 -> u16 conversion failed)",
174                tick_spacing
175            ))
176        })?;
177        let tick_list = TickList::from(tick_spacing_u16, ticks)?;
178        Ok(UniswapV4State {
179            liquidity,
180            sqrt_price,
181            fees,
182            tick,
183            ticks: tick_list,
184            tick_spacing,
185            hook: None,
186            pending_overrides: None,
187        })
188    }
189
190    pub fn pending_overrides(&self) -> Option<&PendingOverrides> {
191        self.pending_overrides.as_deref()
192    }
193
194    fn swap(
195        &self,
196        zero_for_one: bool,
197        amount_specified: I256,
198        sqrt_price_limit: Option<U256>,
199        lp_fee_override: Option<u32>,
200    ) -> Result<SwapResults, SimulationError> {
201        if amount_specified == I256::ZERO {
202            return Ok(SwapResults {
203                amount_calculated: I256::ZERO,
204                amount_specified: I256::ZERO,
205                amount_remaining: I256::ZERO,
206                sqrt_price: self.sqrt_price,
207                liquidity: self.liquidity,
208                tick: self.tick,
209                gas_used: U256::from(3_000), // baseline gas cost for no-op swap
210            });
211        }
212
213        if self.liquidity == 0 {
214            return Err(SimulationError::RecoverableError("No liquidity".to_string()));
215        }
216        let price_limit = if let Some(limit) = sqrt_price_limit {
217            limit
218        } else if zero_for_one {
219            safe_add_u256(MIN_SQRT_RATIO, U256::from(1u64))?
220        } else {
221            safe_sub_u256(MAX_SQRT_RATIO, U256::from(1u64))?
222        };
223
224        let price_limit_valid = if zero_for_one {
225            price_limit > MIN_SQRT_RATIO && price_limit < self.sqrt_price
226        } else {
227            price_limit < MAX_SQRT_RATIO && price_limit > self.sqrt_price
228        };
229        if !price_limit_valid {
230            return Err(SimulationError::InvalidInput("Price limit out of range".into(), None));
231        }
232
233        let exact_input = amount_specified < I256::ZERO;
234
235        let mut state = SwapState {
236            amount_remaining: amount_specified,
237            amount_calculated: I256::ZERO,
238            sqrt_price: self.sqrt_price,
239            tick: self.tick,
240            liquidity: self.liquidity,
241        };
242        let mut gas_used = U256::from(SWAP_BASE_GAS);
243
244        while state.amount_remaining != I256::ZERO && state.sqrt_price != price_limit {
245            let (mut next_tick, initialized) = match self
246                .ticks
247                .next_initialized_tick_within_one_word(state.tick, zero_for_one)
248            {
249                Ok((tick, init)) => {
250                    gas_used = safe_add_u256(gas_used, U256::from(GAS_PER_BITMAP_LOOKUP))?;
251                    (tick, init)
252                }
253                Err(tick_err) => match tick_err.kind {
254                    TickListErrorKind::TicksExeeded => {
255                        let mut new_state = self.clone();
256                        new_state.liquidity = state.liquidity;
257                        new_state.tick = state.tick;
258                        new_state.sqrt_price = state.sqrt_price;
259                        return Err(SimulationError::InvalidInput(
260                            "Ticks exceeded".into(),
261                            Some(GetAmountOutResult::new(
262                                u256_to_biguint(state.amount_calculated.abs().into_raw()),
263                                u256_to_biguint(gas_used),
264                                Box::new(new_state),
265                            )),
266                        ));
267                    }
268                    _ => return Err(SimulationError::FatalError("Unknown error".to_string())),
269                },
270            };
271
272            next_tick = next_tick.clamp(MIN_TICK, MAX_TICK);
273
274            let sqrt_price_next = get_sqrt_ratio_at_tick(next_tick)?;
275            let fee_pips = self
276                .fees
277                .calculate_swap_fees_pips(zero_for_one, lp_fee_override);
278
279            let sqrt_price_start = state.sqrt_price;
280            let (sqrt_price, amount_in, amount_out, fee_amount) = swap_math::compute_swap_step(
281                state.sqrt_price,
282                UniswapV4State::get_sqrt_ratio_target(sqrt_price_next, price_limit, zero_for_one),
283                state.liquidity,
284                // The core univ4 swap logic assumes that if the amount is > 0 it's exact in, and
285                // if it's < 0 it's exact out. The compute_swap_step assumes the
286                // opposite (it's like that for univ3).
287                -state.amount_remaining,
288                fee_pips,
289            )?;
290            state.sqrt_price = sqrt_price;
291
292            let step = StepComputation {
293                sqrt_price_start,
294                tick_next: next_tick,
295                initialized,
296                sqrt_price_next,
297                amount_in,
298                amount_out,
299                fee_amount,
300            };
301            if exact_input {
302                state.amount_remaining += I256::checked_from_sign_and_abs(
303                    Sign::Positive,
304                    safe_add_u256(step.amount_in, step.fee_amount)?,
305                )
306                .unwrap();
307                state.amount_calculated -=
308                    I256::checked_from_sign_and_abs(Sign::Positive, step.amount_out).unwrap();
309            } else {
310                state.amount_remaining -=
311                    I256::checked_from_sign_and_abs(Sign::Positive, step.amount_out).unwrap();
312                state.amount_calculated += I256::checked_from_sign_and_abs(
313                    Sign::Positive,
314                    safe_add_u256(step.amount_in, step.fee_amount)?,
315                )
316                .unwrap();
317            }
318            if state.sqrt_price == step.sqrt_price_next {
319                if step.initialized {
320                    let liquidity_raw = self
321                        .ticks
322                        .get_tick(step.tick_next)
323                        .unwrap()
324                        .net_liquidity;
325                    let liquidity_net = if zero_for_one { -liquidity_raw } else { liquidity_raw };
326                    state.liquidity =
327                        liquidity_math::add_liquidity_delta(state.liquidity, liquidity_net)?;
328                    gas_used = safe_add_u256(gas_used, U256::from(GAS_PER_TICK))?;
329                }
330                state.tick = if zero_for_one { step.tick_next - 1 } else { step.tick_next };
331            } else if state.sqrt_price != step.sqrt_price_start {
332                state.tick = get_tick_at_sqrt_ratio(state.sqrt_price)?;
333            }
334        }
335
336        Ok(SwapResults {
337            amount_calculated: state.amount_calculated,
338            amount_specified,
339            amount_remaining: state.amount_remaining,
340            sqrt_price: state.sqrt_price,
341            liquidity: state.liquidity,
342            tick: state.tick,
343            gas_used: safe_add_u256(gas_used, U256::from(V4_CALLBACK_SETTLEMENT_GAS))?,
344        })
345    }
346
347    pub fn set_hook_handler(&mut self, handler: Box<dyn HookHandler>) {
348        self.hook = Some(handler);
349    }
350
351    fn get_sqrt_ratio_target(
352        sqrt_price_next: U256,
353        sqrt_price_limit: U256,
354        zero_for_one: bool,
355    ) -> U256 {
356        let cond1 = if zero_for_one {
357            sqrt_price_next < sqrt_price_limit
358        } else {
359            sqrt_price_next > sqrt_price_limit
360        };
361
362        if cond1 {
363            sqrt_price_limit
364        } else {
365            sqrt_price_next
366        }
367    }
368
369    fn find_limits_experimentally(
370        &self,
371        token_in: Bytes,
372        token_out: Bytes,
373    ) -> Result<(BigUint, BigUint), SimulationError> {
374        // Create dummy token objects with proper addresses. This is fine since `get_amount_out`
375        // only uses the token addresses.
376        let token_in_obj =
377            Token::new(&token_in, "TOKEN_IN", 18, 0, &[Some(10_000)], Default::default(), 100);
378        let token_out_obj =
379            Token::new(&token_out, "TOKEN_OUT", 18, 0, &[Some(10_000)], Default::default(), 100);
380
381        self.find_max_amount(&token_in_obj, &token_out_obj)
382    }
383
384    /// Finds max amount by performing exponential search.
385    ///
386    /// Reasoning:
387    /// - get_amount_out(I256::MAX) will almost always fail, so this will waste time checking values
388    ///   unrealistically high.
389    /// - If you were to start binary search from 1 to 10^76, you'd need hundreds of iterations.
390    ///
391    /// More about exponential search: https://en.wikipedia.org/wiki/Exponential_search
392    ///
393    /// # Returns
394    ///
395    /// Returns a tuple containing the max amount in and max amount out respectively.
396    fn find_max_amount(
397        &self,
398        token_in: &Token,
399        token_out: &Token,
400    ) -> Result<(BigUint, BigUint), SimulationError> {
401        let mut low = BigUint::from(1u64);
402
403        // The max you can swap on a USV4 is I256::MAX is 5.7e76, since input amount is I256.
404        // So start with something much smaller to search for a reasonable upper bound.
405        let mut high = BigUint::from(10u64).pow(18); // 1 ether in wei
406        let mut last_successful_amount_in = BigUint::from(1u64);
407        let mut last_successful_amount_out = BigUint::from(0u64);
408
409        // First, find an upper bound where the swap fails using exponential search.
410        // Save and return both the amount in and amount out.
411        while let Ok(result) = self.get_amount_out(high.clone(), token_in, token_out) {
412            // We haven't found the upper bound yet, increase the attempted upper bound
413            // by order of magnitude and store the last success as the lower bound.
414            low = last_successful_amount_in.clone();
415            last_successful_amount_in = high.clone();
416            last_successful_amount_out = result.amount;
417            high *= BigUint::from(10u64);
418
419            // Stop if we're getting too large for I256 (about 10^75)
420            if high > BigUint::from(10u64).pow(75) {
421                return Ok((last_successful_amount_in, last_successful_amount_out));
422            }
423        }
424
425        // Use binary search to narrow down value between low and high
426        while &high - &low > BigUint::from(1u64) {
427            let mid = (&low + &high) / BigUint::from(2u64);
428
429            match self.get_amount_out(mid.clone(), token_in, token_out) {
430                Ok(result) => {
431                    last_successful_amount_in = mid.clone();
432                    last_successful_amount_out = result.amount;
433                    low = mid;
434                }
435                Err(_) => {
436                    high = mid;
437                }
438            }
439        }
440
441        Ok((last_successful_amount_in, last_successful_amount_out))
442    }
443
444    /// Helper method to check if there are no initialized ticks in either direction
445    fn has_no_initialized_ticks(&self) -> bool {
446        !self.ticks.has_initialized_ticks()
447    }
448
449    /// The pool's own spot buy price for `base` in units of `quote`: the amount of `quote` one
450    /// `base` costs at the current price, marked up by the pool's swap fee and ignoring any hook.
451    fn core_spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
452        let base_is_currency0 = base < quote;
453        let fee_pips = self
454            .fees
455            .calculate_swap_fees_pips(base_is_currency0, None);
456        let fee = fee_pips as f64 / 1_000_000.0;
457
458        let price = if base_is_currency0 {
459            sqrt_price_q96_to_f64(self.sqrt_price, base.decimals, quote.decimals)?
460        } else {
461            1.0f64 / sqrt_price_q96_to_f64(self.sqrt_price, quote.decimals, base.decimals)?
462        };
463
464        Ok(add_fee_markup(price, fee))
465    }
466
467    /// The share of a swap's output that the pool's hook keeps, as a fraction of one. `None`
468    /// when the pool has no hook, or when its hook does not model its fee analytically and has
469    /// to be simulated instead.
470    ///
471    /// Fails if the hook would keep the whole output or more, which is not a rate a price can be
472    /// marked up by.
473    fn hook_fee_rate(&self, zero_for_one: bool) -> Result<Option<f64>, SimulationError> {
474        let Some(hook) = &self.hook else { return Ok(None) };
475
476        let probe = U256::from(10u64).pow(U256::from(HOOK_FEE_PROBE_EXP));
477        let Some(fee) = hook.unspecified_fee_amount(probe, zero_for_one)? else {
478            return Ok(None);
479        };
480
481        let rate = u256_to_f64(fee)? / u256_to_f64(probe)?;
482        if rate >= 1.0 {
483            return Err(SimulationError::FatalError(format!(
484                "Hook {} keeps {rate} of the output, leaving no price to quote",
485                hook.address()
486            )));
487        }
488
489        Ok(Some(rate))
490    }
491}
492
493#[typetag::serde]
494impl ProtocolSim for UniswapV4State {
495    /// Runs the hook of every quote and limit from this state under `overrides`, which must be
496    /// [`PendingOverrides`].
497    fn set_pending_overrides(
498        &mut self,
499        overrides: Arc<dyn Any + Send + Sync>,
500    ) -> Result<(), SimulationError> {
501        let overrides = overrides
502            .downcast::<PendingOverrides>()
503            .map_err(|_| {
504                SimulationError::FatalError(
505                    "Uniswap V4 pending overrides must be `PendingOverrides`".to_string(),
506                )
507            })?;
508        if let Some(hook) = &mut self.hook {
509            hook.set_pending_overrides(Arc::clone(&overrides));
510        }
511        self.pending_overrides = Some(overrides);
512        Ok(())
513    }
514
515    // Not possible to implement correctly with the current interface because we need to know the
516    // swap direction.
517    fn fee(&self) -> f64 {
518        todo!()
519    }
520
521    fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
522        if let Some(hook) = &self.hook {
523            // Buying `base` means selling `quote` into the pool, so the hook sees a swap whose
524            // input is `quote` and whose output, the leg it charges, is `base`. A hook that
525            // prices that cut analytically needs no simulation: its price is the pool's own buy
526            // price marked up by the cut.
527            if let Some(rate) = self.hook_fee_rate(quote < base)? {
528                return Ok(add_fee_markup(self.core_spot_price(base, quote)?, rate));
529            }
530
531            match hook.spot_price(base, quote) {
532                Ok(price) => return Ok(price),
533                Err(SimulationError::RecoverableError(_)) => {
534                    // Calculate spot price by swapping two amounts and use the approximation
535                    // to get the derivative, following the pattern from vm/state.rs
536
537                    // Calculate the first sell amount (x1) as a small amount
538                    let x1 = BigUint::from(10u64).pow(base.decimals) / BigUint::from(100u64); // 0.01 token
539
540                    // Calculate the second sell amount (x2) as x1 + 1% of x1
541                    let x2 = &x1 + (&x1 / BigUint::from(100u64));
542
543                    // Perform swaps to get the received amounts
544                    let y1 = self.get_amount_out(x1.clone(), base, quote)?;
545                    let y2 = self.get_amount_out(x2.clone(), base, quote)?;
546
547                    // Calculate the marginal price
548                    let num = y2
549                        .amount
550                        .checked_sub(&y1.amount)
551                        .ok_or_else(|| {
552                            SimulationError::FatalError(
553                                "Cannot calculate spot price: y2 < y1".to_string(),
554                            )
555                        })?;
556                    let den = x2.checked_sub(&x1).ok_or_else(|| {
557                        SimulationError::FatalError(
558                            "Cannot calculate spot price: x2 < x1".to_string(),
559                        )
560                    })?;
561
562                    if den == BigUint::from(0u64) {
563                        return Err(SimulationError::FatalError(
564                            "Cannot calculate spot price: denominator is zero".to_string(),
565                        ));
566                    }
567
568                    // Convert to f64 and adjust for decimals
569                    let num_f64 = num.to_f64().ok_or_else(|| {
570                        SimulationError::FatalError(
571                            "Failed to convert numerator to f64".to_string(),
572                        )
573                    })?;
574                    let den_f64 = den.to_f64().ok_or_else(|| {
575                        SimulationError::FatalError(
576                            "Failed to convert denominator to f64".to_string(),
577                        )
578                    })?;
579
580                    let token_correction = 10f64.powi(base.decimals as i32 - quote.decimals as i32);
581
582                    return Ok(num_f64 / den_f64 * token_correction);
583                }
584                Err(e) => return Err(e),
585            }
586        }
587
588        self.core_spot_price(base, quote)
589    }
590
591    fn get_amount_out(
592        &self,
593        amount_in: BigUint,
594        token_in: &Token,
595        token_out: &Token,
596    ) -> Result<GetAmountOutResult, SimulationError> {
597        let zero_for_one = token_in < token_out;
598        let amount_specified = I256::checked_from_sign_and_abs(
599            Sign::Negative,
600            U256::from_be_slice(&amount_in.to_bytes_be()),
601        )
602        .ok_or_else(|| {
603            SimulationError::InvalidInput("I256 overflow: amount_in".to_string(), None)
604        })?;
605
606        let mut amount_to_swap = amount_specified;
607        let mut lp_fee_override: Option<u32> = None;
608        let mut before_swap_gas = 0u64;
609        let mut after_swap_gas = 0u64;
610        let mut before_swap_delta = BeforeSwapDelta(I256::ZERO);
611        let mut storage_overwrites = None;
612
613        let token_in_address = Address::from_slice(&token_in.address);
614        let token_out_address = Address::from_slice(&token_out.address);
615
616        let state_context = StateContext {
617            currency_0: if zero_for_one { token_in_address } else { token_out_address },
618            currency_1: if zero_for_one { token_out_address } else { token_in_address },
619            fees: self.fees.clone(),
620            tick_spacing: self.tick_spacing,
621        };
622
623        let swap_params = SwapParams {
624            zero_for_one,
625            amount_specified: amount_to_swap,
626            sqrt_price_limit: self.sqrt_price,
627        };
628
629        // Check if hook is set and has before_swap permissions
630        if let Some(ref hook) = self.hook {
631            if has_permission(hook.address(), HookOptions::BeforeSwap) {
632                let before_swap_params = BeforeSwapParameters {
633                    context: state_context.clone(),
634                    sender: *EXTERNAL_ACCOUNT,
635                    swap_params: swap_params.clone(),
636                    hook_data: Bytes::new(),
637                };
638
639                let before_swap_result = hook
640                    .before_swap(before_swap_params, None, None)
641                    .map_err(|e| {
642                        SimulationError::FatalError(format!(
643                            "BeforeSwap hook simulation failed: {e:?}"
644                        ))
645                    })?;
646
647                before_swap_gas = before_swap_result.gas_estimate;
648                before_swap_delta = before_swap_result.result.amount_delta;
649                storage_overwrites = Some(before_swap_result.result.overwrites);
650
651                // Convert amountDelta to amountToSwap as per Uniswap V4 spec
652                // See: https://github.com/Uniswap/v4-core/blob/main/src/libraries/Hooks.sol#L270
653                if before_swap_delta.as_i256() != I256::ZERO {
654                    amount_to_swap += I256::from(before_swap_delta.get_specified_delta());
655                    if amount_to_swap > I256::ZERO {
656                        return Err(SimulationError::FatalError(
657                            "Hook delta exceeds swap amount".into(),
658                        ));
659                    }
660                }
661
662                // Set LP fee override if provided by hook
663                // The fee returned by beforeSwap may have the override flag (bit 22) set,
664                // which needs to be removed before using the fee value.
665                // See: https://github.com/Uniswap/v4-core/blob/main/src/libraries/LPFeeLibrary.sol
666                let hook_fee = before_swap_result
667                    .result
668                    .fee
669                    .to::<u32>();
670                if hook_fee != 0 {
671                    // Remove the override flag (bit 22) as per LPFeeLibrary.sol
672                    let cleaned_fee = lp_fee::remove_override_flag(hook_fee);
673
674                    // Validate the fee doesn't exceed MAX_LP_FEE (1,000,000 pips = 100%)
675                    if !lp_fee::is_valid(cleaned_fee) {
676                        return Err(SimulationError::FatalError(format!(
677                            "LP fee override {} exceeds maximum {} pips",
678                            cleaned_fee,
679                            lp_fee::MAX_LP_FEE
680                        )));
681                    }
682
683                    lp_fee_override = Some(cleaned_fee);
684                }
685            }
686        }
687
688        // Perform the swap with potential hook modifications
689        let result = self.swap(zero_for_one, amount_to_swap, None, lp_fee_override)?;
690
691        // Create BalanceDelta from swap result using the proper constructor
692        let mut swap_delta = BalanceDelta::from_swap_result(result.amount_calculated, zero_for_one);
693
694        // Get deltas (change in the specified/given and unspecified/computed token balances after
695        // calling before swap)
696        let hook_delta_specified = before_swap_delta.get_specified_delta();
697        let mut hook_delta_unspecified = before_swap_delta.get_unspecified_delta();
698
699        if let Some(ref hook) = self.hook {
700            if has_permission(hook.address(), HookOptions::AfterSwap) {
701                let after_swap_params = AfterSwapParameters {
702                    context: state_context,
703                    sender: *EXTERNAL_ACCOUNT,
704                    swap_params,
705                    delta: swap_delta,
706                    hook_data: Bytes::new(),
707                };
708
709                let after_swap_result = hook
710                    .after_swap(after_swap_params, storage_overwrites, None)
711                    .map_err(|e| {
712                        SimulationError::FatalError(format!(
713                            "AfterSwap hook simulation failed: {e:?}"
714                        ))
715                    })?;
716                after_swap_gas = after_swap_result.gas_estimate;
717                // Hooks.sol calls afterSwap whenever AFTER_SWAP_FLAG is set, but only parses the
718                // returned delta when AFTER_SWAP_RETURNS_DELTA_FLAG is set too. Without that
719                // permission the PoolManager discards the return value, so the hook still costs
720                // gas but cannot move the swapper's balance.
721                if has_permission(hook.address(), HookOptions::AfterSwapReturnsDelta) {
722                    hook_delta_unspecified += after_swap_result.result;
723                }
724            }
725        }
726
727        // Replicates the behaviour of the Hooks library wrapper of the afterSwap method:
728        // https://github.com/Uniswap/v4-core/blob/59d3ecf53afa9264a16bba0e38f4c5d2231f80bc/src/libraries/Hooks.sol
729        if (hook_delta_specified != I128::ZERO) || (hook_delta_unspecified != I128::ZERO) {
730            let hook_delta = if (amount_specified < I256::ZERO) == zero_for_one {
731                BalanceDelta::new(hook_delta_specified, hook_delta_unspecified)
732            } else {
733                BalanceDelta::new(hook_delta_unspecified, hook_delta_specified)
734            };
735            // This is a BalanceDelta subtraction
736            swap_delta = swap_delta - hook_delta
737        }
738
739        let amount_out = if (amount_specified < I256::ZERO) == zero_for_one {
740            swap_delta.amount1()
741        } else {
742            swap_delta.amount0()
743        };
744
745        trace!(?amount_in, ?token_in, ?token_out, ?zero_for_one, ?result, "V4 SWAP");
746        let mut new_state = self.clone();
747        new_state.liquidity = result.liquidity;
748        new_state.tick = result.tick;
749        new_state.sqrt_price = result.sqrt_price;
750
751        // Add hook gas costs to baseline swap cost.
752        // before_swap_gas / after_swap_gas capture the hook contract's internal
753        // logic (from VM simulation). PM_PER_HOOK_CALL_OVERHEAD accounts for the
754        // PoolManager's Hooks.sol dispatch wrapper that is not captured by either
755        // the native swap constants or the VM simulation.
756        let mut hook_overhead = before_swap_gas + after_swap_gas;
757        if before_swap_gas > 0 {
758            hook_overhead += PM_PER_HOOK_CALL_OVERHEAD;
759        }
760        if after_swap_gas > 0 {
761            hook_overhead += PM_PER_HOOK_CALL_OVERHEAD;
762        }
763        let total_gas_used = result.gas_used + U256::from(hook_overhead);
764        Ok(GetAmountOutResult::new(
765            u256_to_biguint(U256::from(amount_out.abs())),
766            u256_to_biguint(total_gas_used),
767            Box::new(new_state),
768        ))
769    }
770
771    fn get_limits(
772        &self,
773        token_in: Bytes,
774        token_out: Bytes,
775    ) -> Result<(BigUint, BigUint), SimulationError> {
776        if let Some(hook) = &self.hook {
777            // Check if pool has no liquidity & ticks -> hook manages liquidity
778            if self.liquidity == 0 && self.has_no_initialized_ticks() {
779                // If the hook has a get_amount_ranges entrypoint, call it and return (0, limits[1])
780                match hook.get_amount_ranges(token_in.clone(), token_out.clone()) {
781                    Ok(amount_ranges) => {
782                        return Ok((
783                            u256_to_biguint(amount_ranges.amount_in_range.1),
784                            u256_to_biguint(amount_ranges.amount_out_range.1),
785                        ))
786                    }
787                    // Check if hook get_amount_ranges is not implemented or the limits entrypoint
788                    // is not set for this hook
789                    Err(SimulationError::RecoverableError(msg))
790                        if msg.contains("not implemented") || msg.contains("not set") =>
791                    {
792                        // Hook manages liquidity but doesn't have get_amount_ranges
793                        // Use binary search to find limits by calling swap with increasing amounts
794                        return self.find_limits_experimentally(token_in, token_out);
795                        // Otherwise fall back to default implementation
796                    }
797                    Err(e) => return Err(e),
798                }
799            }
800        }
801
802        // If the pool has no liquidity, return zeros for both limits
803        if self.liquidity == 0 {
804            return Ok((BigUint::zero(), BigUint::zero()));
805        }
806
807        let zero_for_one = token_in < token_out;
808        let mut current_tick = self.tick;
809        let mut current_sqrt_price = self.sqrt_price;
810        let mut current_liquidity = self.liquidity;
811        let mut total_amount_in = U256::ZERO;
812        let mut total_amount_out = U256::ZERO;
813        let mut ticks_crossed: u64 = 0;
814
815        // Iterate through ticks in the direction of the swap
816        // Stops when: no more liquidity, no more ticks, or gas limit would be exceeded
817        while let Ok((tick, initialized)) = self
818            .ticks
819            .next_initialized_tick_within_one_word(current_tick, zero_for_one)
820        {
821            // Cap iteration to prevent exceeding Ethereum's gas limit
822            if ticks_crossed >= MAX_TICKS_CROSSED {
823                break;
824            }
825            ticks_crossed += 1;
826
827            // Clamp the tick value to ensure it's within valid range
828            let next_tick = tick.clamp(MIN_TICK, MAX_TICK);
829
830            // Calculate the sqrt price at the next tick boundary
831            let sqrt_price_next = get_sqrt_ratio_at_tick(next_tick)?;
832
833            // Calculate the amount of tokens swapped when moving from current_sqrt_price to
834            // sqrt_price_next. Direction determines which token is being swapped in vs out
835            let (amount_in, amount_out) = if zero_for_one {
836                let amount0 = get_amount0_delta(
837                    sqrt_price_next,
838                    current_sqrt_price,
839                    current_liquidity,
840                    true,
841                )?;
842                let amount1 = get_amount1_delta(
843                    sqrt_price_next,
844                    current_sqrt_price,
845                    current_liquidity,
846                    false,
847                )?;
848                (amount0, amount1)
849            } else {
850                let amount0 = get_amount0_delta(
851                    sqrt_price_next,
852                    current_sqrt_price,
853                    current_liquidity,
854                    false,
855                )?;
856                let amount1 = get_amount1_delta(
857                    sqrt_price_next,
858                    current_sqrt_price,
859                    current_liquidity,
860                    true,
861                )?;
862                (amount1, amount0)
863            };
864
865            // Accumulate total amounts for this tick range
866            total_amount_in = safe_add_u256(total_amount_in, amount_in)?;
867            total_amount_out = safe_add_u256(total_amount_out, amount_out)?;
868
869            // If this tick is "initialized" (meaning its someone's position boundary), update the
870            // liquidity when crossing it
871            // For zero_for_one, liquidity is removed when crossing a tick
872            // For one_for_zero, liquidity is added when crossing a tick
873            if initialized {
874                let liquidity_raw = self
875                    .ticks
876                    .get_tick(next_tick)
877                    .unwrap()
878                    .net_liquidity;
879                let liquidity_delta = if zero_for_one { -liquidity_raw } else { liquidity_raw };
880
881                // Check if applying this liquidity delta would cause underflow
882                // If so, stop here rather than continuing with invalid state
883                match liquidity_math::add_liquidity_delta(current_liquidity, liquidity_delta) {
884                    Ok(new_liquidity) => {
885                        current_liquidity = new_liquidity;
886                    }
887                    Err(_) => {
888                        // Liquidity would underflow, stop iteration here
889                        // This represents the maximum liquidity we can actually use
890                        break;
891                    }
892                }
893            }
894
895            // Move to the next tick position
896            current_tick = if zero_for_one { next_tick - 1 } else { next_tick };
897            current_sqrt_price = sqrt_price_next;
898
899            // If we've consumed all liquidity, no point continuing the loop
900            if current_liquidity == 0 {
901                break;
902            }
903        }
904
905        // A hook that charges the output reduces what a swapper can actually receive, so the
906        // limit has to be reported net of its cut.
907        if let Some(hook) = &self.hook {
908            if let Some(fee) = hook.unspecified_fee_amount(total_amount_out, zero_for_one)? {
909                total_amount_out = safe_sub_u256(total_amount_out, fee)?;
910            }
911        }
912
913        Ok((u256_to_biguint(total_amount_in), u256_to_biguint(total_amount_out)))
914    }
915
916    fn delta_transition(
917        &mut self,
918        delta: ProtocolStateDelta,
919        tokens: &HashMap<Bytes, Token>,
920        balances: &Balances,
921    ) -> Result<(), TransitionError> {
922        if let Some(mut hook) = self.hook.clone() {
923            match hook.delta_transition(delta.clone(), tokens, balances) {
924                Ok(()) => self.set_hook_handler(hook),
925                Err(TransitionError::SimulationError(SimulationError::RecoverableError(msg)))
926                    if msg.contains("not implemented") =>
927                {
928                    // Fall back to default implementation
929                }
930                Err(e) => return Err(e),
931            }
932        }
933
934        // Apply attribute changes
935        if let Some(liquidity) = delta
936            .updated_attributes
937            .get("liquidity")
938        {
939            self.liquidity = u128::from(liquidity.clone());
940        }
941        if let Some(sqrt_price) = delta
942            .updated_attributes
943            .get("sqrt_price_x96")
944        {
945            self.sqrt_price = U256::from_be_slice(sqrt_price);
946        }
947        if let Some(tick) = delta.updated_attributes.get("tick") {
948            self.tick = i24_be_bytes_to_i32(tick);
949        }
950        if let Some(lp_fee) = delta.updated_attributes.get("fee") {
951            self.fees.lp_fee = u32::from(lp_fee.clone());
952        }
953        if let Some(zero2one_protocol_fee) = delta
954            .updated_attributes
955            .get("protocol_fees/zero2one")
956        {
957            self.fees.zero_for_one = u32::from(zero2one_protocol_fee.clone());
958        }
959        if let Some(one2zero_protocol_fee) = delta
960            .updated_attributes
961            .get("protocol_fees/one2zero")
962        {
963            self.fees.one_for_zero = u32::from(one2zero_protocol_fee.clone());
964        }
965
966        // apply tick changes
967        for (key, value) in delta.updated_attributes.iter() {
968            // tick liquidity keys are in the format "ticks/{tick_index}/net_liquidity"
969            if key.starts_with("ticks/") {
970                let parts: Vec<&str> = key.split('/').collect();
971                self.ticks
972                    .set_tick_liquidity(
973                        parts[1]
974                            .parse::<i32>()
975                            .map_err(|err| TransitionError::DecodeError(err.to_string()))?,
976                        i128::from(value.clone()),
977                    )
978                    .map_err(|err| TransitionError::DecodeError(err.to_string()))?;
979            }
980        }
981        // delete ticks - ignores deletes for attributes other than tick liquidity
982        for key in delta.deleted_attributes.iter() {
983            // tick liquidity keys are in the format "ticks/{tick_index}/net_liquidity"
984            if key.starts_with("ticks/") {
985                let parts: Vec<&str> = key.split('/').collect();
986                self.ticks
987                    .set_tick_liquidity(
988                        parts[1]
989                            .parse::<i32>()
990                            .map_err(|err| TransitionError::DecodeError(err.to_string()))?,
991                        0,
992                    )
993                    .map_err(|err| TransitionError::DecodeError(err.to_string()))?;
994            }
995        }
996
997        Ok(())
998    }
999
1000    /// See [`ProtocolSim::query_pool_swap`] for the trait documentation.
1001    ///
1002    /// This method uses Uniswap V4 internal swap logic by swapping an infinite amount of token_in
1003    /// until the target price is reached. Takes into account V4-specific features like protocol
1004    /// fees and dynamic LP fees.
1005    ///
1006    /// Note: This implementation does not invoke hooks, as it is a query-only operation meant to
1007    /// determine available liquidity at a given price without executing an actual swap.
1008    fn query_pool_swap(&self, params: &QueryPoolSwapParams) -> Result<PoolSwap, SimulationError> {
1009        if self.liquidity == 0 {
1010            return Err(SimulationError::FatalError("No liquidity".to_string()));
1011        }
1012
1013        // Calculate total fee (protocol + LP fee) for V4
1014        let zero_for_one = params.token_in().address < params.token_out().address;
1015        let fee_pips = self
1016            .fees
1017            .calculate_swap_fees_pips(zero_for_one, None);
1018
1019        match params.swap_constraint() {
1020            SwapConstraint::TradeLimitPrice { .. } => Err(SimulationError::InvalidInput(
1021                "Uniswap V4 does not support TradeLimitPrice constraint in query_pool_swap"
1022                    .to_string(),
1023                None,
1024            )),
1025            SwapConstraint::PoolTargetPrice {
1026                target,
1027                tolerance: _,
1028                min_amount_in: _,
1029                max_amount_in: _,
1030            } => {
1031                if self.liquidity == 0 {
1032                    return Err(SimulationError::FatalError("No liquidity".to_string()));
1033                }
1034
1035                let (amount_in, amount_out, swap_result) = clmm_swap_to_price(
1036                    self.sqrt_price,
1037                    &params.token_in().address,
1038                    &params.token_out().address,
1039                    target,
1040                    fee_pips,
1041                    Sign::Negative, // V4 uses negative for exact input
1042                    |zero_for_one, amount_specified, sqrt_price_limit| {
1043                        self.swap(zero_for_one, amount_specified, Some(sqrt_price_limit), None)
1044                    },
1045                )?;
1046
1047                let mut new_state = self.clone();
1048                new_state.liquidity = swap_result.liquidity;
1049                new_state.tick = swap_result.tick;
1050                new_state.sqrt_price = swap_result.sqrt_price;
1051
1052                Ok(PoolSwap::new(amount_in, amount_out, Box::new(new_state), None))
1053            }
1054        }
1055    }
1056
1057    fn clone_box(&self) -> Box<dyn ProtocolSim> {
1058        Box::new(self.clone())
1059    }
1060
1061    fn as_any(&self) -> &dyn Any {
1062        self
1063    }
1064
1065    fn as_any_mut(&mut self) -> &mut dyn Any {
1066        self
1067    }
1068
1069    fn eq(&self, other: &dyn ProtocolSim) -> bool {
1070        if let Some(other_state) = other
1071            .as_any()
1072            .downcast_ref::<UniswapV4State>()
1073        {
1074            self.liquidity == other_state.liquidity &&
1075                self.sqrt_price == other_state.sqrt_price &&
1076                self.fees == other_state.fees &&
1077                self.tick == other_state.tick &&
1078                self.ticks == other_state.ticks
1079        } else {
1080            false
1081        }
1082    }
1083}
1084
1085#[cfg(test)]
1086mod tests {
1087    use std::{collections::HashSet, fs, path::Path, str::FromStr};
1088
1089    use alloy::primitives::{aliases::U24, U160};
1090    use num_traits::FromPrimitive;
1091    use rstest::rstest;
1092    use serde_json::Value;
1093    use tycho_client::feed::{synchronizer::ComponentWithState, BlockHeader};
1094    use tycho_common::{models::Chain, simulation::protocol_sim::Price};
1095
1096    use super::*;
1097    use crate::{
1098        evm::{
1099            engine_db::{
1100                create_engine,
1101                simulation_db::SimulationDB,
1102                utils::{get_client, get_runtime},
1103            },
1104            protocol::{
1105                u256_num::biguint_to_u256,
1106                uniswap_v4::hooks::{
1107                    angstrom::hook_handler::{AngstromFees, AngstromHookHandler},
1108                    generic_vm_hook_handler::GenericVMHookHandler,
1109                    models::{AfterSwapDelta, AmountRanges, BeforeSwapOutput, WithGasEstimate},
1110                    pons_v2::hook_handler::{PonsV2HookHandler, PONS_V2_HOOK_ROBINHOOD},
1111                },
1112                utils::uniswap::{lp_fee, sqrt_price_math::get_sqrt_price_q96},
1113            },
1114        },
1115        protocol::models::{DecoderContext, TryFromWithBlock},
1116    };
1117
1118    // Helper methods to create commonly used tokens
1119    fn usdc() -> Token {
1120        Token::new(
1121            &Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
1122            "USDC",
1123            6,
1124            0,
1125            &[Some(10_000)],
1126            Default::default(),
1127            100,
1128        )
1129    }
1130
1131    fn weth() -> Token {
1132        Token::new(
1133            &Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(),
1134            "WETH",
1135            18,
1136            0,
1137            &[Some(10_000)],
1138            Default::default(),
1139            100,
1140        )
1141    }
1142
1143    fn eth() -> Token {
1144        Token::new(
1145            &Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(),
1146            "ETH",
1147            18,
1148            0,
1149            &[Some(10_000)],
1150            Default::default(),
1151            100,
1152        )
1153    }
1154
1155    fn token_x() -> Token {
1156        Token::new(
1157            &Bytes::from_str("0x0000000000000000000000000000000000000001").unwrap(),
1158            "X",
1159            18,
1160            0,
1161            &[Some(10_000)],
1162            Default::default(),
1163            100,
1164        )
1165    }
1166
1167    fn token_y() -> Token {
1168        Token::new(
1169            &Bytes::from_str("0x0000000000000000000000000000000000000002").unwrap(),
1170            "Y",
1171            18,
1172            0,
1173            &[Some(10_000)],
1174            Default::default(),
1175            100,
1176        )
1177    }
1178
1179    #[test]
1180    fn test_delta_transition() {
1181        let mut pool = UniswapV4State::new(
1182            1000,
1183            U256::from_str("1000").unwrap(),
1184            UniswapV4Fees { zero_for_one: 100, one_for_zero: 90, lp_fee: 700 },
1185            100,
1186            60,
1187            vec![TickInfo::new(120, 10000).unwrap(), TickInfo::new(180, -10000).unwrap()],
1188        )
1189        .unwrap();
1190
1191        let attributes: HashMap<String, Bytes> = [
1192            ("liquidity".to_string(), Bytes::from(2000_u64.to_be_bytes().to_vec())),
1193            ("sqrt_price_x96".to_string(), Bytes::from(1001_u64.to_be_bytes().to_vec())),
1194            ("tick".to_string(), Bytes::from(120_i32.to_be_bytes().to_vec())),
1195            ("protocol_fees/zero2one".to_string(), Bytes::from(50_u32.to_be_bytes().to_vec())),
1196            ("protocol_fees/one2zero".to_string(), Bytes::from(75_u32.to_be_bytes().to_vec())),
1197            ("fee".to_string(), Bytes::from(100_u32.to_be_bytes().to_vec())),
1198            ("ticks/-120/net_liquidity".to_string(), Bytes::from(10200_u64.to_be_bytes().to_vec())),
1199            ("ticks/120/net_liquidity".to_string(), Bytes::from(9800_u64.to_be_bytes().to_vec())),
1200            ("block_number".to_string(), Bytes::from(2000_u64.to_be_bytes().to_vec())),
1201            ("block_timestamp".to_string(), Bytes::from(1758201935_u64.to_be_bytes().to_vec())),
1202        ]
1203        .into_iter()
1204        .collect();
1205
1206        let delta = ProtocolStateDelta {
1207            component_id: "State1".to_owned(),
1208            updated_attributes: attributes,
1209            deleted_attributes: HashSet::new(),
1210        };
1211
1212        pool.delta_transition(delta, &HashMap::new(), &Balances::default())
1213            .unwrap();
1214
1215        assert_eq!(pool.liquidity, 2000);
1216        assert_eq!(pool.sqrt_price, U256::from(1001));
1217        assert_eq!(pool.tick, 120);
1218        assert_eq!(pool.fees.zero_for_one, 50);
1219        assert_eq!(pool.fees.one_for_zero, 75);
1220        assert_eq!(pool.fees.lp_fee, 100);
1221        assert_eq!(
1222            pool.ticks
1223                .get_tick(-120)
1224                .unwrap()
1225                .net_liquidity,
1226            10200
1227        );
1228        assert_eq!(
1229            pool.ticks
1230                .get_tick(120)
1231                .unwrap()
1232                .net_liquidity,
1233            9800
1234        );
1235    }
1236
1237    #[tokio::test]
1238    /// Compares a quote from the UniswapV4 Quoter contract on Sepolia with a simulation.
1239    async fn test_swap_sim() {
1240        use tycho_client::feed::dto;
1241        let project_root = env!("CARGO_MANIFEST_DIR");
1242        let asset_path = Path::new(project_root)
1243            .join("tests/assets/decoder/uniswap_v4_snapshot_sepolia_block_7239119.json");
1244        let json_data = fs::read_to_string(asset_path).expect("Failed to read test asset");
1245        let data: Value = serde_json::from_str(&json_data).expect("Failed to parse JSON");
1246        let state: ComponentWithState = serde_json::from_value::<dto::ComponentWithState>(data)
1247            .expect("Expected json to match ComponentWithState structure")
1248            .into();
1249
1250        let block = BlockHeader {
1251            number: 7239119,
1252            hash: Bytes::from_str(
1253                "0x28d41d40f2ac275a4f5f621a636b9016b527d11d37d610a45ac3a821346ebf8c",
1254            )
1255            .expect("Invalid block hash"),
1256            parent_hash: Bytes::from(vec![0; 32]),
1257            ..Default::default()
1258        };
1259
1260        let t0 = Token::new(
1261            &Bytes::from_str("0x647e32181a64f4ffd4f0b0b4b052ec05b277729c").unwrap(),
1262            "T0",
1263            18,
1264            0,
1265            &[Some(10_000)],
1266            Chain::Ethereum,
1267            100,
1268        );
1269        let t1 = Token::new(
1270            &Bytes::from_str("0xe390a1c311b26f14ed0d55d3b0261c2320d15ca5").unwrap(),
1271            "T1",
1272            18,
1273            0,
1274            &[Some(10_000)],
1275            Chain::Ethereum,
1276            100,
1277        );
1278
1279        let all_tokens = [t0.clone(), t1.clone()]
1280            .iter()
1281            .map(|t| (t.address.clone(), t.clone()))
1282            .collect();
1283
1284        let usv4_state = UniswapV4State::try_from_with_header(
1285            state,
1286            block,
1287            &Default::default(),
1288            &all_tokens,
1289            &DecoderContext::new(),
1290        )
1291        .await
1292        .unwrap();
1293
1294        // The fixture's `hooks` attribute is the zero address, so this is a plain V4 pool and
1295        // needs no chain to decode.
1296        assert!(usv4_state.hook.is_none());
1297
1298        let res = usv4_state
1299            .get_amount_out(BigUint::from_u64(1000000000000000000).unwrap(), &t0, &t1)
1300            .unwrap();
1301
1302        let expected_amount = BigUint::from(9999909699895_u64);
1303        assert_eq!(res.amount, expected_amount);
1304    }
1305
1306    #[tokio::test]
1307    async fn test_get_limits() {
1308        use tycho_client::feed::dto;
1309        let block = BlockHeader {
1310            number: 22689129,
1311            hash: Bytes::from_str(
1312                "0x7763ea30d11aef68da729b65250c09a88ad00458c041064aad8c9a9dbf17adde",
1313            )
1314            .expect("Invalid block hash"),
1315            parent_hash: Bytes::from(vec![0; 32]),
1316            ..Default::default()
1317        };
1318
1319        let project_root = env!("CARGO_MANIFEST_DIR");
1320        let asset_path =
1321            Path::new(project_root).join("tests/assets/decoder/uniswap_v4_snapshot.json");
1322        let json_data = fs::read_to_string(asset_path).expect("Failed to read test asset");
1323        let data: Value = serde_json::from_str(&json_data).expect("Failed to parse JSON");
1324        let state: ComponentWithState = serde_json::from_value::<dto::ComponentWithState>(data)
1325            .expect("Expected json to match ComponentWithState structure")
1326            .into();
1327
1328        let t0 = Token::new(
1329            &Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap(),
1330            "WBTC",
1331            8,
1332            0,
1333            &[Some(10_000)],
1334            Chain::Ethereum,
1335            100,
1336        );
1337        let t1 = Token::new(
1338            &Bytes::from_str("0xdac17f958d2ee523a2206206994597c13d831ec7").unwrap(),
1339            "USDT",
1340            6,
1341            0,
1342            &[Some(10_000)],
1343            Chain::Ethereum,
1344            100,
1345        );
1346
1347        let all_tokens = [t0.clone(), t1.clone()]
1348            .iter()
1349            .map(|t| (t.address.clone(), t.clone()))
1350            .collect();
1351
1352        let usv4_state = UniswapV4State::try_from_with_header(
1353            state,
1354            block,
1355            &Default::default(),
1356            &all_tokens,
1357            &DecoderContext::new(),
1358        )
1359        .await
1360        .unwrap();
1361
1362        // The fixture's `hooks` attribute is the zero address, so this is a plain V4 pool and
1363        // needs no chain to decode.
1364        assert!(usv4_state.hook.is_none());
1365
1366        let res = usv4_state
1367            .get_limits(t0.address.clone(), t1.address.clone())
1368            .unwrap();
1369
1370        assert_eq!(&res.0, &BigUint::from_u128(71698353688830259750744466706).unwrap());
1371
1372        let out = usv4_state
1373            .get_amount_out(res.0, &t0, &t1)
1374            .expect("swap for limit in didn't work");
1375
1376        assert_eq!(&res.1, &out.amount);
1377    }
1378    /// The hook calls a quote made, each with the overrides it ran under.
1379    type SeenCalls = Vec<(&'static str, Option<PendingOverrides>)>;
1380
1381    /// A hook whose beforeSwap fee is whatever the pending block wrote to its slot 0, and
1382    /// which records the overrides each call ran under.
1383    #[derive(Debug, Clone)]
1384    struct ConfigurableFeeHook {
1385        address: Address,
1386        pending_overrides: Option<Arc<PendingOverrides>>,
1387        seen: Arc<std::sync::Mutex<SeenCalls>>,
1388    }
1389
1390    impl ConfigurableFeeHook {
1391        /// Low address bits are the permission flags: bit 7 beforeSwap, bit 6 afterSwap.
1392        fn new() -> Self {
1393            let mut address = [0u8; 20];
1394            address[19] = 0xC0;
1395            Self {
1396                address: Address::from_slice(&address),
1397                pending_overrides: None,
1398                seen: Arc::default(),
1399            }
1400        }
1401
1402        fn pending_fee(&self) -> U256 {
1403            self.pending_overrides
1404                .as_ref()
1405                .and_then(|p| p.storage.as_ref())
1406                .and_then(|storage| storage.get(&self.address))
1407                .and_then(|slots| slots.get(&U256::ZERO))
1408                .copied()
1409                .unwrap_or(U256::ZERO)
1410        }
1411    }
1412
1413    impl HookHandler for ConfigurableFeeHook {
1414        fn address(&self) -> Address {
1415            self.address
1416        }
1417
1418        fn before_swap(
1419            &self,
1420            _params: BeforeSwapParameters,
1421            _overwrites: Option<HashMap<Address, HashMap<U256, U256>>>,
1422            _transient_storage: Option<HashMap<Address, HashMap<U256, U256>>>,
1423        ) -> Result<WithGasEstimate<BeforeSwapOutput>, SimulationError> {
1424            self.seen.lock().unwrap().push((
1425                "before",
1426                self.pending_overrides
1427                    .as_deref()
1428                    .cloned(),
1429            ));
1430            Ok(WithGasEstimate {
1431                gas_estimate: 0,
1432                result: BeforeSwapOutput {
1433                    amount_delta: BeforeSwapDelta(I256::ZERO),
1434                    fee: U24::from(self.pending_fee()),
1435                    overwrites: HashMap::new(),
1436                    transient_storage: HashMap::new(),
1437                },
1438            })
1439        }
1440
1441        fn after_swap(
1442            &self,
1443            _params: AfterSwapParameters,
1444            _overwrites: Option<HashMap<Address, HashMap<U256, U256>>>,
1445            _transient_storage_params: Option<HashMap<Address, HashMap<U256, U256>>>,
1446        ) -> Result<WithGasEstimate<AfterSwapDelta>, SimulationError> {
1447            self.seen.lock().unwrap().push((
1448                "after",
1449                self.pending_overrides
1450                    .as_deref()
1451                    .cloned(),
1452            ));
1453            Ok(WithGasEstimate { gas_estimate: 0, result: I128::ZERO })
1454        }
1455
1456        fn fee(
1457            &self,
1458            _context: &UniswapV4State,
1459            _params: SwapParams,
1460        ) -> Result<f64, SimulationError> {
1461            Ok(0.0)
1462        }
1463
1464        fn spot_price(&self, _base: &Token, _quote: &Token) -> Result<f64, SimulationError> {
1465            Err(SimulationError::RecoverableError("not implemented".into()))
1466        }
1467
1468        fn get_amount_ranges(
1469            &self,
1470            _token_in: Bytes,
1471            _token_out: Bytes,
1472        ) -> Result<AmountRanges, SimulationError> {
1473            Err(SimulationError::RecoverableError("not implemented".into()))
1474        }
1475
1476        fn set_pending_overrides(&mut self, overrides: Arc<PendingOverrides>) {
1477            self.pending_overrides = Some(overrides);
1478        }
1479
1480        fn delta_transition(
1481            &mut self,
1482            _delta: ProtocolStateDelta,
1483            _tokens: &HashMap<Bytes, Token>,
1484            _balances: &Balances,
1485        ) -> Result<(), TransitionError> {
1486            Ok(())
1487        }
1488
1489        fn clone_box(&self) -> Box<dyn HookHandler> {
1490            Box::new(self.clone())
1491        }
1492
1493        fn as_any(&self) -> &dyn Any {
1494            self
1495        }
1496
1497        fn is_equal(&self, other: &dyn HookHandler) -> bool {
1498            other
1499                .as_any()
1500                .downcast_ref::<Self>()
1501                .is_some_and(|o| o.address == self.address)
1502        }
1503    }
1504
1505    /// A pool at price one with liquidity on both sides of the current tick.
1506    fn hooked_pool(hook: &ConfigurableFeeHook) -> UniswapV4State {
1507        let liquidity = 1_000_000_000_000_000_000u128;
1508        let mut pool = UniswapV4State::new(
1509            liquidity,
1510            U256::from(1u8) << 96,
1511            UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: lp_fee::DYNAMIC_FEE_FLAG },
1512            0,
1513            60,
1514            vec![
1515                TickInfo::new(-60, liquidity as i128).unwrap(),
1516                TickInfo::new(60, -(liquidity as i128)).unwrap(),
1517            ],
1518        )
1519        .unwrap();
1520        pool.set_hook_handler(Box::new(hook.clone()));
1521        pool
1522    }
1523
1524    fn pending_fee_update(hook: &ConfigurableFeeHook, fee_pips: u64) -> PendingOverrides {
1525        PendingOverrides {
1526            storage: Some(HashMap::from([(
1527                hook.address,
1528                HashMap::from([(U256::ZERO, U256::from(fee_pips))]),
1529            )])),
1530            native_balances: None,
1531            block: None,
1532        }
1533    }
1534
1535    #[test]
1536    fn test_pending_overrides_reach_both_hook_calls_and_change_the_quote() {
1537        let hook = ConfigurableFeeHook::new();
1538        let amount_in = BigUint::from(1_000_000u64);
1539
1540        let confirmed = hooked_pool(&hook)
1541            .get_amount_out(amount_in.clone(), &token_x(), &token_y())
1542            .unwrap();
1543        let mut pool = hooked_pool(&hook);
1544        pool.set_pending_overrides(Arc::new(pending_fee_update(&hook, 100_000)))
1545            .unwrap();
1546        let pending = pool
1547            .get_amount_out(amount_in, &token_x(), &token_y())
1548            .unwrap();
1549
1550        assert!(
1551            pending.amount < confirmed.amount,
1552            "a 10% fee the pending block wrote must cut the output: {} vs {}",
1553            pending.amount,
1554            confirmed.amount
1555        );
1556        let seen = hook.seen.lock().unwrap();
1557        let calls: Vec<(&str, bool)> = seen
1558            .iter()
1559            .map(|(call, pending)| {
1560                (
1561                    *call,
1562                    pending
1563                        .as_ref()
1564                        .is_some_and(|p| p.storage.is_some()),
1565                )
1566            })
1567            .collect();
1568        assert_eq!(
1569            calls,
1570            vec![("before", false), ("after", false), ("before", true), ("after", true)],
1571            "the confirmed quote runs both calls without overrides, the pending one with them"
1572        );
1573    }
1574
1575    #[test]
1576    fn test_pending_overrides_survive_clone_box() {
1577        let hook = ConfigurableFeeHook::new();
1578        let mut pool = hooked_pool(&hook);
1579        pool.set_pending_overrides(Arc::new(pending_fee_update(&hook, 1)))
1580            .unwrap();
1581
1582        let cloned = pool.clone_box();
1583        let cloned = cloned
1584            .as_any()
1585            .downcast_ref::<UniswapV4State>()
1586            .unwrap();
1587
1588        assert!(cloned.pending_overrides().is_some(), "a clone quotes under the same overrides");
1589    }
1590
1591    #[test]
1592    fn test_set_pending_overrides_rejects_another_payload_type() {
1593        let hook = ConfigurableFeeHook::new();
1594        let mut pool = hooked_pool(&hook);
1595
1596        let result = pool.set_pending_overrides(Arc::new(0u8));
1597
1598        assert!(matches!(result, Err(SimulationError::FatalError(_))));
1599        assert!(pool.pending_overrides().is_none(), "a rejected payload sets nothing");
1600        assert!(hook.seen.lock().unwrap().is_empty());
1601    }
1602
1603    #[test]
1604    fn test_pending_overrides_are_not_part_of_equality() {
1605        let hook = ConfigurableFeeHook::new();
1606        let confirmed = hooked_pool(&hook);
1607        let mut pending = hooked_pool(&hook);
1608        pending
1609            .set_pending_overrides(Arc::new(pending_fee_update(&hook, 1)))
1610            .unwrap();
1611
1612        assert!(confirmed == pending, "PartialEq ignores the overrides");
1613        assert!(ProtocolSim::eq(&confirmed, &pending), "ProtocolSim::eq ignores the overrides");
1614    }
1615
1616    #[test]
1617    fn test_get_amount_out_no_hook() {
1618        // Test using transaction 0x78ea4bbb7d4405000f33fdf6f3fa08b5e557d50e5e7f826a79766d50bd643b6f
1619
1620        // Pool ID: 0x00b9edc1583bf6ef09ff3a09f6c23ecb57fd7d0bb75625717ec81eed181e22d7
1621        // Information taken from Tenderly simulation / event emitted on Etherscan
1622        let usv4_state = UniswapV4State::new(
1623            541501951282951892,
1624            U256::from_str("5362798333066270795901222").unwrap(), // Sqrt price
1625            UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 100 },
1626            -192022,
1627            1,
1628            // Ticks taken from indexer logs
1629            vec![
1630                TickInfo {
1631                    index: -887272,
1632                    net_liquidity: 460382969070005,
1633                    sqrt_price: U256::from(4295128739_u64),
1634                },
1635                TickInfo {
1636                    index: -207244,
1637                    net_liquidity: 561268407024557,
1638                    sqrt_price: U256::from_str("2505291706254206075074035").unwrap(),
1639                },
1640                TickInfo {
1641                    index: -196411,
1642                    net_liquidity: 825711941800452,
1643                    sqrt_price: U256::from_str("4306080513146952705853399").unwrap(),
1644                },
1645                TickInfo {
1646                    index: -196257,
1647                    net_liquidity: 64844666874010,
1648                    sqrt_price: U256::from_str("4339363644587371378270009").unwrap(),
1649                },
1650                TickInfo {
1651                    index: -195611,
1652                    net_liquidity: 2344045150766798,
1653                    sqrt_price: U256::from_str("4481806029599743916020126").unwrap(),
1654                },
1655                TickInfo {
1656                    index: -194715,
1657                    net_liquidity: 391037380558274654,
1658                    sqrt_price: U256::from_str("4687145946111116896040494").unwrap(),
1659                },
1660                TickInfo {
1661                    index: -194599,
1662                    net_liquidity: 89032603464508,
1663                    sqrt_price: U256::from_str("4714409015946702405379370").unwrap(),
1664                },
1665                TickInfo {
1666                    index: -194389,
1667                    net_liquidity: 66635600426483168,
1668                    sqrt_price: U256::from_str("4764168603367683402636621").unwrap(),
1669                },
1670                TickInfo {
1671                    index: -194160,
1672                    net_liquidity: 6123093436523361,
1673                    sqrt_price: U256::from_str("4819029067726467394386780").unwrap(),
1674                },
1675                TickInfo {
1676                    index: -194025,
1677                    net_liquidity: 79940813798964,
1678                    sqrt_price: U256::from_str("4851665907541490407930032").unwrap(),
1679                },
1680                TickInfo {
1681                    index: -193922,
1682                    net_liquidity: 415630967437234,
1683                    sqrt_price: U256::from_str("4876715181040466809166531").unwrap(),
1684                },
1685                TickInfo {
1686                    index: -193876,
1687                    net_liquidity: 9664144015186047,
1688                    sqrt_price: U256::from_str("4887943972687250473582419").unwrap(),
1689                },
1690                TickInfo {
1691                    index: -193818,
1692                    net_liquidity: 435344726052344,
1693                    sqrt_price: U256::from_str("4902138873132735049121973").unwrap(),
1694                },
1695                TickInfo {
1696                    index: -193804,
1697                    net_liquidity: 221726179374067,
1698                    sqrt_price: U256::from_str("4905571399964683340605904").unwrap(),
1699                },
1700                TickInfo {
1701                    index: -193719,
1702                    net_liquidity: 101340835774487,
1703                    sqrt_price: U256::from_str("4926463397882393957462188").unwrap(),
1704                },
1705                TickInfo {
1706                    index: -193690,
1707                    net_liquidity: 193367475630077,
1708                    sqrt_price: U256::from_str("4933611593595025190448924").unwrap(),
1709                },
1710                TickInfo {
1711                    index: -193643,
1712                    net_liquidity: 357016631583746,
1713                    sqrt_price: U256::from_str("4945218633428068823432932").unwrap(),
1714                },
1715                TickInfo {
1716                    index: -193520,
1717                    net_liquidity: 917243184365178,
1718                    sqrt_price: U256::from_str("4975723910367862081017120").unwrap(),
1719                },
1720                TickInfo {
1721                    index: -193440,
1722                    net_liquidity: 114125890211958292,
1723                    sqrt_price: U256::from_str("4995665665861492533686137").unwrap(),
1724                },
1725                TickInfo {
1726                    index: -193380,
1727                    net_liquidity: -65980729148766579,
1728                    sqrt_price: U256::from_str("5010674414300823856025303").unwrap(),
1729                },
1730                TickInfo {
1731                    index: -192891,
1732                    net_liquidity: 1687883551433195,
1733                    sqrt_price: U256::from_str("5134689105039642314202223").unwrap(),
1734                },
1735                TickInfo {
1736                    index: -192573,
1737                    net_liquidity: 11108903221360975,
1738                    sqrt_price: U256::from_str("5216979018647067786855495").unwrap(),
1739                },
1740                TickInfo {
1741                    index: -192448,
1742                    net_liquidity: 32888457482352,
1743                    sqrt_price: U256::from_str("5249685603828944002327927").unwrap(),
1744                },
1745                TickInfo {
1746                    index: -191525,
1747                    net_liquidity: -221726179374067,
1748                    sqrt_price: U256::from_str("5497623359964843320146512").unwrap(),
1749                },
1750                TickInfo {
1751                    index: -191447,
1752                    net_liquidity: -32888457482352,
1753                    sqrt_price: U256::from_str("5519104878745833608097296").unwrap(),
1754                },
1755                TickInfo {
1756                    index: -191444,
1757                    net_liquidity: -114125890211958292,
1758                    sqrt_price: U256::from_str("5519932765173943847315221").unwrap(),
1759                },
1760                TickInfo {
1761                    index: -191417,
1762                    net_liquidity: -101340835774487,
1763                    sqrt_price: U256::from_str("5527389333636021285046380").unwrap(),
1764                },
1765                TickInfo {
1766                    index: -191384,
1767                    net_liquidity: -9664144015186047,
1768                    sqrt_price: U256::from_str("5536516597603056457376182").unwrap(),
1769                },
1770                TickInfo {
1771                    index: -191148,
1772                    net_liquidity: -561268407024557,
1773                    sqrt_price: U256::from_str("5602231161238705865493165").unwrap(),
1774                },
1775                TickInfo {
1776                    index: -191147,
1777                    net_liquidity: -1687883551433195,
1778                    sqrt_price: U256::from_str("5602511265794328966803451").unwrap(),
1779                },
1780                TickInfo {
1781                    index: -191091,
1782                    net_liquidity: -89032603464508,
1783                    sqrt_price: U256::from_str("5618219493196441347292357").unwrap(),
1784                },
1785                TickInfo {
1786                    index: -190950,
1787                    net_liquidity: -189177935487638,
1788                    sqrt_price: U256::from_str("5657965894785859782969011").unwrap(),
1789                },
1790                TickInfo {
1791                    index: -190756,
1792                    net_liquidity: -6123093436523361,
1793                    sqrt_price: U256::from_str("5713112435031881967192022").unwrap(),
1794                },
1795                TickInfo {
1796                    index: -190548,
1797                    net_liquidity: -193367475630077,
1798                    sqrt_price: U256::from_str("5772835841671084402427710").unwrap(),
1799                },
1800                TickInfo {
1801                    index: -190430,
1802                    net_liquidity: -11108903221360975,
1803                    sqrt_price: U256::from_str("5806994534290341208820930").unwrap(),
1804                },
1805                TickInfo {
1806                    index: -190195,
1807                    net_liquidity: -391583014714302569,
1808                    sqrt_price: U256::from_str("5875625707132601785181387").unwrap(),
1809                },
1810                TickInfo {
1811                    index: -190043,
1812                    net_liquidity: -357016631583746,
1813                    sqrt_price: U256::from_str("5920448331650864936739481").unwrap(),
1814                },
1815                TickInfo {
1816                    index: -189779,
1817                    net_liquidity: -917243184365178,
1818                    sqrt_price: U256::from_str("5999112356918485175181346").unwrap(),
1819                },
1820                TickInfo {
1821                    index: -189663,
1822                    net_liquidity: -2344045150766798,
1823                    sqrt_price: U256::from_str("6034006559279282606084981").unwrap(),
1824                },
1825                TickInfo {
1826                    index: -189620,
1827                    net_liquidity: -435344726052344,
1828                    sqrt_price: U256::from_str("6046992979471024289177519").unwrap(),
1829                },
1830                TickInfo {
1831                    index: -189409,
1832                    net_liquidity: -825711941800452,
1833                    sqrt_price: U256::from_str("6111123241285165242130911").unwrap(),
1834                },
1835                TickInfo {
1836                    index: -189325,
1837                    net_liquidity: -3947182209207,
1838                    sqrt_price: U256::from_str("6136842645893819031257990").unwrap(),
1839                },
1840                TickInfo {
1841                    index: -189324,
1842                    net_liquidity: -415630967437234,
1843                    sqrt_price: U256::from_str("6137149480355443943537284").unwrap(),
1844                },
1845                TickInfo {
1846                    index: -115136,
1847                    net_liquidity: 462452451821,
1848                    sqrt_price: U256::from_str("250529060232794967902094762").unwrap(),
1849                },
1850                TickInfo {
1851                    index: -92109,
1852                    net_liquidity: -462452451821,
1853                    sqrt_price: U256::from_str("792242363124136400178523925").unwrap(),
1854                },
1855                TickInfo {
1856                    index: 887272,
1857                    net_liquidity: -521280453734808,
1858                    sqrt_price: U256::from_str("1461446703485210103287273052203988822378723970342")
1859                        .unwrap(),
1860                },
1861            ],
1862        )
1863        .unwrap();
1864
1865        let t0 = usdc();
1866        let t1 = eth();
1867
1868        let out = usv4_state
1869            .get_amount_out(BigUint::from_u64(2000000).unwrap(), &t0, &t1)
1870            .unwrap();
1871
1872        assert_eq!(out.amount, BigUint::from_str("436478419853848").unwrap())
1873    }
1874
1875    #[test]
1876    fn test_get_amount_out_euler_hook() {
1877        // Test using transaction 0xb372306a81c6e840f4ec55f006da6b0b097f435802a2e6fd216998dd12fb4aca
1878        //
1879        // Output of beforeSwap:
1880        // "output":{
1881        //      "amountToSwap":"0"
1882        //      "hookReturn":"2520471492123673565794154180707800634502860978735"
1883        //      "lpFeeOverride":"0"
1884        // }
1885        //
1886        // Output of entire swap, including hooks:
1887        // "swapDelta":"-2520471491783391198873215717244426027071092767279"
1888        //
1889        // Get amount out:
1890        // "amountOut":"2681115183499232721"
1891
1892        let block = BlockHeader {
1893            number: 22689128,
1894            hash: Bytes::from_str(
1895                "0xfbfa716523d25d6d5248c18d001ca02b1caf10cabd1ab7321465e2262c41157b",
1896            )
1897            .expect("Invalid block hash"),
1898            timestamp: 1749739055,
1899            ..Default::default()
1900        };
1901
1902        // Pool ID: 0xdd8dd509e58ec98631b800dd6ba86ee569c517ffbd615853ed5ab815bbc48ccb
1903        // Information taken from Tenderly simulation
1904        let mut usv4_state = UniswapV4State::new(
1905            0,
1906            U256::from_str("4295128740").unwrap(),
1907            UniswapV4Fees { zero_for_one: 100, one_for_zero: 90, lp_fee: 500 },
1908            0,
1909            1,
1910            vec![],
1911        )
1912        .unwrap();
1913
1914        let hook_address: Address = Address::from_str("0x69058613588536167ba0aa94f0cc1fe420ef28a8")
1915            .expect("Invalid hook address");
1916
1917        let db = SimulationDB::new(
1918            get_client(None).expect("Failed to create client"),
1919            get_runtime().expect("Failed to get runtime"),
1920            Some(block.clone()),
1921        );
1922        let engine = create_engine(db, true).expect("Failed to create simulation engine");
1923        let pool_manager = Address::from_str("0x000000000004444c5dc75cb358380d2e3de08a90")
1924            .expect("Invalid pool manager address");
1925
1926        let hook_handler = GenericVMHookHandler::new(
1927            hook_address,
1928            engine,
1929            pool_manager,
1930            HashMap::new(),
1931            HashMap::new(),
1932            None,
1933            true, // Euler hook
1934        )
1935        .unwrap();
1936
1937        let t0 = usdc();
1938        let t1 = weth();
1939
1940        usv4_state.set_hook_handler(Box::new(hook_handler));
1941        let out = usv4_state
1942            .get_amount_out(BigUint::from_u64(7407000000).unwrap(), &t0, &t1)
1943            .unwrap();
1944
1945        assert_eq!(out.amount, BigUint::from_str("2681115183499232721").unwrap())
1946    }
1947
1948    #[test]
1949    fn test_get_amount_out_angstrom_hook() {
1950        // Test using transaction 0x671b8e1d0966cee520dc2bb9628de8e22a17b036e70077504796d0a476932d21
1951        let mut usv4_state = UniswapV4State::new(
1952            // Liquidity and tick taken from tycho indexer for same block as transaction
1953            66319800403673162,
1954            U256::from_str("1314588940601923011323000261788004").unwrap(),
1955            // 8388608 (i.e. 0x800000) signifies a dynamic fee.
1956            UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 8388608 },
1957            194343,
1958            10,
1959            vec![
1960                TickInfo::new(-887270, 198117767801).unwrap(),
1961                TickInfo::new(191990, 24561988698695).unwrap(),
1962                TickInfo::new(192280, 2839631428751224).unwrap(),
1963                TickInfo::new(193130, 318786492813931).unwrap(),
1964                TickInfo::new(194010, 26209207141081).unwrap(),
1965                TickInfo::new(194210, -26209207141081).unwrap(),
1966                TickInfo::new(194220, 63136622375641511).unwrap(),
1967                TickInfo::new(194420, -63136622375641511).unwrap(),
1968                TickInfo::new(195130, -318786492813931).unwrap(),
1969                TickInfo::new(196330, -2839631428751224).unwrap(),
1970                TickInfo::new(197100, -24561988698695).unwrap(),
1971                TickInfo::new(887270, -198117767801).unwrap(),
1972            ],
1973        )
1974        .unwrap();
1975
1976        let fees = AngstromFees {
1977            // To get these values, enable storage access logs on tenderly,
1978            // and look at the hex value retrieved right after calling afterSwap
1979            //
1980            // The value (hex: 0x70000152) contains two packed uint24 values:
1981            // Lower 24 bits (unlockedFee):         0x152   = 338
1982            // Upper 24 bits (protocolUnlockedFee): 0x70    = 112
1983            unlock: U24::from(338),
1984            protocol_unlock: U24::from(112),
1985        };
1986        let hook_handler = AngstromHookHandler::new(
1987            Address::from_str("0x0000000aa232009084bd71a5797d089aa4edfad4").unwrap(),
1988            Address::from_str("0x000000000004444c5dc75cb358380d2e3de08a90").unwrap(),
1989            fees,
1990            false,
1991        );
1992
1993        let t0 = usdc();
1994        let t1 = weth();
1995
1996        usv4_state.set_hook_handler(Box::new(hook_handler));
1997        let out = usv4_state
1998            .get_amount_out(
1999                BigUint::from_u64(
2000                    6645198144, // usdc
2001                )
2002                .unwrap(),
2003                &t0, // usdc IN
2004                &t1, // weth OUT
2005            )
2006            .unwrap();
2007
2008        assert_eq!(out.amount, BigUint::from_str("1825627051870330472").unwrap())
2009    }
2010
2011    /// Hook that only answers `after_swap`, with a configurable address and returned delta.
2012    #[derive(Debug, Clone, PartialEq, Eq)]
2013    struct AfterSwapTestHook {
2014        address: Address,
2015        delta: I128,
2016        /// Share of the output the hook reports analytically, in basis points. `None` models a
2017        /// hook that cannot price its fee without a simulation.
2018        analytic_fee_bps: Option<u32>,
2019        /// Price the hook answers `spot_price` with. `None` models a hook that leaves the price
2020        /// to the pool by failing recoverably.
2021        spot_price_override: Option<u64>,
2022    }
2023
2024    impl HookHandler for AfterSwapTestHook {
2025        fn address(&self) -> Address {
2026            self.address
2027        }
2028
2029        fn before_swap(
2030            &self,
2031            _: BeforeSwapParameters,
2032            _: Option<HashMap<Address, HashMap<U256, U256>>>,
2033            _: Option<HashMap<Address, HashMap<U256, U256>>>,
2034        ) -> Result<WithGasEstimate<BeforeSwapOutput>, SimulationError> {
2035            Err(SimulationError::RecoverableError("not implemented".into()))
2036        }
2037
2038        fn after_swap(
2039            &self,
2040            _: AfterSwapParameters,
2041            _: Option<HashMap<Address, HashMap<U256, U256>>>,
2042            _: Option<HashMap<Address, HashMap<U256, U256>>>,
2043        ) -> Result<WithGasEstimate<AfterSwapDelta>, SimulationError> {
2044            Ok(WithGasEstimate { gas_estimate: AFTER_SWAP_TEST_HOOK_GAS, result: self.delta })
2045        }
2046
2047        fn fee(&self, _: &UniswapV4State, _: SwapParams) -> Result<f64, SimulationError> {
2048            Err(SimulationError::RecoverableError("not implemented".into()))
2049        }
2050
2051        fn spot_price(&self, _: &Token, _: &Token) -> Result<f64, SimulationError> {
2052            match self.spot_price_override {
2053                Some(price) => Ok(price as f64),
2054                None => Err(SimulationError::RecoverableError("not implemented".into())),
2055            }
2056        }
2057
2058        fn unspecified_fee_amount(
2059            &self,
2060            unspecified: U256,
2061            _: bool,
2062        ) -> Result<Option<U256>, SimulationError> {
2063            let Some(bps) = self.analytic_fee_bps else { return Ok(None) };
2064            Ok(Some(unspecified * U256::from(bps) / U256::from(10_000u64)))
2065        }
2066
2067        fn get_amount_ranges(&self, _: Bytes, _: Bytes) -> Result<AmountRanges, SimulationError> {
2068            Err(SimulationError::RecoverableError("not implemented".into()))
2069        }
2070
2071        fn delta_transition(
2072            &mut self,
2073            _: ProtocolStateDelta,
2074            _: &HashMap<Bytes, Token>,
2075            _: &Balances,
2076        ) -> Result<(), TransitionError> {
2077            Ok(())
2078        }
2079
2080        fn clone_box(&self) -> Box<dyn HookHandler> {
2081            Box::new(self.clone())
2082        }
2083
2084        fn as_any(&self) -> &dyn Any {
2085            self
2086        }
2087
2088        fn is_equal(&self, other: &dyn HookHandler) -> bool {
2089            other.as_any().downcast_ref::<Self>() == Some(self)
2090        }
2091    }
2092
2093    // Deliberately different values: the gate tests below assert on a gas delta and on an amount
2094    // delta, and equal constants would let a mix-up between the two pass.
2095    const AFTER_SWAP_TEST_HOOK_GAS: u64 = 1_000;
2096    const AFTER_SWAP_TEST_HOOK_DELTA: u64 = 777;
2097
2098    fn construct_hook_address(hook_options: &[HookOptions]) -> Address {
2099        let mut hook_flags = U160::ZERO;
2100        let one = U160::from_limbs([1, 0, 0]);
2101        for hook_option in hook_options {
2102            hook_flags |= one << (*hook_option as u8);
2103        }
2104        Address::from(hook_flags)
2105    }
2106
2107    fn after_swap_test_hook(hook_options: &[HookOptions]) -> Box<dyn HookHandler> {
2108        Box::new(AfterSwapTestHook {
2109            address: construct_hook_address(hook_options),
2110            delta: I128::unchecked_from(AFTER_SWAP_TEST_HOOK_DELTA),
2111            analytic_fee_bps: None,
2112            spot_price_override: None,
2113        })
2114    }
2115
2116    /// A hook that prices its fee analytically as `analytic_fee_bps` of the output.
2117    fn analytic_fee_test_hook(analytic_fee_bps: u32) -> Box<dyn HookHandler> {
2118        Box::new(AfterSwapTestHook {
2119            address: construct_hook_address(&[
2120                HookOptions::AfterSwap,
2121                HookOptions::AfterSwapReturnsDelta,
2122            ]),
2123            delta: I128::ZERO,
2124            analytic_fee_bps: Some(analytic_fee_bps),
2125            spot_price_override: None,
2126        })
2127    }
2128
2129    /// The price a test hook answers `spot_price` with when it is asked for one. Far from any
2130    /// price the test pool could quote, so a test cannot confuse the two.
2131    const TEST_HOOK_SPOT_PRICE: u64 = 1_000_000;
2132
2133    /// A hook that answers `spot_price` with [`TEST_HOOK_SPOT_PRICE`], and prices its fee
2134    /// analytically when `analytic_fee_bps` is `Some`.
2135    fn pricing_test_hook(analytic_fee_bps: Option<u32>) -> Box<dyn HookHandler> {
2136        Box::new(AfterSwapTestHook {
2137            address: construct_hook_address(&[
2138                HookOptions::AfterSwap,
2139                HookOptions::AfterSwapReturnsDelta,
2140            ]),
2141            delta: I128::ZERO,
2142            analytic_fee_bps,
2143            spot_price_override: Some(TEST_HOOK_SPOT_PRICE),
2144        })
2145    }
2146
2147    fn basic_v4_test_pool_tokens(zero_for_one: bool) -> (Token, Token) {
2148        if zero_for_one {
2149            (token_x(), token_y())
2150        } else {
2151            (token_y(), token_x())
2152        }
2153    }
2154
2155    /// An `afterSwap` delta must be discarded unless the hook also carries
2156    /// `AfterSwapReturnsDelta`, mirroring `Hooks.afterSwap` in v4-core.
2157    #[rstest]
2158    #[case::zero_for_one(true)]
2159    #[case::one_for_zero(false)]
2160    fn test_after_swap_delta_ignored_without_returns_delta_permission(#[case] zero_for_one: bool) {
2161        let (token_in, token_out) = basic_v4_test_pool_tokens(zero_for_one);
2162        let amount_in = BigUint::from(1_000_000_000_000_000u64);
2163
2164        let hookless = create_basic_v4_test_pool()
2165            .get_amount_out(amount_in.clone(), &token_in, &token_out)
2166            .expect("hookless swap should succeed");
2167
2168        let mut pool = create_basic_v4_test_pool();
2169        pool.set_hook_handler(after_swap_test_hook(&[HookOptions::AfterSwap]));
2170        let gated = pool
2171            .get_amount_out(amount_in, &token_in, &token_out)
2172            .expect("gated swap should succeed");
2173
2174        assert_eq!(gated.amount, hookless.amount);
2175        assert_eq!(
2176            gated.gas,
2177            hookless.gas + BigUint::from(AFTER_SWAP_TEST_HOOK_GAS + PM_PER_HOOK_CALL_OVERHEAD)
2178        );
2179    }
2180
2181    /// With `AfterSwapReturnsDelta` set, the returned delta is taken out of the unspecified
2182    /// (output) currency.
2183    #[rstest]
2184    #[case::zero_for_one(true)]
2185    #[case::one_for_zero(false)]
2186    fn test_after_swap_delta_applied_with_returns_delta_permission(#[case] zero_for_one: bool) {
2187        let (token_in, token_out) = basic_v4_test_pool_tokens(zero_for_one);
2188        let amount_in = BigUint::from(1_000_000_000_000_000u64);
2189
2190        let hookless = create_basic_v4_test_pool()
2191            .get_amount_out(amount_in.clone(), &token_in, &token_out)
2192            .expect("hookless swap should succeed");
2193
2194        let mut pool = create_basic_v4_test_pool();
2195        pool.set_hook_handler(after_swap_test_hook(&[
2196            HookOptions::AfterSwap,
2197            HookOptions::AfterSwapReturnsDelta,
2198        ]));
2199        let with_delta = pool
2200            .get_amount_out(amount_in, &token_in, &token_out)
2201            .expect("swap with hook delta should succeed");
2202
2203        assert_eq!(with_delta.amount, &hookless.amount - BigUint::from(AFTER_SWAP_TEST_HOOK_DELTA));
2204        assert_eq!(
2205            with_delta.gas,
2206            hookless.gas + BigUint::from(AFTER_SWAP_TEST_HOOK_GAS + PM_PER_HOOK_CALL_OVERHEAD)
2207        );
2208    }
2209
2210    const FEELESS_POOL_LIQUIDITY: u128 = 100_000_000_000_000_000_000; // 100e18
2211
2212    /// A pool with no LP fee and no protocol fee, holding a single position that spans the whole
2213    /// walkable tick range, so every difference between a hooked and a hookless answer is the
2214    /// hook's doing alone.
2215    fn create_feeless_v4_test_pool(liquidity: u128) -> UniswapV4State {
2216        let sqrt_price = get_sqrt_price_q96(U256::from(20_000_000u64), U256::from(10_000_000u64))
2217            .expect("a price of two has a square root");
2218        let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("the sqrt price maps to a tick");
2219        let position = FEELESS_POOL_LIQUIDITY as i128;
2220
2221        UniswapV4State::new(
2222            liquidity,
2223            sqrt_price,
2224            UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 0 },
2225            tick,
2226            60,
2227            vec![
2228                TickInfo::new(-46_080, position).unwrap(),
2229                TickInfo::new(46_080, -position).unwrap(),
2230            ],
2231        )
2232        .expect("the pool builds")
2233    }
2234
2235    fn pons_test_handler() -> PonsV2HookHandler {
2236        PonsV2HookHandler::new(PONS_V2_HOOK_ROBINHOOD, 100, 100)
2237    }
2238
2239    /// A pool whose `hooks` attribute is the zero address carries no handler, so it prices
2240    /// through `core_spot_price`: the documented `ProtocolSim::spot_price` contract, which is the
2241    /// buy price `P / (1 − f)` rather than the sell-side slope `P · (1 − f)` a finite-difference
2242    /// fallback would return. Pinned to a concrete number in both token orderings.
2243    #[test]
2244    fn hookless_spot_price_is_the_documented_buy_price_with_lp_fee_markup() {
2245        const LP_FEE_PIPS: u32 = 3_000;
2246        const LP_FEE: f64 = LP_FEE_PIPS as f64 / 1_000_000.0;
2247        const LIQUIDITY: u128 = 100_000_000_000_000_000_000; // 100e18
2248        const TOLERANCE: f64 = 1e-12;
2249
2250        let sqrt_price = get_sqrt_price_q96(U256::from(20_000_000u64), U256::from(10_000_000u64))
2251            .expect("a price of two has a square root");
2252        let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("the sqrt price maps to a tick");
2253        let pool = UniswapV4State::new(
2254            LIQUIDITY,
2255            sqrt_price,
2256            UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: LP_FEE_PIPS },
2257            tick,
2258            60,
2259            vec![
2260                TickInfo::new(-46_080, LIQUIDITY as i128).unwrap(),
2261                TickInfo::new(46_080, -(LIQUIDITY as i128)).unwrap(),
2262            ],
2263        )
2264        .expect("the pool builds");
2265
2266        assert!(pool.hook.is_none(), "the pinned values only hold with no hook handler");
2267
2268        let (t0, t1) = (token_x(), token_y());
2269        let pre_fee = sqrt_price_q96_to_f64(sqrt_price, t0.decimals, t1.decimals)
2270            .expect("the sqrt price converts to a price");
2271
2272        let expected_buy_t0 = pre_fee / (1.0 - LP_FEE);
2273        let expected_buy_t1 = (1.0 / pre_fee) / (1.0 - LP_FEE);
2274
2275        let buy_t0 = pool
2276            .spot_price(&t0, &t1)
2277            .expect("a hookless pool always prices");
2278        let buy_t1 = pool
2279            .spot_price(&t1, &t0)
2280            .expect("a hookless pool always prices");
2281
2282        assert!(
2283            (buy_t0 / expected_buy_t0 - 1.0).abs() < TOLERANCE,
2284            "buying t0 quoted {buy_t0}, expected {expected_buy_t0}"
2285        );
2286        assert!(
2287            (buy_t1 / expected_buy_t1 - 1.0).abs() < TOLERANCE,
2288            "buying t1 quoted {buy_t1}, expected {expected_buy_t1}"
2289        );
2290    }
2291
2292    /// Pons takes its cut out of the swap's output, so buying one `base` through it costs
2293    /// `core / (1 − rate)` of `quote`. At 100 + 100 bps that is `1 / 0.98` of the hookless
2294    /// price: above it, never below.
2295    #[rstest]
2296    #[case::base_is_currency0(true)]
2297    #[case::base_is_currency1(false)]
2298    fn test_spot_price_marks_up_an_analytic_hook_fee(#[case] base_is_currency0: bool) {
2299        let (base, quote) = basic_v4_test_pool_tokens(base_is_currency0);
2300        let mut hooked = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY);
2301        hooked.set_hook_handler(Box::new(pons_test_handler()));
2302
2303        let core = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY)
2304            .spot_price(&base, &quote)
2305            .expect("a hookless pool always prices");
2306        let price = hooked
2307            .spot_price(&base, &quote)
2308            .expect("the hook prices its own fee");
2309
2310        assert!(price > core, "hooked {price} is not above hookless {core}");
2311        let ratio = price / core;
2312        assert!((ratio * 0.98 - 1.0).abs() < 1e-9, "hooked/hookless is {ratio}, not 1/0.98");
2313    }
2314
2315    /// The marked-up spot price is the limit of the hooked quote as the trade shrinks: buying
2316    /// 1e-4 of a token out of 100e18 of liquidity executes within 1e-4 of it.
2317    #[rstest]
2318    #[case::base_is_currency0(true)]
2319    #[case::base_is_currency1(false)]
2320    fn test_hooked_spot_price_matches_a_small_hooked_buy(#[case] base_is_currency0: bool) {
2321        let (base, quote) = basic_v4_test_pool_tokens(base_is_currency0);
2322        let mut hooked = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY);
2323        hooked.set_hook_handler(Box::new(pons_test_handler()));
2324
2325        let quote_in = BigUint::from(100_000_000_000_000u64);
2326        let base_out = hooked
2327            .get_amount_out(quote_in.clone(), &quote, &base)
2328            .expect("a tiny buy always fits the pool")
2329            .amount;
2330
2331        let executed = quote_in.to_f64().unwrap() / base_out.to_f64().unwrap();
2332        let spot = hooked
2333            .spot_price(&base, &quote)
2334            .expect("the hook prices its own fee");
2335
2336        assert!((executed / spot - 1.0).abs() < 1e-4, "executed {executed}, quoted {spot}");
2337    }
2338
2339    /// A hook that prices its fee analytically is priced from the pool, not from whatever its
2340    /// own `spot_price` answers: the analytic rate is the whole of what it does to the price,
2341    /// and asking the handler would cost a simulation for no gain.
2342    #[rstest]
2343    #[case::base_is_currency0(true)]
2344    #[case::base_is_currency1(false)]
2345    fn test_spot_price_prefers_an_analytic_fee_over_the_hooks_own_price(
2346        #[case] base_is_currency0: bool,
2347    ) {
2348        let (base, quote) = basic_v4_test_pool_tokens(base_is_currency0);
2349        let mut hooked = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY);
2350        hooked.set_hook_handler(pricing_test_hook(Some(200)));
2351
2352        let core = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY)
2353            .spot_price(&base, &quote)
2354            .expect("a hookless pool always prices");
2355        let price = hooked
2356            .spot_price(&base, &quote)
2357            .expect("the hook prices its own fee");
2358
2359        assert_ne!(
2360            price, TEST_HOOK_SPOT_PRICE as f64,
2361            "the pool asked the hook for a price instead of marking up its own"
2362        );
2363        let ratio = price / core;
2364        assert!((ratio * 0.98 - 1.0).abs() < 1e-9, "hooked/hookless is {ratio}, not 1/0.98");
2365    }
2366
2367    /// The companion of the test above: the very same handler, with nothing but its analytic fee
2368    /// taken away, is asked for a price and its answer is passed through. That is what the pool
2369    /// would return for the hook above if it consulted the handler first.
2370    #[rstest]
2371    #[case::base_is_currency0(true)]
2372    #[case::base_is_currency1(false)]
2373    fn test_spot_price_uses_the_hooks_own_price_without_an_analytic_fee(
2374        #[case] base_is_currency0: bool,
2375    ) {
2376        let (base, quote) = basic_v4_test_pool_tokens(base_is_currency0);
2377        let mut hooked = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY);
2378        hooked.set_hook_handler(pricing_test_hook(None));
2379
2380        let price = hooked
2381            .spot_price(&base, &quote)
2382            .expect("the hook answers with a price");
2383
2384        assert_eq!(price, TEST_HOOK_SPOT_PRICE as f64);
2385    }
2386
2387    /// A handler that does not price its fee analytically keeps the finite-difference fallback,
2388    /// which reads the slope of two quotes and so cancels out a constant per-swap take.
2389    #[rstest]
2390    #[case::base_is_currency0(true)]
2391    #[case::base_is_currency1(false)]
2392    fn test_spot_price_falls_back_to_finite_difference_without_an_analytic_fee(
2393        #[case] base_is_currency0: bool,
2394    ) {
2395        let (base, quote) = basic_v4_test_pool_tokens(base_is_currency0);
2396        let mut hooked = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY);
2397        hooked.set_hook_handler(after_swap_test_hook(&[
2398            HookOptions::AfterSwap,
2399            HookOptions::AfterSwapReturnsDelta,
2400        ]));
2401
2402        let x1 = BigUint::from(10u64).pow(base.decimals) / BigUint::from(100u64);
2403        let x2 = &x1 + (&x1 / BigUint::from(100u64));
2404        let y1 = hooked
2405            .get_amount_out(x1.clone(), &base, &quote)
2406            .expect("the smaller probe swap fits")
2407            .amount;
2408        let y2 = hooked
2409            .get_amount_out(x2.clone(), &base, &quote)
2410            .expect("the larger probe swap fits")
2411            .amount;
2412        let slope = (&y2 - &y1).to_f64().unwrap() / (&x2 - &x1).to_f64().unwrap();
2413
2414        let price = hooked
2415            .spot_price(&base, &quote)
2416            .expect("the fallback always prices");
2417
2418        assert_eq!(price, slope);
2419        let core = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY)
2420            .spot_price(&base, &quote)
2421            .expect("a hookless pool always prices");
2422        assert!((price / core - 1.0).abs() < 1e-3, "fallback {price} strayed from core {core}");
2423    }
2424
2425    /// `get_limits` reports what a swapper actually receives, so the hook's cut comes off the
2426    /// output. The input the pool can absorb is unchanged: the hook charges the other leg.
2427    #[rstest]
2428    #[case::zero_for_one(true)]
2429    #[case::one_for_zero(false)]
2430    fn test_get_limits_reports_the_output_net_of_an_analytic_hook_fee(#[case] zero_for_one: bool) {
2431        let (token_in, token_out) = basic_v4_test_pool_tokens(zero_for_one);
2432        let mut hooked = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY);
2433        hooked.set_hook_handler(Box::new(pons_test_handler()));
2434
2435        let (hookless_in, hookless_out) = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY)
2436            .get_limits(token_in.address.clone(), token_out.address.clone())
2437            .expect("a pool with liquidity has limits");
2438        let (limit_in, limit_out) = hooked
2439            .get_limits(token_in.address.clone(), token_out.address.clone())
2440            .expect("a pool with liquidity has limits");
2441
2442        assert!(hookless_out > BigUint::zero(), "the reference pool must move some output");
2443        assert_eq!(limit_in, hookless_in);
2444        let taken = pons_test_handler()
2445            .fee_and_tax(biguint_to_u256(&hookless_out))
2446            .expect("a pool sized output never overflows");
2447        assert_eq!(limit_out, &hookless_out - u256_to_biguint(taken));
2448        assert!(limit_out < hookless_out);
2449    }
2450
2451    /// A handler with no analytic fee leaves the limits exactly where the pool's own liquidity
2452    /// puts them, so hooks that quote by simulation are unaffected.
2453    #[rstest]
2454    #[case::zero_for_one(true)]
2455    #[case::one_for_zero(false)]
2456    fn test_get_limits_unchanged_without_an_analytic_hook_fee(#[case] zero_for_one: bool) {
2457        let (token_in, token_out) = basic_v4_test_pool_tokens(zero_for_one);
2458        let mut hooked = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY);
2459        hooked.set_hook_handler(after_swap_test_hook(&[
2460            HookOptions::AfterSwap,
2461            HookOptions::AfterSwapReturnsDelta,
2462        ]));
2463
2464        let hookless = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY)
2465            .get_limits(token_in.address.clone(), token_out.address.clone())
2466            .expect("a pool with liquidity has limits");
2467        let limits = hooked
2468            .get_limits(token_in.address.clone(), token_out.address.clone())
2469            .expect("a pool with liquidity has limits");
2470
2471        assert_eq!(limits, hookless);
2472    }
2473
2474    /// A pool with no liquidity of its own still reports no limits: the hook does not manage the
2475    /// liquidity here, so there is no output for it to take a cut of.
2476    #[test]
2477    fn test_get_limits_on_a_drained_pool_is_zero_with_an_analytic_hook() {
2478        let (token_in, token_out) = basic_v4_test_pool_tokens(true);
2479        let mut drained = create_feeless_v4_test_pool(0);
2480        drained.set_hook_handler(Box::new(pons_test_handler()));
2481
2482        let limits = drained
2483            .get_limits(token_in.address, token_out.address)
2484            .expect("a drained pool reports zero rather than failing");
2485
2486        assert_eq!(limits, (BigUint::zero(), BigUint::zero()));
2487    }
2488
2489    /// A hook that keeps the whole output leaves no price to quote. The markup would divide by
2490    /// zero or go negative, so the pool has to report the error instead.
2491    #[rstest]
2492    #[case::the_whole_output(10_000)]
2493    #[case::more_than_the_whole_output(20_000)]
2494    fn test_spot_price_rejects_a_hook_that_takes_the_whole_output(#[case] analytic_fee_bps: u32) {
2495        let mut pool = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY);
2496        pool.set_hook_handler(analytic_fee_test_hook(analytic_fee_bps));
2497
2498        let error = pool
2499            .spot_price(&token_x(), &token_y())
2500            .expect_err("a rate of one or more is not a price");
2501
2502        assert!(matches!(error, SimulationError::FatalError(_)), "{error:?}");
2503    }
2504
2505    #[test]
2506    fn test_spot_price_with_recoverable_error() {
2507        // Test that spot_price correctly falls back to swap-based calculation
2508        // when a RecoverableError (other than "not implemented") is returned
2509
2510        let usv4_state = UniswapV4State::new(
2511            1000000000000000000u128,                                  // 1e18 liquidity
2512            U256::from_str("79228162514264337593543950336").unwrap(), // 1:1 price
2513            UniswapV4Fees { zero_for_one: 100, one_for_zero: 100, lp_fee: 100 },
2514            0,
2515            60,
2516            vec![
2517                TickInfo::new(-600, 500000000000000000i128).unwrap(),
2518                TickInfo::new(600, -500000000000000000i128).unwrap(),
2519            ],
2520        )
2521        .unwrap();
2522
2523        // Test spot price calculation without a hook (should use default implementation)
2524        let spot_price_result = usv4_state.spot_price(&usdc(), &weth());
2525        assert!(spot_price_result.is_ok());
2526
2527        // The price should be approximately 1.0 (since we set sqrt_price for 1:1)
2528        // Adjusting for decimals difference (USDC has 6, WETH has 18)
2529        let price = spot_price_result.unwrap();
2530        assert!(price > 0.0);
2531    }
2532
2533    #[test]
2534    fn test_get_limits_with_hook_managed_liquidity_no_ranges_entrypoint() {
2535        // This test demonstrates the experimental limit finding logic for hooks that:
2536        // 1. Manage liquidity (pool has no liquidity & no ticks)
2537        // 2. Don't have get_amount_ranges entrypoint
2538
2539        let block = BlockHeader {
2540            number: 22689128,
2541            hash: Bytes::from_str(
2542                "0xfbfa716523d25d6d5248c18d001ca02b1caf10cabd1ab7321465e2262c41157b",
2543            )
2544            .expect("Invalid block hash"),
2545            timestamp: 1749739055,
2546            ..Default::default()
2547        };
2548
2549        let hook_address: Address = Address::from_str("0x69058613588536167ba0aa94f0cc1fe420ef28a8")
2550            .expect("Invalid hook address");
2551
2552        let db = SimulationDB::new(
2553            get_client(None).expect("Failed to create client"),
2554            get_runtime().expect("Failed to get runtime"),
2555            Some(block.clone()),
2556        );
2557        let engine = create_engine(db, true).expect("Failed to create simulation engine");
2558        let pool_manager = Address::from_str("0x000000000004444c5dc75cb358380d2e3de08a90")
2559            .expect("Invalid pool manager address");
2560
2561        // Create a GenericVMHookHandler without limits_entrypoint
2562        // This will trigger the "not set" error path and use experimental limit finding
2563        let hook_handler = GenericVMHookHandler::new(
2564            hook_address,
2565            engine,
2566            pool_manager,
2567            HashMap::new(),
2568            HashMap::new(),
2569            None,
2570            true, // Euler hook
2571        )
2572        .unwrap();
2573
2574        // Create a UniswapV4State with NO liquidity and NO ticks (hook manages all liquidity)
2575        let mut usv4_state = UniswapV4State::new(
2576            0, // no liquidity - hook provides it
2577            U256::from_str("4295128740").unwrap(),
2578            UniswapV4Fees { zero_for_one: 100, one_for_zero: 90, lp_fee: 500 },
2579            0,      // current tick
2580            1,      // tick spacing
2581            vec![], // no ticks - hook manages liquidity
2582        )
2583        .unwrap();
2584
2585        usv4_state.set_hook_handler(Box::new(hook_handler));
2586
2587        let token_in = usdc().address;
2588        let token_out = weth().address;
2589
2590        let (amount_in_limit, amount_out_limit) = usv4_state
2591            .get_limits(token_in, token_out)
2592            .expect("Should find limits through experimental swapping");
2593
2594        // Assuming pool supply doesn't change drastically at time of this test
2595        // At least 1 million USDC, not more than 100 million USDC
2596        assert!(amount_in_limit > BigUint::from(10u64).pow(12));
2597        assert!(amount_in_limit < BigUint::from(10u64).pow(14));
2598
2599        // At least 100 ETH, not more than 10 000 ETH
2600        assert!(amount_out_limit > BigUint::from(10u64).pow(20));
2601        assert!(amount_out_limit < BigUint::from(10u64).pow(22));
2602    }
2603
2604    #[rstest]
2605    #[case::high_liquidity(u128::MAX / 2)] // Very large liquidity
2606    #[case::medium_liquidity(10000000000000000000u128)] // Moderate liquidity: 10e18
2607    #[case::minimal_liquidity(1000u128)] // Very small liquidity
2608    fn test_find_max_amount(#[case] liquidity: u128) {
2609        // Use fixed configuration for all test cases
2610        let fees = UniswapV4Fees { zero_for_one: 100, one_for_zero: 100, lp_fee: 100 };
2611        let tick_spacing = 60;
2612        let ticks = vec![
2613            TickInfo::new(-600, (liquidity / 4) as i128).unwrap(),
2614            TickInfo::new(600, -((liquidity / 4) as i128)).unwrap(),
2615        ];
2616
2617        let usv4_state = UniswapV4State::new(
2618            liquidity,
2619            U256::from_str("79228162514264337593543950336").unwrap(),
2620            fees,
2621            0,
2622            tick_spacing,
2623            ticks,
2624        )
2625        .unwrap();
2626
2627        let token_in = usdc();
2628        let token_out = weth();
2629
2630        let (max_amount_in, _max_amount_out) = usv4_state
2631            .find_max_amount(&token_in, &token_out)
2632            .unwrap();
2633
2634        let success = usv4_state
2635            .get_amount_out(max_amount_in.clone(), &token_in, &token_out)
2636            .is_ok();
2637        assert!(success, "Should be able to swap the exact max amount.");
2638
2639        let one_more = &max_amount_in + BigUint::from(1u64);
2640        let should_fail = usv4_state
2641            .get_amount_out(one_more, &token_in, &token_out)
2642            .is_err();
2643        assert!(should_fail, "Swapping max_amount + 1 should fail.");
2644    }
2645
2646    #[test]
2647    fn test_calculate_swap_fees_with_override() {
2648        // Test that calculate_swap_fees_pips works correctly with overridden fees
2649        let fees = UniswapV4Fees::new(100, 90, 500);
2650
2651        // Without override, should use UniswapV4 formula: protocol + lp - (protocol * lp /
2652        // 1_000_000)
2653        let total_zero_for_one = fees.calculate_swap_fees_pips(true, None);
2654        // 100 + 500 - (100 * 500 / 1_000_000) = 600 - 0 = 600 (rounded down)
2655        assert_eq!(total_zero_for_one, 600);
2656
2657        // With override, should use override fee + protocol fee with same formula
2658        let total_with_override = fees.calculate_swap_fees_pips(true, Some(1000));
2659        // 100 + 1000 - (100 * 1000 / 1_000_000) = 1100 - 0 = 1100 (rounded down)
2660        assert_eq!(total_with_override, 1100);
2661    }
2662
2663    #[test]
2664    fn test_max_combined_fees_stays_valid() {
2665        // Test that even with max protocol + max LP fees, we stay under compute_swap_step limit
2666        let fees = UniswapV4Fees::new(1000, 1000, 1000);
2667        let total = fees.calculate_swap_fees_pips(true, Some(lp_fee::MAX_LP_FEE));
2668
2669        // Using UniswapV4 formula: 1000 + 1000000 - (1000 * 1000000 / 1_000_000)
2670        // = 1001000 - 1000 = 1000000
2671        assert_eq!(total, 1_000_000);
2672    }
2673
2674    #[test]
2675    fn test_get_limits_graceful_underflow() {
2676        // Verifies graceful handling of liquidity underflow in get_limits for V4
2677        let usv4_state = UniswapV4State::new(
2678            1000000,
2679            U256::from_str("79228162514264337593543950336").unwrap(), // 1:1 price
2680            UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 3000 },
2681            0,
2682            60,
2683            vec![
2684                // A tick with net_liquidity > current_liquidity
2685                // When zero_for_one=true, this gets negated and would cause underflow
2686                TickInfo {
2687                    index: -60,
2688                    net_liquidity: 2000000, // 2x current liquidity
2689                    sqrt_price: U256::from_str("79051508376726796163471739988").unwrap(),
2690                },
2691            ],
2692        )
2693        .unwrap();
2694
2695        let usdc = usdc();
2696        let weth = weth();
2697
2698        let (limit_in, limit_out) = usv4_state
2699            .get_limits(usdc.address.clone(), weth.address.clone())
2700            .unwrap();
2701
2702        // Should return some conservative limits
2703        assert!(limit_in > BigUint::zero());
2704        assert!(limit_out > BigUint::zero());
2705    }
2706
2707    // Tests based on Uniswap V4's ProtocolFeeLibrary.t.sol
2708    // See: https://github.com/Uniswap/v4-core/blob/main/test/libraries/ProtocolFeeLibrary.t.sol
2709
2710    /// Maximum protocol fee in pips (1000 = 0.1%)
2711    const MAX_PROTOCOL_FEE: u32 = 1000;
2712
2713    #[rstest]
2714    #[case::max_protocol_and_max_lp(MAX_PROTOCOL_FEE, lp_fee::MAX_LP_FEE, lp_fee::MAX_LP_FEE)]
2715    #[case::max_protocol_with_3000_lp(MAX_PROTOCOL_FEE, 3000, 3997)]
2716    #[case::max_protocol_with_zero_lp(MAX_PROTOCOL_FEE, 0, MAX_PROTOCOL_FEE)]
2717    #[case::zero_protocol_zero_lp(0, 0, 0)]
2718    #[case::zero_protocol_with_1000_lp(0, 1000, 1000)]
2719    fn test_calculate_swap_fees_uniswap_test_cases(
2720        #[case] protocol_fee: u32,
2721        #[case] lp_fee: u32,
2722        #[case] expected: u32,
2723    ) {
2724        let fees = UniswapV4Fees::new(protocol_fee, protocol_fee, lp_fee);
2725        let result = fees.calculate_swap_fees_pips(true, None);
2726        assert_eq!(result, expected);
2727    }
2728
2729    #[test]
2730    fn test_calculate_swap_fees_with_dynamic_fee() {
2731        // Test that dynamic fees default to 0 when no override is provided
2732        let fees = UniswapV4Fees::new(100, 90, lp_fee::DYNAMIC_FEE_FLAG);
2733
2734        // Without override, dynamic fee should be treated as 0
2735        let total_zero_for_one = fees.calculate_swap_fees_pips(true, None);
2736        // 100 + 0 - (100 * 0 / 1_000_000) = 100
2737        assert_eq!(total_zero_for_one, 100);
2738
2739        // With override, should use the override value
2740        let total_with_override = fees.calculate_swap_fees_pips(true, Some(500));
2741        // 100 + 500 - (100 * 500 / 1_000_000) = 600 - 0 = 600
2742        assert_eq!(total_with_override, 600);
2743    }
2744
2745    #[test]
2746    fn test_calculate_swap_fees_direction_matters() {
2747        // Test that zero_for_one direction affects which protocol fee is used
2748        let fees = UniswapV4Fees::new(100, 200, 500);
2749
2750        let zero_for_one_fee = fees.calculate_swap_fees_pips(true, None);
2751        // 100 + 500 - (100 * 500 / 1_000_000) = 600 - 0 = 600
2752        assert_eq!(zero_for_one_fee, 600);
2753
2754        let one_for_zero_fee = fees.calculate_swap_fees_pips(false, None);
2755        // 200 + 500 - (200 * 500 / 1_000_000) = 700 - 0 = 700
2756        assert_eq!(one_for_zero_fee, 700);
2757    }
2758
2759    #[rstest]
2760    #[case::high_lp_fee(1000, 500_000, 500_500)] // 1000 + 500k - 500 = 500.5k
2761    #[case::mid_fees(500, 500_000, 500_250)] // 500 + 500k - 250 = 500.25k
2762    #[case::low_fees(100, 100_000, 100_090)] // 100 + 100k - 10 = 100.09k
2763    fn test_calculate_swap_fees_formula_precision(
2764        #[case] protocol_fee: u32,
2765        #[case] lp_fee: u32,
2766        #[case] expected: u32,
2767    ) {
2768        // Test cases where the subtraction term (protocol * lp / 1M) significantly affects the
2769        // result
2770        let fees = UniswapV4Fees::new(protocol_fee, protocol_fee, lp_fee);
2771        let result = fees.calculate_swap_fees_pips(true, None);
2772        assert_eq!(result, expected, "Failed for protocol={}, lp={}", protocol_fee, lp_fee);
2773    }
2774
2775    #[test]
2776    fn test_calculate_swap_fees_override_takes_precedence() {
2777        // Test that lp_fee_override completely replaces stored lp_fee
2778        let fees = UniswapV4Fees::new(100, 100, 3000);
2779
2780        // With override, stored lp_fee should be ignored
2781        let result = fees.calculate_swap_fees_pips(true, Some(5000));
2782        // 100 + 5000 - (100 * 5000 / 1_000_000) = 5100 - 0 = 5100
2783        assert_eq!(result, 5100);
2784
2785        // Without override, should use stored lp_fee
2786        let result_no_override = fees.calculate_swap_fees_pips(true, None);
2787        // 100 + 3000 - (100 * 3000 / 1_000_000) = 3100 - 0 = 3100
2788        assert_eq!(result_no_override, 3100);
2789    }
2790
2791    #[test]
2792    fn test_calculate_swap_fees_zero_protocol_fee() {
2793        // When protocol fee is 0, formula simplifies to just lpFee
2794        let fees = UniswapV4Fees::new(0, 0, 3000);
2795        let result = fees.calculate_swap_fees_pips(true, None);
2796        // 0 + 3000 - (0 * 3000 / 1_000_000) = 3000
2797        assert_eq!(result, 3000);
2798    }
2799
2800    #[test]
2801    fn test_calculate_swap_fees_zero_lp_fee() {
2802        // When lp fee is 0, formula simplifies to just protocolFee
2803        let fees = UniswapV4Fees::new(500, 500, 0);
2804        let result = fees.calculate_swap_fees_pips(true, None);
2805        // 500 + 0 - (500 * 0 / 1_000_000) = 500
2806        assert_eq!(result, 500);
2807    }
2808
2809    // Helper to create a basic test pool for swap_to_price tests
2810    fn create_basic_v4_test_pool() -> UniswapV4State {
2811        let liquidity = 100_000_000_000_000_000_000u128; // 100e18
2812        let sqrt_price = get_sqrt_price_q96(U256::from(20_000_000u64), U256::from(10_000_000u64))
2813            .expect("Failed to calculate sqrt price");
2814        let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
2815
2816        let ticks = vec![TickInfo::new(0, 0).unwrap(), TickInfo::new(46080, 0).unwrap()];
2817
2818        UniswapV4State::new(
2819            liquidity,
2820            sqrt_price,
2821            UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 3000 }, // 0.3% fee
2822            tick,
2823            60, // tick spacing
2824            ticks,
2825        )
2826        .expect("Failed to create pool")
2827    }
2828
2829    fn create_tick_boundary_v4_test_pool() -> UniswapV4State {
2830        let sqrt_price = get_sqrt_ratio_at_tick(0).expect("Failed to calculate sqrt price");
2831        let ticks = vec![TickInfo::new(-120, 0).unwrap(), TickInfo::new(120, 0).unwrap()];
2832
2833        UniswapV4State::new(
2834            100_000_000_000_000_000_000u128,
2835            sqrt_price,
2836            UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 3000 },
2837            0,
2838            60,
2839            ticks,
2840        )
2841        .expect("Failed to create pool")
2842    }
2843
2844    #[test]
2845    fn test_partial_step_updates_tick_when_price_moves_without_crossing_initialized_tick() {
2846        let pool = create_tick_boundary_v4_test_pool();
2847        let amount = -I256::from_raw(U256::from(100_000_000_000_000_000u64));
2848
2849        let result = pool
2850            .swap(true, amount, None, None)
2851            .expect("swap should stay within the current liquidity range");
2852        let expected_tick =
2853            get_tick_at_sqrt_ratio(result.sqrt_price).expect("new sqrt price should map to a tick");
2854
2855        assert_ne!(result.sqrt_price, pool.sqrt_price);
2856        assert_ne!(result.sqrt_price, get_sqrt_ratio_at_tick(-120).unwrap());
2857        assert_ne!(expected_tick, pool.tick);
2858        assert_eq!(result.tick, expected_tick);
2859    }
2860
2861    #[test]
2862    fn test_swap_keeps_boundary_tick_when_price_does_not_move() {
2863        let mut pool = create_tick_boundary_v4_test_pool();
2864        pool.tick = -1;
2865        let amount = -I256::from_raw(U256::from(1u64));
2866
2867        let result = pool
2868            .swap(true, amount, None, None)
2869            .expect("swap should consume the input as fee without moving price");
2870
2871        assert_eq!(result.sqrt_price, pool.sqrt_price);
2872        assert_eq!(get_tick_at_sqrt_ratio(result.sqrt_price).unwrap(), 0);
2873        assert_eq!(result.tick, pool.tick);
2874    }
2875
2876    #[test]
2877    fn test_swap_to_price_price_too_high() {
2878        let pool = create_basic_v4_test_pool();
2879
2880        let token_x = token_x();
2881        let token_y = token_y();
2882
2883        // Price far above pool price - should return zero
2884        let target_price = Price::new(BigUint::from(10_000_000u64), BigUint::from(1_000_000u64));
2885
2886        let result = pool.query_pool_swap(&QueryPoolSwapParams::new(
2887            token_x,
2888            token_y,
2889            SwapConstraint::PoolTargetPrice {
2890                target: target_price,
2891                tolerance: 0f64,
2892                min_amount_in: None,
2893                max_amount_in: None,
2894            },
2895        ));
2896        assert!(result.is_err(), "Should return error when target price is unreachable");
2897    }
2898
2899    #[test]
2900    fn test_swap_to_price_no_liquidity() {
2901        // Test that swap_to_price returns zero for pool with no liquidity
2902        let pool = UniswapV4State::new(
2903            0, // No liquidity
2904            U256::from_str("79228162514264337593543950336").unwrap(),
2905            UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 3000 },
2906            0,
2907            60,
2908            vec![],
2909        )
2910        .unwrap();
2911
2912        let token_x = token_x();
2913        let token_y = token_y();
2914
2915        let target_price = Price::new(BigUint::from(2_000_000u64), BigUint::from(1_000_000u64));
2916
2917        let pool_swap = pool.query_pool_swap(&QueryPoolSwapParams::new(
2918            token_x,
2919            token_y,
2920            SwapConstraint::PoolTargetPrice {
2921                target: target_price,
2922                tolerance: 0f64,
2923                min_amount_in: None,
2924                max_amount_in: None,
2925            },
2926        ));
2927
2928        assert!(pool_swap.is_err());
2929    }
2930
2931    #[test]
2932    fn test_swap_to_price_with_protocol_fees() {
2933        let liquidity = 100_000_000_000_000_000_000u128;
2934        let sqrt_price = get_sqrt_price_q96(U256::from(20_000_000u64), U256::from(10_000_000u64))
2935            .expect("Failed to calculate sqrt price");
2936        let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
2937
2938        let ticks = vec![TickInfo::new(0, 0).unwrap(), TickInfo::new(46080, 0).unwrap()];
2939
2940        // Create pool with different protocol fees for each direction
2941        let pool = UniswapV4State::new(
2942            liquidity,
2943            sqrt_price,
2944            UniswapV4Fees {
2945                zero_for_one: 1000, // 0.1% protocol fee for zero_for_one
2946                one_for_zero: 200,  // 0.02% protocol fee for one_for_zero
2947                lp_fee: 3000,       // 0.3% LP fee
2948            },
2949            tick,
2950            60,
2951            ticks,
2952        )
2953        .expect("Failed to create pool");
2954
2955        let token_x = token_x();
2956        let token_y = token_y();
2957
2958        // Pool at 2.0 Y/X = 0.5 X/Y, swap_to_price moves price DOWN to target
2959
2960        // Test zero_for_one direction (X -> Y, uses zero_for_one fee)
2961        let target_price = Price::new(BigUint::from(2_000_000u64), BigUint::from(1_010_000u64));
2962        let pool_swap_forward = pool
2963            .query_pool_swap(&QueryPoolSwapParams::new(
2964                token_x.clone(),
2965                token_y.clone(),
2966                SwapConstraint::PoolTargetPrice {
2967                    target: target_price,
2968                    tolerance: 0f64,
2969                    min_amount_in: None,
2970                    max_amount_in: None,
2971                },
2972            ))
2973            .expect("swap_to_price failed");
2974
2975        // Test one_for_zero direction (Y -> X, uses one_for_zero fee)
2976        let target_price_reverse =
2977            Price::new(BigUint::from(1_010_000u64), BigUint::from(2_040_000u64));
2978        let pool_swap_backward = pool
2979            .query_pool_swap(&QueryPoolSwapParams::new(
2980                token_y,
2981                token_x,
2982                SwapConstraint::PoolTargetPrice {
2983                    target: target_price_reverse,
2984                    tolerance: 0f64,
2985                    min_amount_in: None,
2986                    max_amount_in: None,
2987                },
2988            ))
2989            .expect("swap_to_price failed");
2990
2991        assert!(
2992            pool_swap_backward.amount_out().clone() > BigUint::ZERO,
2993            "One for zero swap should return non-zero output"
2994        );
2995
2996        // Higher fees require more volume to reach the same price target
2997        // trade_zfo has 0.1% protocol fee, trade_ofz has 0.02% protocol fee
2998        assert!(
2999            pool_swap_forward.amount_out() < pool_swap_backward.amount_in(),
3000            "Backward fees should be lower therefore backward swap should be bigger"
3001        );
3002        assert!(
3003            pool_swap_forward.amount_in() < pool_swap_backward.amount_out(),
3004            "Backward fees should be lower therefore backward swap should be bigger"
3005        );
3006    }
3007
3008    #[test]
3009    fn test_swap_to_price_different_targets() {
3010        // Test with various target prices using working format
3011        let pool = create_basic_v4_test_pool();
3012
3013        let token_x = token_x();
3014        let token_y = token_y();
3015
3016        // Pool at 2.0 Y/X (20M/10M)
3017        // Test 1: Target close to spot (1.98 Y/X)
3018        let target_price = Price::new(BigUint::from(2_000_000u64), BigUint::from(1_010_000u64));
3019        let pool_swap_close = pool
3020            .query_pool_swap(&QueryPoolSwapParams::new(
3021                token_x.clone(),
3022                token_y.clone(),
3023                SwapConstraint::PoolTargetPrice {
3024                    target: target_price,
3025                    tolerance: 0f64,
3026                    min_amount_in: None,
3027                    max_amount_in: None,
3028                },
3029            ))
3030            .expect("swap_to_price failed");
3031        assert!(
3032            *pool_swap_close.amount_out() > BigUint::ZERO,
3033            "Expected non-zero for 1.98 Y/X target"
3034        );
3035
3036        // Test 2: Target further from spot (1.90 Y/X)
3037        let target_price = Price::new(BigUint::from(1_900_000u64), BigUint::from(1_000_000u64));
3038        let pool_swap_below = pool
3039            .query_pool_swap(&QueryPoolSwapParams::new(
3040                token_x.clone(),
3041                token_y.clone(),
3042                SwapConstraint::PoolTargetPrice {
3043                    target: target_price,
3044                    tolerance: 0f64,
3045                    min_amount_in: None,
3046                    max_amount_in: None,
3047                },
3048            ))
3049            .expect("swap_to_price failed");
3050        assert!(
3051            pool_swap_below.amount_out().clone() > BigUint::ZERO,
3052            "Expected non-zero for 1.90 Y/X target"
3053        );
3054
3055        // Test 3: Target far from spot (1.5 Y/X)
3056        let target_price = Price::new(BigUint::from(1_500_000u64), BigUint::from(1_000_000u64));
3057        let pool_swap_far = pool
3058            .query_pool_swap(&QueryPoolSwapParams::new(
3059                token_x,
3060                token_y,
3061                SwapConstraint::PoolTargetPrice {
3062                    target: target_price,
3063                    tolerance: 0f64,
3064                    min_amount_in: None,
3065                    max_amount_in: None,
3066                },
3067            ))
3068            .expect("swap_to_price failed");
3069        assert!(
3070            pool_swap_far.amount_out().clone() > BigUint::ZERO,
3071            "Expected non-zero for 1.5 Y/X target"
3072        );
3073
3074        // Verify that further targets require more volume
3075        assert!(
3076            pool_swap_close.amount_out().clone() < pool_swap_below.amount_out().clone(),
3077            "Closer target (1.98 Y/X) should require less volume than medium target (1.90 Y/X). \
3078             Got close: {}, medium: {}",
3079            pool_swap_close.amount_out().clone(),
3080            pool_swap_below.amount_out().clone()
3081        );
3082        assert!(
3083            pool_swap_below.amount_out().clone() < pool_swap_far.amount_out().clone(),
3084            "Medium target (1.90 Y/X) should require less volume than far target (1.5 Y/X). \
3085             Got medium: {}, far: {}",
3086            pool_swap_below.amount_out().clone(),
3087            pool_swap_far.amount_out().clone()
3088        );
3089    }
3090
3091    #[test]
3092    fn test_swap_to_price_around_spot_price() {
3093        let liquidity = 10_000_000_000_000_000u128;
3094        let sqrt_price =
3095            get_sqrt_price_q96(U256::from(2_000_000_000u64), U256::from(1_000_000_000u64))
3096                .expect("Failed to calculate sqrt price");
3097        let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
3098
3099        let ticks = vec![TickInfo::new(0, 0).unwrap(), TickInfo::new(46080, 0).unwrap()];
3100
3101        // Use FeeAmount::Low equivalent (500 pips = 0.05%)
3102        let pool = UniswapV4State::new(
3103            liquidity,
3104            sqrt_price,
3105            UniswapV4Fees {
3106                zero_for_one: 0,
3107                one_for_zero: 0,
3108                lp_fee: 500, // 0.05% to match V3 FeeAmount::Low
3109            },
3110            tick,
3111            60,
3112            ticks,
3113        )
3114        .expect("Failed to create pool");
3115
3116        let token_x = token_x();
3117        let token_y = token_y();
3118
3119        // Test 1: Price just above spot price, too little to cover fees
3120        let target_price = Price::new(BigUint::from(1_999_750u64), BigUint::from(1_000_250u64));
3121
3122        let result = pool.query_pool_swap(&QueryPoolSwapParams::new(
3123            token_x.clone(),
3124            token_y.clone(),
3125            SwapConstraint::PoolTargetPrice {
3126                target: target_price,
3127                tolerance: 0f64,
3128                min_amount_in: None,
3129                max_amount_in: None,
3130            },
3131        ));
3132        assert!(result.is_err(), "Should return error when target price is unreachable");
3133
3134        // Test 2: Price far enough from spot prices to enable trading despite fees (0.1% lower)
3135        let target_price = Price::new(BigUint::from(1_999_000u64), BigUint::from(1_001_000u64));
3136
3137        let pool_swap = pool
3138            .query_pool_swap(&QueryPoolSwapParams::new(
3139                token_x,
3140                token_y,
3141                SwapConstraint::PoolTargetPrice {
3142                    target: target_price,
3143                    tolerance: 0f64,
3144                    min_amount_in: None,
3145                    max_amount_in: None,
3146                },
3147            ))
3148            .expect("swap_to_price failed");
3149
3150        // Should match V3 output exactly with same fees
3151        let expected_amount_out =
3152            BigUint::from_str("7062236922008").expect("Failed to parse expected value");
3153        assert_eq!(
3154            pool_swap.amount_out().clone(),
3155            expected_amount_out,
3156            "V4 should match V3 output with same fees (0.05%)"
3157        );
3158    }
3159
3160    #[test]
3161    fn test_swap_to_price_matches_get_amount_out() {
3162        let pool = create_basic_v4_test_pool();
3163
3164        let token_x = token_x();
3165        let token_y = token_y();
3166
3167        // Get the trade from swap_to_price
3168        let target_price = Price::new(BigUint::from(2_000_000u64), BigUint::from(1_010_000u64));
3169        let pool_swap = pool
3170            .query_pool_swap(&QueryPoolSwapParams::new(
3171                token_x.clone(),
3172                token_y.clone(),
3173                SwapConstraint::PoolTargetPrice {
3174                    target: target_price,
3175                    tolerance: 0f64,
3176                    min_amount_in: None,
3177                    max_amount_in: None,
3178                },
3179            ))
3180            .expect("swap_to_price failed");
3181        assert!(*pool_swap.amount_in() > BigUint::ZERO, "Amount in should be positive");
3182
3183        // Use the amount_in from swap_to_price with get_amount_out
3184        let result = pool
3185            .get_amount_out(pool_swap.amount_in().clone(), &token_x, &token_y)
3186            .expect("get_amount_out failed");
3187
3188        // The amount_out from get_amount_out should be close to swap_to_price's amount_out
3189        // Allow for small rounding differences
3190        assert!(result.amount > BigUint::ZERO);
3191        assert!(result.amount >= *pool_swap.amount_out());
3192    }
3193
3194    #[test]
3195    fn test_swap_to_price_basic() {
3196        let liquidity = 100_000_000_000_000_000_000u128;
3197        let sqrt_price = get_sqrt_price_q96(U256::from(20_000_000u64), U256::from(10_000_000u64))
3198            .expect("Failed to calculate sqrt price");
3199        let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
3200
3201        let ticks = vec![TickInfo::new(0, 0).unwrap(), TickInfo::new(46080, 0).unwrap()];
3202
3203        let pool = UniswapV4State::new(
3204            liquidity,
3205            sqrt_price,
3206            UniswapV4Fees {
3207                zero_for_one: 0,
3208                one_for_zero: 0,
3209                lp_fee: 3000, // 0.3% LP fee
3210            },
3211            tick,
3212            60,
3213            ticks,
3214        )
3215        .expect("Failed to create pool");
3216
3217        let token_x = token_x();
3218        let token_y = token_y();
3219
3220        // Target price: 2_000_000/1_010_000 ≈ 1.98 Y/X
3221        let target_price = Price::new(BigUint::from(2_000_000u64), BigUint::from(1_010_000u64));
3222
3223        let pool_swap = pool
3224            .query_pool_swap(&QueryPoolSwapParams::new(
3225                token_x,
3226                token_y,
3227                SwapConstraint::PoolTargetPrice {
3228                    target: target_price,
3229                    tolerance: 0f64,
3230                    min_amount_in: None,
3231                    max_amount_in: None,
3232                },
3233            ))
3234            .expect("swap_to_price failed");
3235
3236        // Should match V3's output exactly with same fees (0.3%)
3237        let expected_amount_in = BigUint::from_str("246739021727519745").unwrap();
3238        let expected_amount_out = BigUint::from_str("490291909043340795").unwrap();
3239
3240        assert_eq!(
3241            *pool_swap.amount_in(),
3242            expected_amount_in,
3243            "amount_in should match expected value"
3244        );
3245        assert_eq!(
3246            *pool_swap.amount_out(),
3247            expected_amount_out,
3248            "amount_out should match expected value"
3249        );
3250    }
3251
3252    #[test]
3253    fn test_swap_price_limit_out_of_range_returns_error() {
3254        let pool = create_basic_v4_test_pool();
3255        let amount = -I256::from_raw(U256::from(1000u64)); // V4 uses negative for exact input
3256
3257        // zero_for_one: price_limit equal to sqrt_price is invalid (must be strictly less)
3258        let result = pool.swap(true, amount, Some(pool.sqrt_price), None);
3259        assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
3260
3261        // zero_for_one: price_limit at MIN_SQRT_RATIO is invalid (must be strictly greater)
3262        let result = pool.swap(true, amount, Some(MIN_SQRT_RATIO), None);
3263        assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
3264
3265        // one_for_zero: price_limit equal to sqrt_price is invalid (must be strictly greater)
3266        let result = pool.swap(false, amount, Some(pool.sqrt_price), None);
3267        assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
3268
3269        // one_for_zero: price_limit at MAX_SQRT_RATIO is invalid (must be strictly less)
3270        let result = pool.swap(false, amount, Some(MAX_SQRT_RATIO), None);
3271        assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
3272    }
3273
3274    #[test]
3275    fn test_swap_at_extreme_price_returns_error() {
3276        // Simulates the depth calculation scenario: pool sqrt_price is at MIN_SQRT_RATIO + 1,
3277        // so the default price limit for zero_for_one equals sqrt_price and fails validation.
3278        let sqrt_price = MIN_SQRT_RATIO + U256::from(1u64);
3279        let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
3280        // tick_spacing 60; ticks must be aligned
3281        let aligned_tick = (MIN_TICK / 60) * 60 + 60; // first multiple of 60 above MIN_TICK
3282        let ticks = vec![
3283            TickInfo::new(aligned_tick, 0).unwrap(),
3284            TickInfo::new(aligned_tick + 60, 0).unwrap(),
3285        ];
3286        let pool = UniswapV4State::new(
3287            100_000_000_000_000_000_000u128,
3288            sqrt_price,
3289            UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 3000 },
3290            tick,
3291            60,
3292            ticks,
3293        )
3294        .unwrap();
3295
3296        let amount = -I256::from_raw(U256::from(1000u64));
3297        // Default price limit for zero_for_one is MIN_SQRT_RATIO + 1 == sqrt_price, so invalid
3298        let result = pool.swap(true, amount, None, None);
3299        assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
3300    }
3301}