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