Skip to main content

tycho_simulation/evm/protocol/uniswap_v4/
state.rs

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