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