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