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; const 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 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_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(); 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)] #[case(false, 1221.7335469177287)] 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(), };
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 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 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 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 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 let new_state = pool_swap
483 .new_state()
484 .as_any()
485 .downcast_ref::<UniswapV2State>()
486 .unwrap();
487
488 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 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 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 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 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 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 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 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 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 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 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 let reserve_usdc = reserve_1;
730 let reserve_dai = reserve_0;
731
732 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 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 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 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}