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