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