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