Skip to main content

tycho_simulation/evm/protocol/uniswap_v2/
state.rs

1use std::{any::Any, collections::HashMap};
2
3use alloy::primitives::U256;
4use num_bigint::BigUint;
5use num_traits::Zero;
6use serde::{Deserialize, Serialize};
7use tycho_common::{
8    dto::ProtocolStateDelta,
9    models::token::Token,
10    simulation::{
11        errors::{SimulationError, TransitionError},
12        protocol_sim::{
13            Balances, GetAmountOutResult, PoolSwap, ProtocolSim, QueryPoolSwapParams,
14            SwapConstraint,
15        },
16    },
17    Bytes,
18};
19
20use crate::evm::protocol::{
21    cpmm::protocol::{
22        cpmm_delta_transition, cpmm_fee, cpmm_get_amount_out, cpmm_get_limits, cpmm_spot_price,
23        cpmm_swap_to_price, ProtocolFee,
24    },
25    safe_math::{safe_add_u256, safe_sub_u256},
26    u256_num::{biguint_to_u256, u256_to_biguint},
27    utils::add_fee_markup,
28};
29
30const UNISWAP_V2_FEE_BPS: u32 = 30; // 0.3% fee
31const FEE_PRECISION: U256 = U256::from_limbs([10000, 0, 0, 0]);
32const FEE_NUMERATOR: U256 = U256::from_limbs([9970, 0, 0, 0]);
33
34#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
35pub struct UniswapV2State {
36    pub reserve0: U256,
37    pub reserve1: U256,
38}
39
40impl UniswapV2State {
41    /// Creates a new instance of `UniswapV2State` with the given reserves.
42    ///
43    /// # Arguments
44    ///
45    /// * `reserve0` - Reserve of token 0.
46    /// * `reserve1` - Reserve of token 1.
47    pub fn new(reserve0: U256, reserve1: U256) -> Self {
48        UniswapV2State { reserve0, reserve1 }
49    }
50}
51
52#[typetag::serde]
53impl ProtocolSim for UniswapV2State {
54    fn fee(&self) -> f64 {
55        cpmm_fee(UNISWAP_V2_FEE_BPS)
56    }
57
58    fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
59        let price = cpmm_spot_price(base, quote, self.reserve0, self.reserve1)?;
60        Ok(add_fee_markup(price, self.fee()))
61    }
62
63    fn get_amount_out(
64        &self,
65        amount_in: BigUint,
66        token_in: &Token,
67        token_out: &Token,
68    ) -> Result<GetAmountOutResult, SimulationError> {
69        let amount_in = biguint_to_u256(&amount_in);
70        let zero2one = token_in.address < token_out.address;
71        let (reserve_in, reserve_out) =
72            if zero2one { (self.reserve0, self.reserve1) } else { (self.reserve1, self.reserve0) };
73        let fee = ProtocolFee::new(FEE_NUMERATOR, FEE_PRECISION);
74        let amount_out = cpmm_get_amount_out(amount_in, reserve_in, reserve_out, fee)?;
75        let mut new_state = self.clone();
76        let (reserve0_mut, reserve1_mut) = (&mut new_state.reserve0, &mut new_state.reserve1);
77        if zero2one {
78            *reserve0_mut = safe_add_u256(self.reserve0, amount_in)?;
79            *reserve1_mut = safe_sub_u256(self.reserve1, amount_out)?;
80        } else {
81            *reserve0_mut = safe_sub_u256(self.reserve0, amount_out)?;
82            *reserve1_mut = safe_add_u256(self.reserve1, amount_in)?;
83        };
84        Ok(GetAmountOutResult::new(
85            u256_to_biguint(amount_out),
86            BigUint::from(120_000u32),
87            Box::new(new_state),
88        ))
89    }
90
91    fn get_limits(
92        &self,
93        sell_token: Bytes,
94        buy_token: Bytes,
95    ) -> Result<(BigUint, BigUint), SimulationError> {
96        cpmm_get_limits(sell_token, buy_token, self.reserve0, self.reserve1, UNISWAP_V2_FEE_BPS)
97    }
98
99    fn delta_transition(
100        &mut self,
101        delta: ProtocolStateDelta,
102        _tokens: &HashMap<Bytes, Token>,
103        _balances: &Balances,
104    ) -> Result<(), TransitionError> {
105        let (reserve0_mut, reserve1_mut) = (&mut self.reserve0, &mut self.reserve1);
106        cpmm_delta_transition(delta, reserve0_mut, reserve1_mut)
107    }
108
109    fn query_pool_swap(&self, params: &QueryPoolSwapParams) -> Result<PoolSwap, SimulationError> {
110        match params.swap_constraint() {
111            SwapConstraint::PoolTargetPrice {
112                target: price,
113                tolerance: _,
114                min_amount_in: _,
115                max_amount_in: _,
116            } => {
117                let zero2one = params.token_in().address < params.token_out().address;
118                let (reserve_in, reserve_out) = if zero2one {
119                    (self.reserve0, self.reserve1)
120                } else {
121                    (self.reserve1, self.reserve0)
122                };
123
124                let fee = ProtocolFee::new(FEE_NUMERATOR, FEE_PRECISION);
125                let (amount_in, _) = cpmm_swap_to_price(reserve_in, reserve_out, price, fee)?;
126                if amount_in.is_zero() {
127                    return Ok(PoolSwap::new(
128                        BigUint::ZERO,
129                        BigUint::ZERO,
130                        Box::new(self.clone()),
131                        None,
132                    ));
133                }
134
135                let res =
136                    self.get_amount_out(amount_in.clone(), params.token_in(), params.token_out())?;
137                Ok(PoolSwap::new(amount_in, res.amount, res.new_state, None))
138            }
139            SwapConstraint::TradeLimitPrice { .. } => Err(SimulationError::InvalidInput(
140                "UniswapV2State does not support TradeLimitPrice constraint in query_pool_swap"
141                    .to_string(),
142                None,
143            )),
144        }
145    }
146
147    fn clone_box(&self) -> Box<dyn ProtocolSim> {
148        Box::new(self.clone())
149    }
150
151    fn as_any(&self) -> &dyn Any {
152        self
153    }
154
155    fn as_any_mut(&mut self) -> &mut dyn Any {
156        self
157    }
158
159    fn eq(&self, other: &dyn ProtocolSim) -> bool {
160        if let Some(other_state) = other.as_any().downcast_ref::<Self>() {
161            let (self_reserve0, self_reserve1) = (self.reserve0, self.reserve1);
162            let (other_reserve0, other_reserve1) = (other_state.reserve0, other_state.reserve1);
163            self_reserve0 == other_reserve0 &&
164                self_reserve1 == other_reserve1 &&
165                self.fee() == other_state.fee()
166        } else {
167            false
168        }
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use std::{
175        collections::{HashMap, HashSet},
176        str::FromStr,
177    };
178
179    use approx::assert_ulps_eq;
180    use num_bigint::BigUint;
181    use num_traits::One;
182    use rstest::rstest;
183    use tycho_common::{
184        dto::ProtocolStateDelta,
185        hex_bytes::Bytes,
186        models::{token::Token, Chain},
187        simulation::{
188            errors::{SimulationError, TransitionError},
189            protocol_sim::{Balances, Price, ProtocolSim},
190        },
191    };
192
193    use super::*;
194    use crate::evm::protocol::u256_num::biguint_to_u256;
195
196    fn token_0() -> Token {
197        Token::new(
198            &Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(),
199            "T0",
200            18,
201            0,
202            &[Some(10_000)],
203            Chain::Ethereum,
204            100,
205        )
206    }
207
208    fn token_1() -> Token {
209        Token::new(
210            &Bytes::from_str("0x0000000000000000000000000000000000000001").unwrap(),
211            "T1",
212            18,
213            0,
214            &[Some(10_000)],
215            Chain::Ethereum,
216            100,
217        )
218    }
219
220    #[rstest]
221    #[case::same_dec(
222        U256::from_str("6770398782322527849696614").unwrap(),
223        U256::from_str("5124813135806900540214").unwrap(),
224        18,
225        18,
226    BigUint::from_str("10000000000000000000000").unwrap(),
227    BigUint::from_str("7535635391574243447").unwrap()
228    )]
229    #[case::diff_dec(
230        U256::from_str("33372357002392258830279").unwrap(),
231        U256::from_str("43356945776493").unwrap(),
232        18,
233        6,
234    BigUint::from_str("10000000000000000000").unwrap(),
235    BigUint::from_str("12949029867").unwrap()
236    )]
237    fn test_get_amount_out(
238        #[case] r0: U256,
239        #[case] r1: U256,
240        #[case] token_0_decimals: u32,
241        #[case] token_1_decimals: u32,
242        #[case] amount_in: BigUint,
243        #[case] exp: BigUint,
244    ) {
245        let t0 = Token::new(
246            &Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(),
247            "T0",
248            token_0_decimals,
249            0,
250            &[Some(10_000)],
251            Chain::Ethereum,
252            100,
253        );
254        let t1 = Token::new(
255            &Bytes::from_str("0x0000000000000000000000000000000000000001").unwrap(),
256            "T0",
257            token_1_decimals,
258            0,
259            &[Some(10_000)],
260            Chain::Ethereum,
261            100,
262        );
263        let state = UniswapV2State::new(r0, r1);
264
265        let res = state
266            .get_amount_out(amount_in.clone(), &t0, &t1)
267            .unwrap();
268
269        assert_eq!(res.amount, exp);
270        let new_state = res
271            .new_state
272            .as_any()
273            .downcast_ref::<UniswapV2State>()
274            .unwrap();
275        assert_eq!(new_state.reserve0, r0 + biguint_to_u256(&amount_in));
276        assert_eq!(new_state.reserve1, r1 - biguint_to_u256(&exp));
277        // Assert that the old state is unchanged
278        assert_eq!(state.reserve0, r0);
279        assert_eq!(state.reserve1, r1);
280    }
281
282    #[test]
283    fn test_get_amount_out_overflow() {
284        let r0 = U256::from_str("33372357002392258830279").unwrap();
285        let r1 = U256::from_str("43356945776493").unwrap();
286        let amount_in = (BigUint::one() << 256) - BigUint::one(); // U256 max value
287        let t0d = 18;
288        let t1d = 16;
289        let t0 = Token::new(
290            &Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(),
291            "T0",
292            t0d,
293            0,
294            &[Some(10_000)],
295            Chain::Ethereum,
296            100,
297        );
298        let t1 = Token::new(
299            &Bytes::from_str("0x0000000000000000000000000000000000000001").unwrap(),
300            "T0",
301            t1d,
302            0,
303            &[Some(10_000)],
304            Chain::Ethereum,
305            100,
306        );
307        let state = UniswapV2State::new(r0, r1);
308
309        let res = state.get_amount_out(amount_in, &t0, &t1);
310        assert!(res.is_err());
311        let err = res.err().unwrap();
312        assert!(matches!(err, SimulationError::FatalError(_)));
313    }
314
315    #[rstest]
316    #[case(true, 0.000823442321727627)] // 0.0008209719947624441 / 0.997
317    #[case(false, 1221.7335469177287)] // 1218.0683462769755 / 0.997
318    fn test_spot_price(#[case] zero_to_one: bool, #[case] exp: f64) {
319        let state = UniswapV2State::new(
320            U256::from_str("36925554990922").unwrap(),
321            U256::from_str("30314846538607556521556").unwrap(),
322        );
323        let usdc = Token::new(
324            &Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap(),
325            "USDC",
326            6,
327            0,
328            &[Some(10_000)],
329            Chain::Ethereum,
330            100,
331        );
332        let weth = Token::new(
333            &Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(),
334            "WETH",
335            18,
336            0,
337            &[Some(10_000)],
338            Chain::Ethereum,
339            100,
340        );
341
342        let res = if zero_to_one {
343            state.spot_price(&usdc, &weth).unwrap()
344        } else {
345            state.spot_price(&weth, &usdc).unwrap()
346        };
347
348        assert_ulps_eq!(res, exp);
349    }
350
351    #[test]
352    fn test_fee() {
353        let state = UniswapV2State::new(
354            U256::from_str("36925554990922").unwrap(),
355            U256::from_str("30314846538607556521556").unwrap(),
356        );
357
358        let res = state.fee();
359
360        assert_ulps_eq!(res, 0.003);
361    }
362
363    #[test]
364    fn test_delta_transition() {
365        let mut state =
366            UniswapV2State::new(U256::from_str("1000").unwrap(), U256::from_str("1000").unwrap());
367        let attributes: HashMap<String, Bytes> = vec![
368            ("reserve0".to_string(), Bytes::from(1500_u64.to_be_bytes().to_vec())),
369            ("reserve1".to_string(), Bytes::from(2000_u64.to_be_bytes().to_vec())),
370        ]
371        .into_iter()
372        .collect();
373        let delta = ProtocolStateDelta {
374            component_id: "State1".to_owned(),
375            updated_attributes: attributes,
376            deleted_attributes: HashSet::new(), // usv2 doesn't have any deletable attributes
377        };
378
379        let res = state.delta_transition(delta, &HashMap::new(), &Balances::default());
380
381        assert!(res.is_ok());
382        assert_eq!(state.reserve0, U256::from_str("1500").unwrap());
383        assert_eq!(state.reserve1, U256::from_str("2000").unwrap());
384    }
385
386    #[test]
387    fn test_delta_transition_missing_attribute() {
388        let mut state =
389            UniswapV2State::new(U256::from_str("1000").unwrap(), U256::from_str("1000").unwrap());
390        let attributes: HashMap<String, Bytes> =
391            vec![("reserve0".to_string(), Bytes::from(1500_u64.to_be_bytes().to_vec()))]
392                .into_iter()
393                .collect();
394        let delta = ProtocolStateDelta {
395            component_id: "State1".to_owned(),
396            updated_attributes: attributes,
397            deleted_attributes: HashSet::new(),
398        };
399
400        let res = state.delta_transition(delta, &HashMap::new(), &Balances::default());
401
402        assert!(res.is_err());
403        // assert it errors for the missing reserve1 attribute delta
404        match res {
405            Err(e) => {
406                assert!(matches!(e, TransitionError::MissingAttribute(ref x) if x=="reserve1"))
407            }
408            _ => panic!("Test failed: was expecting an Err value"),
409        };
410    }
411
412    #[test]
413    fn test_get_limits_price_impact() {
414        let state =
415            UniswapV2State::new(U256::from_str("1000").unwrap(), U256::from_str("100000").unwrap());
416
417        let (amount_in, _) = state
418            .get_limits(
419                Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(),
420                Bytes::from_str("0x0000000000000000000000000000000000000001").unwrap(),
421            )
422            .unwrap();
423
424        let token_0 = token_0();
425        let token_1 = token_1();
426
427        let result = state
428            .get_amount_out(amount_in.clone(), &token_0, &token_1)
429            .unwrap();
430        let new_state = result.new_state;
431
432        let initial_price = state
433            .spot_price(&token_0, &token_1)
434            .unwrap();
435        let new_price = new_state
436            .spot_price(&token_0, &token_1)
437            .unwrap();
438
439        // Price impact should be approximately 90% (new_price ≈ initial_price / 10)
440        // Due to fees being added to pool liquidity, actual impact is slightly less than 90%
441        // (see cpmm_get_limits documentation for details)
442        let price_impact = 1.0 - new_price / initial_price;
443        assert!(
444            (0.899..=0.90).contains(&price_impact),
445            "Price impact should be approximately 90%. Actual impact: {:.2}%",
446            price_impact * 100.0
447        );
448    }
449
450    #[test]
451    fn test_swap_to_price_below_spot() {
452        // Pool with reserve0=2000000, reserve1=1000000
453        // Current price: reserve1/reserve0 = 1000000/2000000 = 0.5 token_out per token_in
454        let state = UniswapV2State::new(U256::from(2_000_000u32), U256::from(1_000_000u32));
455
456        let token_in = token_0();
457        let token_out = token_1();
458
459        // Target price: 2/5 = 0.4 token_out per token_in (lower than current 0.5)
460        // Selling token_in decreases price from 0.5 down to 0.4
461        let target_price = Price::new(BigUint::from(2u32), BigUint::from(5u32));
462        let params = &QueryPoolSwapParams::new(
463            token_in.clone(),
464            token_out.clone(),
465            SwapConstraint::PoolTargetPrice {
466                target: target_price,
467                tolerance: 0f64,
468                min_amount_in: None,
469                max_amount_in: None,
470            },
471        );
472        let pool_swap = state.query_pool_swap(params).unwrap();
473
474        assert_eq!(
475            *pool_swap.amount_in(),
476            BigUint::from(232711u32),
477            "Should require some input amount"
478        );
479        assert_eq!(*pool_swap.amount_out(), BigUint::from(103947u32));
480
481        // Verify that swapping this amount brings us close to the target price
482        let new_state = pool_swap
483            .new_state()
484            .as_any()
485            .downcast_ref::<UniswapV2State>()
486            .unwrap();
487
488        // The new reserves should reflect the target price
489        // Price = reserve1/reserve0, so if price = 0.4, then reserve0/reserve1 = 2.5
490        let new_reserve_ratio =
491            new_state.reserve0.to::<u128>() as f64 / new_state.reserve1.to::<u128>() as f64;
492        let expected_ratio = 2.5;
493
494        // Allow for some difference due to fees and rounding
495        assert!(
496            (new_reserve_ratio - expected_ratio).abs() < 0.01,
497            "New reserve ratio {new_reserve_ratio} should be close to expected {expected_ratio}"
498        );
499    }
500
501    #[test]
502    fn test_swap_to_price_unreachable() {
503        // Pool with 2:1 ratio
504        let state = UniswapV2State::new(U256::from(2_000_000u32), U256::from(1_000_000u32));
505
506        let token_in = token_0();
507        let token_out = token_1();
508        // Target price is unreachable (should return error)
509        // Current pool price: reserve_out/reserve_in = 1000000/2000000 = 0.5 token_out per
510        // token_in Target: 1/1 = 1.0 token_out per token_in
511        // Selling token_in decreases pool price, so we can't reach 1.0 from 0.5
512        let target_price = Price::new(BigUint::from(1u32), BigUint::from(1u32));
513
514        let result = state.query_pool_swap(&QueryPoolSwapParams::new(
515            token_in,
516            token_out,
517            SwapConstraint::PoolTargetPrice {
518                target: target_price,
519                tolerance: 0f64,
520                min_amount_in: None,
521                max_amount_in: None,
522            },
523        ));
524
525        assert!(result.is_err(), "Should return error when target price is unreachable");
526    }
527
528    #[test]
529    fn test_swap_to_price_at_spot_price() {
530        let state = UniswapV2State::new(U256::from(2_000_000u32), U256::from(1_000_000u32));
531
532        let token_in = token_0();
533        let token_out = token_1();
534
535        // Calculate spot price with fee (token_out/token_in):
536        // Marginal price = (FEE_NUMERATOR * reserve_out) / (FEE_PRECISION * reserve_in)
537        let spot_price_num = U256::from(1_000_000u32) * FEE_NUMERATOR;
538        let spot_price_den = U256::from(2_000_000u32) * FEE_PRECISION;
539
540        let target_price =
541            Price::new(u256_to_biguint(spot_price_num), u256_to_biguint(spot_price_den));
542
543        let pool_swap = state
544            .query_pool_swap(&QueryPoolSwapParams::new(
545                token_in.clone(),
546                token_out.clone(),
547                SwapConstraint::PoolTargetPrice {
548                    target: target_price,
549                    tolerance: 0f64,
550                    min_amount_in: None,
551                    max_amount_in: None,
552                },
553            ))
554            .unwrap();
555
556        // At exact spot price, we should return zero amount
557        assert_eq!(
558            *pool_swap.amount_in(),
559            BigUint::ZERO,
560            "At spot price should require zero input amount"
561        );
562        assert_eq!(
563            *pool_swap.amount_out(),
564            BigUint::ZERO,
565            "At spot price should return zero output amount"
566        );
567    }
568
569    #[test]
570    fn test_swap_to_price_slightly_below_spot() {
571        let state = UniswapV2State::new(U256::from(2_000_000u32), U256::from(1_000_000u32));
572
573        let token_in = token_0();
574        let token_out = token_1();
575
576        // Calculate spot price with fee and subtract small amount to move slightly below
577        // Current spot (token_out/token_in with fees): (FEE_NUMERATOR * reserve_out) /
578        // (FEE_PRECISION * reserve_in) Target: slightly below current spot (multiply numerator by
579        // 99999/100000)
580        let spot_price_num = U256::from(1_000_000u32) * FEE_NUMERATOR * U256::from(99_999u32);
581        let spot_price_den = U256::from(2_000_000u32) * FEE_PRECISION * U256::from(100_000u32);
582
583        let target_price =
584            Price::new(u256_to_biguint(spot_price_num), u256_to_biguint(spot_price_den));
585
586        let pool_swap = state
587            .query_pool_swap(&QueryPoolSwapParams::new(
588                token_in,
589                token_out,
590                SwapConstraint::PoolTargetPrice {
591                    target: target_price,
592                    tolerance: 0f64,
593                    min_amount_in: None,
594                    max_amount_in: None,
595                },
596            ))
597            .unwrap();
598
599        assert!(
600            *pool_swap.amount_in() > BigUint::ZERO,
601            "Should return non-zero amount for target slightly below spot"
602        );
603    }
604
605    #[test]
606    fn test_swap_to_price_large_pool() {
607        // Test with realistic large reserves
608        let state = UniswapV2State::new(
609            U256::from_str("6770398782322527849696614").unwrap(),
610            U256::from_str("5124813135806900540214").unwrap(),
611        );
612
613        let token_in = token_0();
614        let token_out = token_1();
615
616        // Current price (token_out/token_in) = reserve1/reserve0
617        // To target a slightly lower price (move price down 10%), we can use:
618        // target_price = (reserve1 * 9) / (reserve0 * 10)
619        // This avoids floating point precision issues with large numbers
620        let price_numerator = u256_to_biguint(state.reserve1) * BigUint::from(9u32);
621        let price_denominator = u256_to_biguint(state.reserve0) * BigUint::from(10u32);
622
623        let target_price = Price::new(price_numerator, price_denominator);
624
625        let pool_swap = state
626            .query_pool_swap(&QueryPoolSwapParams::new(
627                token_in,
628                token_out,
629                SwapConstraint::PoolTargetPrice {
630                    target: target_price,
631                    tolerance: 0f64,
632                    min_amount_in: None,
633                    max_amount_in: None,
634                },
635            ))
636            .unwrap();
637
638        assert!(pool_swap.amount_in().clone() > BigUint::ZERO, "Should require some input amount");
639        assert!(pool_swap.amount_out().clone() > BigUint::ZERO, "Should get some output");
640    }
641
642    #[test]
643    fn test_swap_to_price_basic() {
644        let state = UniswapV2State::new(U256::from(1_000_000u32), U256::from(2_000_000u32));
645
646        let token_in = token_0();
647        let token_out = token_1();
648
649        let target_price = Price::new(BigUint::from(2u32), BigUint::from(3u32));
650
651        let pool_swap = state
652            .query_pool_swap(&QueryPoolSwapParams::new(
653                token_in,
654                token_out,
655                SwapConstraint::PoolTargetPrice {
656                    target: target_price,
657                    tolerance: 0f64,
658                    min_amount_in: None,
659                    max_amount_in: None,
660                },
661            ))
662            .unwrap();
663        assert!(*pool_swap.amount_in() > BigUint::ZERO, "Amount in should be non-zero");
664        assert!(*pool_swap.amount_out() > BigUint::ZERO, "Amount out should be non-zero");
665    }
666
667    #[test]
668    fn test_swap_to_price_validates_actual_output() {
669        // Test that query_supply validates actual_output >= expected_output
670        let state = UniswapV2State::new(
671            U256::from(1_000_000u128) * U256::from(1_000_000_000_000_000_000u128),
672            U256::from(2_000_000u128) * U256::from(1_000_000_000_000_000_000u128),
673        );
674
675        let token_in = token_0();
676        let token_out = token_1();
677
678        // Current pool price: 2M/1M = 2.0 token_out per token_in
679        // Target: slightly lower (e.g., 1.95:1 = 1_950_000/1_000_000)
680        // This is reachable by selling token_in
681        let target_price = Price::new(BigUint::from(1_950_000u128), BigUint::from(1_000_000u128));
682
683        let pool_swap = state
684            .query_pool_swap(&QueryPoolSwapParams::new(
685                token_in,
686                token_out,
687                SwapConstraint::PoolTargetPrice {
688                    target: target_price,
689                    tolerance: 0f64,
690                    min_amount_in: None,
691                    max_amount_in: None,
692                },
693            ))
694            .unwrap();
695        assert!(
696            *pool_swap.amount_out() > BigUint::ZERO,
697            "Should return amount out for valid price"
698        );
699        assert!(*pool_swap.amount_in() > BigUint::ZERO, "Should return amount in for valid price");
700    }
701
702    #[test]
703    fn test_swap_around_spot_price() {
704        let usdc = Token::new(
705            &Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap(),
706            "USDC",
707            6,
708            0,
709            &[Some(10_000)],
710            Chain::Ethereum,
711            100,
712        );
713        let dai = Token::new(
714            &Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(),
715            "DAI",
716            18,
717            0,
718            &[Some(10_000)],
719            Chain::Ethereum,
720            100,
721        );
722
723        let reserve_0 = U256::from_str("735952457913070155214197").unwrap();
724        let reserve_1 = U256::from_str("735997725943000000000000").unwrap();
725
726        let pool = UniswapV2State::new(reserve_0, reserve_1);
727
728        // Reserves: reserve_0 = DAI, reserve_1 = USDC (DAI address < USDC address)
729        let reserve_usdc = reserve_1;
730        let reserve_dai = reserve_0;
731
732        // Calculate spot price (USDC/DAI with fee)
733        let spot_price_dai_per_usdc_num = reserve_dai
734            .checked_mul(U256::from(1000u32))
735            .unwrap();
736        let spot_price_dai_per_usdc_den = reserve_usdc
737            .checked_mul(U256::from(1003u32))
738            .unwrap();
739
740        // Test 1: Price above reachable limit (more DAI per USDC than pool can provide) -should
741        // return error Multiply by 1001/1000 to go above reachable limit
742        let above_limit_num = spot_price_dai_per_usdc_num
743            .checked_mul(U256::from(1001u32))
744            .unwrap();
745        let above_limit_den = spot_price_dai_per_usdc_den
746            .checked_mul(U256::from(1000u32))
747            .unwrap();
748        let target_price =
749            Price::new(u256_to_biguint(above_limit_num), u256_to_biguint(above_limit_den));
750
751        let swap_above_limit = pool.query_pool_swap(&QueryPoolSwapParams::new(
752            usdc.clone(),
753            dai.clone(),
754            SwapConstraint::PoolTargetPrice {
755                target: target_price,
756                tolerance: 0f64,
757                min_amount_in: None,
758                max_amount_in: None,
759            },
760        ));
761        assert!(swap_above_limit.is_err(), "Should return error for price above reachable limit");
762
763        // Test 2: Price just below reachable limit - should return non-zero
764        // Multiply by 100_000/100_001 to go slightly below (more reachable)
765        let below_limit_num = spot_price_dai_per_usdc_num
766            .checked_mul(U256::from(100_000u32))
767            .unwrap();
768        let below_limit_den = spot_price_dai_per_usdc_den
769            .checked_mul(U256::from(100_001u32))
770            .unwrap();
771        let target_price =
772            Price::new(u256_to_biguint(below_limit_num), u256_to_biguint(below_limit_den));
773
774        let swap_below_limit = pool
775            .query_pool_swap(&QueryPoolSwapParams::new(
776                usdc.clone(),
777                dai.clone(),
778                SwapConstraint::PoolTargetPrice {
779                    target: target_price,
780                    tolerance: 0f64,
781                    min_amount_in: None,
782                    max_amount_in: None,
783                },
784            ))
785            .unwrap();
786
787        assert!(
788            swap_below_limit.amount_out().clone() > BigUint::ZERO,
789            "Should return non-zero for reachable price"
790        );
791
792        // Verify with actual swap
793        let actual_result = pool
794            .get_amount_out(swap_below_limit.amount_in().clone(), &usdc, &dai)
795            .unwrap();
796
797        assert_eq!(
798            biguint_to_u256(&actual_result.amount),
799            U256::from(366839007208379339u128),
800            "Should return non-zero amount"
801        );
802        assert!(
803            actual_result.amount >= swap_below_limit.amount_out().clone(),
804            "Actual swap should give at least predicted amount"
805        );
806    }
807}