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::{Balances, GetAmountOutResult, ProtocolSim},
14 },
15 Bytes,
16};
17
18use crate::evm::protocol::{
19 safe_math::{safe_add_u256, safe_sub_u256},
20 u256_num::u256_to_biguint,
21 utils::uniswap::{
22 liquidity_math,
23 sqrt_price_math::{get_amount0_delta, get_amount1_delta, sqrt_price_q96_to_f64},
24 swap_math,
25 tick_list::{TickInfo, TickList, TickListErrorKind},
26 tick_math::{
27 get_sqrt_ratio_at_tick, get_tick_at_sqrt_ratio, MAX_SQRT_RATIO, MAX_TICK,
28 MIN_SQRT_RATIO, MIN_TICK,
29 },
30 StepComputation, SwapResults, SwapState,
31 },
32};
33
34const GAS_PER_TICK: u64 = 25_000;
36const GAS_PER_LOOP: u64 = 10_000;
38
39#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
40pub struct VelodromeSlipstreamsState {
41 liquidity: u128,
42 sqrt_price: U256,
43 default_fee: u32,
44 custom_fee: u32,
45 tick_spacing: i32,
46 tick: i32,
47 ticks: TickList,
48}
49
50impl VelodromeSlipstreamsState {
51 #[allow(clippy::too_many_arguments)]
62 pub fn new(
63 liquidity: u128,
64 sqrt_price: U256,
65 default_fee: u32,
66 custom_fee: u32,
67 tick_spacing: i32,
68 tick: i32,
69 ticks: Vec<TickInfo>,
70 ) -> Result<Self, SimulationError> {
71 let tick_list = TickList::from(tick_spacing as u16, ticks)?;
72 Ok(VelodromeSlipstreamsState {
73 liquidity,
74 sqrt_price,
75 default_fee,
76 custom_fee,
77 tick_spacing,
78 tick,
79 ticks: tick_list,
80 })
81 }
82
83 fn get_fee(&self) -> u32 {
84 if self.custom_fee > 0 {
85 self.custom_fee
86 } else {
87 self.default_fee
88 }
89 }
90
91 fn swap(
92 &self,
93 zero_for_one: bool,
94 amount_specified: I256,
95 sqrt_price_limit: Option<U256>,
96 ) -> Result<SwapResults, SimulationError> {
97 if self.liquidity == 0 {
98 return Err(SimulationError::RecoverableError("No liquidity".to_string()));
99 }
100 let price_limit = if let Some(limit) = sqrt_price_limit {
101 limit
102 } else if zero_for_one {
103 safe_add_u256(MIN_SQRT_RATIO, U256::from(1u64))?
104 } else {
105 safe_sub_u256(MAX_SQRT_RATIO, U256::from(1u64))?
106 };
107
108 let price_limit_valid = if zero_for_one {
109 price_limit > MIN_SQRT_RATIO && price_limit < self.sqrt_price
110 } else {
111 price_limit < MAX_SQRT_RATIO && price_limit > self.sqrt_price
112 };
113 if !price_limit_valid {
114 return Err(SimulationError::InvalidInput("Price limit out of range".into(), None));
115 }
116
117 let exact_input = amount_specified > I256::from_raw(U256::from(0u64));
118
119 let mut state = SwapState {
120 amount_remaining: amount_specified,
121 amount_calculated: I256::from_raw(U256::from(0u64)),
122 sqrt_price: self.sqrt_price,
123 tick: self.tick,
124 liquidity: self.liquidity,
125 };
126 let mut gas_used = U256::from(130_000);
127
128 let fee = self.get_fee();
129 while state.amount_remaining != I256::from_raw(U256::from(0u64)) &&
130 state.sqrt_price != price_limit
131 {
132 let (mut next_tick, initialized) = match self
133 .ticks
134 .next_initialized_tick_within_one_word(state.tick, zero_for_one)
135 {
136 Ok((tick, init)) => (tick, init),
137 Err(tick_err) => match tick_err.kind {
138 TickListErrorKind::TicksExeeded => {
139 let mut new_state = self.clone();
140 new_state.liquidity = state.liquidity;
141 new_state.tick = state.tick;
142 new_state.sqrt_price = state.sqrt_price;
143 return Err(SimulationError::InvalidInput(
144 "Ticks exceeded".into(),
145 Some(GetAmountOutResult::new(
146 u256_to_biguint(state.amount_calculated.abs().into_raw()),
147 u256_to_biguint(gas_used),
148 Box::new(new_state),
149 )),
150 ));
151 }
152 _ => return Err(SimulationError::FatalError("Unknown error".to_string())),
153 },
154 };
155
156 next_tick = next_tick.clamp(MIN_TICK, MAX_TICK);
157
158 let sqrt_price_start = state.sqrt_price;
159 let sqrt_price_next = get_sqrt_ratio_at_tick(next_tick)?;
160 let (sqrt_price, amount_in, amount_out, fee_amount) = swap_math::compute_swap_step(
161 state.sqrt_price,
162 VelodromeSlipstreamsState::get_sqrt_ratio_target(
163 sqrt_price_next,
164 price_limit,
165 zero_for_one,
166 ),
167 state.liquidity,
168 state.amount_remaining,
169 fee,
170 )?;
171 state.sqrt_price = sqrt_price;
172
173 let step = StepComputation {
174 sqrt_price_start,
175 tick_next: next_tick,
176 initialized,
177 sqrt_price_next,
178 amount_in,
179 amount_out,
180 fee_amount,
181 };
182 if exact_input {
183 state.amount_remaining -= I256::checked_from_sign_and_abs(
184 Sign::Positive,
185 safe_add_u256(step.amount_in, step.fee_amount)?,
186 )
187 .unwrap();
188 state.amount_calculated -=
189 I256::checked_from_sign_and_abs(Sign::Positive, step.amount_out).unwrap();
190 } else {
191 state.amount_remaining +=
192 I256::checked_from_sign_and_abs(Sign::Positive, step.amount_out).unwrap();
193 state.amount_calculated += I256::checked_from_sign_and_abs(
194 Sign::Positive,
195 safe_add_u256(step.amount_in, step.fee_amount)?,
196 )
197 .unwrap();
198 }
199 if state.sqrt_price == step.sqrt_price_next {
200 if step.initialized {
201 let liquidity_raw = self
202 .ticks
203 .get_tick(step.tick_next)
204 .unwrap()
205 .net_liquidity;
206 let liquidity_net = if zero_for_one { -liquidity_raw } else { liquidity_raw };
207 state.liquidity =
208 liquidity_math::add_liquidity_delta(state.liquidity, liquidity_net)?;
209 gas_used = safe_add_u256(gas_used, U256::from(GAS_PER_TICK))?;
210 }
211 state.tick = if zero_for_one { step.tick_next - 1 } else { step.tick_next };
212 } else if state.sqrt_price != step.sqrt_price_start {
213 state.tick = get_tick_at_sqrt_ratio(state.sqrt_price)?;
214 }
215 gas_used = safe_add_u256(gas_used, U256::from(GAS_PER_LOOP))?;
216 }
217 Ok(SwapResults {
218 amount_calculated: state.amount_calculated,
219 amount_specified,
220 amount_remaining: state.amount_remaining,
221 sqrt_price: state.sqrt_price,
222 liquidity: state.liquidity,
223 tick: state.tick,
224 gas_used,
225 })
226 }
227
228 fn get_sqrt_ratio_target(
229 sqrt_price_next: U256,
230 sqrt_price_limit: U256,
231 zero_for_one: bool,
232 ) -> U256 {
233 let cond1 = if zero_for_one {
234 sqrt_price_next < sqrt_price_limit
235 } else {
236 sqrt_price_next > sqrt_price_limit
237 };
238
239 if cond1 {
240 sqrt_price_limit
241 } else {
242 sqrt_price_next
243 }
244 }
245}
246
247#[typetag::serde]
248impl ProtocolSim for VelodromeSlipstreamsState {
249 fn fee(&self) -> f64 {
250 self.get_fee() as f64 / 1_000_000.0
251 }
252
253 fn spot_price(&self, a: &Token, b: &Token) -> Result<f64, SimulationError> {
254 if a < b {
255 sqrt_price_q96_to_f64(self.sqrt_price, a.decimals, b.decimals)
256 } else {
257 sqrt_price_q96_to_f64(self.sqrt_price, b.decimals, a.decimals)
258 .map(|price| 1.0f64 / price)
259 }
260 }
261
262 fn get_amount_out(
263 &self,
264 amount_in: BigUint,
265 token_a: &Token,
266 token_b: &Token,
267 ) -> Result<GetAmountOutResult, SimulationError> {
268 let zero_for_one = token_a < token_b;
269 let amount_specified = I256::checked_from_sign_and_abs(
270 Sign::Positive,
271 U256::from_be_slice(&amount_in.to_bytes_be()),
272 )
273 .ok_or_else(|| {
274 SimulationError::InvalidInput("I256 overflow: amount_in".to_string(), None)
275 })?;
276
277 let result = self.swap(zero_for_one, amount_specified, None)?;
278
279 trace!(?amount_in, ?token_a, ?token_b, ?zero_for_one, ?result, "SLIPSTREAMS SWAP");
280 let mut new_state = self.clone();
281 new_state.liquidity = result.liquidity;
282 new_state.tick = result.tick;
283 new_state.sqrt_price = result.sqrt_price;
284
285 Ok(GetAmountOutResult::new(
286 u256_to_biguint(
287 result
288 .amount_calculated
289 .abs()
290 .into_raw(),
291 ),
292 u256_to_biguint(result.gas_used),
293 Box::new(new_state),
294 ))
295 }
296
297 fn get_limits(
298 &self,
299 token_in: Bytes,
300 token_out: Bytes,
301 ) -> Result<(BigUint, BigUint), SimulationError> {
302 if self.liquidity == 0 {
304 return Ok((BigUint::zero(), BigUint::zero()));
305 }
306
307 let zero_for_one = token_in < token_out;
308 let mut current_tick = self.tick;
309 let mut current_sqrt_price = self.sqrt_price;
310 let mut current_liquidity = self.liquidity;
311 let mut total_amount_in = U256::from(0u64);
312 let mut total_amount_out = U256::from(0u64);
313
314 while let Ok((tick, initialized)) = self
317 .ticks
318 .next_initialized_tick_within_one_word(current_tick, zero_for_one)
319 {
320 let next_tick = tick.clamp(MIN_TICK, MAX_TICK);
322
323 let sqrt_price_next = get_sqrt_ratio_at_tick(next_tick)?;
325
326 let (amount_in, amount_out) = if zero_for_one {
329 let amount0 = get_amount0_delta(
330 sqrt_price_next,
331 current_sqrt_price,
332 current_liquidity,
333 true,
334 )?;
335 let amount1 = get_amount1_delta(
336 sqrt_price_next,
337 current_sqrt_price,
338 current_liquidity,
339 false,
340 )?;
341 (amount0, amount1)
342 } else {
343 let amount0 = get_amount0_delta(
344 sqrt_price_next,
345 current_sqrt_price,
346 current_liquidity,
347 false,
348 )?;
349 let amount1 = get_amount1_delta(
350 sqrt_price_next,
351 current_sqrt_price,
352 current_liquidity,
353 true,
354 )?;
355 (amount1, amount0)
356 };
357
358 total_amount_in = safe_add_u256(total_amount_in, amount_in)?;
360 total_amount_out = safe_add_u256(total_amount_out, amount_out)?;
361
362 if initialized {
367 let liquidity_raw = self
368 .ticks
369 .get_tick(next_tick)
370 .unwrap()
371 .net_liquidity;
372 let liquidity_delta = if zero_for_one { -liquidity_raw } else { liquidity_raw };
373 current_liquidity =
374 liquidity_math::add_liquidity_delta(current_liquidity, liquidity_delta)?;
375 }
376
377 current_tick = if zero_for_one { next_tick - 1 } else { next_tick };
379 current_sqrt_price = sqrt_price_next;
380 }
381
382 Ok((u256_to_biguint(total_amount_in), u256_to_biguint(total_amount_out)))
383 }
384
385 fn delta_transition(
386 &mut self,
387 delta: ProtocolStateDelta,
388 _tokens: &HashMap<Bytes, Token>,
389 _balances: &Balances,
390 ) -> Result<(), TransitionError> {
391 if let Some(liquidity) = delta
393 .updated_attributes
394 .get("liquidity")
395 {
396 self.liquidity = u128::from(liquidity.clone());
397 }
398 if let Some(sqrt_price) = delta
399 .updated_attributes
400 .get("sqrt_price_x96")
401 {
402 self.sqrt_price = U256::from_be_slice(sqrt_price);
403 }
404 if let Some(default_fee) = delta
405 .updated_attributes
406 .get("default_fee")
407 {
408 self.default_fee = u32::from(default_fee.clone());
409 }
410 if let Some(custom_fee) = delta
411 .updated_attributes
412 .get("custom_fee")
413 {
414 self.custom_fee = u32::from(custom_fee.clone());
415 }
416 if let Some(tick) = delta.updated_attributes.get("tick") {
417 self.tick = i32::from(tick.clone());
418 }
419
420 for (key, value) in delta.updated_attributes.iter() {
422 if key.starts_with("ticks/") {
424 let parts: Vec<&str> = key.split('/').collect();
425 self.ticks
426 .set_tick_liquidity(
427 parts[1]
428 .parse::<i32>()
429 .map_err(|err| TransitionError::DecodeError(err.to_string()))?,
430 i128::from(value.clone()),
431 )
432 .map_err(|err| TransitionError::DecodeError(err.to_string()))?;
433 }
434 }
435 for key in delta.deleted_attributes.iter() {
437 if key.starts_with("ticks/") {
439 let parts: Vec<&str> = key.split('/').collect();
440 self.ticks
441 .set_tick_liquidity(
442 parts[1]
443 .parse::<i32>()
444 .map_err(|err| TransitionError::DecodeError(err.to_string()))?,
445 0,
446 )
447 .map_err(|err| TransitionError::DecodeError(err.to_string()))?;
448 }
449 }
450 Ok(())
451 }
452
453 fn query_pool_swap(
454 &self,
455 params: &tycho_common::simulation::protocol_sim::QueryPoolSwapParams,
456 ) -> Result<tycho_common::simulation::protocol_sim::PoolSwap, SimulationError> {
457 crate::evm::query_pool_swap::query_pool_swap(self, params)
458 }
459
460 fn clone_box(&self) -> Box<dyn ProtocolSim> {
461 Box::new(self.clone())
462 }
463
464 fn as_any(&self) -> &dyn Any {
465 self
466 }
467
468 fn as_any_mut(&mut self) -> &mut dyn Any {
469 self
470 }
471
472 fn eq(&self, other: &dyn ProtocolSim) -> bool {
473 if let Some(other_state) = other
474 .as_any()
475 .downcast_ref::<VelodromeSlipstreamsState>()
476 {
477 self.liquidity == other_state.liquidity &&
478 self.sqrt_price == other_state.sqrt_price &&
479 self.get_fee() == other_state.get_fee() &&
480 self.tick == other_state.tick &&
481 self.ticks == other_state.ticks
482 } else {
483 false
484 }
485 }
486}
487
488#[cfg(test)]
489mod tests {
490 use alloy::primitives::{Sign, I256, U256};
491 use tycho_common::simulation::errors::SimulationError;
492
493 use super::*;
494 use crate::evm::protocol::utils::uniswap::{
495 tick_list::TickInfo,
496 tick_math::{
497 get_sqrt_ratio_at_tick, get_tick_at_sqrt_ratio, MAX_SQRT_RATIO, MIN_SQRT_RATIO,
498 MIN_TICK,
499 },
500 };
501
502 fn create_basic_test_pool() -> VelodromeSlipstreamsState {
503 let sqrt_price = get_sqrt_ratio_at_tick(0).expect("Failed to calculate sqrt price");
504 let ticks = vec![TickInfo::new(-120, 0).unwrap(), TickInfo::new(120, 0).unwrap()];
505 VelodromeSlipstreamsState::new(
506 100_000_000_000_000_000_000u128,
507 sqrt_price,
508 3000,
509 0,
510 1,
511 0,
512 ticks,
513 )
514 .expect("Failed to create pool")
515 }
516
517 #[test]
518 fn test_partial_step_updates_tick_when_price_moves_without_crossing_initialized_tick() {
519 let pool = create_basic_test_pool();
520 let amount =
521 I256::checked_from_sign_and_abs(Sign::Positive, U256::from(100_000_000_000_000_000u64))
522 .unwrap();
523
524 let result = pool
525 .swap(true, amount, None)
526 .expect("swap should stay within the current liquidity range");
527 let expected_tick =
528 get_tick_at_sqrt_ratio(result.sqrt_price).expect("new sqrt price should map to a tick");
529
530 assert_ne!(result.sqrt_price, pool.sqrt_price);
531 assert_ne!(result.sqrt_price, get_sqrt_ratio_at_tick(-120).unwrap());
532 assert_ne!(expected_tick, pool.tick);
533 assert_eq!(result.tick, expected_tick);
534 }
535
536 #[test]
537 fn test_swap_keeps_boundary_tick_when_price_does_not_move() {
538 let mut pool = create_basic_test_pool();
539 pool.tick = -1;
540 let amount = I256::checked_from_sign_and_abs(Sign::Positive, U256::from(1u64)).unwrap();
541
542 let result = pool
543 .swap(true, amount, None)
544 .expect("swap should consume the input as fee without moving price");
545
546 assert_eq!(result.sqrt_price, pool.sqrt_price);
547 assert_eq!(get_tick_at_sqrt_ratio(result.sqrt_price).unwrap(), 0);
548 assert_eq!(result.tick, pool.tick);
549 }
550
551 #[test]
552 fn test_swap_price_limit_out_of_range_returns_error() {
553 let pool = create_basic_test_pool();
554 let amount = I256::checked_from_sign_and_abs(Sign::Positive, U256::from(1000u64)).unwrap();
555
556 let result = pool.swap(true, amount, Some(pool.sqrt_price));
557 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
558
559 let result = pool.swap(true, amount, Some(MIN_SQRT_RATIO));
560 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
561
562 let result = pool.swap(false, amount, Some(pool.sqrt_price));
563 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
564
565 let result = pool.swap(false, amount, Some(MAX_SQRT_RATIO));
566 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
567 }
568
569 #[test]
570 fn test_swap_at_extreme_price_returns_error() {
571 let sqrt_price = MIN_SQRT_RATIO + U256::from(1u64);
572 let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
573 let ticks =
574 vec![TickInfo::new(MIN_TICK, 0).unwrap(), TickInfo::new(MIN_TICK + 1, 0).unwrap()];
575 let pool = VelodromeSlipstreamsState::new(
576 100_000_000_000_000_000_000u128,
577 sqrt_price,
578 3000,
579 0,
580 1,
581 tick,
582 ticks,
583 )
584 .expect("Failed to create pool");
585
586 let amount = I256::checked_from_sign_and_abs(Sign::Positive, U256::from(1000u64)).unwrap();
587 let result = pool.swap(true, amount, None);
588 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
589 }
590}