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
41const FIRST_LOOP_OVERHEAD: i32 = 15_000;
45const LOOP_GAS_COST: i32 = 12_500;
48const TICK_CROSSING_GAS_COST: i32 = 70_000;
52const TWAP_FEE_OVERHEAD: i32 = 65_000;
56const SWAP_BASE_GAS: i32 = 125_000;
58const MAX_SWAP_GAS: u64 = 16_700_000;
60const MAX_TICKS_CROSSED: u64 =
62 (MAX_SWAP_GAS - SWAP_BASE_GAS as u64) / TICK_CROSSING_GAS_COST as u64;
63
64#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
65pub struct AerodromeSlipstreamsState {
66 id: String,
67 block_timestamp: u64,
68 liquidity: u128,
69 sqrt_price: U256,
70 observation_index: u16,
71 observation_cardinality: u16,
72 default_fee: u32,
73 tick_spacing: i32,
74 tick: i32,
75 ticks: TickList,
76 observations: Observations,
77 dfc: DynamicFeeConfig,
78}
79
80impl AerodromeSlipstreamsState {
81 #[allow(clippy::too_many_arguments)]
98 pub fn new(
99 id: String,
100 block_timestamp: u64,
101 liquidity: u128,
102 sqrt_price: U256,
103 observation_index: u16,
104 observation_cardinality: u16,
105 default_fee: u32,
106 tick_spacing: i32,
107 tick: i32,
108 ticks: Vec<TickInfo>,
109 observations: Vec<Observation>,
110 dfc: DynamicFeeConfig,
111 ) -> Result<Self, SimulationError> {
112 let tick_list = TickList::from(tick_spacing as u16, ticks)?;
113 Ok(AerodromeSlipstreamsState {
114 id,
115 block_timestamp,
116 liquidity,
117 sqrt_price,
118 observation_index,
119 observation_cardinality,
120 default_fee,
121 tick_spacing,
122 tick,
123 ticks: tick_list,
124 observations: Observations::new(observations),
125 dfc,
126 })
127 }
128
129 fn get_fee(&self) -> Result<u32, SimulationError> {
130 get_dynamic_fee(
131 &self.dfc,
132 self.default_fee,
133 self.tick,
134 self.liquidity,
135 self.observation_index,
136 self.observation_cardinality,
137 &self.observations,
138 self.block_timestamp as u32,
139 )
140 }
141
142 fn swap(
143 &self,
144 zero_for_one: bool,
145 amount_specified: I256,
146 sqrt_price_limit: Option<U256>,
147 ) -> Result<SwapResults, SimulationError> {
148 if self.liquidity == 0 {
149 return Err(SimulationError::RecoverableError("No liquidity".to_string()));
150 }
151 let price_limit = if let Some(limit) = sqrt_price_limit {
152 limit
153 } else if zero_for_one {
154 safe_add_u256(MIN_SQRT_RATIO, U256::from(1u64))?
155 } else {
156 safe_sub_u256(MAX_SQRT_RATIO, U256::from(1u64))?
157 };
158
159 let price_limit_valid = if zero_for_one {
160 price_limit > MIN_SQRT_RATIO && price_limit < self.sqrt_price
161 } else {
162 price_limit < MAX_SQRT_RATIO && price_limit > self.sqrt_price
163 };
164 if !price_limit_valid {
165 return Err(SimulationError::InvalidInput("Price limit out of range".into(), None));
166 }
167
168 let exact_input = amount_specified > I256::from_raw(U256::from(0u64));
169
170 let mut state = SwapState {
171 amount_remaining: amount_specified,
172 amount_calculated: I256::from_raw(U256::from(0u64)),
173 sqrt_price: self.sqrt_price,
174 tick: self.tick,
175 liquidity: self.liquidity,
176 };
177 let twap_overhead = if self.dfc.scaling_factor() != 0 { TWAP_FEE_OVERHEAD } else { 0 };
178 let mut gas_used = U256::from((SWAP_BASE_GAS + twap_overhead) as u64);
179 let mut n_loops = 0;
180
181 let fee = self.get_fee()?;
182 while state.amount_remaining != I256::from_raw(U256::from(0u64)) &&
183 state.sqrt_price != price_limit
184 {
185 let (mut next_tick, initialized) = match self
186 .ticks
187 .next_initialized_tick_within_one_word(state.tick, zero_for_one)
188 {
189 Ok((tick, init)) => (tick, init),
190 Err(tick_err) => match tick_err.kind {
191 TickListErrorKind::TicksExeeded => {
192 let mut new_state = self.clone();
193 new_state.liquidity = state.liquidity;
194 new_state.tick = state.tick;
195 new_state.sqrt_price = state.sqrt_price;
196 return Err(SimulationError::InvalidInput(
197 "Ticks exceeded".into(),
198 Some(GetAmountOutResult::new(
199 u256_to_biguint(state.amount_calculated.abs().into_raw()),
200 u256_to_biguint(gas_used),
201 Box::new(new_state),
202 )),
203 ));
204 }
205 _ => return Err(SimulationError::FatalError("Unknown error".to_string())),
206 },
207 };
208
209 next_tick = next_tick.clamp(MIN_TICK, MAX_TICK);
210
211 let sqrt_price_start = state.sqrt_price;
212 let sqrt_price_next = get_sqrt_ratio_at_tick(next_tick)?;
213 let (sqrt_price, amount_in, amount_out, fee_amount) = swap_math::compute_swap_step(
214 state.sqrt_price,
215 AerodromeSlipstreamsState::get_sqrt_ratio_target(
216 sqrt_price_next,
217 price_limit,
218 zero_for_one,
219 ),
220 state.liquidity,
221 state.amount_remaining,
222 fee,
223 )?;
224 state.sqrt_price = sqrt_price;
225
226 let step = StepComputation {
227 sqrt_price_start,
228 tick_next: next_tick,
229 initialized,
230 sqrt_price_next,
231 amount_in,
232 amount_out,
233 fee_amount,
234 };
235 if exact_input {
236 state.amount_remaining -= I256::checked_from_sign_and_abs(
237 Sign::Positive,
238 safe_add_u256(step.amount_in, step.fee_amount)?,
239 )
240 .unwrap();
241 state.amount_calculated -=
242 I256::checked_from_sign_and_abs(Sign::Positive, step.amount_out).unwrap();
243 } else {
244 state.amount_remaining +=
245 I256::checked_from_sign_and_abs(Sign::Positive, step.amount_out).unwrap();
246 state.amount_calculated += I256::checked_from_sign_and_abs(
247 Sign::Positive,
248 safe_add_u256(step.amount_in, step.fee_amount)?,
249 )
250 .unwrap();
251 }
252 if state.sqrt_price == step.sqrt_price_next {
253 if step.initialized {
254 let liquidity_raw = self
255 .ticks
256 .get_tick(step.tick_next)
257 .unwrap()
258 .net_liquidity;
259 let liquidity_net = if zero_for_one { -liquidity_raw } else { liquidity_raw };
260 state.liquidity =
261 liquidity_math::add_liquidity_delta(state.liquidity, liquidity_net)?;
262 gas_used = safe_add_u256(gas_used, U256::from(TICK_CROSSING_GAS_COST))?;
263 }
264 state.tick = if zero_for_one { step.tick_next - 1 } else { step.tick_next };
265 } else if state.sqrt_price != step.sqrt_price_start {
266 state.tick = get_tick_at_sqrt_ratio(state.sqrt_price)?;
267 }
268 gas_used = safe_add_u256(gas_used, U256::from(LOOP_GAS_COST))?;
269 if n_loops == 0 {
270 gas_used = safe_add_u256(gas_used, U256::from(FIRST_LOOP_OVERHEAD))?;
271 }
272 n_loops += 1;
273 }
274 Ok(SwapResults {
275 amount_calculated: state.amount_calculated,
276 amount_specified,
277 amount_remaining: state.amount_remaining,
278 sqrt_price: state.sqrt_price,
279 liquidity: state.liquidity,
280 tick: state.tick,
281 gas_used,
282 })
283 }
284
285 fn get_sqrt_ratio_target(
286 sqrt_price_next: U256,
287 sqrt_price_limit: U256,
288 zero_for_one: bool,
289 ) -> U256 {
290 let cond1 = if zero_for_one {
291 sqrt_price_next < sqrt_price_limit
292 } else {
293 sqrt_price_next > sqrt_price_limit
294 };
295
296 if cond1 {
297 sqrt_price_limit
298 } else {
299 sqrt_price_next
300 }
301 }
302}
303
304#[typetag::serde]
305impl ProtocolSim for AerodromeSlipstreamsState {
306 fn fee(&self) -> f64 {
307 match self.get_fee() {
308 Ok(fee) => fee as f64 / 1_000_000.0,
309 Err(err) => {
310 error!(
311 pool = %self.id,
312 block_timestamp = self.block_timestamp,
313 %err,
314 "Error while calculating dynamic fee"
315 );
316 f64::MAX / 1_000_000.0
317 }
318 }
319 }
320
321 fn spot_price(&self, a: &Token, b: &Token) -> Result<f64, SimulationError> {
322 let price = if a < b {
323 sqrt_price_q96_to_f64(self.sqrt_price, a.decimals, b.decimals)?
324 } else {
325 1.0f64 / sqrt_price_q96_to_f64(self.sqrt_price, b.decimals, a.decimals)?
326 };
327 Ok(add_fee_markup(price, self.get_fee()? as f64 / 1_000_000.0))
328 }
329
330 fn get_amount_out(
331 &self,
332 amount_in: BigUint,
333 token_a: &Token,
334 token_b: &Token,
335 ) -> Result<GetAmountOutResult, SimulationError> {
336 let zero_for_one = token_a < token_b;
337 let amount_specified = I256::checked_from_sign_and_abs(
338 Sign::Positive,
339 U256::from_be_slice(&amount_in.to_bytes_be()),
340 )
341 .ok_or_else(|| {
342 SimulationError::InvalidInput("I256 overflow: amount_in".to_string(), None)
343 })?;
344
345 let result = self.swap(zero_for_one, amount_specified, None)?;
346
347 trace!(?amount_in, ?token_a, ?token_b, ?zero_for_one, ?result, "SLIPSTREAMS SWAP");
348 let mut new_state = self.clone();
349 new_state.liquidity = result.liquidity;
350 new_state.tick = result.tick;
351 new_state.sqrt_price = result.sqrt_price;
352
353 Ok(GetAmountOutResult::new(
354 u256_to_biguint(
355 result
356 .amount_calculated
357 .abs()
358 .into_raw(),
359 ),
360 u256_to_biguint(result.gas_used),
361 Box::new(new_state),
362 ))
363 }
364
365 fn get_limits(
366 &self,
367 token_in: Bytes,
368 token_out: Bytes,
369 ) -> Result<(BigUint, BigUint), SimulationError> {
370 if self.liquidity == 0 {
372 return Ok((BigUint::zero(), BigUint::zero()));
373 }
374
375 let zero_for_one = token_in < token_out;
376 let mut current_tick = self.tick;
377 let mut current_sqrt_price = self.sqrt_price;
378 let mut current_liquidity = self.liquidity;
379 let mut total_amount_in = U256::from(0u64);
380 let mut total_amount_out = U256::from(0u64);
381
382 let mut ticks_crossed: u64 = 0;
385 while let Ok((tick, initialized)) = self
386 .ticks
387 .next_initialized_tick_within_one_word(current_tick, zero_for_one)
388 {
389 if ticks_crossed >= MAX_TICKS_CROSSED {
390 break;
391 }
392 ticks_crossed += 1;
393 let next_tick = tick.clamp(MIN_TICK, MAX_TICK);
395
396 let sqrt_price_next = get_sqrt_ratio_at_tick(next_tick)?;
398
399 let (amount_in, amount_out) = if zero_for_one {
402 let amount0 = get_amount0_delta(
403 sqrt_price_next,
404 current_sqrt_price,
405 current_liquidity,
406 true,
407 )?;
408 let amount1 = get_amount1_delta(
409 sqrt_price_next,
410 current_sqrt_price,
411 current_liquidity,
412 false,
413 )?;
414 (amount0, amount1)
415 } else {
416 let amount0 = get_amount0_delta(
417 sqrt_price_next,
418 current_sqrt_price,
419 current_liquidity,
420 false,
421 )?;
422 let amount1 = get_amount1_delta(
423 sqrt_price_next,
424 current_sqrt_price,
425 current_liquidity,
426 true,
427 )?;
428 (amount1, amount0)
429 };
430
431 total_amount_in = safe_add_u256(total_amount_in, amount_in)?;
433 total_amount_out = safe_add_u256(total_amount_out, amount_out)?;
434
435 if initialized {
440 let liquidity_raw = self
441 .ticks
442 .get_tick(next_tick)
443 .unwrap()
444 .net_liquidity;
445 let liquidity_delta = if zero_for_one { -liquidity_raw } else { liquidity_raw };
446 current_liquidity =
447 liquidity_math::add_liquidity_delta(current_liquidity, liquidity_delta)?;
448 }
449
450 current_tick = if zero_for_one { next_tick - 1 } else { next_tick };
452 current_sqrt_price = sqrt_price_next;
453 }
454
455 Ok((u256_to_biguint(total_amount_in), u256_to_biguint(total_amount_out)))
456 }
457
458 fn delta_transition(
459 &mut self,
460 delta: ProtocolStateDelta,
461 _tokens: &HashMap<Bytes, Token>,
462 _balances: &Balances,
463 ) -> Result<(), TransitionError> {
464 if let Some(block_timestamp) = delta
465 .updated_attributes
466 .get("block_timestamp")
467 {
468 self.block_timestamp = BigInt::from_signed_bytes_be(block_timestamp)
469 .to_u64()
470 .unwrap();
471 }
472 if let Some(liquidity) = delta
474 .updated_attributes
475 .get("liquidity")
476 {
477 let liq_16_bytes = if liquidity.len() == 32 {
481 if liquidity == &Bytes::zero(32) {
483 Bytes::from([0; 16])
484 } else {
485 return Err(TransitionError::DecodeError(format!(
486 "Liquidity bytes too long for {liquidity}, expected 16",
487 )));
488 }
489 } else {
490 liquidity.clone()
491 };
492
493 self.liquidity = u128::from(liq_16_bytes);
494 }
495 if let Some(sqrt_price) = delta
496 .updated_attributes
497 .get("sqrt_price_x96")
498 {
499 self.sqrt_price = U256::from_be_slice(sqrt_price);
500 }
501 if let Some(observation_index) = delta
502 .updated_attributes
503 .get("observationIndex")
504 {
505 self.observation_index = u16::from(observation_index.clone());
506 }
507 if let Some(observation_cardinality) = delta
508 .updated_attributes
509 .get("observationCardinality")
510 {
511 self.observation_cardinality = u16::from(observation_cardinality.clone());
512 }
513 if let Some(default_fee) = delta
514 .updated_attributes
515 .get("default_fee")
516 {
517 self.default_fee = u32::from(default_fee.clone());
518 }
519 if let Some(dfc_base_fee) = delta
520 .updated_attributes
521 .get("dfc_baseFee")
522 {
523 self.dfc
524 .update_base_fee(u32::from(dfc_base_fee.clone()));
525 }
526 if let Some(dfc_fee_cap) = delta
527 .updated_attributes
528 .get("dfc_feeCap")
529 {
530 self.dfc
531 .update_fee_cap(u32::from(dfc_fee_cap.clone()));
532 }
533 if let Some(dfc_scaling_factor) = delta
534 .updated_attributes
535 .get("dfc_scalingFactor")
536 {
537 self.dfc
538 .update_scaling_factor(u64::from(dfc_scaling_factor.clone()));
539 }
540 if let Some(tick) = delta.updated_attributes.get("tick") {
541 let ticks_4_bytes = if tick.len() == 32 {
545 if tick == &Bytes::zero(32) {
547 Bytes::from([0; 4])
548 } else {
549 return Err(TransitionError::DecodeError(format!(
550 "Tick bytes too long for {tick}, expected 4"
551 )));
552 }
553 } else {
554 tick.clone()
555 };
556 self.tick = i24_be_bytes_to_i32(&ticks_4_bytes);
557 }
558
559 for (key, value) in delta.updated_attributes.iter() {
561 if key.starts_with("ticks/") {
563 let parts: Vec<&str> = key.split('/').collect();
564 self.ticks
565 .set_tick_liquidity(
566 parts[1]
567 .parse::<i32>()
568 .map_err(|err| TransitionError::DecodeError(err.to_string()))?,
569 i128::from(value.clone()),
570 )
571 .map_err(|err| TransitionError::DecodeError(err.to_string()))?;
572 }
573
574 if let Some(idx_str) = key.strip_prefix("observations/") {
576 if let Ok(idx) = idx_str.parse::<i32>() {
577 let _ = self
578 .observations
579 .upsert_observation(idx, value);
580 }
581 }
582 }
583 for key in delta.deleted_attributes.iter() {
585 if key.starts_with("ticks/") {
587 let parts: Vec<&str> = key.split('/').collect();
588 self.ticks
589 .set_tick_liquidity(
590 parts[1]
591 .parse::<i32>()
592 .map_err(|err| TransitionError::DecodeError(err.to_string()))?,
593 0,
594 )
595 .map_err(|err| TransitionError::DecodeError(err.to_string()))?;
596 }
597
598 if let Some(idx_str) = key.strip_prefix("observations/") {
600 if let Ok(idx) = idx_str.parse::<i32>() {
601 let _ = self
602 .observations
603 .upsert_observation(idx, &[]);
604 }
605 }
606 }
607 Ok(())
608 }
609
610 fn clone_box(&self) -> Box<dyn ProtocolSim> {
611 Box::new(self.clone())
612 }
613
614 fn as_any(&self) -> &dyn Any {
615 self
616 }
617
618 fn as_any_mut(&mut self) -> &mut dyn Any {
619 self
620 }
621
622 fn eq(&self, other: &dyn ProtocolSim) -> bool {
623 if let Some(other_state) = other
624 .as_any()
625 .downcast_ref::<AerodromeSlipstreamsState>()
626 {
627 let self_fee = match self.get_fee() {
628 Ok(fee) => fee,
629 Err(_) => return false,
630 };
631 let other_fee = match other_state.get_fee() {
632 Ok(fee) => fee,
633 Err(_) => return false,
634 };
635
636 self.liquidity == other_state.liquidity &&
637 self.sqrt_price == other_state.sqrt_price &&
638 self_fee == other_fee &&
639 self.tick == other_state.tick &&
640 self.ticks == other_state.ticks
641 } else {
642 false
643 }
644 }
645
646 fn query_pool_swap(
647 &self,
648 params: &tycho_common::simulation::protocol_sim::QueryPoolSwapParams,
649 ) -> Result<tycho_common::simulation::protocol_sim::PoolSwap, SimulationError> {
650 crate::evm::query_pool_swap::query_pool_swap(self, params)
651 }
652}
653
654#[cfg(test)]
655mod tests {
656 use alloy::primitives::{Sign, I256, U256};
657 use tycho_common::simulation::errors::SimulationError;
658
659 use super::*;
660 use crate::evm::protocol::utils::{
661 slipstreams::{dynamic_fee_module::DynamicFeeConfig, observations::Observation},
662 uniswap::{
663 tick_list::TickInfo,
664 tick_math::{
665 get_sqrt_ratio_at_tick, get_tick_at_sqrt_ratio, MAX_SQRT_RATIO, MIN_SQRT_RATIO,
666 MIN_TICK,
667 },
668 },
669 };
670
671 fn create_basic_test_pool() -> AerodromeSlipstreamsState {
672 let sqrt_price = get_sqrt_ratio_at_tick(0).expect("Failed to calculate sqrt price");
673 let ticks = vec![TickInfo::new(-120, 0).unwrap(), TickInfo::new(120, 0).unwrap()];
674 AerodromeSlipstreamsState::new(
675 "test-pool".to_string(),
676 1_000_000,
677 100_000_000_000_000_000_000u128,
678 sqrt_price,
679 0,
680 1,
681 3000,
682 1,
683 0,
684 ticks,
685 vec![Observation::default()],
686 DynamicFeeConfig::new(3000, 10_000, 1),
687 )
688 .expect("Failed to create pool")
689 }
690
691 #[test]
692 fn test_partial_step_updates_tick_when_price_moves_without_crossing_initialized_tick() {
693 let pool = create_basic_test_pool();
694 let amount =
695 I256::checked_from_sign_and_abs(Sign::Positive, U256::from(100_000_000_000_000_000u64))
696 .unwrap();
697
698 let result = pool
699 .swap(true, amount, None)
700 .expect("swap should stay within the current liquidity range");
701 let expected_tick =
702 get_tick_at_sqrt_ratio(result.sqrt_price).expect("new sqrt price should map to a tick");
703
704 assert_ne!(result.sqrt_price, pool.sqrt_price);
705 assert_ne!(result.sqrt_price, get_sqrt_ratio_at_tick(-120).unwrap());
706 assert_ne!(expected_tick, pool.tick);
707 assert_eq!(result.tick, expected_tick);
708 }
709
710 #[test]
711 fn test_swap_keeps_boundary_tick_when_price_does_not_move() {
712 let mut pool = create_basic_test_pool();
713 pool.tick = -1;
714 let amount = I256::checked_from_sign_and_abs(Sign::Positive, U256::from(1u64)).unwrap();
715
716 let result = pool
717 .swap(true, amount, None)
718 .expect("swap should consume the input as fee without moving price");
719
720 assert_eq!(result.sqrt_price, pool.sqrt_price);
721 assert_eq!(get_tick_at_sqrt_ratio(result.sqrt_price).unwrap(), 0);
722 assert_eq!(result.tick, pool.tick);
723 }
724
725 #[test]
726 fn test_swap_price_limit_out_of_range_returns_error() {
727 let pool = create_basic_test_pool();
728 let amount = I256::checked_from_sign_and_abs(Sign::Positive, U256::from(1000u64)).unwrap();
729
730 let result = pool.swap(true, amount, Some(pool.sqrt_price));
731 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
732
733 let result = pool.swap(true, amount, Some(MIN_SQRT_RATIO));
734 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
735
736 let result = pool.swap(false, amount, Some(pool.sqrt_price));
737 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
738
739 let result = pool.swap(false, amount, Some(MAX_SQRT_RATIO));
740 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
741 }
742
743 #[test]
744 fn test_swap_at_extreme_price_returns_error() {
745 let sqrt_price = MIN_SQRT_RATIO + U256::from(1u64);
746 let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
747 let ticks =
748 vec![TickInfo::new(MIN_TICK, 0).unwrap(), TickInfo::new(MIN_TICK + 1, 0).unwrap()];
749 let pool = AerodromeSlipstreamsState::new(
750 "test-pool".to_string(),
751 1_000_000,
752 100_000_000_000_000_000_000u128,
753 sqrt_price,
754 0,
755 1,
756 3000,
757 1,
758 tick,
759 ticks,
760 vec![Observation::default()],
761 DynamicFeeConfig::new(3000, 10_000, 1),
762 )
763 .expect("Failed to create pool");
764
765 let amount = I256::checked_from_sign_and_abs(Sign::Positive, U256::from(1000u64)).unwrap();
766 let result = pool.swap(true, amount, None);
767 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
768 }
769}