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