Skip to main content

tycho_simulation/evm/protocol/uniswap_v3/
state.rs

1use std::{any::Any, collections::HashMap};
2
3use alloy::primitives::{Sign, I256, U256};
4use num_bigint::BigUint;
5use num_traits::Zero;
6use serde::{Deserialize, Serialize};
7use tracing::trace;
8use tycho_common::{
9    dto::ProtocolStateDelta,
10    models::token::Token,
11    simulation::{
12        errors::{SimulationError, TransitionError},
13        protocol_sim::{
14            Balances, GetAmountOutResult, PoolSwap, ProtocolSim, QueryPoolSwapParams,
15            SwapConstraint,
16        },
17    },
18    Bytes,
19};
20
21use super::enums::FeeAmount;
22use crate::evm::protocol::{
23    clmm::clmm_swap_to_price,
24    safe_math::{safe_add_u256, safe_sub_u256},
25    u256_num::u256_to_biguint,
26    utils::{
27        add_fee_markup,
28        uniswap::{
29            i24_be_bytes_to_i32, liquidity_math,
30            sqrt_price_math::{get_amount0_delta, get_amount1_delta, sqrt_price_q96_to_f64},
31            swap_math,
32            tick_list::{TickInfo, TickList, TickListErrorKind},
33            tick_math::{
34                get_sqrt_ratio_at_tick, get_tick_at_sqrt_ratio, MAX_SQRT_RATIO, MAX_TICK,
35                MIN_SQRT_RATIO, MIN_TICK,
36            },
37            StepComputation, SwapResults, SwapState,
38        },
39    },
40};
41
42// Gas limit constants for capping get_limits calculations
43// These prevent simulations from exceeding Ethereum's block gas limit
44const SWAP_BASE_GAS: u64 = 130_000;
45// This gas is estimated from UniswapV3Pool cross() calls on Tenderly
46const GAS_PER_TICK: u64 = 17_540;
47// Conservative max gas budget for a single swap (Ethereum transaction gas limit)
48const MAX_SWAP_GAS: u64 = 16_700_000;
49const MAX_TICKS_CROSSED: u64 = (MAX_SWAP_GAS - SWAP_BASE_GAS) / GAS_PER_TICK;
50
51#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
52pub struct UniswapV3State {
53    liquidity: u128,
54    sqrt_price: U256,
55    fee: FeeAmount,
56    tick: i32,
57    ticks: TickList,
58}
59
60impl UniswapV3State {
61    /// Creates a new instance of `UniswapV3State`.
62    ///
63    /// # Arguments
64    /// - `liquidity`: The initial liquidity of the pool.
65    /// - `sqrt_price`: The square root of the current price.
66    /// - `fee`: The fee tier for the pool.
67    /// - `tick`: The current tick of the pool.
68    /// - `ticks`: A vector of `TickInfo` representing the tick information for the pool.
69    pub fn new(
70        liquidity: u128,
71        sqrt_price: U256,
72        fee: FeeAmount,
73        tick: i32,
74        ticks: Vec<TickInfo>,
75    ) -> Result<Self, SimulationError> {
76        let spacing = UniswapV3State::get_spacing(fee);
77        let tick_list = TickList::from(spacing, ticks)?;
78        Ok(UniswapV3State { liquidity, sqrt_price, fee, tick, ticks: tick_list })
79    }
80
81    fn get_spacing(fee: FeeAmount) -> u16 {
82        match fee {
83            FeeAmount::Lowest => 1,
84            FeeAmount::Lowest2 => 2,
85            FeeAmount::Lowest3 => 3,
86            FeeAmount::Lowest4 => 4,
87            FeeAmount::Low => 10,
88            FeeAmount::MediumLow => 50,
89            FeeAmount::Medium => 60,
90            FeeAmount::MediumHigh => 100,
91            FeeAmount::High => 200,
92        }
93    }
94
95    fn swap(
96        &self,
97        zero_for_one: bool,
98        amount_specified: I256,
99        sqrt_price_limit: Option<U256>,
100    ) -> Result<SwapResults, SimulationError> {
101        if self.liquidity == 0 {
102            return Err(SimulationError::RecoverableError("No liquidity".to_string()));
103        }
104        let price_limit = if let Some(limit) = sqrt_price_limit {
105            limit
106        } else if zero_for_one {
107            safe_add_u256(MIN_SQRT_RATIO, U256::from(1u64))?
108        } else {
109            safe_sub_u256(MAX_SQRT_RATIO, U256::from(1u64))?
110        };
111
112        let price_limit_valid = if zero_for_one {
113            price_limit > MIN_SQRT_RATIO && price_limit < self.sqrt_price
114        } else {
115            price_limit < MAX_SQRT_RATIO && price_limit > self.sqrt_price
116        };
117        if !price_limit_valid {
118            return Err(SimulationError::InvalidInput("Price limit out of range".into(), None));
119        }
120
121        let exact_input = amount_specified > I256::from_raw(U256::from(0u64));
122
123        let mut state = SwapState {
124            amount_remaining: amount_specified,
125            amount_calculated: I256::from_raw(U256::from(0u64)),
126            sqrt_price: self.sqrt_price,
127            tick: self.tick,
128            liquidity: self.liquidity,
129        };
130        let mut gas_used = U256::from(130_000);
131
132        while state.amount_remaining != I256::from_raw(U256::from(0u64)) &&
133            state.sqrt_price != price_limit
134        {
135            let (mut next_tick, initialized) = match self
136                .ticks
137                .next_initialized_tick_within_one_word(state.tick, zero_for_one)
138            {
139                Ok((tick, init)) => (tick, init),
140                Err(tick_err) => match tick_err.kind {
141                    TickListErrorKind::TicksExeeded => {
142                        let mut new_state = self.clone();
143                        new_state.liquidity = state.liquidity;
144                        new_state.tick = state.tick;
145                        new_state.sqrt_price = state.sqrt_price;
146                        return Err(SimulationError::InvalidInput(
147                            "Ticks exceeded".into(),
148                            Some(GetAmountOutResult::new(
149                                u256_to_biguint(state.amount_calculated.abs().into_raw()),
150                                u256_to_biguint(gas_used),
151                                Box::new(new_state),
152                            )),
153                        ));
154                    }
155                    _ => return Err(SimulationError::FatalError("Unknown error".to_string())),
156                },
157            };
158
159            next_tick = next_tick.clamp(MIN_TICK, MAX_TICK);
160
161            let sqrt_price_next = get_sqrt_ratio_at_tick(next_tick)?;
162            let (sqrt_price, amount_in, amount_out, fee_amount) = swap_math::compute_swap_step(
163                state.sqrt_price,
164                UniswapV3State::get_sqrt_ratio_target(sqrt_price_next, price_limit, zero_for_one),
165                state.liquidity,
166                state.amount_remaining,
167                self.fee as u32,
168            )?;
169            state.sqrt_price = sqrt_price;
170
171            let step = StepComputation {
172                sqrt_price_start: state.sqrt_price,
173                tick_next: next_tick,
174                initialized,
175                sqrt_price_next,
176                amount_in,
177                amount_out,
178                fee_amount,
179            };
180            if exact_input {
181                state.amount_remaining -= I256::checked_from_sign_and_abs(
182                    Sign::Positive,
183                    safe_add_u256(step.amount_in, step.fee_amount)?,
184                )
185                .unwrap();
186                state.amount_calculated -=
187                    I256::checked_from_sign_and_abs(Sign::Positive, step.amount_out).unwrap();
188            } else {
189                state.amount_remaining +=
190                    I256::checked_from_sign_and_abs(Sign::Positive, step.amount_out).unwrap();
191                state.amount_calculated += I256::checked_from_sign_and_abs(
192                    Sign::Positive,
193                    safe_add_u256(step.amount_in, step.fee_amount)?,
194                )
195                .unwrap();
196            }
197            if state.sqrt_price == step.sqrt_price_next {
198                if step.initialized {
199                    let liquidity_raw = self
200                        .ticks
201                        .get_tick(step.tick_next)
202                        .unwrap()
203                        .net_liquidity;
204                    let liquidity_net = if zero_for_one { -liquidity_raw } else { liquidity_raw };
205                    state.liquidity =
206                        liquidity_math::add_liquidity_delta(state.liquidity, liquidity_net)?;
207                }
208                state.tick = if zero_for_one { step.tick_next - 1 } else { step.tick_next };
209            } else if state.sqrt_price != step.sqrt_price_start {
210                state.tick = get_tick_at_sqrt_ratio(state.sqrt_price)?;
211            }
212            gas_used = safe_add_u256(gas_used, U256::from(2000))?;
213        }
214        Ok(SwapResults {
215            amount_calculated: state.amount_calculated,
216            amount_specified,
217            amount_remaining: state.amount_remaining,
218            sqrt_price: state.sqrt_price,
219            liquidity: state.liquidity,
220            tick: state.tick,
221            gas_used,
222        })
223    }
224
225    fn get_sqrt_ratio_target(
226        sqrt_price_next: U256,
227        sqrt_price_limit: U256,
228        zero_for_one: bool,
229    ) -> U256 {
230        let cond1 = if zero_for_one {
231            sqrt_price_next < sqrt_price_limit
232        } else {
233            sqrt_price_next > sqrt_price_limit
234        };
235
236        if cond1 {
237            sqrt_price_limit
238        } else {
239            sqrt_price_next
240        }
241    }
242}
243
244#[typetag::serde]
245impl ProtocolSim for UniswapV3State {
246    fn fee(&self) -> f64 {
247        (self.fee as u32) as f64 / 1_000_000.0
248    }
249
250    fn spot_price(&self, a: &Token, b: &Token) -> Result<f64, SimulationError> {
251        let price = if a < b {
252            sqrt_price_q96_to_f64(self.sqrt_price, a.decimals, b.decimals)?
253        } else {
254            1.0f64 / sqrt_price_q96_to_f64(self.sqrt_price, b.decimals, a.decimals)?
255        };
256        Ok(add_fee_markup(price, self.fee()))
257    }
258
259    fn get_amount_out(
260        &self,
261        amount_in: BigUint,
262        token_a: &Token,
263        token_b: &Token,
264    ) -> Result<GetAmountOutResult, SimulationError> {
265        let zero_for_one = token_a < token_b;
266        let amount_specified = I256::checked_from_sign_and_abs(
267            Sign::Positive,
268            U256::from_be_slice(&amount_in.to_bytes_be()),
269        )
270        .ok_or_else(|| {
271            SimulationError::InvalidInput("I256 overflow: amount_in".to_string(), None)
272        })?;
273
274        let result = self.swap(zero_for_one, amount_specified, None)?;
275
276        trace!(?amount_in, ?token_a, ?token_b, ?zero_for_one, ?result, "V3 SWAP");
277        let mut new_state = self.clone();
278        new_state.liquidity = result.liquidity;
279        new_state.tick = result.tick;
280        new_state.sqrt_price = result.sqrt_price;
281
282        Ok(GetAmountOutResult::new(
283            u256_to_biguint(
284                result
285                    .amount_calculated
286                    .abs()
287                    .into_raw(),
288            ),
289            u256_to_biguint(result.gas_used),
290            Box::new(new_state),
291        ))
292    }
293
294    fn get_limits(
295        &self,
296        token_in: Bytes,
297        token_out: Bytes,
298    ) -> Result<(BigUint, BigUint), SimulationError> {
299        // If the pool has no liquidity, return zeros for both limits
300        if self.liquidity == 0 {
301            return Ok((BigUint::zero(), BigUint::zero()));
302        }
303
304        let zero_for_one = token_in < token_out;
305        let mut current_tick = self.tick;
306        let mut current_sqrt_price = self.sqrt_price;
307        let mut current_liquidity = self.liquidity;
308        let mut total_amount_in = U256::from(0u64);
309        let mut total_amount_out = U256::from(0u64);
310        let mut ticks_crossed: u64 = 0;
311
312        // Iterate through ticks in the direction of the swap
313        // Stops when: no more liquidity, no more ticks, or gas limit would be exceeded
314        while let Ok((tick, initialized)) = self
315            .ticks
316            .next_initialized_tick_within_one_word(current_tick, zero_for_one)
317        {
318            // Cap iteration to prevent exceeding Ethereum's gas limit
319            if ticks_crossed >= MAX_TICKS_CROSSED {
320                break;
321            }
322            ticks_crossed += 1;
323
324            // Clamp the tick value to ensure it's within valid range
325            let next_tick = tick.clamp(MIN_TICK, MAX_TICK);
326
327            // Calculate the sqrt price at the next tick boundary
328            let sqrt_price_next = get_sqrt_ratio_at_tick(next_tick)?;
329
330            // Calculate the amount of tokens swapped when moving from current_sqrt_price to
331            // sqrt_price_next. Direction determines which token is being swapped in vs out
332            let (amount_in, amount_out) = if zero_for_one {
333                let amount0 = get_amount0_delta(
334                    sqrt_price_next,
335                    current_sqrt_price,
336                    current_liquidity,
337                    true,
338                )?;
339                let amount1 = get_amount1_delta(
340                    sqrt_price_next,
341                    current_sqrt_price,
342                    current_liquidity,
343                    false,
344                )?;
345                (amount0, amount1)
346            } else {
347                let amount0 = get_amount0_delta(
348                    sqrt_price_next,
349                    current_sqrt_price,
350                    current_liquidity,
351                    false,
352                )?;
353                let amount1 = get_amount1_delta(
354                    sqrt_price_next,
355                    current_sqrt_price,
356                    current_liquidity,
357                    true,
358                )?;
359                (amount1, amount0)
360            };
361
362            // Accumulate total amounts for this tick range
363            total_amount_in = safe_add_u256(total_amount_in, amount_in)?;
364            total_amount_out = safe_add_u256(total_amount_out, amount_out)?;
365
366            // If this tick is "initialized" (meaning its someone's position boundary), update the
367            // liquidity when crossing it
368            // For zero_for_one, liquidity is removed when crossing a tick
369            // For one_for_zero, liquidity is added when crossing a tick
370            if initialized {
371                let liquidity_raw = self
372                    .ticks
373                    .get_tick(next_tick)
374                    .unwrap()
375                    .net_liquidity;
376                let liquidity_delta = if zero_for_one { -liquidity_raw } else { liquidity_raw };
377
378                // Check if applying this liquidity delta would cause underflow
379                // If so, stop here rather than continuing with invalid state
380                match liquidity_math::add_liquidity_delta(current_liquidity, liquidity_delta) {
381                    Ok(new_liquidity) => {
382                        current_liquidity = new_liquidity;
383                    }
384                    Err(_) => {
385                        // Liquidity would underflow, stop iteration here
386                        // This represents the maximum liquidity we can actually use
387                        break;
388                    }
389                }
390            }
391
392            // Move to the next tick position
393            current_tick = if zero_for_one { next_tick - 1 } else { next_tick };
394            current_sqrt_price = sqrt_price_next;
395        }
396
397        Ok((u256_to_biguint(total_amount_in), u256_to_biguint(total_amount_out)))
398    }
399
400    fn delta_transition(
401        &mut self,
402        delta: ProtocolStateDelta,
403        _tokens: &HashMap<Bytes, Token>,
404        _balances: &Balances,
405    ) -> Result<(), TransitionError> {
406        // apply attribute changes
407        if let Some(liquidity) = delta
408            .updated_attributes
409            .get("liquidity")
410        {
411            // This is a hotfix because if the liquidity has never been updated after creation, it's
412            // currently encoded as H256::zero(), therefore, we can't decode this as u128.
413            // We can remove this once it has been fixed on the tycho side.
414            let liq_16_bytes = if liquidity.len() == 32 {
415                // Make sure it only happens for 0 values, otherwise error.
416                if liquidity == &Bytes::zero(32) {
417                    Bytes::from([0; 16])
418                } else {
419                    return Err(TransitionError::DecodeError(format!(
420                        "Liquidity bytes too long for {liquidity}, expected 16",
421                    )));
422                }
423            } else {
424                liquidity.clone()
425            };
426
427            self.liquidity = u128::from(liq_16_bytes);
428        }
429        if let Some(sqrt_price) = delta
430            .updated_attributes
431            .get("sqrt_price_x96")
432        {
433            self.sqrt_price = U256::from_be_slice(sqrt_price);
434        }
435        if let Some(tick) = delta.updated_attributes.get("tick") {
436            // This is a hotfix because if the tick has never been updated after creation, it's
437            // currently encoded as H256::zero(), therefore, we can't decode this as i32.
438            // We can remove this once it has been fixed on the tycho side.
439            let ticks_4_bytes = if tick.len() == 32 {
440                // Make sure it only happens for 0 values, otherwise error.
441                if tick == &Bytes::zero(32) {
442                    Bytes::from([0; 4])
443                } else {
444                    return Err(TransitionError::DecodeError(format!(
445                        "Tick bytes too long for {tick}, expected 4"
446                    )));
447                }
448            } else {
449                tick.clone()
450            };
451            self.tick = i24_be_bytes_to_i32(&ticks_4_bytes);
452        }
453
454        // apply tick changes
455        for (key, value) in delta.updated_attributes.iter() {
456            // tick liquidity keys are in the format "ticks/{tick_index}/net_liquidity"
457            if key.starts_with("ticks/") {
458                let parts: Vec<&str> = key.split('/').collect();
459                self.ticks
460                    .set_tick_liquidity(
461                        parts[1]
462                            .parse::<i32>()
463                            .map_err(|err| TransitionError::DecodeError(err.to_string()))?,
464                        i128::from(value.clone()),
465                    )
466                    .map_err(|err| TransitionError::DecodeError(err.to_string()))?;
467            }
468        }
469        // delete ticks - ignores deletes for attributes other than tick liquidity
470        for key in delta.deleted_attributes.iter() {
471            // tick liquidity keys are in the format "ticks/{tick_index}/net_liquidity"
472            if key.starts_with("ticks/") {
473                let parts: Vec<&str> = key.split('/').collect();
474                self.ticks
475                    .set_tick_liquidity(
476                        parts[1]
477                            .parse::<i32>()
478                            .map_err(|err| TransitionError::DecodeError(err.to_string()))?,
479                        0,
480                    )
481                    .map_err(|err| TransitionError::DecodeError(err.to_string()))?;
482            }
483        }
484        Ok(())
485    }
486
487    /// See [`ProtocolSim::query_pool_swap`] for the trait documentation.
488    ///
489    /// This method uses Uniswap V3 internal swap logic by swapping an infinite amount of token_in
490    /// until the target price is reached.
491    fn query_pool_swap(&self, params: &QueryPoolSwapParams) -> Result<PoolSwap, SimulationError> {
492        if self.liquidity == 0 {
493            return Err(SimulationError::FatalError("No liquidity".to_string()));
494        }
495
496        match params.swap_constraint() {
497            SwapConstraint::TradeLimitPrice { .. } => Err(SimulationError::InvalidInput(
498                "Uniswap V3 does not support TradeLimitPrice constraint in query_pool_swap"
499                    .to_string(),
500                None,
501            )),
502            SwapConstraint::PoolTargetPrice {
503                target,
504                tolerance: _,
505                min_amount_in: _,
506                max_amount_in: _,
507            } => {
508                let (amount_in, amount_out, swap_result) = clmm_swap_to_price(
509                    self.sqrt_price,
510                    &params.token_in().address,
511                    &params.token_out().address,
512                    target,
513                    self.fee as u32,
514                    Sign::Positive,
515                    |zero_for_one, amount_specified, sqrt_price_limit| {
516                        self.swap(zero_for_one, amount_specified, Some(sqrt_price_limit))
517                    },
518                )?;
519
520                let mut new_state = self.clone();
521                new_state.liquidity = swap_result.liquidity;
522                new_state.tick = swap_result.tick;
523                new_state.sqrt_price = swap_result.sqrt_price;
524
525                Ok(PoolSwap::new(amount_in, amount_out, Box::new(new_state), None))
526            }
527        }
528    }
529
530    fn clone_box(&self) -> Box<dyn ProtocolSim> {
531        Box::new(self.clone())
532    }
533
534    fn as_any(&self) -> &dyn Any {
535        self
536    }
537
538    fn as_any_mut(&mut self) -> &mut dyn Any {
539        self
540    }
541
542    fn eq(&self, other: &dyn ProtocolSim) -> bool {
543        if let Some(other_state) = other
544            .as_any()
545            .downcast_ref::<UniswapV3State>()
546        {
547            self.liquidity == other_state.liquidity &&
548                self.sqrt_price == other_state.sqrt_price &&
549                self.fee == other_state.fee &&
550                self.tick == other_state.tick &&
551                self.ticks == other_state.ticks
552        } else {
553            false
554        }
555    }
556}
557
558#[cfg(test)]
559mod tests {
560    use std::{
561        collections::{HashMap, HashSet},
562        fs,
563        path::Path,
564        str::FromStr,
565    };
566
567    use num_bigint::ToBigUint;
568    use num_traits::FromPrimitive;
569    use serde_json::Value;
570    use tycho_client::feed::synchronizer::ComponentWithState;
571    use tycho_common::{hex_bytes::Bytes, models::Chain, simulation::protocol_sim::Price};
572
573    use super::*;
574    use crate::{
575        evm::protocol::utils::uniswap::sqrt_price_math::get_sqrt_price_q96,
576        protocol::models::{DecoderContext, TryFromWithBlock},
577    };
578
579    #[test]
580    fn test_get_amount_out_full_range_liquidity() {
581        let token_x = Token::new(
582            &Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(),
583            "X",
584            18,
585            0,
586            &[Some(10_000)],
587            Chain::Ethereum,
588            100,
589        );
590        let token_y = Token::new(
591            &Bytes::from_str("0xf1ca9cb74685755965c7458528a36934df52a3ef").unwrap(),
592            "Y",
593            18,
594            0,
595            &[Some(10_000)],
596            Chain::Ethereum,
597            100,
598        );
599
600        let pool = UniswapV3State::new(
601            8330443394424070888454257,
602            U256::from_str("188562464004052255423565206602").unwrap(),
603            FeeAmount::Medium,
604            17342,
605            vec![TickInfo::new(0, 0).unwrap(), TickInfo::new(46080, 0).unwrap()],
606        )
607        .unwrap();
608        let sell_amount = BigUint::from_str("11_000_000000000000000000").unwrap();
609        let expected = BigUint::from_str("61927070842678722935941").unwrap();
610
611        let res = pool
612            .get_amount_out(sell_amount, &token_x, &token_y)
613            .unwrap();
614
615        assert_eq!(res.amount, expected);
616    }
617
618    struct SwapTestCase {
619        symbol: &'static str,
620        sell: BigUint,
621        exp: BigUint,
622    }
623
624    #[test]
625    fn test_get_amount_out() {
626        let wbtc = Token::new(
627            &Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap(),
628            "WBTC",
629            8,
630            0,
631            &[Some(10_000)],
632            Chain::Ethereum,
633            100,
634        );
635        let weth = Token::new(
636            &Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(),
637            "WETH",
638            18,
639            0,
640            &[Some(10_000)],
641            Chain::Ethereum,
642            100,
643        );
644        let pool = UniswapV3State::new(
645            377952820878029838,
646            U256::from_str("28437325270877025820973479874632004").unwrap(),
647            FeeAmount::Low,
648            255830,
649            vec![
650                TickInfo::new(255760, 1759015528199933i128).unwrap(),
651                TickInfo::new(255770, 6393138051835308i128).unwrap(),
652                TickInfo::new(255780, 228206673808681i128).unwrap(),
653                TickInfo::new(255820, 1319490609195820i128).unwrap(),
654                TickInfo::new(255830, 678916926147901i128).unwrap(),
655                TickInfo::new(255840, 12208947683433103i128).unwrap(),
656                TickInfo::new(255850, 1177970713095301i128).unwrap(),
657                TickInfo::new(255860, 8752304680520407i128).unwrap(),
658                TickInfo::new(255880, 1486478248067104i128).unwrap(),
659                TickInfo::new(255890, 1878744276123248i128).unwrap(),
660                TickInfo::new(255900, 77340284046725227i128).unwrap(),
661            ],
662        )
663        .unwrap();
664        let cases = vec![
665            SwapTestCase {
666                symbol: "WBTC",
667                sell: 500000000.to_biguint().unwrap(),
668                exp: BigUint::from_str("64352395915550406461").unwrap(),
669            },
670            SwapTestCase {
671                symbol: "WBTC",
672                sell: 550000000.to_biguint().unwrap(),
673                exp: BigUint::from_str("70784271504035662865").unwrap(),
674            },
675            SwapTestCase {
676                symbol: "WBTC",
677                sell: 600000000.to_biguint().unwrap(),
678                exp: BigUint::from_str("77215534856185613494").unwrap(),
679            },
680            SwapTestCase {
681                symbol: "WBTC",
682                sell: BigUint::from_str("1000000000").unwrap(),
683                exp: BigUint::from_str("128643569649663616249").unwrap(),
684            },
685            SwapTestCase {
686                symbol: "WBTC",
687                sell: BigUint::from_str("3000000000").unwrap(),
688                exp: BigUint::from_str("385196519076234662939").unwrap(),
689            },
690            SwapTestCase {
691                symbol: "WETH",
692                sell: BigUint::from_str("64000000000000000000").unwrap(),
693                exp: BigUint::from_str("496294784").unwrap(),
694            },
695            SwapTestCase {
696                symbol: "WETH",
697                sell: BigUint::from_str("70000000000000000000").unwrap(),
698                exp: BigUint::from_str("542798479").unwrap(),
699            },
700            SwapTestCase {
701                symbol: "WETH",
702                sell: BigUint::from_str("77000000000000000000").unwrap(),
703                exp: BigUint::from_str("597047757").unwrap(),
704            },
705            SwapTestCase {
706                symbol: "WETH",
707                sell: BigUint::from_str("128000000000000000000").unwrap(),
708                exp: BigUint::from_str("992129037").unwrap(),
709            },
710            SwapTestCase {
711                symbol: "WETH",
712                sell: BigUint::from_str("385000000000000000000").unwrap(),
713                exp: BigUint::from_str("2978713582").unwrap(),
714            },
715        ];
716
717        for case in cases {
718            let (token_a, token_b) =
719                if case.symbol == "WBTC" { (&wbtc, &weth) } else { (&weth, &wbtc) };
720            let res = pool
721                .get_amount_out(case.sell, token_a, token_b)
722                .unwrap();
723
724            assert_eq!(res.amount, case.exp);
725        }
726    }
727
728    #[test]
729    fn test_err_with_partial_trade() {
730        let dai = Token::new(
731            &Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(),
732            "DAI",
733            18,
734            0,
735            &[Some(10_000)],
736            Chain::Ethereum,
737            100,
738        );
739        let usdc = Token::new(
740            &Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
741            "USDC",
742            6,
743            0,
744            &[Some(10_000)],
745            Chain::Ethereum,
746            100,
747        );
748        let pool = UniswapV3State::new(
749            73015811375239994,
750            U256::from_str("148273042406850898575413").unwrap(),
751            FeeAmount::High,
752            -263789,
753            vec![
754                TickInfo::new(-269600, 3612326326695492i128).unwrap(),
755                TickInfo::new(-268800, 1487613939516867i128).unwrap(),
756                TickInfo::new(-267800, 1557587121322546i128).unwrap(),
757                TickInfo::new(-267400, 424592076717375i128).unwrap(),
758                TickInfo::new(-267200, 11691597431643916i128).unwrap(),
759                TickInfo::new(-266800, -218742815100986i128).unwrap(),
760                TickInfo::new(-266600, 1118947532495477i128).unwrap(),
761                TickInfo::new(-266200, 1233064286622365i128).unwrap(),
762                TickInfo::new(-265000, 4252603063356107i128).unwrap(),
763                TickInfo::new(-263200, -351282010325232i128).unwrap(),
764                TickInfo::new(-262800, -2352011819117842i128).unwrap(),
765                TickInfo::new(-262600, -424592076717375i128).unwrap(),
766                TickInfo::new(-262200, -11923662433672566i128).unwrap(),
767                TickInfo::new(-261600, -2432911749667741i128).unwrap(),
768                TickInfo::new(-260200, -4032727022572273i128).unwrap(),
769                TickInfo::new(-260000, -22889492064625028i128).unwrap(),
770                TickInfo::new(-259400, -1557587121322546i128).unwrap(),
771                TickInfo::new(-259200, -1487613939516867i128).unwrap(),
772                TickInfo::new(-258400, -400137022888262i128).unwrap(),
773            ],
774        )
775        .unwrap();
776        let amount_in = BigUint::from_str("50000000000").unwrap();
777        let exp = BigUint::from_str("6820591625999718100883").unwrap();
778
779        let err = pool
780            .get_amount_out(amount_in, &usdc, &dai)
781            .unwrap_err();
782
783        match err {
784            SimulationError::InvalidInput(ref _err, ref amount_out_result) => {
785                match amount_out_result {
786                    Some(amount_out_result) => {
787                        assert_eq!(amount_out_result.amount, exp);
788                        let new_state = amount_out_result
789                            .new_state
790                            .as_any()
791                            .downcast_ref::<UniswapV3State>()
792                            .unwrap();
793                        assert_ne!(new_state.tick, pool.tick);
794                        assert_ne!(new_state.liquidity, pool.liquidity);
795                    }
796                    _ => panic!("Partial amount out result is None. Expected partial result."),
797                }
798            }
799            _ => panic!("Test failed: was expecting a SimulationError::InsufficientData"),
800        }
801    }
802
803    #[test]
804    fn test_delta_transition() {
805        let mut pool = UniswapV3State::new(
806            1000,
807            U256::from_str("1000").unwrap(),
808            FeeAmount::Low,
809            100,
810            vec![TickInfo::new(255760, 10000).unwrap(), TickInfo::new(255900, -10000).unwrap()],
811        )
812        .unwrap();
813        let attributes: HashMap<String, Bytes> = [
814            ("liquidity".to_string(), Bytes::from(2000_u64.to_be_bytes().to_vec())),
815            ("sqrt_price_x96".to_string(), Bytes::from(1001_u64.to_be_bytes().to_vec())),
816            ("tick".to_string(), Bytes::from(120_i32.to_be_bytes().to_vec())),
817            (
818                "ticks/-255760/net_liquidity".to_string(),
819                Bytes::from(10200_u64.to_be_bytes().to_vec()),
820            ),
821            (
822                "ticks/255900/net_liquidity".to_string(),
823                Bytes::from(9800_u64.to_be_bytes().to_vec()),
824            ),
825        ]
826        .into_iter()
827        .collect();
828        let delta = ProtocolStateDelta {
829            component_id: "State1".to_owned(),
830            updated_attributes: attributes,
831            deleted_attributes: HashSet::new(),
832        };
833
834        pool.delta_transition(delta, &HashMap::new(), &Balances::default())
835            .unwrap();
836
837        assert_eq!(pool.liquidity, 2000);
838        assert_eq!(pool.sqrt_price, U256::from(1001));
839        assert_eq!(pool.tick, 120);
840        assert_eq!(
841            pool.ticks
842                .get_tick(-255760)
843                .unwrap()
844                .net_liquidity,
845            10200
846        );
847        assert_eq!(
848            pool.ticks
849                .get_tick(255900)
850                .unwrap()
851                .net_liquidity,
852            9800
853        );
854    }
855
856    #[tokio::test]
857    async fn test_get_limits() {
858        let project_root = env!("CARGO_MANIFEST_DIR");
859        let asset_path =
860            Path::new(project_root).join("tests/assets/decoder/uniswap_v3_snapshot.json");
861        let json_data = fs::read_to_string(asset_path).expect("Failed to read test asset");
862        let data: Value = serde_json::from_str(&json_data).expect("Failed to parse JSON");
863
864        let state: ComponentWithState = serde_json::from_value(data)
865            .expect("Expected json to match ComponentWithState structure");
866
867        let usv3_state = UniswapV3State::try_from_with_header(
868            state,
869            Default::default(),
870            &Default::default(),
871            &Default::default(),
872            &DecoderContext::new(),
873        )
874        .await
875        .unwrap();
876
877        let t0 = Token::new(
878            &Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap(),
879            "WBTC",
880            8,
881            0,
882            &[Some(10_000)],
883            Chain::Ethereum,
884            100,
885        );
886        let t1 = Token::new(
887            &Bytes::from_str("0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf").unwrap(),
888            "cbBTC",
889            8,
890            0,
891            &[Some(10_000)],
892            Chain::Ethereum,
893            100,
894        );
895
896        let res = usv3_state
897            .get_limits(t0.address.clone(), t1.address.clone())
898            .unwrap();
899
900        assert_eq!(&res.0, &BigUint::from_u128(155144999154).unwrap()); // Crazy amount because of this tick: "ticks/-887272/net-liquidity": "0x10d73d"
901
902        let out = usv3_state
903            .get_amount_out(res.0, &t0, &t1)
904            .expect("swap for limit in didn't work");
905
906        assert_eq!(&res.1, &out.amount);
907    }
908
909    // Helper to create a basic test pool
910    fn create_basic_test_pool() -> UniswapV3State {
911        let liquidity = 100_000_000_000_000_000_000u128; // 100e18
912        let sqrt_price = get_sqrt_price_q96(U256::from(20_000_000u64), U256::from(10_000_000u64))
913            .expect("Failed to calculate sqrt price");
914        let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
915
916        let ticks = vec![TickInfo::new(0, 0).unwrap(), TickInfo::new(46080, 0).unwrap()];
917
918        UniswapV3State::new(liquidity, sqrt_price, FeeAmount::Low, tick, ticks)
919            .expect("Failed to create pool")
920    }
921
922    #[test]
923    fn test_swap_basic() {
924        let pool = create_basic_test_pool();
925
926        // Test selling token X for token Y
927        let amount_in =
928            I256::checked_from_sign_and_abs(Sign::Positive, U256::from(1000000u64)).unwrap();
929        let result = pool
930            .swap(true, amount_in, None)
931            .unwrap();
932
933        // At current pool price, we should get a little less than 2 times the amount of X
934        let expected_amount = U256::from(2000000u64);
935        let actual_amount = result
936            .amount_calculated
937            .abs()
938            .into_raw();
939        assert_eq!(expected_amount - actual_amount, U256::from(1001u64));
940        println!("Swap X->Y: amount_in={}, amount_out={}", amount_in, actual_amount);
941    }
942
943    #[test]
944    fn test_swap_to_price_basic() {
945        // Create pool with Medium fee (0.3%) to match V4's basic test
946        let liquidity = 100_000_000_000_000_000_000u128;
947        let sqrt_price = get_sqrt_price_q96(U256::from(20_000_000u64), U256::from(10_000_000u64))
948            .expect("Failed to calculate sqrt price");
949        let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
950        let ticks = vec![TickInfo::new(0, 0).unwrap(), TickInfo::new(46080, 0).unwrap()];
951
952        let pool = UniswapV3State::new(liquidity, sqrt_price, FeeAmount::Medium, tick, ticks)
953            .expect("Failed to create pool");
954
955        // Token X has lower address (0x01), Y has higher address (0x02)
956        let token_x = Token::new(
957            &Bytes::from_str("0x0000000000000000000000000000000000000001").unwrap(),
958            "X",
959            18,
960            0,
961            &[Some(10_000)],
962            Chain::Ethereum,
963            100,
964        );
965        let token_y = Token::new(
966            &Bytes::from_str("0x0000000000000000000000000000000000000002").unwrap(),
967            "Y",
968            18,
969            0,
970            &[Some(10_000)],
971            Chain::Ethereum,
972            100,
973        );
974
975        // Swap price: buying X for Y (token_out/token_in)
976        let target_price =
977            Price::new(2_000_000u64.to_biguint().unwrap(), 1_010_000u64.to_biguint().unwrap());
978
979        // Query how much Y the pool can supply when buying X at this price
980        let trade = pool
981            .query_pool_swap(&QueryPoolSwapParams::new(
982                token_x,
983                token_y,
984                SwapConstraint::PoolTargetPrice {
985                    target: target_price,
986                    tolerance: 0f64,
987                    min_amount_in: None,
988                    max_amount_in: None,
989                },
990            ))
991            .expect("swap_to_price failed");
992
993        // Should match V4's output exactly with same fees (0.3%)
994        let expected_amount_in =
995            BigUint::from_str("246739021727519745").expect("Failed to parse expected amount_in");
996        let expected_amount_out =
997            BigUint::from_str("490291909043340795").expect("Failed to parse expected amount_out");
998
999        assert_eq!(
1000            trade.amount_in().clone(),
1001            expected_amount_in,
1002            "amount_in should match expected value"
1003        );
1004        assert_eq!(
1005            trade.amount_out().clone(),
1006            expected_amount_out,
1007            "amount_out should match expected value"
1008        );
1009    }
1010
1011    #[test]
1012    fn test_swap_to_price_price_too_high() {
1013        let pool = create_basic_test_pool();
1014
1015        let token_x = Token::new(
1016            &Bytes::from_str("0x0000000000000000000000000000000000000001").unwrap(),
1017            "X",
1018            18,
1019            0,
1020            &[Some(10_000)],
1021            Chain::Ethereum,
1022            100,
1023        );
1024        let token_y = Token::new(
1025            &Bytes::from_str("0x0000000000000000000000000000000000000002").unwrap(),
1026            "Y",
1027            18,
1028            0,
1029            &[Some(10_000)],
1030            Chain::Ethereum,
1031            100,
1032        );
1033
1034        // Price far above pool price - should return zero
1035        let target_price =
1036            Price::new(10_000_000u64.to_biguint().unwrap(), 1_000_000u64.to_biguint().unwrap());
1037
1038        let result = pool.query_pool_swap(&QueryPoolSwapParams::new(
1039            token_x,
1040            token_y,
1041            SwapConstraint::PoolTargetPrice {
1042                target: target_price,
1043                tolerance: 0f64,
1044                min_amount_in: None,
1045                max_amount_in: None,
1046            },
1047        ));
1048        assert!(result.is_err(), "Should return error when target price is unreachable");
1049    }
1050
1051    #[test]
1052    fn test_swap_parameterized() {
1053        // Parameterized swap tests with real WBTC/WETH pool data
1054        let liquidity = 377_952_820_878_029_838u128;
1055        let sqrt_price = U256::from_str("28437325270877025820973479874632004")
1056            .expect("Failed to parse sqrt_price");
1057        let tick = 255830;
1058
1059        let ticks = vec![
1060            TickInfo::new(255760, 1_759_015_528_199_933).unwrap(),
1061            TickInfo::new(255770, 6_393_138_051_835_308).unwrap(),
1062            TickInfo::new(255780, 228_206_673_808_681).unwrap(),
1063            TickInfo::new(255820, 1_319_490_609_195_820).unwrap(),
1064            TickInfo::new(255830, 678_916_926_147_901).unwrap(),
1065            TickInfo::new(255840, 12_208_947_683_433_103).unwrap(),
1066            TickInfo::new(255850, 1_177_970_713_095_301).unwrap(),
1067            TickInfo::new(255860, 8_752_304_680_520_407).unwrap(),
1068            TickInfo::new(255880, 1_486_478_248_067_104).unwrap(),
1069            TickInfo::new(255890, 1_878_744_276_123_248).unwrap(),
1070            TickInfo::new(255900, 77_340_284_046_725_227).unwrap(),
1071        ];
1072
1073        let pool = UniswapV3State::new(liquidity, sqrt_price, FeeAmount::Low, tick, ticks)
1074            .expect("Failed to create pool");
1075
1076        // Test cases: (zero_for_one, amount_in, expected_amount_out, test_id)
1077        // WBTC address (0x2260...) < WETH address (0xC02a...), so WBTC is token0
1078        let test_cases = vec![
1079            // WBTC to WETH cases (zero_for_one = true)
1080            (true, "500000000", "64352395915550406461", "WBTC->WETH 500000000"),
1081            (true, "550000000", "70784271504035662865", "WBTC->WETH 550000000"),
1082            (true, "600000000", "77215534856185613494", "WBTC->WETH 600000000"),
1083            (true, "1000000000", "128643569649663616249", "WBTC->WETH 1000000000"),
1084            (true, "3000000000", "385196519076234662939", "WBTC->WETH 3000000000"),
1085            // WETH to WBTC cases (zero_for_one = false)
1086            (false, "64000000000000000000", "496294784", "WETH->WBTC 64 ETH"),
1087            (false, "70000000000000000000", "542798479", "WETH->WBTC 70 ETH"),
1088            (false, "77000000000000000000", "597047757", "WETH->WBTC 77 ETH"),
1089            (false, "128000000000000000000", "992129037", "WETH->WBTC 128 ETH"),
1090            (false, "385000000000000000000", "2978713582", "WETH->WBTC 385 ETH"),
1091        ];
1092
1093        for (zero_for_one, amount_in_str, expected_amount_out_str, test_id) in test_cases {
1094            let amount_in = U256::from_str(amount_in_str).expect("Failed to parse amount_in");
1095            let amount_specified = I256::checked_from_sign_and_abs(Sign::Positive, amount_in)
1096                .unwrap_or_else(|| panic!("{} - Failed to convert amount to I256", test_id));
1097
1098            let result = pool
1099                .swap(zero_for_one, amount_specified, None)
1100                .unwrap_or_else(|e| panic!("{} - swap failed: {:?}", test_id, e));
1101
1102            let amount_out = result
1103                .amount_calculated
1104                .abs()
1105                .into_raw();
1106            let expected = U256::from_str(expected_amount_out_str)
1107                .expect("Failed to parse expected_amount_out");
1108
1109            assert_eq!(amount_out, expected, "{}", test_id);
1110        }
1111    }
1112
1113    #[test]
1114    fn test_swap_to_price_parameterized() {
1115        // Tests query_supply with various price points
1116        let wbtc = Token::new(
1117            &Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap(),
1118            "WBTC",
1119            8,
1120            0,
1121            &[Some(10_000)],
1122            Chain::Ethereum,
1123            100,
1124        );
1125        let weth = Token::new(
1126            &Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(),
1127            "WETH",
1128            18,
1129            0,
1130            &[Some(10_000)],
1131            Chain::Ethereum,
1132            100,
1133        );
1134
1135        let liquidity = 377_952_820_878_029_838u128;
1136        let sqrt_price = get_sqrt_price_q96(U256::from(130_000_000u64), U256::from(10_000_000u64))
1137            .expect("Failed to calculate sqrt price");
1138        let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
1139
1140        let ticks = vec![
1141            TickInfo::new(25560, 1759015528199933).unwrap(),
1142            TickInfo::new(25570, 6393138051835308).unwrap(),
1143            TickInfo::new(25580, 228206673808681).unwrap(),
1144            TickInfo::new(25620, 1319490609195820).unwrap(),
1145            TickInfo::new(25630, 678916926147901).unwrap(),
1146            TickInfo::new(25640, 12208947683433103).unwrap(),
1147            TickInfo::new(25660, 8752304680520407).unwrap(),
1148            TickInfo::new(25680, 1486478248067104).unwrap(),
1149            TickInfo::new(25690, 1878744276123248).unwrap(),
1150            TickInfo::new(25700, 77340284046725227).unwrap(),
1151        ];
1152
1153        let pool = UniswapV3State::new(liquidity, sqrt_price, FeeAmount::Low, tick, ticks)
1154            .expect("Failed to create pool");
1155
1156        // Test cases: (sell_token, sell_price, buy_price, expected_supply, test_id)
1157        let test_cases = vec![
1158            (&wbtc, 129u64, 10u64, "0", "WBTC sell_price=129, buy_price=10"),
1159            (&wbtc, 130u64, 10u64, "0", "WBTC sell_price=130, buy_price=10"),
1160            (&wbtc, 1305u64, 100u64, "163535995630461", "WBTC sell_price=1305, buy_price=100"),
1161            (&weth, 99u64, 1300u64, "0", "WETH sell_price=99, buy_price=1300"),
1162            (&weth, 100u64, 1300u64, "0", "WETH sell_price=100, buy_price=1300"),
1163            (&weth, 101u64, 1299u64, "524227092059180", "WETH sell_price=101, buy_price=1299"),
1164        ];
1165
1166        for (sell_token, sell_price, buy_price, expected_str, test_id) in test_cases {
1167            let buy_token = if sell_token == &wbtc { &weth } else { &wbtc };
1168
1169            let target_price =
1170                Price::new(buy_price.to_biguint().unwrap(), sell_price.to_biguint().unwrap());
1171
1172            if expected_str == "0" {
1173                let result = pool.query_pool_swap(&QueryPoolSwapParams::new(
1174                    buy_token.clone(),
1175                    sell_token.clone(),
1176                    SwapConstraint::PoolTargetPrice {
1177                        target: target_price,
1178                        tolerance: 0f64,
1179                        min_amount_in: None,
1180                        max_amount_in: None,
1181                    },
1182                ));
1183                assert!(result.is_err(), "Should return error when target price is unreachable");
1184            } else {
1185                let expected =
1186                    BigUint::from_str(expected_str).expect("Failed to parse expected value");
1187
1188                let trade = pool
1189                    .query_pool_swap(&QueryPoolSwapParams::new(
1190                        buy_token.clone(),
1191                        sell_token.clone(),
1192                        SwapConstraint::PoolTargetPrice {
1193                            target: target_price,
1194                            tolerance: 0f64,
1195                            min_amount_in: None,
1196                            max_amount_in: None,
1197                        },
1198                    ))
1199                    .unwrap_or_else(|e| panic!("{} - query_supply failed: {:?}", test_id, e));
1200                assert_eq!(trade.amount_out().clone(), expected, "{}", test_id);
1201            }
1202        }
1203    }
1204
1205    #[test]
1206    fn test_swap_to_price_around_spot_price() {
1207        // Tests query_supply edge cases around the spot price with fees
1208        let liquidity = 10_000_000_000_000_000u128;
1209        let sqrt_price =
1210            get_sqrt_price_q96(U256::from(2_000_000_000u64), U256::from(1_000_000_000u64))
1211                .expect("Failed to calculate sqrt price");
1212        let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
1213
1214        let ticks = vec![TickInfo::new(0, 0).unwrap(), TickInfo::new(46080, 0).unwrap()];
1215
1216        let pool = UniswapV3State::new(liquidity, sqrt_price, FeeAmount::Low, tick, ticks)
1217            .expect("Failed to create pool");
1218
1219        let token_x = Token::new(
1220            &Bytes::from_str("0x0000000000000000000000000000000000000001").unwrap(),
1221            "X",
1222            18,
1223            0,
1224            &[Some(10_000)],
1225            Chain::Ethereum,
1226            100,
1227        );
1228        let token_y = Token::new(
1229            &Bytes::from_str("0x0000000000000000000000000000000000000002").unwrap(),
1230            "Y",
1231            18,
1232            0,
1233            &[Some(10_000)],
1234            Chain::Ethereum,
1235            100,
1236        );
1237
1238        // Test 1: Price just above spot price, too little to cover fees
1239        let target_price =
1240            Price::new(1_999_750u64.to_biguint().unwrap(), 1_000_250u64.to_biguint().unwrap());
1241
1242        let result = pool.query_pool_swap(&QueryPoolSwapParams::new(
1243            token_x.clone(),
1244            token_y.clone(),
1245            SwapConstraint::PoolTargetPrice {
1246                target: target_price,
1247                tolerance: 0f64,
1248                min_amount_in: None,
1249                max_amount_in: None,
1250            },
1251        ));
1252        assert!(result.is_err(), "Should return error when target price is unreachable");
1253
1254        // Test 2: Price high enough to cover fees (0.1% higher)
1255        let target_price =
1256            Price::new(1_999_000u64.to_biguint().unwrap(), 1_001_000u64.to_biguint().unwrap());
1257
1258        let pool_swap = pool
1259            .query_pool_swap(&QueryPoolSwapParams::new(
1260                token_x,
1261                token_y,
1262                SwapConstraint::PoolTargetPrice {
1263                    target: target_price,
1264                    tolerance: 0f64,
1265                    min_amount_in: None,
1266                    max_amount_in: None,
1267                },
1268            ))
1269            .expect("swap_to_price failed");
1270
1271        let expected_amount_out =
1272            BigUint::from_str("7062236922008").expect("Failed to parse expected value");
1273        assert_eq!(
1274            pool_swap.amount_out().clone(),
1275            expected_amount_out,
1276            "Expected amount out when price covers fees"
1277        );
1278    }
1279
1280    #[test]
1281    fn test_swap_to_price_matches_get_amount_out() {
1282        let liquidity = 100_000_000_000_000_000_000u128;
1283        let sqrt_price = get_sqrt_price_q96(U256::from(20_000_000u64), U256::from(10_000_000u64))
1284            .expect("Failed to calculate sqrt price");
1285        let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
1286
1287        let ticks = vec![TickInfo::new(0, 0).unwrap(), TickInfo::new(46080, 0).unwrap()];
1288
1289        let pool = UniswapV3State::new(liquidity, sqrt_price, FeeAmount::Medium, tick, ticks)
1290            .expect("Failed to create pool");
1291
1292        let token_x_addr = Bytes::from_str("0x0000000000000000000000000000000000000001").unwrap();
1293        let token_y_addr = Bytes::from_str("0x0000000000000000000000000000000000000002").unwrap();
1294
1295        let token_x = Token::new(&token_x_addr, "X", 18, 0, &[], Chain::Ethereum, 1);
1296        let token_y = Token::new(&token_y_addr, "Y", 18, 0, &[], Chain::Ethereum, 1);
1297
1298        // Get the trade from swap_to_price
1299        let target_price = Price::new(BigUint::from(2_000_000u64), BigUint::from(1_010_000u64));
1300        let pool_swap = pool
1301            .query_pool_swap(&QueryPoolSwapParams::new(
1302                token_x.clone(),
1303                token_y.clone(),
1304                SwapConstraint::PoolTargetPrice {
1305                    target: target_price,
1306                    tolerance: 0f64,
1307                    min_amount_in: None,
1308                    max_amount_in: None,
1309                },
1310            ))
1311            .expect("swap_to_price failed");
1312        assert!(pool_swap.amount_in().clone() > BigUint::ZERO, "Amount in should be positive");
1313
1314        // Use the amount_in from swap_to_price with get_amount_out
1315        let result = pool
1316            .get_amount_out(pool_swap.amount_in().clone(), &token_x, &token_y)
1317            .expect("get_amount_out failed");
1318
1319        // The amount_out from get_amount_out should be close to swap_to_price's amount_out
1320        // Allow for small rounding differences
1321        assert!(result.amount > BigUint::ZERO);
1322        assert!(result.amount >= *pool_swap.amount_out());
1323    }
1324
1325    #[test]
1326    fn test_swap_price_limit_out_of_range_returns_error() {
1327        let pool = create_basic_test_pool();
1328        let amount = I256::checked_from_sign_and_abs(Sign::Positive, U256::from(1000u64)).unwrap();
1329
1330        // zero_for_one: price_limit equal to sqrt_price is invalid (must be strictly less)
1331        let result = pool.swap(true, amount, Some(pool.sqrt_price));
1332        assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
1333
1334        // zero_for_one: price_limit at MIN_SQRT_RATIO is invalid (must be strictly greater)
1335        let result = pool.swap(true, amount, Some(MIN_SQRT_RATIO));
1336        assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
1337
1338        // one_for_zero: price_limit equal to sqrt_price is invalid (must be strictly greater)
1339        let result = pool.swap(false, amount, Some(pool.sqrt_price));
1340        assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
1341
1342        // one_for_zero: price_limit at MAX_SQRT_RATIO is invalid (must be strictly less)
1343        let result = pool.swap(false, amount, Some(MAX_SQRT_RATIO));
1344        assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
1345    }
1346
1347    #[test]
1348    fn test_swap_at_extreme_price_returns_error() {
1349        // Simulates the depth calculation scenario: pool sqrt_price is at MIN_SQRT_RATIO + 1,
1350        // so the default price limit for zero_for_one equals sqrt_price and fails validation.
1351        let sqrt_price = MIN_SQRT_RATIO + U256::from(1u64);
1352        let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
1353        // FeeAmount::Low has tick spacing 10; ticks must be aligned
1354        let aligned_tick = (MIN_TICK / 10) * 10 + 10; // first multiple of 10 above MIN_TICK
1355        let ticks = vec![
1356            TickInfo::new(aligned_tick, 0).unwrap(),
1357            TickInfo::new(aligned_tick + 10, 0).unwrap(),
1358        ];
1359        let pool = UniswapV3State::new(
1360            100_000_000_000_000_000_000u128,
1361            sqrt_price,
1362            FeeAmount::Low,
1363            tick,
1364            ticks,
1365        )
1366        .unwrap();
1367
1368        let amount = I256::checked_from_sign_and_abs(Sign::Positive, U256::from(1000u64)).unwrap();
1369        // Default price limit for zero_for_one is MIN_SQRT_RATIO + 1 == sqrt_price, so invalid
1370        let result = pool.swap(true, amount, None);
1371        assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
1372    }
1373}
1374
1375#[cfg(test)]
1376mod tests_forks {
1377    use std::{fs, path::Path, str::FromStr};
1378
1379    use serde_json::Value;
1380    use tycho_client::feed::synchronizer::ComponentWithState;
1381    use tycho_common::models::Chain;
1382
1383    use super::*;
1384    use crate::protocol::models::{DecoderContext, TryFromWithBlock};
1385
1386    #[tokio::test]
1387    async fn test_pancakeswap_get_amount_out() {
1388        let project_root = env!("CARGO_MANIFEST_DIR");
1389        let asset_path =
1390            Path::new(project_root).join("tests/assets/decoder/pancakeswap_v3_snapshot.json");
1391        let json_data = fs::read_to_string(asset_path).expect("Failed to read test asset");
1392        let data: Value = serde_json::from_str(&json_data).expect("Failed to parse JSON");
1393
1394        let state: ComponentWithState = serde_json::from_value(data)
1395            .expect("Expected json to match ComponentWithState structure");
1396
1397        let pool_state = UniswapV3State::try_from_with_header(
1398            state,
1399            Default::default(),
1400            &Default::default(),
1401            &Default::default(),
1402            &DecoderContext::new(),
1403        )
1404        .await
1405        .unwrap();
1406
1407        let usdc = Token::new(
1408            &Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
1409            "USDC",
1410            6,
1411            0,
1412            &[Some(10_000)],
1413            Chain::Ethereum,
1414            100,
1415        );
1416        let usdt = Token::new(
1417            &Bytes::from_str("0xdac17f958d2ee523a2206206994597c13d831ec7").unwrap(),
1418            "USDT",
1419            6,
1420            0,
1421            &[Some(10_000)],
1422            Chain::Ethereum,
1423            100,
1424        );
1425
1426        // Swap from https://etherscan.io/tx/0x641b1e98990ae49fd00157a29e1530ff6403706b2864aa52b1c30849ce020b2c#eventlog
1427        let res = pool_state
1428            .get_amount_out(BigUint::from_str("5976361609").unwrap(), &usdt, &usdc)
1429            .unwrap();
1430
1431        assert_eq!(res.amount, BigUint::from_str("5975901673").unwrap());
1432    }
1433
1434    #[test]
1435    fn test_get_limits_graceful_underflow() {
1436        // Verifies graceful handling of liquidity underflow in get_limits for V3
1437        let pool = UniswapV3State::new(
1438            1000000,
1439            U256::from_str("79228162514264337593543950336").unwrap(),
1440            FeeAmount::Medium,
1441            0,
1442            vec![
1443                // A tick with net_liquidity > current_liquidity
1444                // When zero_for_one=true, this gets negated and would cause underflow
1445                TickInfo::new(-60, 2000000).unwrap(), // 2x current liquidity
1446            ],
1447        )
1448        .unwrap();
1449
1450        let usdc = Token::new(
1451            &Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
1452            "USDC",
1453            6,
1454            0,
1455            &[Some(10_000)],
1456            Chain::Ethereum,
1457            100,
1458        );
1459        let weth = Token::new(
1460            &Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(),
1461            "WETH",
1462            18,
1463            0,
1464            &[Some(10_000)],
1465            Chain::Ethereum,
1466            100,
1467        );
1468
1469        let (limit_in, limit_out) = pool
1470            .get_limits(usdc.address.clone(), weth.address.clone())
1471            .unwrap();
1472
1473        // Should return some conservative limits
1474        assert!(limit_in > BigUint::zero());
1475        assert!(limit_out > BigUint::zero());
1476    }
1477}