Skip to main content

tycho_simulation/evm/protocol/aerodrome_slipstreams/
state.rs

1use std::{any::Any, collections::HashMap};
2
3use alloy::primitives::{Sign, I256, U256};
4use num_bigint::{BigInt, BigUint};
5use num_traits::{ToPrimitive, Zero};
6use serde::{Deserialize, Serialize};
7use tracing::{error, 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::{
22        add_fee_markup,
23        slipstreams::{
24            dynamic_fee_module::{get_dynamic_fee, DynamicFeeConfig},
25            observations::{Observation, Observations},
26        },
27        uniswap::{
28            i24_be_bytes_to_i32, liquidity_math,
29            sqrt_price_math::{get_amount0_delta, get_amount1_delta, sqrt_price_q96_to_f64},
30            swap_math,
31            tick_list::{TickInfo, TickList, TickListErrorKind},
32            tick_math::{
33                get_sqrt_ratio_at_tick, get_tick_at_sqrt_ratio, MAX_SQRT_RATIO, MAX_TICK,
34                MIN_SQRT_RATIO, MIN_TICK,
35            },
36            StepComputation, SwapResults, SwapState,
37        },
38    },
39};
40
41// The names of the constants reflect the exact method from the tenderly log.
42const TICK_CROSSING_GAS_COST: i32 = 25_000;
43// nextInitializedTickWithinOneWord +  computeSwapStep + calculateFees
44const LOOP_GAS_COST: i32 = 10_000;
45
46#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
47pub struct AerodromeSlipstreamsState {
48    id: String,
49    block_timestamp: u64,
50    liquidity: u128,
51    sqrt_price: U256,
52    observation_index: u16,
53    observation_cardinality: u16,
54    default_fee: u32,
55    tick_spacing: i32,
56    tick: i32,
57    ticks: TickList,
58    observations: Observations,
59    dfc: DynamicFeeConfig,
60}
61
62impl AerodromeSlipstreamsState {
63    /// Creates a new instance of `AerodromeSlipstreamsState`.
64    ///
65    /// # Arguments
66    /// - `id`: The id of the protocol component.
67    /// - `block_timestamp`: The timestamp of the block.
68    /// - `liquidity`: The initial liquidity of the pool.
69    /// - `sqrt_price`: The square root of the current price.
70    /// - `observation_index`: The index of the current observation.
71    /// - `observation_cardinality`: The cardinality of the observation.
72    /// - `default_fee`: The default fee for the pool.
73    /// - `tick_spacing`: The tick spacing for the pool.
74    /// - `tick`: The current tick of the pool.
75    /// - `ticks`: A vector of `TickInfo` representing the tick information for the pool.
76    /// - `observations`: A vector of `Observation` representing the observation information for the
77    ///   pool.
78    /// - `dfc`: The dynamic fee configuration for the pool.
79    #[allow(clippy::too_many_arguments)]
80    pub fn new(
81        id: String,
82        block_timestamp: u64,
83        liquidity: u128,
84        sqrt_price: U256,
85        observation_index: u16,
86        observation_cardinality: u16,
87        default_fee: u32,
88        tick_spacing: i32,
89        tick: i32,
90        ticks: Vec<TickInfo>,
91        observations: Vec<Observation>,
92        dfc: DynamicFeeConfig,
93    ) -> Result<Self, SimulationError> {
94        let tick_list = TickList::from(tick_spacing as u16, ticks)?;
95        Ok(AerodromeSlipstreamsState {
96            id,
97            block_timestamp,
98            liquidity,
99            sqrt_price,
100            observation_index,
101            observation_cardinality,
102            default_fee,
103            tick_spacing,
104            tick,
105            ticks: tick_list,
106            observations: Observations::new(observations),
107            dfc,
108        })
109    }
110
111    fn get_fee(&self) -> Result<u32, SimulationError> {
112        get_dynamic_fee(
113            &self.dfc,
114            self.default_fee,
115            self.tick,
116            self.liquidity,
117            self.observation_index,
118            self.observation_cardinality,
119            &self.observations,
120            self.block_timestamp as u32,
121        )
122    }
123
124    fn swap(
125        &self,
126        zero_for_one: bool,
127        amount_specified: I256,
128        sqrt_price_limit: Option<U256>,
129    ) -> Result<SwapResults, SimulationError> {
130        if self.liquidity == 0 {
131            return Err(SimulationError::RecoverableError("No liquidity".to_string()));
132        }
133        let price_limit = if let Some(limit) = sqrt_price_limit {
134            limit
135        } else if zero_for_one {
136            safe_add_u256(MIN_SQRT_RATIO, U256::from(1u64))?
137        } else {
138            safe_sub_u256(MAX_SQRT_RATIO, U256::from(1u64))?
139        };
140
141        let price_limit_valid = if zero_for_one {
142            price_limit > MIN_SQRT_RATIO && price_limit < self.sqrt_price
143        } else {
144            price_limit < MAX_SQRT_RATIO && price_limit > self.sqrt_price
145        };
146        if !price_limit_valid {
147            return Err(SimulationError::InvalidInput("Price limit out of range".into(), None));
148        }
149
150        let exact_input = amount_specified > I256::from_raw(U256::from(0u64));
151
152        let mut state = SwapState {
153            amount_remaining: amount_specified,
154            amount_calculated: I256::from_raw(U256::from(0u64)),
155            sqrt_price: self.sqrt_price,
156            tick: self.tick,
157            liquidity: self.liquidity,
158        };
159        let mut gas_used = U256::from(130_000);
160
161        let fee = self.get_fee()?;
162        while state.amount_remaining != I256::from_raw(U256::from(0u64)) &&
163            state.sqrt_price != price_limit
164        {
165            let (mut next_tick, initialized) = match self
166                .ticks
167                .next_initialized_tick_within_one_word(state.tick, zero_for_one)
168            {
169                Ok((tick, init)) => (tick, init),
170                Err(tick_err) => match tick_err.kind {
171                    TickListErrorKind::TicksExeeded => {
172                        let mut new_state = self.clone();
173                        new_state.liquidity = state.liquidity;
174                        new_state.tick = state.tick;
175                        new_state.sqrt_price = state.sqrt_price;
176                        return Err(SimulationError::InvalidInput(
177                            "Ticks exceeded".into(),
178                            Some(GetAmountOutResult::new(
179                                u256_to_biguint(state.amount_calculated.abs().into_raw()),
180                                u256_to_biguint(gas_used),
181                                Box::new(new_state),
182                            )),
183                        ));
184                    }
185                    _ => return Err(SimulationError::FatalError("Unknown error".to_string())),
186                },
187            };
188
189            next_tick = next_tick.clamp(MIN_TICK, MAX_TICK);
190
191            let sqrt_price_start = state.sqrt_price;
192            let sqrt_price_next = get_sqrt_ratio_at_tick(next_tick)?;
193            let (sqrt_price, amount_in, amount_out, fee_amount) = swap_math::compute_swap_step(
194                state.sqrt_price,
195                AerodromeSlipstreamsState::get_sqrt_ratio_target(
196                    sqrt_price_next,
197                    price_limit,
198                    zero_for_one,
199                ),
200                state.liquidity,
201                state.amount_remaining,
202                fee,
203            )?;
204            state.sqrt_price = sqrt_price;
205
206            let step = StepComputation {
207                sqrt_price_start,
208                tick_next: next_tick,
209                initialized,
210                sqrt_price_next,
211                amount_in,
212                amount_out,
213                fee_amount,
214            };
215            if exact_input {
216                state.amount_remaining -= I256::checked_from_sign_and_abs(
217                    Sign::Positive,
218                    safe_add_u256(step.amount_in, step.fee_amount)?,
219                )
220                .unwrap();
221                state.amount_calculated -=
222                    I256::checked_from_sign_and_abs(Sign::Positive, step.amount_out).unwrap();
223            } else {
224                state.amount_remaining +=
225                    I256::checked_from_sign_and_abs(Sign::Positive, step.amount_out).unwrap();
226                state.amount_calculated += I256::checked_from_sign_and_abs(
227                    Sign::Positive,
228                    safe_add_u256(step.amount_in, step.fee_amount)?,
229                )
230                .unwrap();
231            }
232            if state.sqrt_price == step.sqrt_price_next {
233                if step.initialized {
234                    let liquidity_raw = self
235                        .ticks
236                        .get_tick(step.tick_next)
237                        .unwrap()
238                        .net_liquidity;
239                    let liquidity_net = if zero_for_one { -liquidity_raw } else { liquidity_raw };
240                    state.liquidity =
241                        liquidity_math::add_liquidity_delta(state.liquidity, liquidity_net)?;
242                    gas_used = safe_add_u256(gas_used, U256::from(TICK_CROSSING_GAS_COST))?;
243                }
244                state.tick = if zero_for_one { step.tick_next - 1 } else { step.tick_next };
245            } else if state.sqrt_price != step.sqrt_price_start {
246                state.tick = get_tick_at_sqrt_ratio(state.sqrt_price)?;
247            }
248            gas_used = safe_add_u256(gas_used, U256::from(LOOP_GAS_COST))?;
249        }
250        Ok(SwapResults {
251            amount_calculated: state.amount_calculated,
252            amount_specified,
253            amount_remaining: state.amount_remaining,
254            sqrt_price: state.sqrt_price,
255            liquidity: state.liquidity,
256            tick: state.tick,
257            gas_used,
258        })
259    }
260
261    fn get_sqrt_ratio_target(
262        sqrt_price_next: U256,
263        sqrt_price_limit: U256,
264        zero_for_one: bool,
265    ) -> U256 {
266        let cond1 = if zero_for_one {
267            sqrt_price_next < sqrt_price_limit
268        } else {
269            sqrt_price_next > sqrt_price_limit
270        };
271
272        if cond1 {
273            sqrt_price_limit
274        } else {
275            sqrt_price_next
276        }
277    }
278}
279
280#[typetag::serde]
281impl ProtocolSim for AerodromeSlipstreamsState {
282    fn fee(&self) -> f64 {
283        match self.get_fee() {
284            Ok(fee) => fee as f64 / 1_000_000.0,
285            Err(err) => {
286                error!(
287                    pool = %self.id,
288                    block_timestamp = self.block_timestamp,
289                    %err,
290                    "Error while calculating dynamic fee"
291                );
292                f64::MAX / 1_000_000.0
293            }
294        }
295    }
296
297    fn spot_price(&self, a: &Token, b: &Token) -> Result<f64, SimulationError> {
298        let price = if a < b {
299            sqrt_price_q96_to_f64(self.sqrt_price, a.decimals, b.decimals)?
300        } else {
301            1.0f64 / sqrt_price_q96_to_f64(self.sqrt_price, b.decimals, a.decimals)?
302        };
303        Ok(add_fee_markup(price, self.get_fee()? as f64 / 1_000_000.0))
304    }
305
306    fn get_amount_out(
307        &self,
308        amount_in: BigUint,
309        token_a: &Token,
310        token_b: &Token,
311    ) -> Result<GetAmountOutResult, SimulationError> {
312        let zero_for_one = token_a < token_b;
313        let amount_specified = I256::checked_from_sign_and_abs(
314            Sign::Positive,
315            U256::from_be_slice(&amount_in.to_bytes_be()),
316        )
317        .ok_or_else(|| {
318            SimulationError::InvalidInput("I256 overflow: amount_in".to_string(), None)
319        })?;
320
321        let result = self.swap(zero_for_one, amount_specified, None)?;
322
323        trace!(?amount_in, ?token_a, ?token_b, ?zero_for_one, ?result, "SLIPSTREAMS SWAP");
324        let mut new_state = self.clone();
325        new_state.liquidity = result.liquidity;
326        new_state.tick = result.tick;
327        new_state.sqrt_price = result.sqrt_price;
328
329        Ok(GetAmountOutResult::new(
330            u256_to_biguint(
331                result
332                    .amount_calculated
333                    .abs()
334                    .into_raw(),
335            ),
336            u256_to_biguint(result.gas_used),
337            Box::new(new_state),
338        ))
339    }
340
341    fn get_limits(
342        &self,
343        token_in: Bytes,
344        token_out: Bytes,
345    ) -> Result<(BigUint, BigUint), SimulationError> {
346        // If the pool has no liquidity, return zeros for both limits
347        if self.liquidity == 0 {
348            return Ok((BigUint::zero(), BigUint::zero()));
349        }
350
351        let zero_for_one = token_in < token_out;
352        let mut current_tick = self.tick;
353        let mut current_sqrt_price = self.sqrt_price;
354        let mut current_liquidity = self.liquidity;
355        let mut total_amount_in = U256::from(0u64);
356        let mut total_amount_out = U256::from(0u64);
357
358        // Iterate through all ticks in the direction of the swap
359        // Continues until there is no more liquidity in the pool or no more ticks to process
360        while let Ok((tick, initialized)) = self
361            .ticks
362            .next_initialized_tick_within_one_word(current_tick, zero_for_one)
363        {
364            // Clamp the tick value to ensure it's within valid range
365            let next_tick = tick.clamp(MIN_TICK, MAX_TICK);
366
367            // Calculate the sqrt price at the next tick boundary
368            let sqrt_price_next = get_sqrt_ratio_at_tick(next_tick)?;
369
370            // Calculate the amount of tokens swapped when moving from current_sqrt_price to
371            // sqrt_price_next. Direction determines which token is being swapped in vs out
372            let (amount_in, amount_out) = if zero_for_one {
373                let amount0 = get_amount0_delta(
374                    sqrt_price_next,
375                    current_sqrt_price,
376                    current_liquidity,
377                    true,
378                )?;
379                let amount1 = get_amount1_delta(
380                    sqrt_price_next,
381                    current_sqrt_price,
382                    current_liquidity,
383                    false,
384                )?;
385                (amount0, amount1)
386            } else {
387                let amount0 = get_amount0_delta(
388                    sqrt_price_next,
389                    current_sqrt_price,
390                    current_liquidity,
391                    false,
392                )?;
393                let amount1 = get_amount1_delta(
394                    sqrt_price_next,
395                    current_sqrt_price,
396                    current_liquidity,
397                    true,
398                )?;
399                (amount1, amount0)
400            };
401
402            // Accumulate total amounts for this tick range
403            total_amount_in = safe_add_u256(total_amount_in, amount_in)?;
404            total_amount_out = safe_add_u256(total_amount_out, amount_out)?;
405
406            // If this tick is "initialized" (meaning its someone's position boundary), update the
407            // liquidity when crossing it
408            // For zero_for_one, liquidity is removed when crossing a tick
409            // For one_for_zero, liquidity is added when crossing a tick
410            if initialized {
411                let liquidity_raw = self
412                    .ticks
413                    .get_tick(next_tick)
414                    .unwrap()
415                    .net_liquidity;
416                let liquidity_delta = if zero_for_one { -liquidity_raw } else { liquidity_raw };
417                current_liquidity =
418                    liquidity_math::add_liquidity_delta(current_liquidity, liquidity_delta)?;
419            }
420
421            // Move to the next tick position
422            current_tick = if zero_for_one { next_tick - 1 } else { next_tick };
423            current_sqrt_price = sqrt_price_next;
424        }
425
426        Ok((u256_to_biguint(total_amount_in), u256_to_biguint(total_amount_out)))
427    }
428
429    fn delta_transition(
430        &mut self,
431        delta: ProtocolStateDelta,
432        _tokens: &HashMap<Bytes, Token>,
433        _balances: &Balances,
434    ) -> Result<(), TransitionError> {
435        if let Some(block_timestamp) = delta
436            .updated_attributes
437            .get("block_timestamp")
438        {
439            self.block_timestamp = BigInt::from_signed_bytes_be(block_timestamp)
440                .to_u64()
441                .unwrap();
442        }
443        // apply attribute changes
444        if let Some(liquidity) = delta
445            .updated_attributes
446            .get("liquidity")
447        {
448            // This is a hotfix because if the liquidity has never been updated after creation, it's
449            // currently encoded as H256::zero(), therefore, we can't decode this as u128.
450            // We can remove this once it has been fixed on the tycho side.
451            let liq_16_bytes = if liquidity.len() == 32 {
452                // Make sure it only happens for 0 values, otherwise error.
453                if liquidity == &Bytes::zero(32) {
454                    Bytes::from([0; 16])
455                } else {
456                    return Err(TransitionError::DecodeError(format!(
457                        "Liquidity bytes too long for {liquidity}, expected 16",
458                    )));
459                }
460            } else {
461                liquidity.clone()
462            };
463
464            self.liquidity = u128::from(liq_16_bytes);
465        }
466        if let Some(sqrt_price) = delta
467            .updated_attributes
468            .get("sqrt_price_x96")
469        {
470            self.sqrt_price = U256::from_be_slice(sqrt_price);
471        }
472        if let Some(observation_index) = delta
473            .updated_attributes
474            .get("observationIndex")
475        {
476            self.observation_index = u16::from(observation_index.clone());
477        }
478        if let Some(observation_cardinality) = delta
479            .updated_attributes
480            .get("observationCardinality")
481        {
482            self.observation_cardinality = u16::from(observation_cardinality.clone());
483        }
484        if let Some(default_fee) = delta
485            .updated_attributes
486            .get("default_fee")
487        {
488            self.default_fee = u32::from(default_fee.clone());
489        }
490        if let Some(dfc_base_fee) = delta
491            .updated_attributes
492            .get("dfc_baseFee")
493        {
494            self.dfc
495                .update_base_fee(u32::from(dfc_base_fee.clone()));
496        }
497        if let Some(dfc_fee_cap) = delta
498            .updated_attributes
499            .get("dfc_feeCap")
500        {
501            self.dfc
502                .update_fee_cap(u32::from(dfc_fee_cap.clone()));
503        }
504        if let Some(dfc_scaling_factor) = delta
505            .updated_attributes
506            .get("dfc_scalingFactor")
507        {
508            self.dfc
509                .update_scaling_factor(u64::from(dfc_scaling_factor.clone()));
510        }
511        if let Some(tick) = delta.updated_attributes.get("tick") {
512            // This is a hotfix because if the tick has never been updated after creation, it's
513            // currently encoded as H256::zero(), therefore, we can't decode this as i32.
514            // We can remove this once it has been fixed on the tycho side.
515            let ticks_4_bytes = if tick.len() == 32 {
516                // Make sure it only happens for 0 values, otherwise error.
517                if tick == &Bytes::zero(32) {
518                    Bytes::from([0; 4])
519                } else {
520                    return Err(TransitionError::DecodeError(format!(
521                        "Tick bytes too long for {tick}, expected 4"
522                    )));
523                }
524            } else {
525                tick.clone()
526            };
527            self.tick = i24_be_bytes_to_i32(&ticks_4_bytes);
528        }
529
530        // apply tick & observations changes
531        for (key, value) in delta.updated_attributes.iter() {
532            // tick liquidity keys are in the format "ticks/{tick_index}/net_liquidity"
533            if key.starts_with("ticks/") {
534                let parts: Vec<&str> = key.split('/').collect();
535                self.ticks
536                    .set_tick_liquidity(
537                        parts[1]
538                            .parse::<i32>()
539                            .map_err(|err| TransitionError::DecodeError(err.to_string()))?,
540                        i128::from(value.clone()),
541                    )
542                    .map_err(|err| TransitionError::DecodeError(err.to_string()))?;
543            }
544
545            // observations keys are in the format "observations/{observation_index}"
546            if let Some(idx_str) = key.strip_prefix("observations/") {
547                if let Ok(idx) = idx_str.parse::<i32>() {
548                    let _ = self
549                        .observations
550                        .upsert_observation(idx, value);
551                }
552            }
553        }
554        // delete ticks - ignores deletes for attributes other than tick liquidity
555        for key in delta.deleted_attributes.iter() {
556            // tick liquidity keys are in the format "ticks/{tick_index}/net_liquidity"
557            if key.starts_with("ticks/") {
558                let parts: Vec<&str> = key.split('/').collect();
559                self.ticks
560                    .set_tick_liquidity(
561                        parts[1]
562                            .parse::<i32>()
563                            .map_err(|err| TransitionError::DecodeError(err.to_string()))?,
564                        0,
565                    )
566                    .map_err(|err| TransitionError::DecodeError(err.to_string()))?;
567            }
568
569            // observations keys are in the format "observations/{observation_index}"
570            if let Some(idx_str) = key.strip_prefix("observations/") {
571                if let Ok(idx) = idx_str.parse::<i32>() {
572                    let _ = self
573                        .observations
574                        .upsert_observation(idx, &[]);
575                }
576            }
577        }
578        Ok(())
579    }
580
581    fn clone_box(&self) -> Box<dyn ProtocolSim> {
582        Box::new(self.clone())
583    }
584
585    fn as_any(&self) -> &dyn Any {
586        self
587    }
588
589    fn as_any_mut(&mut self) -> &mut dyn Any {
590        self
591    }
592
593    fn eq(&self, other: &dyn ProtocolSim) -> bool {
594        if let Some(other_state) = other
595            .as_any()
596            .downcast_ref::<AerodromeSlipstreamsState>()
597        {
598            let self_fee = match self.get_fee() {
599                Ok(fee) => fee,
600                Err(_) => return false,
601            };
602            let other_fee = match other_state.get_fee() {
603                Ok(fee) => fee,
604                Err(_) => return false,
605            };
606
607            self.liquidity == other_state.liquidity &&
608                self.sqrt_price == other_state.sqrt_price &&
609                self_fee == other_fee &&
610                self.tick == other_state.tick &&
611                self.ticks == other_state.ticks
612        } else {
613            false
614        }
615    }
616
617    fn query_pool_swap(
618        &self,
619        params: &tycho_common::simulation::protocol_sim::QueryPoolSwapParams,
620    ) -> Result<tycho_common::simulation::protocol_sim::PoolSwap, SimulationError> {
621        crate::evm::query_pool_swap::query_pool_swap(self, params)
622    }
623}
624
625#[cfg(test)]
626mod tests {
627    use alloy::primitives::{Sign, I256, U256};
628    use tycho_common::simulation::errors::SimulationError;
629
630    use super::*;
631    use crate::evm::protocol::utils::{
632        slipstreams::{dynamic_fee_module::DynamicFeeConfig, observations::Observation},
633        uniswap::{
634            tick_list::TickInfo,
635            tick_math::{
636                get_sqrt_ratio_at_tick, get_tick_at_sqrt_ratio, MAX_SQRT_RATIO, MIN_SQRT_RATIO,
637                MIN_TICK,
638            },
639        },
640    };
641
642    fn create_basic_test_pool() -> AerodromeSlipstreamsState {
643        let sqrt_price = get_sqrt_ratio_at_tick(0).expect("Failed to calculate sqrt price");
644        let ticks = vec![TickInfo::new(-120, 0).unwrap(), TickInfo::new(120, 0).unwrap()];
645        AerodromeSlipstreamsState::new(
646            "test-pool".to_string(),
647            1_000_000,
648            100_000_000_000_000_000_000u128,
649            sqrt_price,
650            0,
651            1,
652            3000,
653            1,
654            0,
655            ticks,
656            vec![Observation::default()],
657            DynamicFeeConfig::new(3000, 10_000, 1),
658        )
659        .expect("Failed to create pool")
660    }
661
662    #[test]
663    fn test_partial_step_updates_tick_when_price_moves_without_crossing_initialized_tick() {
664        let pool = create_basic_test_pool();
665        let amount =
666            I256::checked_from_sign_and_abs(Sign::Positive, U256::from(100_000_000_000_000_000u64))
667                .unwrap();
668
669        let result = pool
670            .swap(true, amount, None)
671            .expect("swap should stay within the current liquidity range");
672        let expected_tick =
673            get_tick_at_sqrt_ratio(result.sqrt_price).expect("new sqrt price should map to a tick");
674
675        assert_ne!(result.sqrt_price, pool.sqrt_price);
676        assert_ne!(result.sqrt_price, get_sqrt_ratio_at_tick(-120).unwrap());
677        assert_ne!(expected_tick, pool.tick);
678        assert_eq!(result.tick, expected_tick);
679    }
680
681    #[test]
682    fn test_swap_keeps_boundary_tick_when_price_does_not_move() {
683        let mut pool = create_basic_test_pool();
684        pool.tick = -1;
685        let amount = I256::checked_from_sign_and_abs(Sign::Positive, U256::from(1u64)).unwrap();
686
687        let result = pool
688            .swap(true, amount, None)
689            .expect("swap should consume the input as fee without moving price");
690
691        assert_eq!(result.sqrt_price, pool.sqrt_price);
692        assert_eq!(get_tick_at_sqrt_ratio(result.sqrt_price).unwrap(), 0);
693        assert_eq!(result.tick, pool.tick);
694    }
695
696    #[test]
697    fn test_swap_price_limit_out_of_range_returns_error() {
698        let pool = create_basic_test_pool();
699        let amount = I256::checked_from_sign_and_abs(Sign::Positive, U256::from(1000u64)).unwrap();
700
701        let result = pool.swap(true, amount, Some(pool.sqrt_price));
702        assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
703
704        let result = pool.swap(true, amount, Some(MIN_SQRT_RATIO));
705        assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
706
707        let result = pool.swap(false, amount, Some(pool.sqrt_price));
708        assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
709
710        let result = pool.swap(false, amount, Some(MAX_SQRT_RATIO));
711        assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
712    }
713
714    #[test]
715    fn test_swap_at_extreme_price_returns_error() {
716        let sqrt_price = MIN_SQRT_RATIO + U256::from(1u64);
717        let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
718        let ticks =
719            vec![TickInfo::new(MIN_TICK, 0).unwrap(), TickInfo::new(MIN_TICK + 1, 0).unwrap()];
720        let pool = AerodromeSlipstreamsState::new(
721            "test-pool".to_string(),
722            1_000_000,
723            100_000_000_000_000_000_000u128,
724            sqrt_price,
725            0,
726            1,
727            3000,
728            1,
729            tick,
730            ticks,
731            vec![Observation::default()],
732            DynamicFeeConfig::new(3000, 10_000, 1),
733        )
734        .expect("Failed to create pool");
735
736        let amount = I256::checked_from_sign_and_abs(Sign::Positive, U256::from(1000u64)).unwrap();
737        let result = pool.swap(true, amount, None);
738        assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
739    }
740}