1use std::{any::Any, collections::HashMap};
2
3use alloy::primitives::{Sign, I256, U256};
4use num_bigint::BigUint;
5use num_traits::Zero;
6use serde::{Deserialize, Serialize};
7use tracing::trace;
8use tycho_common::{
9 dto::ProtocolStateDelta,
10 models::token::Token,
11 simulation::{
12 errors::{SimulationError, TransitionError},
13 protocol_sim::{
14 Balances, GetAmountOutResult, PoolSwap, ProtocolSim, QueryPoolSwapParams,
15 SwapConstraint,
16 },
17 },
18 Bytes,
19};
20
21use super::enums::FeeAmount;
22use crate::evm::protocol::{
23 clmm::clmm_swap_to_price,
24 safe_math::{safe_add_u256, safe_sub_u256},
25 u256_num::u256_to_biguint,
26 utils::{
27 add_fee_markup,
28 uniswap::{
29 i24_be_bytes_to_i32, liquidity_math,
30 sqrt_price_math::{get_amount0_delta, get_amount1_delta, sqrt_price_q96_to_f64},
31 swap_math,
32 tick_list::{TickInfo, TickList, TickListErrorKind},
33 tick_math::{
34 get_sqrt_ratio_at_tick, get_tick_at_sqrt_ratio, MAX_SQRT_RATIO, MAX_TICK,
35 MIN_SQRT_RATIO, MIN_TICK,
36 },
37 StepComputation, SwapResults, SwapState,
38 },
39 },
40};
41
42const SWAP_BASE_GAS: u64 = 130_000;
45const GAS_PER_TICK: u64 = 17_540;
47const MAX_SWAP_GAS: u64 = 16_700_000;
49const MAX_TICKS_CROSSED: u64 = (MAX_SWAP_GAS - SWAP_BASE_GAS) / GAS_PER_TICK;
50
51#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
52pub struct UniswapV3State {
53 liquidity: u128,
54 sqrt_price: U256,
55 fee: FeeAmount,
56 tick: i32,
57 ticks: TickList,
58}
59
60impl UniswapV3State {
61 pub fn new(
70 liquidity: u128,
71 sqrt_price: U256,
72 fee: FeeAmount,
73 tick: i32,
74 ticks: Vec<TickInfo>,
75 ) -> Result<Self, SimulationError> {
76 let spacing = UniswapV3State::get_spacing(fee);
77 let tick_list = TickList::from(spacing, ticks)?;
78 Ok(UniswapV3State { liquidity, sqrt_price, fee, tick, ticks: tick_list })
79 }
80
81 fn get_spacing(fee: FeeAmount) -> u16 {
82 match fee {
83 FeeAmount::Lowest => 1,
84 FeeAmount::Lowest2 => 2,
85 FeeAmount::Lowest3 => 3,
86 FeeAmount::Lowest4 => 4,
87 FeeAmount::Low => 10,
88 FeeAmount::MediumLow => 50,
89 FeeAmount::Medium => 60,
90 FeeAmount::MediumHigh => 100,
91 FeeAmount::High => 200,
92 }
93 }
94
95 fn swap(
96 &self,
97 zero_for_one: bool,
98 amount_specified: I256,
99 sqrt_price_limit: Option<U256>,
100 ) -> Result<SwapResults, SimulationError> {
101 if self.liquidity == 0 {
102 return Err(SimulationError::RecoverableError("No liquidity".to_string()));
103 }
104 let price_limit = if let Some(limit) = sqrt_price_limit {
105 limit
106 } else if zero_for_one {
107 safe_add_u256(MIN_SQRT_RATIO, U256::from(1u64))?
108 } else {
109 safe_sub_u256(MAX_SQRT_RATIO, U256::from(1u64))?
110 };
111
112 let price_limit_valid = if zero_for_one {
113 price_limit > MIN_SQRT_RATIO && price_limit < self.sqrt_price
114 } else {
115 price_limit < MAX_SQRT_RATIO && price_limit > self.sqrt_price
116 };
117 if !price_limit_valid {
118 return Err(SimulationError::InvalidInput("Price limit out of range".into(), None));
119 }
120
121 let exact_input = amount_specified > I256::from_raw(U256::from(0u64));
122
123 let mut state = SwapState {
124 amount_remaining: amount_specified,
125 amount_calculated: I256::from_raw(U256::from(0u64)),
126 sqrt_price: self.sqrt_price,
127 tick: self.tick,
128 liquidity: self.liquidity,
129 };
130 let mut gas_used = U256::from(130_000);
131
132 while state.amount_remaining != I256::from_raw(U256::from(0u64)) &&
133 state.sqrt_price != price_limit
134 {
135 let (mut next_tick, initialized) = match self
136 .ticks
137 .next_initialized_tick_within_one_word(state.tick, zero_for_one)
138 {
139 Ok((tick, init)) => (tick, init),
140 Err(tick_err) => match tick_err.kind {
141 TickListErrorKind::TicksExeeded => {
142 let mut new_state = self.clone();
143 new_state.liquidity = state.liquidity;
144 new_state.tick = state.tick;
145 new_state.sqrt_price = state.sqrt_price;
146 return Err(SimulationError::InvalidInput(
147 "Ticks exceeded".into(),
148 Some(GetAmountOutResult::new(
149 u256_to_biguint(state.amount_calculated.abs().into_raw()),
150 u256_to_biguint(gas_used),
151 Box::new(new_state),
152 )),
153 ));
154 }
155 _ => return Err(SimulationError::FatalError("Unknown error".to_string())),
156 },
157 };
158
159 next_tick = next_tick.clamp(MIN_TICK, MAX_TICK);
160
161 let sqrt_price_next = get_sqrt_ratio_at_tick(next_tick)?;
162 let (sqrt_price, amount_in, amount_out, fee_amount) = swap_math::compute_swap_step(
163 state.sqrt_price,
164 UniswapV3State::get_sqrt_ratio_target(sqrt_price_next, price_limit, zero_for_one),
165 state.liquidity,
166 state.amount_remaining,
167 self.fee as u32,
168 )?;
169 state.sqrt_price = sqrt_price;
170
171 let step = StepComputation {
172 sqrt_price_start: state.sqrt_price,
173 tick_next: next_tick,
174 initialized,
175 sqrt_price_next,
176 amount_in,
177 amount_out,
178 fee_amount,
179 };
180 if exact_input {
181 state.amount_remaining -= I256::checked_from_sign_and_abs(
182 Sign::Positive,
183 safe_add_u256(step.amount_in, step.fee_amount)?,
184 )
185 .unwrap();
186 state.amount_calculated -=
187 I256::checked_from_sign_and_abs(Sign::Positive, step.amount_out).unwrap();
188 } else {
189 state.amount_remaining +=
190 I256::checked_from_sign_and_abs(Sign::Positive, step.amount_out).unwrap();
191 state.amount_calculated += I256::checked_from_sign_and_abs(
192 Sign::Positive,
193 safe_add_u256(step.amount_in, step.fee_amount)?,
194 )
195 .unwrap();
196 }
197 if state.sqrt_price == step.sqrt_price_next {
198 if step.initialized {
199 let liquidity_raw = self
200 .ticks
201 .get_tick(step.tick_next)
202 .unwrap()
203 .net_liquidity;
204 let liquidity_net = if zero_for_one { -liquidity_raw } else { liquidity_raw };
205 state.liquidity =
206 liquidity_math::add_liquidity_delta(state.liquidity, liquidity_net)?;
207 }
208 state.tick = if zero_for_one { step.tick_next - 1 } else { step.tick_next };
209 } else if state.sqrt_price != step.sqrt_price_start {
210 state.tick = get_tick_at_sqrt_ratio(state.sqrt_price)?;
211 }
212 gas_used = safe_add_u256(gas_used, U256::from(2000))?;
213 }
214 Ok(SwapResults {
215 amount_calculated: state.amount_calculated,
216 amount_specified,
217 amount_remaining: state.amount_remaining,
218 sqrt_price: state.sqrt_price,
219 liquidity: state.liquidity,
220 tick: state.tick,
221 gas_used,
222 })
223 }
224
225 fn get_sqrt_ratio_target(
226 sqrt_price_next: U256,
227 sqrt_price_limit: U256,
228 zero_for_one: bool,
229 ) -> U256 {
230 let cond1 = if zero_for_one {
231 sqrt_price_next < sqrt_price_limit
232 } else {
233 sqrt_price_next > sqrt_price_limit
234 };
235
236 if cond1 {
237 sqrt_price_limit
238 } else {
239 sqrt_price_next
240 }
241 }
242}
243
244#[typetag::serde]
245impl ProtocolSim for UniswapV3State {
246 fn fee(&self) -> f64 {
247 (self.fee as u32) as f64 / 1_000_000.0
248 }
249
250 fn spot_price(&self, a: &Token, b: &Token) -> Result<f64, SimulationError> {
251 let price = if a < b {
252 sqrt_price_q96_to_f64(self.sqrt_price, a.decimals, b.decimals)?
253 } else {
254 1.0f64 / sqrt_price_q96_to_f64(self.sqrt_price, b.decimals, a.decimals)?
255 };
256 Ok(add_fee_markup(price, self.fee()))
257 }
258
259 fn get_amount_out(
260 &self,
261 amount_in: BigUint,
262 token_a: &Token,
263 token_b: &Token,
264 ) -> Result<GetAmountOutResult, SimulationError> {
265 let zero_for_one = token_a < token_b;
266 let amount_specified = I256::checked_from_sign_and_abs(
267 Sign::Positive,
268 U256::from_be_slice(&amount_in.to_bytes_be()),
269 )
270 .ok_or_else(|| {
271 SimulationError::InvalidInput("I256 overflow: amount_in".to_string(), None)
272 })?;
273
274 let result = self.swap(zero_for_one, amount_specified, None)?;
275
276 trace!(?amount_in, ?token_a, ?token_b, ?zero_for_one, ?result, "V3 SWAP");
277 let mut new_state = self.clone();
278 new_state.liquidity = result.liquidity;
279 new_state.tick = result.tick;
280 new_state.sqrt_price = result.sqrt_price;
281
282 Ok(GetAmountOutResult::new(
283 u256_to_biguint(
284 result
285 .amount_calculated
286 .abs()
287 .into_raw(),
288 ),
289 u256_to_biguint(result.gas_used),
290 Box::new(new_state),
291 ))
292 }
293
294 fn get_limits(
295 &self,
296 token_in: Bytes,
297 token_out: Bytes,
298 ) -> Result<(BigUint, BigUint), SimulationError> {
299 if self.liquidity == 0 {
301 return Ok((BigUint::zero(), BigUint::zero()));
302 }
303
304 let zero_for_one = token_in < token_out;
305 let mut current_tick = self.tick;
306 let mut current_sqrt_price = self.sqrt_price;
307 let mut current_liquidity = self.liquidity;
308 let mut total_amount_in = U256::from(0u64);
309 let mut total_amount_out = U256::from(0u64);
310 let mut ticks_crossed: u64 = 0;
311
312 while let Ok((tick, initialized)) = self
315 .ticks
316 .next_initialized_tick_within_one_word(current_tick, zero_for_one)
317 {
318 if ticks_crossed >= MAX_TICKS_CROSSED {
320 break;
321 }
322 ticks_crossed += 1;
323
324 let next_tick = tick.clamp(MIN_TICK, MAX_TICK);
326
327 let sqrt_price_next = get_sqrt_ratio_at_tick(next_tick)?;
329
330 let (amount_in, amount_out) = if zero_for_one {
333 let amount0 = get_amount0_delta(
334 sqrt_price_next,
335 current_sqrt_price,
336 current_liquidity,
337 true,
338 )?;
339 let amount1 = get_amount1_delta(
340 sqrt_price_next,
341 current_sqrt_price,
342 current_liquidity,
343 false,
344 )?;
345 (amount0, amount1)
346 } else {
347 let amount0 = get_amount0_delta(
348 sqrt_price_next,
349 current_sqrt_price,
350 current_liquidity,
351 false,
352 )?;
353 let amount1 = get_amount1_delta(
354 sqrt_price_next,
355 current_sqrt_price,
356 current_liquidity,
357 true,
358 )?;
359 (amount1, amount0)
360 };
361
362 total_amount_in = safe_add_u256(total_amount_in, amount_in)?;
364 total_amount_out = safe_add_u256(total_amount_out, amount_out)?;
365
366 if initialized {
371 let liquidity_raw = self
372 .ticks
373 .get_tick(next_tick)
374 .unwrap()
375 .net_liquidity;
376 let liquidity_delta = if zero_for_one { -liquidity_raw } else { liquidity_raw };
377
378 match liquidity_math::add_liquidity_delta(current_liquidity, liquidity_delta) {
381 Ok(new_liquidity) => {
382 current_liquidity = new_liquidity;
383 }
384 Err(_) => {
385 break;
388 }
389 }
390 }
391
392 current_tick = if zero_for_one { next_tick - 1 } else { next_tick };
394 current_sqrt_price = sqrt_price_next;
395 }
396
397 Ok((u256_to_biguint(total_amount_in), u256_to_biguint(total_amount_out)))
398 }
399
400 fn delta_transition(
401 &mut self,
402 delta: ProtocolStateDelta,
403 _tokens: &HashMap<Bytes, Token>,
404 _balances: &Balances,
405 ) -> Result<(), TransitionError> {
406 if let Some(liquidity) = delta
408 .updated_attributes
409 .get("liquidity")
410 {
411 let liq_16_bytes = if liquidity.len() == 32 {
415 if liquidity == &Bytes::zero(32) {
417 Bytes::from([0; 16])
418 } else {
419 return Err(TransitionError::DecodeError(format!(
420 "Liquidity bytes too long for {liquidity}, expected 16",
421 )));
422 }
423 } else {
424 liquidity.clone()
425 };
426
427 self.liquidity = u128::from(liq_16_bytes);
428 }
429 if let Some(sqrt_price) = delta
430 .updated_attributes
431 .get("sqrt_price_x96")
432 {
433 self.sqrt_price = U256::from_be_slice(sqrt_price);
434 }
435 if let Some(tick) = delta.updated_attributes.get("tick") {
436 let ticks_4_bytes = if tick.len() == 32 {
440 if tick == &Bytes::zero(32) {
442 Bytes::from([0; 4])
443 } else {
444 return Err(TransitionError::DecodeError(format!(
445 "Tick bytes too long for {tick}, expected 4"
446 )));
447 }
448 } else {
449 tick.clone()
450 };
451 self.tick = i24_be_bytes_to_i32(&ticks_4_bytes);
452 }
453
454 for (key, value) in delta.updated_attributes.iter() {
456 if key.starts_with("ticks/") {
458 let parts: Vec<&str> = key.split('/').collect();
459 self.ticks
460 .set_tick_liquidity(
461 parts[1]
462 .parse::<i32>()
463 .map_err(|err| TransitionError::DecodeError(err.to_string()))?,
464 i128::from(value.clone()),
465 )
466 .map_err(|err| TransitionError::DecodeError(err.to_string()))?;
467 }
468 }
469 for key in delta.deleted_attributes.iter() {
471 if key.starts_with("ticks/") {
473 let parts: Vec<&str> = key.split('/').collect();
474 self.ticks
475 .set_tick_liquidity(
476 parts[1]
477 .parse::<i32>()
478 .map_err(|err| TransitionError::DecodeError(err.to_string()))?,
479 0,
480 )
481 .map_err(|err| TransitionError::DecodeError(err.to_string()))?;
482 }
483 }
484 Ok(())
485 }
486
487 fn query_pool_swap(&self, params: &QueryPoolSwapParams) -> Result<PoolSwap, SimulationError> {
492 if self.liquidity == 0 {
493 return Err(SimulationError::FatalError("No liquidity".to_string()));
494 }
495
496 match params.swap_constraint() {
497 SwapConstraint::TradeLimitPrice { .. } => Err(SimulationError::InvalidInput(
498 "Uniswap V3 does not support TradeLimitPrice constraint in query_pool_swap"
499 .to_string(),
500 None,
501 )),
502 SwapConstraint::PoolTargetPrice {
503 target,
504 tolerance: _,
505 min_amount_in: _,
506 max_amount_in: _,
507 } => {
508 let (amount_in, amount_out, swap_result) = clmm_swap_to_price(
509 self.sqrt_price,
510 ¶ms.token_in().address,
511 ¶ms.token_out().address,
512 target,
513 self.fee as u32,
514 Sign::Positive,
515 |zero_for_one, amount_specified, sqrt_price_limit| {
516 self.swap(zero_for_one, amount_specified, Some(sqrt_price_limit))
517 },
518 )?;
519
520 let mut new_state = self.clone();
521 new_state.liquidity = swap_result.liquidity;
522 new_state.tick = swap_result.tick;
523 new_state.sqrt_price = swap_result.sqrt_price;
524
525 Ok(PoolSwap::new(amount_in, amount_out, Box::new(new_state), None))
526 }
527 }
528 }
529
530 fn clone_box(&self) -> Box<dyn ProtocolSim> {
531 Box::new(self.clone())
532 }
533
534 fn as_any(&self) -> &dyn Any {
535 self
536 }
537
538 fn as_any_mut(&mut self) -> &mut dyn Any {
539 self
540 }
541
542 fn eq(&self, other: &dyn ProtocolSim) -> bool {
543 if let Some(other_state) = other
544 .as_any()
545 .downcast_ref::<UniswapV3State>()
546 {
547 self.liquidity == other_state.liquidity &&
548 self.sqrt_price == other_state.sqrt_price &&
549 self.fee == other_state.fee &&
550 self.tick == other_state.tick &&
551 self.ticks == other_state.ticks
552 } else {
553 false
554 }
555 }
556}
557
558#[cfg(test)]
559mod tests {
560 use std::{
561 collections::{HashMap, HashSet},
562 fs,
563 path::Path,
564 str::FromStr,
565 };
566
567 use num_bigint::ToBigUint;
568 use num_traits::FromPrimitive;
569 use serde_json::Value;
570 use tycho_client::feed::synchronizer::ComponentWithState;
571 use tycho_common::{hex_bytes::Bytes, models::Chain, simulation::protocol_sim::Price};
572
573 use super::*;
574 use crate::{
575 evm::protocol::utils::uniswap::sqrt_price_math::get_sqrt_price_q96,
576 protocol::models::{DecoderContext, TryFromWithBlock},
577 };
578
579 #[test]
580 fn test_get_amount_out_full_range_liquidity() {
581 let token_x = Token::new(
582 &Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(),
583 "X",
584 18,
585 0,
586 &[Some(10_000)],
587 Chain::Ethereum,
588 100,
589 );
590 let token_y = Token::new(
591 &Bytes::from_str("0xf1ca9cb74685755965c7458528a36934df52a3ef").unwrap(),
592 "Y",
593 18,
594 0,
595 &[Some(10_000)],
596 Chain::Ethereum,
597 100,
598 );
599
600 let pool = UniswapV3State::new(
601 8330443394424070888454257,
602 U256::from_str("188562464004052255423565206602").unwrap(),
603 FeeAmount::Medium,
604 17342,
605 vec![TickInfo::new(0, 0).unwrap(), TickInfo::new(46080, 0).unwrap()],
606 )
607 .unwrap();
608 let sell_amount = BigUint::from_str("11_000_000000000000000000").unwrap();
609 let expected = BigUint::from_str("61927070842678722935941").unwrap();
610
611 let res = pool
612 .get_amount_out(sell_amount, &token_x, &token_y)
613 .unwrap();
614
615 assert_eq!(res.amount, expected);
616 }
617
618 struct SwapTestCase {
619 symbol: &'static str,
620 sell: BigUint,
621 exp: BigUint,
622 }
623
624 #[test]
625 fn test_get_amount_out() {
626 let wbtc = Token::new(
627 &Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap(),
628 "WBTC",
629 8,
630 0,
631 &[Some(10_000)],
632 Chain::Ethereum,
633 100,
634 );
635 let weth = Token::new(
636 &Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(),
637 "WETH",
638 18,
639 0,
640 &[Some(10_000)],
641 Chain::Ethereum,
642 100,
643 );
644 let pool = UniswapV3State::new(
645 377952820878029838,
646 U256::from_str("28437325270877025820973479874632004").unwrap(),
647 FeeAmount::Low,
648 255830,
649 vec![
650 TickInfo::new(255760, 1759015528199933i128).unwrap(),
651 TickInfo::new(255770, 6393138051835308i128).unwrap(),
652 TickInfo::new(255780, 228206673808681i128).unwrap(),
653 TickInfo::new(255820, 1319490609195820i128).unwrap(),
654 TickInfo::new(255830, 678916926147901i128).unwrap(),
655 TickInfo::new(255840, 12208947683433103i128).unwrap(),
656 TickInfo::new(255850, 1177970713095301i128).unwrap(),
657 TickInfo::new(255860, 8752304680520407i128).unwrap(),
658 TickInfo::new(255880, 1486478248067104i128).unwrap(),
659 TickInfo::new(255890, 1878744276123248i128).unwrap(),
660 TickInfo::new(255900, 77340284046725227i128).unwrap(),
661 ],
662 )
663 .unwrap();
664 let cases = vec![
665 SwapTestCase {
666 symbol: "WBTC",
667 sell: 500000000.to_biguint().unwrap(),
668 exp: BigUint::from_str("64352395915550406461").unwrap(),
669 },
670 SwapTestCase {
671 symbol: "WBTC",
672 sell: 550000000.to_biguint().unwrap(),
673 exp: BigUint::from_str("70784271504035662865").unwrap(),
674 },
675 SwapTestCase {
676 symbol: "WBTC",
677 sell: 600000000.to_biguint().unwrap(),
678 exp: BigUint::from_str("77215534856185613494").unwrap(),
679 },
680 SwapTestCase {
681 symbol: "WBTC",
682 sell: BigUint::from_str("1000000000").unwrap(),
683 exp: BigUint::from_str("128643569649663616249").unwrap(),
684 },
685 SwapTestCase {
686 symbol: "WBTC",
687 sell: BigUint::from_str("3000000000").unwrap(),
688 exp: BigUint::from_str("385196519076234662939").unwrap(),
689 },
690 SwapTestCase {
691 symbol: "WETH",
692 sell: BigUint::from_str("64000000000000000000").unwrap(),
693 exp: BigUint::from_str("496294784").unwrap(),
694 },
695 SwapTestCase {
696 symbol: "WETH",
697 sell: BigUint::from_str("70000000000000000000").unwrap(),
698 exp: BigUint::from_str("542798479").unwrap(),
699 },
700 SwapTestCase {
701 symbol: "WETH",
702 sell: BigUint::from_str("77000000000000000000").unwrap(),
703 exp: BigUint::from_str("597047757").unwrap(),
704 },
705 SwapTestCase {
706 symbol: "WETH",
707 sell: BigUint::from_str("128000000000000000000").unwrap(),
708 exp: BigUint::from_str("992129037").unwrap(),
709 },
710 SwapTestCase {
711 symbol: "WETH",
712 sell: BigUint::from_str("385000000000000000000").unwrap(),
713 exp: BigUint::from_str("2978713582").unwrap(),
714 },
715 ];
716
717 for case in cases {
718 let (token_a, token_b) =
719 if case.symbol == "WBTC" { (&wbtc, &weth) } else { (&weth, &wbtc) };
720 let res = pool
721 .get_amount_out(case.sell, token_a, token_b)
722 .unwrap();
723
724 assert_eq!(res.amount, case.exp);
725 }
726 }
727
728 #[test]
729 fn test_err_with_partial_trade() {
730 let dai = Token::new(
731 &Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(),
732 "DAI",
733 18,
734 0,
735 &[Some(10_000)],
736 Chain::Ethereum,
737 100,
738 );
739 let usdc = Token::new(
740 &Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
741 "USDC",
742 6,
743 0,
744 &[Some(10_000)],
745 Chain::Ethereum,
746 100,
747 );
748 let pool = UniswapV3State::new(
749 73015811375239994,
750 U256::from_str("148273042406850898575413").unwrap(),
751 FeeAmount::High,
752 -263789,
753 vec![
754 TickInfo::new(-269600, 3612326326695492i128).unwrap(),
755 TickInfo::new(-268800, 1487613939516867i128).unwrap(),
756 TickInfo::new(-267800, 1557587121322546i128).unwrap(),
757 TickInfo::new(-267400, 424592076717375i128).unwrap(),
758 TickInfo::new(-267200, 11691597431643916i128).unwrap(),
759 TickInfo::new(-266800, -218742815100986i128).unwrap(),
760 TickInfo::new(-266600, 1118947532495477i128).unwrap(),
761 TickInfo::new(-266200, 1233064286622365i128).unwrap(),
762 TickInfo::new(-265000, 4252603063356107i128).unwrap(),
763 TickInfo::new(-263200, -351282010325232i128).unwrap(),
764 TickInfo::new(-262800, -2352011819117842i128).unwrap(),
765 TickInfo::new(-262600, -424592076717375i128).unwrap(),
766 TickInfo::new(-262200, -11923662433672566i128).unwrap(),
767 TickInfo::new(-261600, -2432911749667741i128).unwrap(),
768 TickInfo::new(-260200, -4032727022572273i128).unwrap(),
769 TickInfo::new(-260000, -22889492064625028i128).unwrap(),
770 TickInfo::new(-259400, -1557587121322546i128).unwrap(),
771 TickInfo::new(-259200, -1487613939516867i128).unwrap(),
772 TickInfo::new(-258400, -400137022888262i128).unwrap(),
773 ],
774 )
775 .unwrap();
776 let amount_in = BigUint::from_str("50000000000").unwrap();
777 let exp = BigUint::from_str("6820591625999718100883").unwrap();
778
779 let err = pool
780 .get_amount_out(amount_in, &usdc, &dai)
781 .unwrap_err();
782
783 match err {
784 SimulationError::InvalidInput(ref _err, ref amount_out_result) => {
785 match amount_out_result {
786 Some(amount_out_result) => {
787 assert_eq!(amount_out_result.amount, exp);
788 let new_state = amount_out_result
789 .new_state
790 .as_any()
791 .downcast_ref::<UniswapV3State>()
792 .unwrap();
793 assert_ne!(new_state.tick, pool.tick);
794 assert_ne!(new_state.liquidity, pool.liquidity);
795 }
796 _ => panic!("Partial amount out result is None. Expected partial result."),
797 }
798 }
799 _ => panic!("Test failed: was expecting a SimulationError::InsufficientData"),
800 }
801 }
802
803 #[test]
804 fn test_delta_transition() {
805 let mut pool = UniswapV3State::new(
806 1000,
807 U256::from_str("1000").unwrap(),
808 FeeAmount::Low,
809 100,
810 vec![TickInfo::new(255760, 10000).unwrap(), TickInfo::new(255900, -10000).unwrap()],
811 )
812 .unwrap();
813 let attributes: HashMap<String, Bytes> = [
814 ("liquidity".to_string(), Bytes::from(2000_u64.to_be_bytes().to_vec())),
815 ("sqrt_price_x96".to_string(), Bytes::from(1001_u64.to_be_bytes().to_vec())),
816 ("tick".to_string(), Bytes::from(120_i32.to_be_bytes().to_vec())),
817 (
818 "ticks/-255760/net_liquidity".to_string(),
819 Bytes::from(10200_u64.to_be_bytes().to_vec()),
820 ),
821 (
822 "ticks/255900/net_liquidity".to_string(),
823 Bytes::from(9800_u64.to_be_bytes().to_vec()),
824 ),
825 ]
826 .into_iter()
827 .collect();
828 let delta = ProtocolStateDelta {
829 component_id: "State1".to_owned(),
830 updated_attributes: attributes,
831 deleted_attributes: HashSet::new(),
832 };
833
834 pool.delta_transition(delta, &HashMap::new(), &Balances::default())
835 .unwrap();
836
837 assert_eq!(pool.liquidity, 2000);
838 assert_eq!(pool.sqrt_price, U256::from(1001));
839 assert_eq!(pool.tick, 120);
840 assert_eq!(
841 pool.ticks
842 .get_tick(-255760)
843 .unwrap()
844 .net_liquidity,
845 10200
846 );
847 assert_eq!(
848 pool.ticks
849 .get_tick(255900)
850 .unwrap()
851 .net_liquidity,
852 9800
853 );
854 }
855
856 #[tokio::test]
857 async fn test_get_limits() {
858 let project_root = env!("CARGO_MANIFEST_DIR");
859 let asset_path =
860 Path::new(project_root).join("tests/assets/decoder/uniswap_v3_snapshot.json");
861 let json_data = fs::read_to_string(asset_path).expect("Failed to read test asset");
862 let data: Value = serde_json::from_str(&json_data).expect("Failed to parse JSON");
863
864 let state: ComponentWithState = serde_json::from_value(data)
865 .expect("Expected json to match ComponentWithState structure");
866
867 let usv3_state = UniswapV3State::try_from_with_header(
868 state,
869 Default::default(),
870 &Default::default(),
871 &Default::default(),
872 &DecoderContext::new(),
873 )
874 .await
875 .unwrap();
876
877 let t0 = Token::new(
878 &Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap(),
879 "WBTC",
880 8,
881 0,
882 &[Some(10_000)],
883 Chain::Ethereum,
884 100,
885 );
886 let t1 = Token::new(
887 &Bytes::from_str("0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf").unwrap(),
888 "cbBTC",
889 8,
890 0,
891 &[Some(10_000)],
892 Chain::Ethereum,
893 100,
894 );
895
896 let res = usv3_state
897 .get_limits(t0.address.clone(), t1.address.clone())
898 .unwrap();
899
900 assert_eq!(&res.0, &BigUint::from_u128(155144999154).unwrap()); let out = usv3_state
903 .get_amount_out(res.0, &t0, &t1)
904 .expect("swap for limit in didn't work");
905
906 assert_eq!(&res.1, &out.amount);
907 }
908
909 fn create_basic_test_pool() -> UniswapV3State {
911 let liquidity = 100_000_000_000_000_000_000u128; let sqrt_price = get_sqrt_price_q96(U256::from(20_000_000u64), U256::from(10_000_000u64))
913 .expect("Failed to calculate sqrt price");
914 let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
915
916 let ticks = vec![TickInfo::new(0, 0).unwrap(), TickInfo::new(46080, 0).unwrap()];
917
918 UniswapV3State::new(liquidity, sqrt_price, FeeAmount::Low, tick, ticks)
919 .expect("Failed to create pool")
920 }
921
922 #[test]
923 fn test_swap_basic() {
924 let pool = create_basic_test_pool();
925
926 let amount_in =
928 I256::checked_from_sign_and_abs(Sign::Positive, U256::from(1000000u64)).unwrap();
929 let result = pool
930 .swap(true, amount_in, None)
931 .unwrap();
932
933 let expected_amount = U256::from(2000000u64);
935 let actual_amount = result
936 .amount_calculated
937 .abs()
938 .into_raw();
939 assert_eq!(expected_amount - actual_amount, U256::from(1001u64));
940 println!("Swap X->Y: amount_in={}, amount_out={}", amount_in, actual_amount);
941 }
942
943 #[test]
944 fn test_swap_to_price_basic() {
945 let liquidity = 100_000_000_000_000_000_000u128;
947 let sqrt_price = get_sqrt_price_q96(U256::from(20_000_000u64), U256::from(10_000_000u64))
948 .expect("Failed to calculate sqrt price");
949 let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
950 let ticks = vec![TickInfo::new(0, 0).unwrap(), TickInfo::new(46080, 0).unwrap()];
951
952 let pool = UniswapV3State::new(liquidity, sqrt_price, FeeAmount::Medium, tick, ticks)
953 .expect("Failed to create pool");
954
955 let token_x = Token::new(
957 &Bytes::from_str("0x0000000000000000000000000000000000000001").unwrap(),
958 "X",
959 18,
960 0,
961 &[Some(10_000)],
962 Chain::Ethereum,
963 100,
964 );
965 let token_y = Token::new(
966 &Bytes::from_str("0x0000000000000000000000000000000000000002").unwrap(),
967 "Y",
968 18,
969 0,
970 &[Some(10_000)],
971 Chain::Ethereum,
972 100,
973 );
974
975 let target_price =
977 Price::new(2_000_000u64.to_biguint().unwrap(), 1_010_000u64.to_biguint().unwrap());
978
979 let trade = pool
981 .query_pool_swap(&QueryPoolSwapParams::new(
982 token_x,
983 token_y,
984 SwapConstraint::PoolTargetPrice {
985 target: target_price,
986 tolerance: 0f64,
987 min_amount_in: None,
988 max_amount_in: None,
989 },
990 ))
991 .expect("swap_to_price failed");
992
993 let expected_amount_in =
995 BigUint::from_str("246739021727519745").expect("Failed to parse expected amount_in");
996 let expected_amount_out =
997 BigUint::from_str("490291909043340795").expect("Failed to parse expected amount_out");
998
999 assert_eq!(
1000 trade.amount_in().clone(),
1001 expected_amount_in,
1002 "amount_in should match expected value"
1003 );
1004 assert_eq!(
1005 trade.amount_out().clone(),
1006 expected_amount_out,
1007 "amount_out should match expected value"
1008 );
1009 }
1010
1011 #[test]
1012 fn test_swap_to_price_price_too_high() {
1013 let pool = create_basic_test_pool();
1014
1015 let token_x = Token::new(
1016 &Bytes::from_str("0x0000000000000000000000000000000000000001").unwrap(),
1017 "X",
1018 18,
1019 0,
1020 &[Some(10_000)],
1021 Chain::Ethereum,
1022 100,
1023 );
1024 let token_y = Token::new(
1025 &Bytes::from_str("0x0000000000000000000000000000000000000002").unwrap(),
1026 "Y",
1027 18,
1028 0,
1029 &[Some(10_000)],
1030 Chain::Ethereum,
1031 100,
1032 );
1033
1034 let target_price =
1036 Price::new(10_000_000u64.to_biguint().unwrap(), 1_000_000u64.to_biguint().unwrap());
1037
1038 let result = pool.query_pool_swap(&QueryPoolSwapParams::new(
1039 token_x,
1040 token_y,
1041 SwapConstraint::PoolTargetPrice {
1042 target: target_price,
1043 tolerance: 0f64,
1044 min_amount_in: None,
1045 max_amount_in: None,
1046 },
1047 ));
1048 assert!(result.is_err(), "Should return error when target price is unreachable");
1049 }
1050
1051 #[test]
1052 fn test_swap_parameterized() {
1053 let liquidity = 377_952_820_878_029_838u128;
1055 let sqrt_price = U256::from_str("28437325270877025820973479874632004")
1056 .expect("Failed to parse sqrt_price");
1057 let tick = 255830;
1058
1059 let ticks = vec![
1060 TickInfo::new(255760, 1_759_015_528_199_933).unwrap(),
1061 TickInfo::new(255770, 6_393_138_051_835_308).unwrap(),
1062 TickInfo::new(255780, 228_206_673_808_681).unwrap(),
1063 TickInfo::new(255820, 1_319_490_609_195_820).unwrap(),
1064 TickInfo::new(255830, 678_916_926_147_901).unwrap(),
1065 TickInfo::new(255840, 12_208_947_683_433_103).unwrap(),
1066 TickInfo::new(255850, 1_177_970_713_095_301).unwrap(),
1067 TickInfo::new(255860, 8_752_304_680_520_407).unwrap(),
1068 TickInfo::new(255880, 1_486_478_248_067_104).unwrap(),
1069 TickInfo::new(255890, 1_878_744_276_123_248).unwrap(),
1070 TickInfo::new(255900, 77_340_284_046_725_227).unwrap(),
1071 ];
1072
1073 let pool = UniswapV3State::new(liquidity, sqrt_price, FeeAmount::Low, tick, ticks)
1074 .expect("Failed to create pool");
1075
1076 let test_cases = vec![
1079 (true, "500000000", "64352395915550406461", "WBTC->WETH 500000000"),
1081 (true, "550000000", "70784271504035662865", "WBTC->WETH 550000000"),
1082 (true, "600000000", "77215534856185613494", "WBTC->WETH 600000000"),
1083 (true, "1000000000", "128643569649663616249", "WBTC->WETH 1000000000"),
1084 (true, "3000000000", "385196519076234662939", "WBTC->WETH 3000000000"),
1085 (false, "64000000000000000000", "496294784", "WETH->WBTC 64 ETH"),
1087 (false, "70000000000000000000", "542798479", "WETH->WBTC 70 ETH"),
1088 (false, "77000000000000000000", "597047757", "WETH->WBTC 77 ETH"),
1089 (false, "128000000000000000000", "992129037", "WETH->WBTC 128 ETH"),
1090 (false, "385000000000000000000", "2978713582", "WETH->WBTC 385 ETH"),
1091 ];
1092
1093 for (zero_for_one, amount_in_str, expected_amount_out_str, test_id) in test_cases {
1094 let amount_in = U256::from_str(amount_in_str).expect("Failed to parse amount_in");
1095 let amount_specified = I256::checked_from_sign_and_abs(Sign::Positive, amount_in)
1096 .unwrap_or_else(|| panic!("{} - Failed to convert amount to I256", test_id));
1097
1098 let result = pool
1099 .swap(zero_for_one, amount_specified, None)
1100 .unwrap_or_else(|e| panic!("{} - swap failed: {:?}", test_id, e));
1101
1102 let amount_out = result
1103 .amount_calculated
1104 .abs()
1105 .into_raw();
1106 let expected = U256::from_str(expected_amount_out_str)
1107 .expect("Failed to parse expected_amount_out");
1108
1109 assert_eq!(amount_out, expected, "{}", test_id);
1110 }
1111 }
1112
1113 #[test]
1114 fn test_swap_to_price_parameterized() {
1115 let wbtc = Token::new(
1117 &Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap(),
1118 "WBTC",
1119 8,
1120 0,
1121 &[Some(10_000)],
1122 Chain::Ethereum,
1123 100,
1124 );
1125 let weth = Token::new(
1126 &Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(),
1127 "WETH",
1128 18,
1129 0,
1130 &[Some(10_000)],
1131 Chain::Ethereum,
1132 100,
1133 );
1134
1135 let liquidity = 377_952_820_878_029_838u128;
1136 let sqrt_price = get_sqrt_price_q96(U256::from(130_000_000u64), U256::from(10_000_000u64))
1137 .expect("Failed to calculate sqrt price");
1138 let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
1139
1140 let ticks = vec![
1141 TickInfo::new(25560, 1759015528199933).unwrap(),
1142 TickInfo::new(25570, 6393138051835308).unwrap(),
1143 TickInfo::new(25580, 228206673808681).unwrap(),
1144 TickInfo::new(25620, 1319490609195820).unwrap(),
1145 TickInfo::new(25630, 678916926147901).unwrap(),
1146 TickInfo::new(25640, 12208947683433103).unwrap(),
1147 TickInfo::new(25660, 8752304680520407).unwrap(),
1148 TickInfo::new(25680, 1486478248067104).unwrap(),
1149 TickInfo::new(25690, 1878744276123248).unwrap(),
1150 TickInfo::new(25700, 77340284046725227).unwrap(),
1151 ];
1152
1153 let pool = UniswapV3State::new(liquidity, sqrt_price, FeeAmount::Low, tick, ticks)
1154 .expect("Failed to create pool");
1155
1156 let test_cases = vec![
1158 (&wbtc, 129u64, 10u64, "0", "WBTC sell_price=129, buy_price=10"),
1159 (&wbtc, 130u64, 10u64, "0", "WBTC sell_price=130, buy_price=10"),
1160 (&wbtc, 1305u64, 100u64, "163535995630461", "WBTC sell_price=1305, buy_price=100"),
1161 (&weth, 99u64, 1300u64, "0", "WETH sell_price=99, buy_price=1300"),
1162 (&weth, 100u64, 1300u64, "0", "WETH sell_price=100, buy_price=1300"),
1163 (&weth, 101u64, 1299u64, "524227092059180", "WETH sell_price=101, buy_price=1299"),
1164 ];
1165
1166 for (sell_token, sell_price, buy_price, expected_str, test_id) in test_cases {
1167 let buy_token = if sell_token == &wbtc { &weth } else { &wbtc };
1168
1169 let target_price =
1170 Price::new(buy_price.to_biguint().unwrap(), sell_price.to_biguint().unwrap());
1171
1172 if expected_str == "0" {
1173 let result = pool.query_pool_swap(&QueryPoolSwapParams::new(
1174 buy_token.clone(),
1175 sell_token.clone(),
1176 SwapConstraint::PoolTargetPrice {
1177 target: target_price,
1178 tolerance: 0f64,
1179 min_amount_in: None,
1180 max_amount_in: None,
1181 },
1182 ));
1183 assert!(result.is_err(), "Should return error when target price is unreachable");
1184 } else {
1185 let expected =
1186 BigUint::from_str(expected_str).expect("Failed to parse expected value");
1187
1188 let trade = pool
1189 .query_pool_swap(&QueryPoolSwapParams::new(
1190 buy_token.clone(),
1191 sell_token.clone(),
1192 SwapConstraint::PoolTargetPrice {
1193 target: target_price,
1194 tolerance: 0f64,
1195 min_amount_in: None,
1196 max_amount_in: None,
1197 },
1198 ))
1199 .unwrap_or_else(|e| panic!("{} - query_supply failed: {:?}", test_id, e));
1200 assert_eq!(trade.amount_out().clone(), expected, "{}", test_id);
1201 }
1202 }
1203 }
1204
1205 #[test]
1206 fn test_swap_to_price_around_spot_price() {
1207 let liquidity = 10_000_000_000_000_000u128;
1209 let sqrt_price =
1210 get_sqrt_price_q96(U256::from(2_000_000_000u64), U256::from(1_000_000_000u64))
1211 .expect("Failed to calculate sqrt price");
1212 let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
1213
1214 let ticks = vec![TickInfo::new(0, 0).unwrap(), TickInfo::new(46080, 0).unwrap()];
1215
1216 let pool = UniswapV3State::new(liquidity, sqrt_price, FeeAmount::Low, tick, ticks)
1217 .expect("Failed to create pool");
1218
1219 let token_x = Token::new(
1220 &Bytes::from_str("0x0000000000000000000000000000000000000001").unwrap(),
1221 "X",
1222 18,
1223 0,
1224 &[Some(10_000)],
1225 Chain::Ethereum,
1226 100,
1227 );
1228 let token_y = Token::new(
1229 &Bytes::from_str("0x0000000000000000000000000000000000000002").unwrap(),
1230 "Y",
1231 18,
1232 0,
1233 &[Some(10_000)],
1234 Chain::Ethereum,
1235 100,
1236 );
1237
1238 let target_price =
1240 Price::new(1_999_750u64.to_biguint().unwrap(), 1_000_250u64.to_biguint().unwrap());
1241
1242 let result = pool.query_pool_swap(&QueryPoolSwapParams::new(
1243 token_x.clone(),
1244 token_y.clone(),
1245 SwapConstraint::PoolTargetPrice {
1246 target: target_price,
1247 tolerance: 0f64,
1248 min_amount_in: None,
1249 max_amount_in: None,
1250 },
1251 ));
1252 assert!(result.is_err(), "Should return error when target price is unreachable");
1253
1254 let target_price =
1256 Price::new(1_999_000u64.to_biguint().unwrap(), 1_001_000u64.to_biguint().unwrap());
1257
1258 let pool_swap = pool
1259 .query_pool_swap(&QueryPoolSwapParams::new(
1260 token_x,
1261 token_y,
1262 SwapConstraint::PoolTargetPrice {
1263 target: target_price,
1264 tolerance: 0f64,
1265 min_amount_in: None,
1266 max_amount_in: None,
1267 },
1268 ))
1269 .expect("swap_to_price failed");
1270
1271 let expected_amount_out =
1272 BigUint::from_str("7062236922008").expect("Failed to parse expected value");
1273 assert_eq!(
1274 pool_swap.amount_out().clone(),
1275 expected_amount_out,
1276 "Expected amount out when price covers fees"
1277 );
1278 }
1279
1280 #[test]
1281 fn test_swap_to_price_matches_get_amount_out() {
1282 let liquidity = 100_000_000_000_000_000_000u128;
1283 let sqrt_price = get_sqrt_price_q96(U256::from(20_000_000u64), U256::from(10_000_000u64))
1284 .expect("Failed to calculate sqrt price");
1285 let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
1286
1287 let ticks = vec![TickInfo::new(0, 0).unwrap(), TickInfo::new(46080, 0).unwrap()];
1288
1289 let pool = UniswapV3State::new(liquidity, sqrt_price, FeeAmount::Medium, tick, ticks)
1290 .expect("Failed to create pool");
1291
1292 let token_x_addr = Bytes::from_str("0x0000000000000000000000000000000000000001").unwrap();
1293 let token_y_addr = Bytes::from_str("0x0000000000000000000000000000000000000002").unwrap();
1294
1295 let token_x = Token::new(&token_x_addr, "X", 18, 0, &[], Chain::Ethereum, 1);
1296 let token_y = Token::new(&token_y_addr, "Y", 18, 0, &[], Chain::Ethereum, 1);
1297
1298 let target_price = Price::new(BigUint::from(2_000_000u64), BigUint::from(1_010_000u64));
1300 let pool_swap = pool
1301 .query_pool_swap(&QueryPoolSwapParams::new(
1302 token_x.clone(),
1303 token_y.clone(),
1304 SwapConstraint::PoolTargetPrice {
1305 target: target_price,
1306 tolerance: 0f64,
1307 min_amount_in: None,
1308 max_amount_in: None,
1309 },
1310 ))
1311 .expect("swap_to_price failed");
1312 assert!(pool_swap.amount_in().clone() > BigUint::ZERO, "Amount in should be positive");
1313
1314 let result = pool
1316 .get_amount_out(pool_swap.amount_in().clone(), &token_x, &token_y)
1317 .expect("get_amount_out failed");
1318
1319 assert!(result.amount > BigUint::ZERO);
1322 assert!(result.amount >= *pool_swap.amount_out());
1323 }
1324
1325 #[test]
1326 fn test_swap_price_limit_out_of_range_returns_error() {
1327 let pool = create_basic_test_pool();
1328 let amount = I256::checked_from_sign_and_abs(Sign::Positive, U256::from(1000u64)).unwrap();
1329
1330 let result = pool.swap(true, amount, Some(pool.sqrt_price));
1332 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
1333
1334 let result = pool.swap(true, amount, Some(MIN_SQRT_RATIO));
1336 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
1337
1338 let result = pool.swap(false, amount, Some(pool.sqrt_price));
1340 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
1341
1342 let result = pool.swap(false, amount, Some(MAX_SQRT_RATIO));
1344 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
1345 }
1346
1347 #[test]
1348 fn test_swap_at_extreme_price_returns_error() {
1349 let sqrt_price = MIN_SQRT_RATIO + U256::from(1u64);
1352 let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
1353 let aligned_tick = (MIN_TICK / 10) * 10 + 10; let ticks = vec![
1356 TickInfo::new(aligned_tick, 0).unwrap(),
1357 TickInfo::new(aligned_tick + 10, 0).unwrap(),
1358 ];
1359 let pool = UniswapV3State::new(
1360 100_000_000_000_000_000_000u128,
1361 sqrt_price,
1362 FeeAmount::Low,
1363 tick,
1364 ticks,
1365 )
1366 .unwrap();
1367
1368 let amount = I256::checked_from_sign_and_abs(Sign::Positive, U256::from(1000u64)).unwrap();
1369 let result = pool.swap(true, amount, None);
1371 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
1372 }
1373}
1374
1375#[cfg(test)]
1376mod tests_forks {
1377 use std::{fs, path::Path, str::FromStr};
1378
1379 use serde_json::Value;
1380 use tycho_client::feed::synchronizer::ComponentWithState;
1381 use tycho_common::models::Chain;
1382
1383 use super::*;
1384 use crate::protocol::models::{DecoderContext, TryFromWithBlock};
1385
1386 #[tokio::test]
1387 async fn test_pancakeswap_get_amount_out() {
1388 let project_root = env!("CARGO_MANIFEST_DIR");
1389 let asset_path =
1390 Path::new(project_root).join("tests/assets/decoder/pancakeswap_v3_snapshot.json");
1391 let json_data = fs::read_to_string(asset_path).expect("Failed to read test asset");
1392 let data: Value = serde_json::from_str(&json_data).expect("Failed to parse JSON");
1393
1394 let state: ComponentWithState = serde_json::from_value(data)
1395 .expect("Expected json to match ComponentWithState structure");
1396
1397 let pool_state = UniswapV3State::try_from_with_header(
1398 state,
1399 Default::default(),
1400 &Default::default(),
1401 &Default::default(),
1402 &DecoderContext::new(),
1403 )
1404 .await
1405 .unwrap();
1406
1407 let usdc = Token::new(
1408 &Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
1409 "USDC",
1410 6,
1411 0,
1412 &[Some(10_000)],
1413 Chain::Ethereum,
1414 100,
1415 );
1416 let usdt = Token::new(
1417 &Bytes::from_str("0xdac17f958d2ee523a2206206994597c13d831ec7").unwrap(),
1418 "USDT",
1419 6,
1420 0,
1421 &[Some(10_000)],
1422 Chain::Ethereum,
1423 100,
1424 );
1425
1426 let res = pool_state
1428 .get_amount_out(BigUint::from_str("5976361609").unwrap(), &usdt, &usdc)
1429 .unwrap();
1430
1431 assert_eq!(res.amount, BigUint::from_str("5975901673").unwrap());
1432 }
1433
1434 #[test]
1435 fn test_get_limits_graceful_underflow() {
1436 let pool = UniswapV3State::new(
1438 1000000,
1439 U256::from_str("79228162514264337593543950336").unwrap(),
1440 FeeAmount::Medium,
1441 0,
1442 vec![
1443 TickInfo::new(-60, 2000000).unwrap(), ],
1447 )
1448 .unwrap();
1449
1450 let usdc = Token::new(
1451 &Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
1452 "USDC",
1453 6,
1454 0,
1455 &[Some(10_000)],
1456 Chain::Ethereum,
1457 100,
1458 );
1459 let weth = Token::new(
1460 &Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(),
1461 "WETH",
1462 18,
1463 0,
1464 &[Some(10_000)],
1465 Chain::Ethereum,
1466 100,
1467 );
1468
1469 let (limit_in, limit_out) = pool
1470 .get_limits(usdc.address.clone(), weth.address.clone())
1471 .unwrap();
1472
1473 assert!(limit_in > BigUint::zero());
1475 assert!(limit_out > BigUint::zero());
1476 }
1477}