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 PANCAKESWAP_V2_FEE: u32 = 25; const FEE_PRECISION: U256 = U256::from_limbs([10000, 0, 0, 0]);
32const FEE_NUMERATOR: U256 = U256::from_limbs([9975, 0, 0, 0]);
33
34#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
35pub struct PancakeswapV2State {
36 pub reserve0: U256,
37 pub reserve1: U256,
38}
39
40impl PancakeswapV2State {
41 pub fn new(reserve0: U256, reserve1: U256) -> Self {
48 PancakeswapV2State { reserve0, reserve1 }
49 }
50}
51
52#[typetag::serde]
53impl ProtocolSim for PancakeswapV2State {
54 fn fee(&self) -> f64 {
55 cpmm_fee(PANCAKESWAP_V2_FEE)
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, PANCAKESWAP_V2_FEE)
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 "PancakeSwapV2State 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
195 fn token_0() -> Token {
196 Token::new(
197 &Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(),
198 "T0",
199 18,
200 0,
201 &[Some(100_000)],
202 Chain::Ethereum,
203 100,
204 )
205 }
206
207 fn token_1() -> Token {
208 Token::new(
209 &Bytes::from_str("0x0000000000000000000000000000000000000001").unwrap(),
210 "T1",
211 18,
212 0,
213 &[Some(10_000)],
214 Chain::Ethereum,
215 100,
216 )
217 }
218 #[test]
219 fn test_get_amount_out() {
220 let t0 = Token::new(
223 &Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(),
224 "WETH",
225 18,
226 0,
227 &[Some(100_000)],
228 Chain::Ethereum,
229 100,
230 );
231
232 let t1 = Token::new(
233 &Bytes::from_str("0x0000000000000000000000000000000000000001").unwrap(),
234 "USDC",
235 6,
236 0,
237 &[Some(10_000)],
238 Chain::Ethereum,
239 100,
240 );
241 let reserve0 = U256::from_str("114293490733").unwrap();
242 let reserve1 = U256::from_str("69592908201923870949").unwrap();
243 let state = PancakeswapV2State::new(reserve0, reserve1);
244 let amount_in = BigUint::from_str("13088600769481610").unwrap();
245
246 let res = state
247 .get_amount_out(amount_in.clone(), &t1, &t0)
248 .unwrap();
249
250 let exp = BigUint::from_str("21437847").unwrap();
251 assert_eq!(res.amount, exp);
252 let new_state = res
253 .new_state
254 .as_any()
255 .downcast_ref::<PancakeswapV2State>()
256 .unwrap();
257 assert_eq!(new_state.reserve0, U256::from_str("114272052886").unwrap());
258 assert_eq!(new_state.reserve1, U256::from_str("69605996802693352559").unwrap());
259 assert_eq!(state.reserve0, reserve0);
261 assert_eq!(state.reserve1, reserve1);
262 }
263
264 #[test]
265 fn test_get_amount_out_overflow() {
266 let r0 = U256::from_str("33372357002392258830279").unwrap();
267 let r1 = U256::from_str("43356945776493").unwrap();
268 let amount_in = (BigUint::one() << 256) - BigUint::one(); let t0d = 18;
270 let t1d = 16;
271 let t0 = Token::new(
272 &Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(),
273 "T0",
274 t0d,
275 0,
276 &[Some(10_000)],
277 Chain::Ethereum,
278 100,
279 );
280 let t1 = Token::new(
281 &Bytes::from_str("0x0000000000000000000000000000000000000001").unwrap(),
282 "T0",
283 t1d,
284 0,
285 &[Some(10_000)],
286 Chain::Ethereum,
287 100,
288 );
289 let state = PancakeswapV2State::new(r0, r1);
290
291 let res = state.get_amount_out(amount_in, &t0, &t1);
292 assert!(res.is_err());
293 let err = res.err().unwrap();
294 assert!(matches!(err, SimulationError::FatalError(_)));
295 }
296
297 #[rstest]
298 #[case(true, 0.0008230295686841545)] #[case(false, 1221.12114914985)] fn test_spot_price(#[case] zero_to_one: bool, #[case] exp: f64) {
301 let state = PancakeswapV2State::new(
302 U256::from_str("36925554990922").unwrap(),
303 U256::from_str("30314846538607556521556").unwrap(),
304 );
305 let usdc = Token::new(
306 &Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap(),
307 "USDC",
308 6,
309 0,
310 &[Some(10_000)],
311 Chain::Ethereum,
312 100,
313 );
314 let weth = Token::new(
315 &Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(),
316 "WETH",
317 18,
318 0,
319 &[Some(10_000)],
320 Chain::Ethereum,
321 100,
322 );
323
324 let res = if zero_to_one {
325 state.spot_price(&usdc, &weth).unwrap()
326 } else {
327 state.spot_price(&weth, &usdc).unwrap()
328 };
329
330 assert_ulps_eq!(res, exp);
331 }
332
333 #[test]
334 fn test_fee() {
335 let state = PancakeswapV2State::new(
336 U256::from_str("36925554990922").unwrap(),
337 U256::from_str("30314846538607556521556").unwrap(),
338 );
339
340 let res = state.fee();
341
342 assert_ulps_eq!(res, 0.0025); }
344
345 #[test]
346 fn test_delta_transition() {
347 let mut state = PancakeswapV2State::new(
348 U256::from_str("1000").unwrap(),
349 U256::from_str("1000").unwrap(),
350 );
351 let attributes: HashMap<String, Bytes> = vec![
352 ("reserve0".to_string(), Bytes::from(1500_u64.to_be_bytes().to_vec())),
353 ("reserve1".to_string(), Bytes::from(2000_u64.to_be_bytes().to_vec())),
354 ]
355 .into_iter()
356 .collect();
357 let delta = ProtocolStateDelta {
358 component_id: "State1".to_owned(),
359 updated_attributes: attributes,
360 deleted_attributes: HashSet::new(),
361 };
362
363 let res = state.delta_transition(delta, &HashMap::new(), &Balances::default());
364
365 assert!(res.is_ok());
366 assert_eq!(state.reserve0, U256::from_str("1500").unwrap());
367 assert_eq!(state.reserve1, U256::from_str("2000").unwrap());
368 }
369
370 #[test]
371 fn test_delta_transition_missing_attribute() {
372 let mut state = PancakeswapV2State::new(
373 U256::from_str("1000").unwrap(),
374 U256::from_str("1000").unwrap(),
375 );
376 let attributes: HashMap<String, Bytes> =
377 vec![("reserve0".to_string(), Bytes::from(1500_u64.to_be_bytes().to_vec()))]
378 .into_iter()
379 .collect();
380 let delta = ProtocolStateDelta {
381 component_id: "State1".to_owned(),
382 updated_attributes: attributes,
383 deleted_attributes: HashSet::new(),
384 };
385
386 let res = state.delta_transition(delta, &HashMap::new(), &Balances::default());
387
388 assert!(res.is_err());
389 match res {
390 Err(e) => {
391 assert!(matches!(e, TransitionError::MissingAttribute(ref x) if x=="reserve1"))
392 }
393 _ => panic!("Test failed: was expecting an Err value"),
394 };
395 }
396
397 #[test]
398 fn test_get_limits_price_impact() {
399 let state = PancakeswapV2State::new(
400 U256::from_str("1000").unwrap(),
401 U256::from_str("100000").unwrap(),
402 );
403
404 let (amount_in, _) = state
405 .get_limits(
406 Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(),
407 Bytes::from_str("0x0000000000000000000000000000000000000001").unwrap(),
408 )
409 .unwrap();
410
411 let token_0 = token_0();
412 let token_1 = token_1();
413
414 let result = state
415 .get_amount_out(amount_in.clone(), &token_0, &token_1)
416 .unwrap();
417 let new_state = result.new_state;
418
419 let initial_price = state
420 .spot_price(&token_0, &token_1)
421 .unwrap();
422 let new_price = new_state
423 .spot_price(&token_0, &token_1)
424 .unwrap();
425
426 let price_impact = 1.0 - new_price / initial_price;
430 assert!(
431 (0.899..=0.90).contains(&price_impact),
432 "Price impact should be approximately 90%. Actual impact: {:.2}%",
433 price_impact * 100.0
434 );
435 }
436
437 #[test]
438 fn test_swap_to_price_below_spot() {
439 let state = PancakeswapV2State::new(U256::from(2_000_000u32), U256::from(1_000_000u32));
442
443 let token_in = token_0();
444 let token_out = token_1();
445
446 let target_price = Price::new(BigUint::from(2u32), BigUint::from(5u32));
449
450 let params = QueryPoolSwapParams::new(
451 token_in.clone(),
452 token_out.clone(),
453 SwapConstraint::PoolTargetPrice {
454 target: target_price,
455 tolerance: 0f64,
456 min_amount_in: None,
457 max_amount_in: None,
458 },
459 );
460
461 let pool_swap = state.query_pool_swap(¶ms).unwrap();
462
463 assert_eq!(
464 *pool_swap.amount_in(),
465 BigUint::from(233271u32),
466 "Should require some input amount"
467 );
468 assert_eq!(*pool_swap.amount_out(), BigUint::from(104218u32));
469
470 let new_state = pool_swap
472 .new_state()
473 .as_any()
474 .downcast_ref::<PancakeswapV2State>()
475 .unwrap();
476
477 let new_reserve_ratio =
480 new_state.reserve0.to::<u128>() as f64 / new_state.reserve1.to::<u128>() as f64;
481 let expected_ratio = 2.5;
482
483 assert!(
485 (new_reserve_ratio - expected_ratio).abs() < 0.01,
486 "New reserve ratio {new_reserve_ratio} should be close to expected {expected_ratio}"
487 );
488 }
489
490 #[test]
491 fn test_swap_to_price_unreachable() {
492 let state = PancakeswapV2State::new(U256::from(2_000_000u32), U256::from(1_000_000u32));
494
495 let token_in = token_0();
496 let token_out = token_1();
497
498 let target_price = Price::new(BigUint::from(1u32), BigUint::from(1u32));
503
504 let result = state.query_pool_swap(&QueryPoolSwapParams::new(
505 token_in,
506 token_out,
507 SwapConstraint::PoolTargetPrice {
508 target: target_price,
509 tolerance: 0f64,
510 min_amount_in: None,
511 max_amount_in: None,
512 },
513 ));
514
515 assert!(result.is_err(), "Should return error when target price is unreachable");
516 }
517
518 #[test]
519 fn test_swap_to_price_at_spot_price() {
520 let state = PancakeswapV2State::new(U256::from(2_000_000u32), U256::from(1_000_000u32));
521
522 let token_in = token_0();
523 let token_out = token_1();
524
525 let spot_price_num = U256::from(1_000_000u32) * FEE_NUMERATOR;
528 let spot_price_den = U256::from(2_000_000u32) * FEE_PRECISION;
529
530 let target_price =
531 Price::new(u256_to_biguint(spot_price_num), u256_to_biguint(spot_price_den));
532
533 let pool_swap = state
534 .query_pool_swap(&QueryPoolSwapParams::new(
535 token_in,
536 token_out,
537 SwapConstraint::PoolTargetPrice {
538 target: target_price,
539 tolerance: 0f64,
540 min_amount_in: None,
541 max_amount_in: None,
542 },
543 ))
544 .unwrap();
545
546 assert_eq!(
548 *pool_swap.amount_in(),
549 BigUint::ZERO,
550 "At spot price should require zero input amount"
551 );
552 assert_eq!(
553 *pool_swap.amount_out(),
554 BigUint::ZERO,
555 "At spot price should return zero output amount"
556 );
557 }
558
559 #[test]
560 fn test_swap_to_price_slightly_below_spot() {
561 let state = PancakeswapV2State::new(U256::from(2_000_000u32), U256::from(1_000_000u32));
562
563 let token_in = token_0();
564 let token_out = token_1();
565
566 let spot_price_num = U256::from(1_000_000u32) * FEE_NUMERATOR * U256::from(99_999u32);
571 let spot_price_den = U256::from(2_000_000u32) * FEE_PRECISION * U256::from(100_000u32);
572
573 let target_price =
574 Price::new(u256_to_biguint(spot_price_num), u256_to_biguint(spot_price_den));
575
576 let pool_swap = state
577 .query_pool_swap(&QueryPoolSwapParams::new(
578 token_in,
579 token_out,
580 SwapConstraint::PoolTargetPrice {
581 target: target_price,
582 tolerance: 0f64,
583 min_amount_in: None,
584 max_amount_in: None,
585 },
586 ))
587 .unwrap();
588
589 assert!(
590 *pool_swap.amount_in() > BigUint::ZERO,
591 "Should return non-zero amount for target slightly below spot"
592 );
593 }
594
595 #[test]
596 fn test_swap_to_price_large_pool() {
597 let state = PancakeswapV2State::new(
599 U256::from_str("6770398782322527849696614").unwrap(),
600 U256::from_str("5124813135806900540214").unwrap(),
601 );
602
603 let token_in = token_0();
604 let token_out = token_1();
605
606 let price_numerator = u256_to_biguint(state.reserve1) * BigUint::from(9u32);
611 let price_denominator = u256_to_biguint(state.reserve0) * BigUint::from(10u32);
612
613 let target_price = Price::new(price_numerator, price_denominator);
614
615 let pool_swap = state
616 .query_pool_swap(&QueryPoolSwapParams::new(
617 token_in,
618 token_out,
619 SwapConstraint::PoolTargetPrice {
620 target: target_price,
621 tolerance: 0f64,
622 min_amount_in: None,
623 max_amount_in: None,
624 },
625 ))
626 .unwrap();
627
628 assert!(*pool_swap.amount_in() > BigUint::ZERO, "Should require some input amount");
629 assert!(*pool_swap.amount_out() > BigUint::ZERO, "Should get some output");
630 }
631
632 #[test]
633 fn test_swap_to_price_basic() {
634 let state = PancakeswapV2State::new(U256::from(1_000_000u32), U256::from(2_000_000u32));
635
636 let token_in = token_0();
637 let token_out = token_1();
638
639 let target_price = Price::new(BigUint::from(2u32), BigUint::from(3u32));
640
641 let pool_swap = state
642 .query_pool_swap(&QueryPoolSwapParams::new(
643 token_in,
644 token_out,
645 SwapConstraint::PoolTargetPrice {
646 target: target_price,
647 tolerance: 0f64,
648 min_amount_in: None,
649 max_amount_in: None,
650 },
651 ))
652 .unwrap();
653 assert!(*pool_swap.amount_in() > BigUint::ZERO, "Amount in should be non-zero");
654 assert!(*pool_swap.amount_out() > BigUint::ZERO, "Amount out should be non-zero");
655 }
656
657 #[test]
658 fn test_swap_to_price_validates_actual_output() {
659 let state = PancakeswapV2State::new(
661 U256::from(1_000_000u128) * U256::from(1_000_000_000_000_000_000u128),
662 U256::from(2_000_000u128) * U256::from(1_000_000_000_000_000_000u128),
663 );
664
665 let token_in = token_0();
666 let token_out = token_1();
667
668 let target_price = Price::new(BigUint::from(1_950_000u128), BigUint::from(1_000_000u128));
672
673 let pool_swap = state
674 .query_pool_swap(&QueryPoolSwapParams::new(
675 token_in,
676 token_out,
677 SwapConstraint::PoolTargetPrice {
678 target: target_price,
679 tolerance: 0f64,
680 min_amount_in: None,
681 max_amount_in: None,
682 },
683 ))
684 .unwrap();
685 assert!(
686 *pool_swap.amount_out() > BigUint::ZERO,
687 "Should return amount out for valid price"
688 );
689 assert!(*pool_swap.amount_in() > BigUint::ZERO, "Should return amount in for valid price");
690 }
691
692 #[test]
693 fn test_swap_around_spot_price() {
694 let usdc = Token::new(
695 &Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap(),
696 "USDC",
697 6,
698 0,
699 &[Some(10_000)],
700 Chain::Ethereum,
701 100,
702 );
703 let dai = Token::new(
704 &Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(),
705 "DAI",
706 18,
707 0,
708 &[Some(10_000)],
709 Chain::Ethereum,
710 100,
711 );
712
713 let reserve_0 = U256::from_str("735952457913070155214197").unwrap();
714 let reserve_1 = U256::from_str("735997725943000000000000").unwrap();
715
716 let pool = PancakeswapV2State::new(reserve_0, reserve_1);
717
718 let reserve_usdc = reserve_1;
720 let reserve_dai = reserve_0;
721
722 let spot_price_dai_per_usdc_num = reserve_dai
724 .checked_mul(U256::from(10000u32))
725 .unwrap();
726 let spot_price_dai_per_usdc_den = reserve_usdc
727 .checked_mul(U256::from(10025u32))
728 .unwrap();
729
730 let above_limit_num = spot_price_dai_per_usdc_num
733 .checked_mul(U256::from(1001u32))
734 .unwrap();
735 let above_limit_den = spot_price_dai_per_usdc_den
736 .checked_mul(U256::from(1000u32))
737 .unwrap();
738 let target_price =
739 Price::new(u256_to_biguint(above_limit_num), u256_to_biguint(above_limit_den));
740
741 let result_above_limit = pool.query_pool_swap(&QueryPoolSwapParams::new(
742 usdc.clone(),
743 dai.clone(),
744 SwapConstraint::PoolTargetPrice {
745 target: target_price,
746 tolerance: 0f64,
747 min_amount_in: None,
748 max_amount_in: None,
749 },
750 ));
751 assert!(result_above_limit.is_err(), "Should return error for price above reachable limit");
752
753 let below_limit_num = spot_price_dai_per_usdc_num
756 .checked_mul(U256::from(100_000u32))
757 .unwrap();
758 let below_limit_den = spot_price_dai_per_usdc_den
759 .checked_mul(U256::from(100_001u32))
760 .unwrap();
761 let target_price =
762 Price::new(u256_to_biguint(below_limit_num), u256_to_biguint(below_limit_den));
763
764 let swap_below_limit = pool
765 .query_pool_swap(&QueryPoolSwapParams::new(
766 usdc.clone(),
767 dai.clone(),
768 SwapConstraint::PoolTargetPrice {
769 target: target_price,
770 tolerance: 0f64,
771 min_amount_in: None,
772 max_amount_in: None,
773 },
774 ))
775 .unwrap();
776
777 assert!(
778 swap_below_limit.amount_out().clone() > BigUint::ZERO,
779 "Should return non-zero for reachable price"
780 );
781
782 let actual_result = pool
784 .get_amount_out(swap_below_limit.amount_in().clone(), &usdc, &dai)
785 .unwrap();
786
787 assert_eq!(
788 biguint_to_u256(&actual_result.amount),
789 U256::from(1376434275718772760u128),
790 "Should return non-zero amount"
791 );
792 assert!(
793 actual_result.amount >= swap_below_limit.amount_out().clone(),
794 "Actual swap should give at least predicted amount"
795 );
796 }
797}