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#[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    /// Creates a new instance of `AerodromeSlipstreamsState`.
47    ///
48    /// # Arguments
49    /// - `liquidity`: The initial liquidity of the pool.
50    /// - `sqrt_price`: The square root of the current price.
51    /// - `default_fee`: The default fee for the pool.
52    /// - `custom_fee`: The custom fee for the pool.
53    /// - `tick_spacing`: The tick spacing for the pool.
54    /// - `tick`: The current tick of the pool.
55    /// - `ticks`: A vector of `TickInfo` representing the tick information for the pool.
56    #[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 the pool has no liquidity, return zeros for both limits
296        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        // Iterate through all ticks in the direction of the swap
308        // Continues until there is no more liquidity in the pool or no more ticks to process
309        while let Ok((tick, initialized)) = self
310            .ticks
311            .next_initialized_tick_within_one_word(current_tick, zero_for_one)
312        {
313            // Clamp the tick value to ensure it's within valid range
314            let next_tick = tick.clamp(MIN_TICK, MAX_TICK);
315
316            // Calculate the sqrt price at the next tick boundary
317            let sqrt_price_next = get_sqrt_ratio_at_tick(next_tick)?;
318
319            // Calculate the amount of tokens swapped when moving from current_sqrt_price to
320            // sqrt_price_next. Direction determines which token is being swapped in vs out
321            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            // Accumulate total amounts for this tick range
352            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 this tick is "initialized" (meaning its someone's position boundary), update the
356            // liquidity when crossing it
357            // For zero_for_one, liquidity is removed when crossing a tick
358            // For one_for_zero, liquidity is added when crossing a tick
359            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            // Move to the next tick position
371            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        // apply attribute changes
385        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        // apply tick & observations changes
414        for (key, value) in delta.updated_attributes.iter() {
415            // tick liquidity keys are in the format "ticks/{tick_index}/net_liquidity"
416            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        // delete ticks - ignores deletes for attributes other than tick liquidity
429        for key in delta.deleted_attributes.iter() {
430            // tick liquidity keys are in the format "ticks/{tick_index}/net_liquidity"
431            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}