Skip to main content

tycho_simulation/evm/protocol/velodrome_slipstreams/
state.rs

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// The names of the constants reflect the exact method from the tenderly log.
35const GAS_PER_TICK: u64 = 25_000;
36// nextInitializedTickWithinOneWord +  computeSwapStep + calculateFees
37const 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    /// Creates a new instance of `AerodromeSlipstreamsState`.
52    ///
53    /// # Arguments
54    /// - `liquidity`: The initial liquidity of the pool.
55    /// - `sqrt_price`: The square root of the current price.
56    /// - `default_fee`: The default fee for the pool.
57    /// - `custom_fee`: The custom fee for the pool.
58    /// - `tick_spacing`: The tick spacing for the pool.
59    /// - `tick`: The current tick of the pool.
60    /// - `ticks`: A vector of `TickInfo` representing the tick information for the pool.
61    #[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 the pool has no liquidity, return zeros for both limits
303        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        // Iterate through all ticks in the direction of the swap
315        // Continues until there is no more liquidity in the pool or no more ticks to process
316        while let Ok((tick, initialized)) = self
317            .ticks
318            .next_initialized_tick_within_one_word(current_tick, zero_for_one)
319        {
320            // Clamp the tick value to ensure it's within valid range
321            let next_tick = tick.clamp(MIN_TICK, MAX_TICK);
322
323            // Calculate the sqrt price at the next tick boundary
324            let sqrt_price_next = get_sqrt_ratio_at_tick(next_tick)?;
325
326            // Calculate the amount of tokens swapped when moving from current_sqrt_price to
327            // sqrt_price_next. Direction determines which token is being swapped in vs out
328            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            // Accumulate total amounts for this tick range
359            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 this tick is "initialized" (meaning its someone's position boundary), update the
363            // liquidity when crossing it
364            // For zero_for_one, liquidity is removed when crossing a tick
365            // For one_for_zero, liquidity is added when crossing a tick
366            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            // Move to the next tick position
378            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        // apply attribute changes
392        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        // apply tick & observations changes
421        for (key, value) in delta.updated_attributes.iter() {
422            // tick liquidity keys are in the format "ticks/{tick_index}/net_liquidity"
423            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        // delete ticks - ignores deletes for attributes other than tick liquidity
436        for key in delta.deleted_attributes.iter() {
437            // tick liquidity keys are in the format "ticks/{tick_index}/net_liquidity"
438            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}