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, 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 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 query_pool_swap::price_to_f64_with_decimals,
42};
43
44const GAS_PER_TICK: u64 = 25_000;
46const GAS_PER_LOOP: u64 = 10_000;
48
49#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
50pub struct VelodromeSlipstreamsState {
51 liquidity: u128,
52 sqrt_price: U256,
53 default_fee: u32,
54 custom_fee: u32,
55 tick_spacing: i32,
56 tick: i32,
57 ticks: TickList,
58}
59
60impl VelodromeSlipstreamsState {
61 #[allow(clippy::too_many_arguments)]
72 pub fn new(
73 liquidity: u128,
74 sqrt_price: U256,
75 default_fee: u32,
76 custom_fee: u32,
77 tick_spacing: i32,
78 tick: i32,
79 ticks: Vec<TickInfo>,
80 ) -> Result<Self, SimulationError> {
81 let tick_list = TickList::from(tick_spacing as u16, ticks)?;
82 Ok(VelodromeSlipstreamsState {
83 liquidity,
84 sqrt_price,
85 default_fee,
86 custom_fee,
87 tick_spacing,
88 tick,
89 ticks: tick_list,
90 })
91 }
92
93 fn get_fee(&self) -> u32 {
96 match self.custom_fee {
97 0 => self.default_fee,
98 ZERO_FEE_INDICATOR => 0,
99 custom_fee => custom_fee,
100 }
101 }
102
103 fn swap_to_target_price(
107 &self,
108 params: &QueryPoolSwapParams,
109 target: &Price,
110 tolerance: f64,
111 ) -> Result<PoolSwap, SimulationError> {
112 let token_in = params.token_in();
113 let token_out = params.token_out();
114 let target_f64 = price_to_f64_with_decimals(target, token_in.decimals, token_out.decimals)?;
115 if target_f64 == self.spot_price(token_in, token_out)? {
116 return Ok(PoolSwap::new(BigUint::ZERO, BigUint::ZERO, Box::new(self.clone()), None));
117 }
118 let Some(limit) = raw_target_price(target, tolerance, 0) else {
120 return crate::evm::query_pool_swap::query_pool_swap(self, params);
121 };
122 let Ok((amount_in, amount_out, result)) = clmm_swap_to_price(
123 self.sqrt_price,
124 &token_in.address,
125 &token_out.address,
126 &limit,
127 0,
128 Sign::Positive,
129 |zero_for_one, amount_specified, sqrt_price_limit| {
130 self.swap(zero_for_one, amount_specified, Some(sqrt_price_limit))
131 },
132 ) else {
133 return crate::evm::query_pool_swap::query_pool_swap(self, params);
134 };
135
136 let mut new_state = self.clone();
137 if !amount_in.is_zero() {
138 new_state.liquidity = result.liquidity;
139 new_state.tick = result.tick;
140 new_state.sqrt_price = result.sqrt_price;
141 }
142 let final_spot = new_state.spot_price(token_in, token_out)?;
144 if final_spot < target_f64 || final_spot > target_f64 * (1.0 + tolerance) {
145 return crate::evm::query_pool_swap::query_pool_swap(self, params);
146 }
147 Ok(PoolSwap::new(amount_in, amount_out, Box::new(new_state), None))
148 }
149
150 fn swap(
151 &self,
152 zero_for_one: bool,
153 amount_specified: I256,
154 sqrt_price_limit: Option<U256>,
155 ) -> Result<SwapResults, SimulationError> {
156 if self.liquidity == 0 {
157 return Err(SimulationError::RecoverableError("No liquidity".to_string()));
158 }
159 let price_limit = if let Some(limit) = sqrt_price_limit {
160 limit
161 } else if zero_for_one {
162 safe_add_u256(MIN_SQRT_RATIO, U256::from(1u64))?
163 } else {
164 safe_sub_u256(MAX_SQRT_RATIO, U256::from(1u64))?
165 };
166
167 let price_limit_valid = if zero_for_one {
168 price_limit > MIN_SQRT_RATIO && price_limit < self.sqrt_price
169 } else {
170 price_limit < MAX_SQRT_RATIO && price_limit > self.sqrt_price
171 };
172 if !price_limit_valid {
173 return Err(SimulationError::InvalidInput("Price limit out of range".into(), None));
174 }
175
176 let exact_input = amount_specified > I256::from_raw(U256::from(0u64));
177
178 let mut state = SwapState {
179 amount_remaining: amount_specified,
180 amount_calculated: I256::from_raw(U256::from(0u64)),
181 sqrt_price: self.sqrt_price,
182 tick: self.tick,
183 liquidity: self.liquidity,
184 };
185 let mut gas_used = U256::from(130_000);
186
187 let fee = self.get_fee();
188 while state.amount_remaining != I256::from_raw(U256::from(0u64)) &&
189 state.sqrt_price != price_limit
190 {
191 let (mut next_tick, initialized) = match self
192 .ticks
193 .next_initialized_tick_within_one_word(state.tick, zero_for_one)
194 {
195 Ok((tick, init)) => (tick, init),
196 Err(tick_err) => match tick_err.kind {
197 TickListErrorKind::TicksExeeded => {
198 let mut new_state = self.clone();
199 new_state.liquidity = state.liquidity;
200 new_state.tick = state.tick;
201 new_state.sqrt_price = state.sqrt_price;
202 return Err(SimulationError::InvalidInput(
203 "Ticks exceeded".into(),
204 Some(GetAmountOutResult::new(
205 u256_to_biguint(state.amount_calculated.abs().into_raw()),
206 u256_to_biguint(gas_used),
207 Box::new(new_state),
208 )),
209 ));
210 }
211 _ => return Err(SimulationError::FatalError("Unknown error".to_string())),
212 },
213 };
214
215 next_tick = next_tick.clamp(MIN_TICK, MAX_TICK);
216
217 let sqrt_price_start = state.sqrt_price;
218 let sqrt_price_next = get_sqrt_ratio_at_tick(next_tick)?;
219 let (sqrt_price, amount_in, amount_out, fee_amount) = swap_math::compute_swap_step(
220 state.sqrt_price,
221 VelodromeSlipstreamsState::get_sqrt_ratio_target(
222 sqrt_price_next,
223 price_limit,
224 zero_for_one,
225 ),
226 state.liquidity,
227 state.amount_remaining,
228 fee,
229 )?;
230 state.sqrt_price = sqrt_price;
231
232 let step = StepComputation {
233 sqrt_price_start,
234 tick_next: next_tick,
235 initialized,
236 sqrt_price_next,
237 amount_in,
238 amount_out,
239 fee_amount,
240 };
241 if exact_input {
242 state.amount_remaining -= I256::checked_from_sign_and_abs(
243 Sign::Positive,
244 safe_add_u256(step.amount_in, step.fee_amount)?,
245 )
246 .unwrap();
247 state.amount_calculated -=
248 I256::checked_from_sign_and_abs(Sign::Positive, step.amount_out).unwrap();
249 } else {
250 state.amount_remaining +=
251 I256::checked_from_sign_and_abs(Sign::Positive, step.amount_out).unwrap();
252 state.amount_calculated += I256::checked_from_sign_and_abs(
253 Sign::Positive,
254 safe_add_u256(step.amount_in, step.fee_amount)?,
255 )
256 .unwrap();
257 }
258 if state.sqrt_price == step.sqrt_price_next {
259 if step.initialized {
260 let liquidity_raw = self
261 .ticks
262 .get_tick(step.tick_next)
263 .unwrap()
264 .net_liquidity;
265 let liquidity_net = if zero_for_one { -liquidity_raw } else { liquidity_raw };
266 state.liquidity =
267 liquidity_math::add_liquidity_delta(state.liquidity, liquidity_net)?;
268 gas_used = safe_add_u256(gas_used, U256::from(GAS_PER_TICK))?;
269 }
270 state.tick = if zero_for_one { step.tick_next - 1 } else { step.tick_next };
271 } else if state.sqrt_price != step.sqrt_price_start {
272 state.tick = get_tick_at_sqrt_ratio(state.sqrt_price)?;
273 }
274 gas_used = safe_add_u256(gas_used, U256::from(GAS_PER_LOOP))?;
275 }
276 Ok(SwapResults {
277 amount_calculated: state.amount_calculated,
278 amount_specified,
279 amount_remaining: state.amount_remaining,
280 sqrt_price: state.sqrt_price,
281 liquidity: state.liquidity,
282 tick: state.tick,
283 gas_used,
284 })
285 }
286
287 fn get_sqrt_ratio_target(
288 sqrt_price_next: U256,
289 sqrt_price_limit: U256,
290 zero_for_one: bool,
291 ) -> U256 {
292 let cond1 = if zero_for_one {
293 sqrt_price_next < sqrt_price_limit
294 } else {
295 sqrt_price_next > sqrt_price_limit
296 };
297
298 if cond1 {
299 sqrt_price_limit
300 } else {
301 sqrt_price_next
302 }
303 }
304}
305
306#[typetag::serde]
307impl ProtocolSim for VelodromeSlipstreamsState {
308 fn fee(&self) -> f64 {
309 self.get_fee() as f64 / 1_000_000.0
310 }
311
312 fn spot_price(&self, a: &Token, b: &Token) -> Result<f64, SimulationError> {
313 if a < b {
314 sqrt_price_q96_to_f64(self.sqrt_price, a.decimals, b.decimals)
315 } else {
316 sqrt_price_q96_to_f64(self.sqrt_price, b.decimals, a.decimals)
317 .map(|price| 1.0f64 / price)
318 }
319 }
320
321 fn get_amount_out(
322 &self,
323 amount_in: BigUint,
324 token_a: &Token,
325 token_b: &Token,
326 ) -> Result<GetAmountOutResult, SimulationError> {
327 let zero_for_one = token_a < token_b;
328 let amount_specified = I256::checked_from_sign_and_abs(
329 Sign::Positive,
330 U256::from_be_slice(&amount_in.to_bytes_be()),
331 )
332 .ok_or_else(|| {
333 SimulationError::InvalidInput("I256 overflow: amount_in".to_string(), None)
334 })?;
335
336 let result = self.swap(zero_for_one, amount_specified, None)?;
337
338 trace!(?amount_in, ?token_a, ?token_b, ?zero_for_one, ?result, "SLIPSTREAMS SWAP");
339 let mut new_state = self.clone();
340 new_state.liquidity = result.liquidity;
341 new_state.tick = result.tick;
342 new_state.sqrt_price = result.sqrt_price;
343
344 Ok(GetAmountOutResult::new(
345 u256_to_biguint(
346 result
347 .amount_calculated
348 .abs()
349 .into_raw(),
350 ),
351 u256_to_biguint(result.gas_used),
352 Box::new(new_state),
353 ))
354 }
355
356 fn get_limits(
357 &self,
358 token_in: Bytes,
359 token_out: Bytes,
360 ) -> Result<(BigUint, BigUint), SimulationError> {
361 if self.liquidity == 0 {
363 return Ok((BigUint::zero(), BigUint::zero()));
364 }
365
366 let zero_for_one = token_in < token_out;
367 let mut current_tick = self.tick;
368 let mut current_sqrt_price = self.sqrt_price;
369 let mut current_liquidity = self.liquidity;
370 let mut total_amount_in = U256::from(0u64);
371 let mut total_amount_out = U256::from(0u64);
372
373 while let Ok((tick, initialized)) = self
376 .ticks
377 .next_initialized_tick_within_one_word(current_tick, zero_for_one)
378 {
379 let next_tick = tick.clamp(MIN_TICK, MAX_TICK);
381
382 let sqrt_price_next = get_sqrt_ratio_at_tick(next_tick)?;
384
385 let (amount_in, amount_out) = if zero_for_one {
388 let amount0 = get_amount0_delta(
389 sqrt_price_next,
390 current_sqrt_price,
391 current_liquidity,
392 true,
393 )?;
394 let amount1 = get_amount1_delta(
395 sqrt_price_next,
396 current_sqrt_price,
397 current_liquidity,
398 false,
399 )?;
400 (amount0, amount1)
401 } else {
402 let amount0 = get_amount0_delta(
403 sqrt_price_next,
404 current_sqrt_price,
405 current_liquidity,
406 false,
407 )?;
408 let amount1 = get_amount1_delta(
409 sqrt_price_next,
410 current_sqrt_price,
411 current_liquidity,
412 true,
413 )?;
414 (amount1, amount0)
415 };
416
417 total_amount_in = safe_add_u256(total_amount_in, amount_in)?;
419 total_amount_out = safe_add_u256(total_amount_out, amount_out)?;
420
421 if initialized {
426 let liquidity_raw = self
427 .ticks
428 .get_tick(next_tick)
429 .unwrap()
430 .net_liquidity;
431 let liquidity_delta = if zero_for_one { -liquidity_raw } else { liquidity_raw };
432 current_liquidity =
433 liquidity_math::add_liquidity_delta(current_liquidity, liquidity_delta)?;
434 }
435
436 current_tick = if zero_for_one { next_tick - 1 } else { next_tick };
438 current_sqrt_price = sqrt_price_next;
439 }
440
441 Ok((u256_to_biguint(total_amount_in), u256_to_biguint(total_amount_out)))
442 }
443
444 fn delta_transition(
445 &mut self,
446 delta: ProtocolStateDelta,
447 _tokens: &HashMap<Bytes, Token>,
448 _balances: &Balances,
449 ) -> Result<(), TransitionError> {
450 if let Some(liquidity) = delta
452 .updated_attributes
453 .get("liquidity")
454 {
455 self.liquidity = u128::from(liquidity.clone());
456 }
457 if let Some(sqrt_price) = delta
458 .updated_attributes
459 .get("sqrt_price_x96")
460 {
461 self.sqrt_price = U256::from_be_slice(sqrt_price);
462 }
463 if let Some(default_fee) = delta
464 .updated_attributes
465 .get("default_fee")
466 {
467 self.default_fee = u32::from(default_fee.clone());
468 }
469 if let Some(custom_fee) = delta
470 .updated_attributes
471 .get("custom_fee")
472 {
473 self.custom_fee = u32::from(custom_fee.clone());
474 }
475 if let Some(tick) = delta.updated_attributes.get("tick") {
476 self.tick = i32::from(tick.clone());
477 }
478
479 for (key, value) in delta.updated_attributes.iter() {
481 if key.starts_with("ticks/") {
483 let parts: Vec<&str> = key.split('/').collect();
484 self.ticks
485 .set_tick_liquidity(
486 parts[1]
487 .parse::<i32>()
488 .map_err(|err| TransitionError::DecodeError(err.to_string()))?,
489 i128::from(value.clone()),
490 )
491 .map_err(|err| TransitionError::DecodeError(err.to_string()))?;
492 }
493 }
494 for key in delta.deleted_attributes.iter() {
496 if key.starts_with("ticks/") {
498 let parts: Vec<&str> = key.split('/').collect();
499 self.ticks
500 .set_tick_liquidity(
501 parts[1]
502 .parse::<i32>()
503 .map_err(|err| TransitionError::DecodeError(err.to_string()))?,
504 0,
505 )
506 .map_err(|err| TransitionError::DecodeError(err.to_string()))?;
507 }
508 }
509 Ok(())
510 }
511
512 fn query_pool_swap(&self, params: &QueryPoolSwapParams) -> Result<PoolSwap, SimulationError> {
515 match params.swap_constraint() {
516 SwapConstraint::TradeLimitPrice { .. } => {
517 crate::evm::query_pool_swap::query_pool_swap(self, params)
518 }
519 SwapConstraint::PoolTargetPrice {
520 target,
521 tolerance,
522 min_amount_in: _,
523 max_amount_in: _,
524 } => self.swap_to_target_price(params, target, *tolerance),
525 }
526 }
527
528 fn clone_box(&self) -> Box<dyn ProtocolSim> {
529 Box::new(self.clone())
530 }
531
532 fn as_any(&self) -> &dyn Any {
533 self
534 }
535
536 fn as_any_mut(&mut self) -> &mut dyn Any {
537 self
538 }
539
540 fn eq(&self, other: &dyn ProtocolSim) -> bool {
541 if let Some(other_state) = other
542 .as_any()
543 .downcast_ref::<VelodromeSlipstreamsState>()
544 {
545 self.liquidity == other_state.liquidity &&
546 self.sqrt_price == other_state.sqrt_price &&
547 self.get_fee() == other_state.get_fee() &&
548 self.tick == other_state.tick &&
549 self.ticks == other_state.ticks
550 } else {
551 false
552 }
553 }
554}
555
556#[cfg(test)]
557mod tests {
558 use alloy::primitives::{Sign, I256, U256};
559 use rstest::rstest;
560 use tycho_common::{models::Chain, simulation::errors::SimulationError};
561
562 use super::*;
563 use crate::evm::{
564 protocol::utils::uniswap::{
565 tick_list::TickInfo,
566 tick_math::{
567 get_sqrt_ratio_at_tick, get_tick_at_sqrt_ratio, MAX_SQRT_RATIO, MIN_SQRT_RATIO,
568 MIN_TICK,
569 },
570 },
571 query_pool_swap::test_helpers::{target_price_params, to_price},
572 };
573
574 fn create_basic_test_pool() -> VelodromeSlipstreamsState {
575 let sqrt_price = get_sqrt_ratio_at_tick(0).expect("Failed to calculate sqrt price");
576 let ticks = vec![TickInfo::new(-120, 0).unwrap(), TickInfo::new(120, 0).unwrap()];
577 VelodromeSlipstreamsState::new(
578 100_000_000_000_000_000_000u128,
579 sqrt_price,
580 3000,
581 0,
582 1,
583 0,
584 ticks,
585 )
586 .expect("Failed to create pool")
587 }
588
589 #[rstest]
590 #[case::no_custom_fee(0, 0.003)]
591 #[case::zero_fee_indicator(420, 0.0)]
592 #[case::custom_fee(500, 0.0005)]
593 fn test_fee_resolves_custom_fee(#[case] custom_fee: u32, #[case] expected: f64) {
594 let sqrt_price = get_sqrt_ratio_at_tick(0).unwrap();
595 let ticks = vec![TickInfo::new(-120, 0).unwrap(), TickInfo::new(120, 0).unwrap()];
596 let pool = VelodromeSlipstreamsState::new(
597 10u128.pow(20),
598 sqrt_price,
599 3000,
600 custom_fee,
601 1,
602 0,
603 ticks,
604 )
605 .unwrap();
606
607 assert_eq!(pool.fee(), expected);
608 }
609
610 #[test]
611 fn test_partial_step_updates_tick_when_price_moves_without_crossing_initialized_tick() {
612 let pool = create_basic_test_pool();
613 let amount =
614 I256::checked_from_sign_and_abs(Sign::Positive, U256::from(100_000_000_000_000_000u64))
615 .unwrap();
616
617 let result = pool
618 .swap(true, amount, None)
619 .expect("swap should stay within the current liquidity range");
620 let expected_tick =
621 get_tick_at_sqrt_ratio(result.sqrt_price).expect("new sqrt price should map to a tick");
622
623 assert_ne!(result.sqrt_price, pool.sqrt_price);
624 assert_ne!(result.sqrt_price, get_sqrt_ratio_at_tick(-120).unwrap());
625 assert_ne!(expected_tick, pool.tick);
626 assert_eq!(result.tick, expected_tick);
627 }
628
629 #[test]
630 fn test_swap_keeps_boundary_tick_when_price_does_not_move() {
631 let mut pool = create_basic_test_pool();
632 pool.tick = -1;
633 let amount = I256::checked_from_sign_and_abs(Sign::Positive, U256::from(1u64)).unwrap();
634
635 let result = pool
636 .swap(true, amount, None)
637 .expect("swap should consume the input as fee without moving price");
638
639 assert_eq!(result.sqrt_price, pool.sqrt_price);
640 assert_eq!(get_tick_at_sqrt_ratio(result.sqrt_price).unwrap(), 0);
641 assert_eq!(result.tick, pool.tick);
642 }
643
644 #[test]
645 fn test_swap_price_limit_out_of_range_returns_error() {
646 let pool = create_basic_test_pool();
647 let amount = I256::checked_from_sign_and_abs(Sign::Positive, U256::from(1000u64)).unwrap();
648
649 let result = pool.swap(true, amount, Some(pool.sqrt_price));
650 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
651
652 let result = pool.swap(true, amount, Some(MIN_SQRT_RATIO));
653 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
654
655 let result = pool.swap(false, amount, Some(pool.sqrt_price));
656 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
657
658 let result = pool.swap(false, amount, Some(MAX_SQRT_RATIO));
659 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
660 }
661
662 #[test]
663 fn test_swap_at_extreme_price_returns_error() {
664 let sqrt_price = MIN_SQRT_RATIO + U256::from(1u64);
665 let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
666 let ticks =
667 vec![TickInfo::new(MIN_TICK, 0).unwrap(), TickInfo::new(MIN_TICK + 1, 0).unwrap()];
668 let pool = VelodromeSlipstreamsState::new(
669 100_000_000_000_000_000_000u128,
670 sqrt_price,
671 3000,
672 0,
673 1,
674 tick,
675 ticks,
676 )
677 .expect("Failed to create pool");
678
679 let amount = I256::checked_from_sign_and_abs(Sign::Positive, U256::from(1000u64)).unwrap();
680 let result = pool.swap(true, amount, None);
681 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
682 }
683
684 fn token_pair() -> (Token, Token) {
685 let token_x = Token::new(&Bytes::from([0x01; 20]), "X", 18, 0, &[], Chain::Ethereum, 100);
686 let token_y = Token::new(&Bytes::from([0x02; 20]), "Y", 18, 0, &[], Chain::Ethereum, 100);
687 (token_x, token_y)
688 }
689
690 #[rstest]
691 fn test_query_pool_swap_target_price_lands_in_band(#[values(true, false)] sell_x: bool) {
692 let pool = create_basic_test_pool();
693 let (token_x, token_y) = token_pair();
694 let (token_in, token_out) =
695 if sell_x { (&token_x, &token_y) } else { (&token_y, &token_x) };
696 let target = 0.995;
697 let tolerance = 1e-4;
698 let params = target_price_params(
699 token_in,
700 token_out,
701 to_price(target, token_in, token_out),
702 tolerance,
703 );
704
705 let swap = pool.query_pool_swap(¶ms).unwrap();
706
707 let new_spot = swap
708 .new_state()
709 .spot_price(token_in, token_out)
710 .unwrap();
711 assert!(new_spot >= target && new_spot <= target * (1.0 + tolerance), "spot {new_spot}");
712 assert!(swap.price_points().is_none(), "the native path returns no price points");
713 let quote = pool
714 .get_amount_out(swap.amount_in().clone(), token_in, token_out)
715 .unwrap();
716 assert_eq!("e.amount, swap.amount_out());
717 }
718
719 #[test]
721 fn test_query_pool_swap_target_price_falls_back_to_search() {
722 let pool = create_basic_test_pool();
723 let (token_x, token_y) = token_pair();
724 let params =
725 target_price_params(&token_x, &token_y, to_price(1.0 - 1e-5, &token_x, &token_y), 1e-4);
726
727 let swap = pool.query_pool_swap(¶ms).unwrap();
728
729 assert!(swap.price_points().is_some(), "the numerical search returns price points");
730 }
731
732 #[test]
735 fn test_query_pool_swap_target_price_out_of_reach_falls_back_to_search() {
736 let sqrt_price = get_sqrt_ratio_at_tick(0).unwrap();
737 let ticks = vec![TickInfo::new(MIN_TICK, 0).unwrap(), TickInfo::new(-MIN_TICK, 0).unwrap()];
738 let pool =
739 VelodromeSlipstreamsState::new(u128::MAX, sqrt_price, 3000, 0, 1, 0, ticks).unwrap();
740 let (token_x, token_y) = token_pair();
741 let target = Price::new(BigUint::from(1u8), BigUint::from(1u8) << 100usize);
742 let target_f64 = 2f64.powi(-100);
743 let tolerance = 1e-4;
744 let params = target_price_params(&token_x, &token_y, target, tolerance);
745
746 let swap = pool.query_pool_swap(¶ms).unwrap();
747
748 let new_spot = swap
749 .new_state()
750 .spot_price(&token_x, &token_y)
751 .unwrap();
752 assert!(
753 new_spot >= target_f64 && new_spot <= target_f64 * (1.0 + tolerance),
754 "spot {new_spot} is outside the band of target {target_f64}"
755 );
756 assert!(swap.price_points().is_some(), "the numerical search returns price points");
757 }
758
759 #[test]
760 fn test_query_pool_swap_target_price_at_spot() {
761 let pool = create_basic_test_pool();
762 let (token_x, token_y) = token_pair();
763 let params = target_price_params(
764 &token_x,
765 &token_y,
766 Price::new(BigUint::from(1u64), BigUint::from(1u64)),
767 1e-4,
768 );
769
770 let swap = pool.query_pool_swap(¶ms).unwrap();
771
772 assert!(swap.amount_in().is_zero());
773 assert!(swap.amount_out().is_zero());
774 assert!(swap.new_state().eq(&pool));
775 }
776
777 #[test]
778 fn test_query_pool_swap_target_price_above_spot() {
779 let pool = create_basic_test_pool();
780 let (token_x, token_y) = token_pair();
781 let params =
782 target_price_params(&token_x, &token_y, to_price(1.01, &token_x, &token_y), 1e-4);
783
784 let result = pool.query_pool_swap(¶ms);
785
786 assert!(matches!(result, Err(SimulationError::InvalidInput(..))), "got {result:?}");
787 }
788}