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, Price, ProtocolSim, QueryPoolSwapParams,
15 SwapConstraint,
16 },
17 },
18 Bytes,
19};
20
21use crate::evm::{
22 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 slipstreams::{dynamic_fee_module::ZERO_FEE_INDICATOR, raw_target_price},
28 uniswap::{
29 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,
37 MIN_TICK,
38 },
39 StepComputation, SwapResults, SwapState,
40 },
41 },
42 },
43 query_pool_swap::price_to_f64_with_decimals,
44};
45
46const GAS_PER_TICK: u64 = 25_000;
48const GAS_PER_LOOP: u64 = 10_000;
50
51#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
52pub struct VelodromeSlipstreamsState {
53 liquidity: u128,
54 sqrt_price: U256,
55 default_fee: u32,
56 custom_fee: u32,
57 tick_spacing: i32,
58 tick: PoolTick,
59 ticks: Arc<TickList>,
63 #[serde(skip)]
68 step_cache: SwapStepCache,
69}
70
71impl VelodromeSlipstreamsState {
72 #[allow(clippy::too_many_arguments)]
83 pub fn new(
84 liquidity: u128,
85 sqrt_price: U256,
86 default_fee: u32,
87 custom_fee: u32,
88 tick_spacing: i32,
89 tick: i32,
90 ticks: Vec<TickInfo>,
91 ) -> Result<Self, SimulationError> {
92 let tick_list = TickList::from(tick_spacing as u16, ticks)?;
93 Ok(VelodromeSlipstreamsState {
94 liquidity,
95 sqrt_price,
96 default_fee,
97 custom_fee,
98 tick_spacing,
99 tick: tick.into(),
100 ticks: Arc::new(tick_list),
101 step_cache: SwapStepCache::default(),
102 })
103 }
104
105 fn get_fee(&self) -> u32 {
108 match self.custom_fee {
109 0 => self.default_fee,
110 ZERO_FEE_INDICATOR => 0,
111 custom_fee => custom_fee,
112 }
113 }
114
115 fn swap_to_target_price(
119 &self,
120 params: &QueryPoolSwapParams,
121 target: &Price,
122 tolerance: f64,
123 ) -> Result<PoolSwap, SimulationError> {
124 let token_in = params.token_in();
125 let token_out = params.token_out();
126 let target_f64 = price_to_f64_with_decimals(target, token_in.decimals, token_out.decimals)?;
127 if target_f64 == self.spot_price(token_in, token_out)? {
128 return Ok(PoolSwap::new(BigUint::ZERO, BigUint::ZERO, Box::new(self.clone()), None));
129 }
130 let Some(limit) = raw_target_price(target, tolerance, 0) else {
132 return crate::evm::query_pool_swap::query_pool_swap(self, params);
133 };
134 let Ok((amount_in, amount_out, result)) = clmm_swap_to_price(
135 self.sqrt_price,
136 &token_in.address,
137 &token_out.address,
138 &limit,
139 0,
140 Sign::Positive,
141 |zero_for_one, amount_specified, sqrt_price_limit| {
142 self.swap(zero_for_one, amount_specified, Some(sqrt_price_limit))
143 },
144 ) else {
145 return crate::evm::query_pool_swap::query_pool_swap(self, params);
146 };
147
148 let new_state = if amount_in.is_zero() {
149 self.clone()
150 } else {
151 self.clone_post_swap(result.sqrt_price, result.tick, result.liquidity)
152 };
153 let final_spot = new_state.spot_price(token_in, token_out)?;
155 if final_spot < target_f64 || final_spot > target_f64 * (1.0 + tolerance) {
156 return crate::evm::query_pool_swap::query_pool_swap(self, params);
157 }
158 Ok(PoolSwap::new(amount_in, amount_out, Box::new(new_state), None))
159 }
160
161 fn clone_post_swap(&self, sqrt_price: U256, tick: PoolTick, liquidity: u128) -> Self {
164 let mut state = self.clone();
165 state.sqrt_price = sqrt_price;
166 state.tick = tick;
167 state.liquidity = liquidity;
168 state.step_cache = SwapStepCache::default();
169 state
170 }
171
172 fn swap(
173 &self,
174 zero_for_one: bool,
175 amount_specified: I256,
176 sqrt_price_limit: Option<U256>,
177 ) -> Result<SwapResults, SimulationError> {
178 if self.liquidity == 0 {
179 return Err(SimulationError::RecoverableError("No liquidity".to_string()));
180 }
181 let price_limit = if let Some(limit) = sqrt_price_limit {
182 limit
183 } else if zero_for_one {
184 safe_add_u256(MIN_SQRT_RATIO, U256::from(1u64))?
185 } else {
186 safe_sub_u256(MAX_SQRT_RATIO, U256::from(1u64))?
187 };
188
189 let price_limit_valid = if zero_for_one {
190 price_limit > MIN_SQRT_RATIO && price_limit < self.sqrt_price
191 } else {
192 price_limit < MAX_SQRT_RATIO && price_limit > self.sqrt_price
193 };
194 if !price_limit_valid {
195 return Err(SimulationError::InvalidInput("Price limit out of range".into(), None));
196 }
197
198 let exact_input = amount_specified > I256::from_raw(U256::from(0u64));
199
200 let mut state = SwapState {
201 amount_remaining: amount_specified,
202 amount_calculated: I256::from_raw(U256::from(0u64)),
203 sqrt_price: self.sqrt_price,
204 tick: self.tick.value().into(),
205 liquidity: self.liquidity,
206 };
207 let mut gas_used = U256::from(130_000);
208
209 let fee = self.get_fee();
210 let cache_key = if exact_input && sqrt_price_limit.is_none() {
214 StepCacheKey::new(self.sqrt_price, state.tick.value(), self.liquidity, fee, gas_used)
215 } else {
216 None
217 };
218 let mut cache_run = StepCacheRun::new(
219 &self.step_cache,
220 zero_for_one,
221 cache_key,
222 amount_specified.unsigned_abs(),
223 );
224 if let Some(progress) = cache_run.apply_cached_progress(&mut state) {
225 gas_used = progress.gas;
226 }
227 while state.amount_remaining != I256::from_raw(U256::from(0u64)) &&
228 state.sqrt_price != price_limit
229 {
230 let (mut next_tick, next_tick_info) = match self
231 .ticks
232 .next_initialized_tick_within_one_word(state.tick.value(), zero_for_one)
233 {
234 Ok((tick, info)) => (tick, info),
235 Err(tick_err) => match tick_err.kind {
236 TickListErrorKind::TicksExeeded => {
237 let new_state = self.clone_post_swap(
238 state.sqrt_price,
239 state.tick.clone(),
240 state.liquidity,
241 );
242 return Err(SimulationError::InvalidInput(
243 "Ticks exceeded".into(),
244 Some(GetAmountOutResult::new(
245 u256_to_biguint(state.amount_calculated.abs().into_raw()),
246 u256_to_biguint(gas_used),
247 Box::new(new_state),
248 )),
249 ));
250 }
251 _ => return Err(SimulationError::FatalError("Unknown error".to_string())),
252 },
253 };
254
255 next_tick = next_tick.clamp(MIN_TICK, MAX_TICK);
256
257 let sqrt_price_start = state.sqrt_price;
258 let sqrt_price_next = match next_tick_info {
259 Some(info) => info.sqrt_price,
260 None => get_sqrt_ratio_at_tick_cached(next_tick)?,
261 };
262 let SwapStepResult { sqrt_price, amount_in_with_fee, amount_out } =
263 swap_math::compute_swap_step(
264 state.sqrt_price,
265 VelodromeSlipstreamsState::get_sqrt_ratio_target(
266 sqrt_price_next,
267 price_limit,
268 zero_for_one,
269 ),
270 state.liquidity,
271 state.amount_remaining,
272 fee,
273 )?;
274 state.sqrt_price = sqrt_price;
275
276 let step = StepComputation {
277 sqrt_price_start,
278 tick_next: next_tick,
279 sqrt_price_next,
280 amount_in_with_fee,
281 amount_out,
282 };
283 if exact_input {
284 state.amount_remaining -=
285 I256::checked_from_sign_and_abs(Sign::Positive, step.amount_in_with_fee)
286 .unwrap();
287 state.amount_calculated -=
288 I256::checked_from_sign_and_abs(Sign::Positive, step.amount_out).unwrap();
289 } else {
290 state.amount_remaining +=
291 I256::checked_from_sign_and_abs(Sign::Positive, step.amount_out).unwrap();
292 state.amount_calculated +=
293 I256::checked_from_sign_and_abs(Sign::Positive, step.amount_in_with_fee)
294 .unwrap();
295 }
296 if state.sqrt_price == step.sqrt_price_next {
297 if let Some(info) = next_tick_info {
298 let liquidity_raw = info.net_liquidity;
299 let liquidity_net = if zero_for_one { -liquidity_raw } else { liquidity_raw };
300 state.liquidity =
301 liquidity_math::add_liquidity_delta(state.liquidity, liquidity_net)?;
302 gas_used = safe_add_u256(gas_used, U256::from(GAS_PER_TICK))?;
303 }
304 state.tick =
305 PoolTick::from(if zero_for_one { step.tick_next - 1 } else { step.tick_next });
306 } else if state.sqrt_price != step.sqrt_price_start {
307 state.tick = PoolTick::at_sqrt_price(state.sqrt_price)?;
308 }
309 gas_used = safe_add_u256(gas_used, U256::from(GAS_PER_LOOP))?;
310 cache_run.cache_progress(&step, &state, gas_used);
311 }
312 Ok(SwapResults {
313 amount_calculated: state.amount_calculated,
314 amount_specified,
315 amount_remaining: state.amount_remaining,
316 sqrt_price: state.sqrt_price,
317 liquidity: state.liquidity,
318 tick: state.tick,
319 gas_used,
320 })
321 }
322
323 fn get_sqrt_ratio_target(
324 sqrt_price_next: U256,
325 sqrt_price_limit: U256,
326 zero_for_one: bool,
327 ) -> U256 {
328 let cond1 = if zero_for_one {
329 sqrt_price_next < sqrt_price_limit
330 } else {
331 sqrt_price_next > sqrt_price_limit
332 };
333
334 if cond1 {
335 sqrt_price_limit
336 } else {
337 sqrt_price_next
338 }
339 }
340}
341
342#[typetag::serde]
343impl ProtocolSim for VelodromeSlipstreamsState {
344 fn fee(&self) -> f64 {
345 self.get_fee() as f64 / 1_000_000.0
346 }
347
348 fn spot_price(&self, a: &Token, b: &Token) -> Result<f64, SimulationError> {
349 if a < b {
350 sqrt_price_q96_to_f64(self.sqrt_price, a.decimals, b.decimals)
351 } else {
352 sqrt_price_q96_to_f64(self.sqrt_price, b.decimals, a.decimals)
353 .map(|price| 1.0f64 / price)
354 }
355 }
356
357 fn get_amount_out(
358 &self,
359 amount_in: BigUint,
360 token_a: &Token,
361 token_b: &Token,
362 ) -> Result<GetAmountOutResult, SimulationError> {
363 let zero_for_one = token_a < token_b;
364 let amount_specified = I256::checked_from_sign_and_abs(
365 Sign::Positive,
366 U256::from_be_slice(&amount_in.to_bytes_be()),
367 )
368 .ok_or_else(|| {
369 SimulationError::InvalidInput("I256 overflow: amount_in".to_string(), None)
370 })?;
371
372 let result = self.swap(zero_for_one, amount_specified, None)?;
373
374 trace!(?amount_in, ?token_a, ?token_b, ?zero_for_one, ?result, "SLIPSTREAMS SWAP");
375 let new_state = self.clone_post_swap(result.sqrt_price, result.tick, result.liquidity);
376
377 Ok(GetAmountOutResult::new(
378 u256_to_biguint(
379 result
380 .amount_calculated
381 .abs()
382 .into_raw(),
383 ),
384 u256_to_biguint(result.gas_used),
385 Box::new(new_state),
386 ))
387 }
388
389 fn get_limits(
390 &self,
391 token_in: Bytes,
392 token_out: Bytes,
393 ) -> Result<(BigUint, BigUint), SimulationError> {
394 if self.liquidity == 0 {
396 return Ok((BigUint::zero(), BigUint::zero()));
397 }
398
399 let zero_for_one = token_in < token_out;
400 let mut current_tick = self.tick.value();
401 let mut current_sqrt_price = self.sqrt_price;
402 let mut current_liquidity = self.liquidity;
403 let mut total_amount_in = U256::from(0u64);
404 let mut total_amount_out = U256::from(0u64);
405
406 while let Ok((tick, next_tick_info)) = self
409 .ticks
410 .next_initialized_tick_within_one_word(current_tick, zero_for_one)
411 {
412 let next_tick = tick.clamp(MIN_TICK, MAX_TICK);
414
415 let sqrt_price_next = match next_tick_info {
416 Some(info) => info.sqrt_price,
417 None => get_sqrt_ratio_at_tick_cached(next_tick)?,
418 };
419
420 let (amount_in, amount_out) = if zero_for_one {
423 let amount0 = get_amount0_delta(
424 sqrt_price_next,
425 current_sqrt_price,
426 current_liquidity,
427 true,
428 )?;
429 let amount1 = get_amount1_delta(
430 sqrt_price_next,
431 current_sqrt_price,
432 current_liquidity,
433 false,
434 )?;
435 (amount0, amount1)
436 } else {
437 let amount0 = get_amount0_delta(
438 sqrt_price_next,
439 current_sqrt_price,
440 current_liquidity,
441 false,
442 )?;
443 let amount1 = get_amount1_delta(
444 sqrt_price_next,
445 current_sqrt_price,
446 current_liquidity,
447 true,
448 )?;
449 (amount1, amount0)
450 };
451
452 total_amount_in = safe_add_u256(total_amount_in, amount_in)?;
454 total_amount_out = safe_add_u256(total_amount_out, amount_out)?;
455
456 if let Some(info) = next_tick_info {
461 let liquidity_raw = info.net_liquidity;
462 let liquidity_delta = if zero_for_one { -liquidity_raw } else { liquidity_raw };
463 current_liquidity =
464 liquidity_math::add_liquidity_delta(current_liquidity, liquidity_delta)?;
465 }
466
467 current_tick = if zero_for_one { next_tick - 1 } else { next_tick };
469 current_sqrt_price = sqrt_price_next;
470 }
471
472 Ok((u256_to_biguint(total_amount_in), u256_to_biguint(total_amount_out)))
473 }
474
475 fn delta_transition(
476 &mut self,
477 delta: ProtocolStateDelta,
478 _tokens: &HashMap<Bytes, Token>,
479 _balances: &Balances,
480 ) -> Result<(), TransitionError> {
481 self.step_cache = SwapStepCache::default();
483 if let Some(liquidity) = delta
485 .updated_attributes
486 .get("liquidity")
487 {
488 self.liquidity = u128::from(liquidity.clone());
489 }
490 if let Some(sqrt_price) = delta
491 .updated_attributes
492 .get("sqrt_price_x96")
493 {
494 self.sqrt_price = U256::from_be_slice(sqrt_price);
495 }
496 if let Some(default_fee) = delta
497 .updated_attributes
498 .get("default_fee")
499 {
500 self.default_fee = u32::from(default_fee.clone());
501 }
502 if let Some(custom_fee) = delta
503 .updated_attributes
504 .get("custom_fee")
505 {
506 self.custom_fee = u32::from(custom_fee.clone());
507 }
508 if let Some(tick) = delta.updated_attributes.get("tick") {
509 self.tick = i32::from(tick.clone()).into();
510 }
511
512 for (key, value) in delta.updated_attributes.iter() {
514 if key.starts_with("ticks/") {
516 let parts: Vec<&str> = key.split('/').collect();
517 Arc::make_mut(&mut self.ticks)
518 .set_tick_liquidity(
519 parts[1]
520 .parse::<i32>()
521 .map_err(|err| TransitionError::DecodeError(err.to_string()))?,
522 i128::from(value.clone()),
523 )
524 .map_err(|err| TransitionError::DecodeError(err.to_string()))?;
525 }
526 }
527 for key in delta.deleted_attributes.iter() {
529 if key.starts_with("ticks/") {
531 let parts: Vec<&str> = key.split('/').collect();
532 Arc::make_mut(&mut self.ticks)
533 .set_tick_liquidity(
534 parts[1]
535 .parse::<i32>()
536 .map_err(|err| TransitionError::DecodeError(err.to_string()))?,
537 0,
538 )
539 .map_err(|err| TransitionError::DecodeError(err.to_string()))?;
540 }
541 }
542 Ok(())
543 }
544
545 fn query_pool_swap(&self, params: &QueryPoolSwapParams) -> Result<PoolSwap, SimulationError> {
548 match params.swap_constraint() {
549 SwapConstraint::TradeLimitPrice { .. } => {
550 crate::evm::query_pool_swap::query_pool_swap(self, params)
551 }
552 SwapConstraint::PoolTargetPrice {
553 target,
554 tolerance,
555 min_amount_in: _,
556 max_amount_in: _,
557 } => self.swap_to_target_price(params, target, *tolerance),
558 }
559 }
560
561 fn clone_box(&self) -> Box<dyn ProtocolSim> {
562 Box::new(self.clone())
563 }
564
565 fn as_any(&self) -> &dyn Any {
566 self
567 }
568
569 fn as_any_mut(&mut self) -> &mut dyn Any {
570 self
571 }
572
573 fn eq(&self, other: &dyn ProtocolSim) -> bool {
574 if let Some(other_state) = other
575 .as_any()
576 .downcast_ref::<VelodromeSlipstreamsState>()
577 {
578 self.liquidity == other_state.liquidity &&
579 self.sqrt_price == other_state.sqrt_price &&
580 self.get_fee() == other_state.get_fee() &&
581 self.tick == other_state.tick &&
582 self.ticks == other_state.ticks
583 } else {
584 false
585 }
586 }
587}
588
589#[cfg(test)]
590mod tests {
591 use alloy::primitives::{Sign, I256, U256};
592 use rstest::rstest;
593 use tycho_common::{models::Chain, simulation::errors::SimulationError};
594
595 use super::*;
596 use crate::evm::{
597 protocol::utils::uniswap::{
598 tick_list::TickInfo,
599 tick_math::{
600 get_sqrt_ratio_at_tick, get_tick_at_sqrt_ratio, MAX_SQRT_RATIO, MIN_SQRT_RATIO,
601 MIN_TICK,
602 },
603 },
604 query_pool_swap::test_helpers::{target_price_params, to_price},
605 };
606
607 fn create_basic_test_pool() -> VelodromeSlipstreamsState {
608 let sqrt_price = get_sqrt_ratio_at_tick(0).expect("Failed to calculate sqrt price");
609 let ticks = vec![TickInfo::new(-120, 0).unwrap(), TickInfo::new(120, 0).unwrap()];
610 VelodromeSlipstreamsState::new(
611 100_000_000_000_000_000_000u128,
612 sqrt_price,
613 3000,
614 0,
615 1,
616 0,
617 ticks,
618 )
619 .expect("Failed to create pool")
620 }
621
622 #[rstest]
623 #[case::no_custom_fee(0, 0.003)]
624 #[case::zero_fee_indicator(420, 0.0)]
625 #[case::custom_fee(500, 0.0005)]
626 fn test_fee_resolves_custom_fee(#[case] custom_fee: u32, #[case] expected: f64) {
627 let sqrt_price = get_sqrt_ratio_at_tick(0).unwrap();
628 let ticks = vec![TickInfo::new(-120, 0).unwrap(), TickInfo::new(120, 0).unwrap()];
629 let pool = VelodromeSlipstreamsState::new(
630 10u128.pow(20),
631 sqrt_price,
632 3000,
633 custom_fee,
634 1,
635 0,
636 ticks,
637 )
638 .unwrap();
639
640 assert_eq!(pool.fee(), expected);
641 }
642
643 #[test]
644 fn test_partial_step_updates_tick_when_price_moves_without_crossing_initialized_tick() {
645 let pool = create_basic_test_pool();
646 let amount =
647 I256::checked_from_sign_and_abs(Sign::Positive, U256::from(100_000_000_000_000_000u64))
648 .unwrap();
649
650 let result = pool
651 .swap(true, amount, None)
652 .expect("swap should stay within the current liquidity range");
653 let expected_tick =
654 get_tick_at_sqrt_ratio(result.sqrt_price).expect("new sqrt price should map to a tick");
655
656 assert_ne!(result.sqrt_price, pool.sqrt_price);
657 assert_ne!(result.sqrt_price, get_sqrt_ratio_at_tick(-120).unwrap());
658 assert_ne!(expected_tick, pool.tick.value());
659 assert_eq!(result.tick.value(), expected_tick);
660 }
661
662 #[test]
663 fn test_swap_keeps_boundary_tick_when_price_does_not_move() {
664 let mut pool = create_basic_test_pool();
665 pool.tick = PoolTick::from(-1);
666 let amount = I256::checked_from_sign_and_abs(Sign::Positive, U256::from(1u64)).unwrap();
667
668 let result = pool
669 .swap(true, amount, None)
670 .expect("swap should consume the input as fee without moving price");
671
672 assert_eq!(result.sqrt_price, pool.sqrt_price);
673 assert_eq!(get_tick_at_sqrt_ratio(result.sqrt_price).unwrap(), 0);
674 assert_eq!(result.tick, pool.tick);
675 }
676
677 #[test]
678 fn test_swap_price_limit_out_of_range_returns_error() {
679 let pool = create_basic_test_pool();
680 let amount = I256::checked_from_sign_and_abs(Sign::Positive, U256::from(1000u64)).unwrap();
681
682 let result = pool.swap(true, amount, Some(pool.sqrt_price));
683 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
684
685 let result = pool.swap(true, amount, Some(MIN_SQRT_RATIO));
686 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
687
688 let result = pool.swap(false, amount, Some(pool.sqrt_price));
689 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
690
691 let result = pool.swap(false, amount, Some(MAX_SQRT_RATIO));
692 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
693 }
694
695 #[test]
696 fn test_swap_at_extreme_price_returns_error() {
697 let sqrt_price = MIN_SQRT_RATIO + U256::from(1u64);
698 let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
699 let ticks =
700 vec![TickInfo::new(MIN_TICK, 0).unwrap(), TickInfo::new(MIN_TICK + 1, 0).unwrap()];
701 let pool = VelodromeSlipstreamsState::new(
702 100_000_000_000_000_000_000u128,
703 sqrt_price,
704 3000,
705 0,
706 1,
707 tick,
708 ticks,
709 )
710 .expect("Failed to create pool");
711
712 let amount = I256::checked_from_sign_and_abs(Sign::Positive, U256::from(1000u64)).unwrap();
713 let result = pool.swap(true, amount, None);
714 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
715 }
716
717 fn token_pair() -> (Token, Token) {
718 let token_x = Token::new(&Bytes::from([0x01; 20]), "X", 18, 0, &[], Chain::Ethereum, 100);
719 let token_y = Token::new(&Bytes::from([0x02; 20]), "Y", 18, 0, &[], Chain::Ethereum, 100);
720 (token_x, token_y)
721 }
722
723 #[rstest]
724 fn test_query_pool_swap_target_price_lands_in_band(#[values(true, false)] sell_x: bool) {
725 let pool = create_basic_test_pool();
726 let (token_x, token_y) = token_pair();
727 let (token_in, token_out) =
728 if sell_x { (&token_x, &token_y) } else { (&token_y, &token_x) };
729 let target = 0.995;
730 let tolerance = 1e-4;
731 let params = target_price_params(
732 token_in,
733 token_out,
734 to_price(target, token_in, token_out),
735 tolerance,
736 );
737
738 let swap = pool.query_pool_swap(¶ms).unwrap();
739
740 let new_spot = swap
741 .new_state()
742 .spot_price(token_in, token_out)
743 .unwrap();
744 assert!(new_spot >= target && new_spot <= target * (1.0 + tolerance), "spot {new_spot}");
745 assert!(swap.price_points().is_none(), "the native path returns no price points");
746 let quote = pool
747 .get_amount_out(swap.amount_in().clone(), token_in, token_out)
748 .unwrap();
749 assert_eq!("e.amount, swap.amount_out());
750 }
751
752 #[test]
754 fn test_query_pool_swap_target_price_falls_back_to_search() {
755 let pool = create_basic_test_pool();
756 let (token_x, token_y) = token_pair();
757 let params =
758 target_price_params(&token_x, &token_y, to_price(1.0 - 1e-5, &token_x, &token_y), 1e-4);
759
760 let swap = pool.query_pool_swap(¶ms).unwrap();
761
762 assert!(swap.price_points().is_some(), "the numerical search returns price points");
763 }
764
765 #[test]
768 fn test_query_pool_swap_target_price_out_of_reach_falls_back_to_search() {
769 let sqrt_price = get_sqrt_ratio_at_tick(0).unwrap();
770 let ticks = vec![TickInfo::new(MIN_TICK, 0).unwrap(), TickInfo::new(-MIN_TICK, 0).unwrap()];
771 let pool =
772 VelodromeSlipstreamsState::new(u128::MAX, sqrt_price, 3000, 0, 1, 0, ticks).unwrap();
773 let (token_x, token_y) = token_pair();
774 let target = Price::new(BigUint::from(1u8), BigUint::from(1u8) << 100usize);
775 let target_f64 = 2f64.powi(-100);
776 let tolerance = 1e-4;
777 let params = target_price_params(&token_x, &token_y, target, tolerance);
778
779 let swap = pool.query_pool_swap(¶ms).unwrap();
780
781 let new_spot = swap
782 .new_state()
783 .spot_price(&token_x, &token_y)
784 .unwrap();
785 assert!(
786 new_spot >= target_f64 && new_spot <= target_f64 * (1.0 + tolerance),
787 "spot {new_spot} is outside the band of target {target_f64}"
788 );
789 assert!(swap.price_points().is_some(), "the numerical search returns price points");
790 }
791
792 #[test]
793 fn test_query_pool_swap_target_price_at_spot() {
794 let pool = create_basic_test_pool();
795 let (token_x, token_y) = token_pair();
796 let params = target_price_params(
797 &token_x,
798 &token_y,
799 Price::new(BigUint::from(1u64), BigUint::from(1u64)),
800 1e-4,
801 );
802
803 let swap = pool.query_pool_swap(¶ms).unwrap();
804
805 assert!(swap.amount_in().is_zero());
806 assert!(swap.amount_out().is_zero());
807 assert!(swap.new_state().eq(&pool));
808 }
809
810 #[test]
811 fn test_query_pool_swap_target_price_above_spot() {
812 let pool = create_basic_test_pool();
813 let (token_x, token_y) = token_pair();
814 let params =
815 target_price_params(&token_x, &token_y, to_price(1.01, &token_x, &token_y), 1e-4);
816
817 let result = pool.query_pool_swap(¶ms);
818
819 assert!(matches!(result, Err(SimulationError::InvalidInput(..))), "got {result:?}");
820 }
821}
822
823#[cfg(test)]
824mod tick_list_sharing_tests {
825 use std::collections::{HashMap, HashSet};
826
827 use tycho_common::{dto::ProtocolStateDelta, hex_bytes::Bytes};
828
829 use super::*;
830 use crate::evm::protocol::utils::uniswap::tick_math::get_sqrt_ratio_at_tick;
831
832 #[test]
833 fn test_delta_transition_leaves_clones_ticks_unchanged() {
834 let original = VelodromeSlipstreamsState::new(
835 100_000_000_000_000_000_000u128,
836 get_sqrt_ratio_at_tick(0).unwrap(),
837 3000,
838 0,
839 1,
840 0,
841 vec![TickInfo::new(-120, 10000).unwrap(), TickInfo::new(120, -10000).unwrap()],
842 )
843 .unwrap();
844 let mut updated = original.clone();
845 let delta = ProtocolStateDelta {
846 component_id: "State1".to_owned(),
847 updated_attributes: HashMap::from([(
848 "ticks/-120/net_liquidity".to_string(),
849 Bytes::from(20000_i128.to_be_bytes().to_vec()),
850 )]),
851 deleted_attributes: HashSet::new(),
852 };
853
854 updated
855 .delta_transition(delta, &HashMap::new(), &Balances::default())
856 .unwrap();
857
858 assert_eq!(
859 updated
860 .ticks
861 .get_tick(-120)
862 .unwrap()
863 .net_liquidity,
864 20000
865 );
866 assert_eq!(
867 original
868 .ticks
869 .get_tick(-120)
870 .unwrap()
871 .net_liquidity,
872 10000
873 );
874 }
875}
876
877#[cfg(test)]
878mod step_cache_tests {
879 use std::{
880 collections::{HashMap, HashSet},
881 str::FromStr,
882 };
883
884 use tycho_common::{dto::ProtocolStateDelta, hex_bytes::Bytes};
885
886 use super::*;
887 use crate::evm::protocol::utils::uniswap::swap_step_cache::test_fixtures::{
888 amount_orders, test_amounts, test_ticks, wbtc_weth, LIQUIDITY, SQRT_PRICE, TICK,
889 };
890
891 fn test_pool() -> VelodromeSlipstreamsState {
892 VelodromeSlipstreamsState::new(
893 LIQUIDITY,
894 U256::from_str(SQRT_PRICE).unwrap(),
895 500,
896 0,
897 10,
898 TICK,
899 test_ticks(),
900 )
901 .unwrap()
902 }
903
904 fn format_quote_result(
906 pool: &VelodromeSlipstreamsState,
907 amount: &BigUint,
908 sell: &Token,
909 buy: &Token,
910 ) -> String {
911 match pool.get_amount_out(amount.clone(), sell, buy) {
912 Ok(result) => {
913 let state = result
914 .new_state
915 .as_any()
916 .downcast_ref::<VelodromeSlipstreamsState>()
917 .unwrap();
918 format!(
919 "{} {} {} {} {}",
920 result.amount,
921 result.gas,
922 state.sqrt_price,
923 state.tick.value(),
924 state.liquidity
925 )
926 }
927 Err(error) => format!("error {error}"),
928 }
929 }
930
931 #[test]
932 fn test_step_cache_quotes_match_a_fresh_state_in_any_order() {
933 let (wbtc, weth) = wbtc_weth();
934 for (sell, buy, smallest) in [(&wbtc, &weth, 1_000u64), (&weth, &wbtc, 1_000_000_000)] {
935 for order in amount_orders(&test_amounts(smallest)) {
936 let shared = test_pool();
937 for amount in &order {
938 assert_eq!(
939 format_quote_result(&shared, amount, sell, buy),
940 format_quote_result(&test_pool(), amount, sell, buy),
941 "selling {amount} {}",
942 sell.symbol
943 );
944 }
945 let steps = shared
946 .step_cache
947 .min_inputs(sell < buy)
948 .len();
949 assert!(steps > 2, "only {steps} steps cached");
950 }
951 }
952 }
953
954 #[test]
955 fn test_step_cache_quotes_match_a_fresh_state_at_each_cached_step() {
956 let (wbtc, weth) = wbtc_weth();
957 for (sell, buy, doublings) in [(&wbtc, &weth, 30u32), (&weth, &wbtc, 70)] {
960 let cached = test_pool();
961 let _ = cached.get_amount_out(BigUint::from(10u32) << doublings, sell, buy);
962 let boundaries = cached.step_cache.min_inputs(sell < buy);
963 assert!(boundaries.len() > 2, "only {} steps cached", boundaries.len());
964 for boundary in boundaries.into_iter().skip(1) {
965 for amount in [boundary - U256::from(1u64), boundary, boundary + U256::from(1u64)] {
966 let amount = u256_to_biguint(amount);
967 assert_eq!(
968 format_quote_result(&cached, &amount, sell, buy),
969 format_quote_result(&test_pool(), &amount, sell, buy),
970 "selling {amount} {}",
971 sell.symbol
972 );
973 }
974 }
975 }
976 }
977
978 #[test]
979 fn test_step_cache_of_a_swapped_state() {
980 let (wbtc, weth) = wbtc_weth();
981 let pool = test_pool();
982 let swapped = pool
983 .get_amount_out(BigUint::from(300_000_000u64), &wbtc, &weth)
984 .unwrap()
985 .new_state;
986 let swapped = swapped
987 .as_any()
988 .downcast_ref::<VelodromeSlipstreamsState>()
989 .unwrap();
990 let fresh: VelodromeSlipstreamsState =
992 serde_json::from_value(serde_json::to_value(swapped).unwrap()).unwrap();
993
994 for amount in test_amounts(1_000).iter().rev() {
995 assert_eq!(
996 format_quote_result(swapped, amount, &wbtc, &weth),
997 format_quote_result(&fresh, amount, &wbtc, &weth),
998 "selling {amount} WBTC"
999 );
1000 }
1001 }
1002
1003 #[test]
1004 fn test_step_cache_restarts_after_delta_transition() {
1005 let (wbtc, weth) = wbtc_weth();
1006 let large = BigUint::from(3_000_000_000u64);
1007 let delta = || ProtocolStateDelta {
1010 component_id: "State1".to_owned(),
1011 updated_attributes: HashMap::from([(
1012 "ticks/255820/net_liquidity".to_string(),
1013 Bytes::from(
1014 1_000_000_000_000_000i128
1015 .to_be_bytes()
1016 .to_vec(),
1017 ),
1018 )]),
1019 deleted_attributes: HashSet::new(),
1020 };
1021 let mut cached = test_pool();
1022 cached
1023 .get_amount_out(large.clone(), &wbtc, &weth)
1024 .unwrap();
1025 let mut fresh = test_pool();
1026
1027 cached
1028 .delta_transition(delta(), &HashMap::new(), &Balances::default())
1029 .unwrap();
1030 fresh
1031 .delta_transition(delta(), &HashMap::new(), &Balances::default())
1032 .unwrap();
1033
1034 assert_eq!(
1035 format_quote_result(&cached, &large, &wbtc, &weth),
1036 format_quote_result(&fresh, &large, &wbtc, &weth)
1037 );
1038 }
1039}