1use std::{any::Any, collections::HashMap, sync::Arc};
2
3use alloy::primitives::{Sign, I256, U256};
4use num_bigint::BigUint;
5use num_traits::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::{
14 Balances, BlockContext, GetAmountOutResult, PoolSwap, Price, ProtocolSim,
15 QueryPoolSwapParams, SwapConstraint,
16 },
17 },
18 Bytes,
19};
20
21use crate::{
22 evm::{
23 protocol::{
24 clmm::clmm_swap_to_price,
25 safe_math::{safe_add_u256, safe_sub_u256},
26 u256_num::u256_to_biguint,
27 utils::{
28 add_fee_markup,
29 slipstreams::{
30 dynamic_fee_module::{get_dynamic_fee, DynamicFeeConfig, ResolvedFee},
31 observations::{Observation, Observations},
32 raw_target_price,
33 },
34 uniswap::{
35 i24_be_bytes_to_i32, liquidity_math,
36 pool_tick::PoolTick,
37 sqrt_price_math::{
38 get_amount0_delta, get_amount1_delta, sqrt_price_q96_to_f64,
39 },
40 swap_math::{self, SwapStepResult},
41 swap_step_cache::{StepCacheKey, StepCacheRun, SwapStepCache},
42 tick_list::{TickInfo, TickList, TickListErrorKind},
43 tick_math::{
44 get_sqrt_ratio_at_tick_cached, MAX_SQRT_RATIO, MAX_TICK, MIN_SQRT_RATIO,
45 MIN_TICK,
46 },
47 StepComputation, SwapResults, SwapState,
48 },
49 },
50 },
51 query_pool_swap::price_to_f64_with_decimals,
52 },
53 protocol::models::BlockPositionAssumption,
54};
55
56const FIRST_LOOP_OVERHEAD: i32 = 15_000;
60const LOOP_GAS_COST: i32 = 12_500;
63const TICK_CROSSING_GAS_COST: i32 = 70_000;
67const TWAP_FEE_OVERHEAD: i32 = 65_000;
71const SWAP_BASE_GAS: i32 = 125_000;
73const MAX_SWAP_GAS: u64 = 16_700_000;
75const MAX_TICKS_CROSSED: u64 =
77 (MAX_SWAP_GAS - SWAP_BASE_GAS as u64) / TICK_CROSSING_GAS_COST as u64;
78
79#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
80pub struct AerodromeSlipstreamsState {
81 id: String,
82 execution_block_timestamp: u64,
89 liquidity: u128,
90 sqrt_price: U256,
91 observation_index: u16,
92 observation_cardinality: u16,
93 default_fee: u32,
94 tick_spacing: i32,
95 tick: i32,
96 ticks: Arc<TickList>,
100 #[serde(skip)]
105 step_cache: SwapStepCache,
106 observations: Observations,
107 dfc: DynamicFeeConfig,
108 position_assumption: BlockPositionAssumption,
111}
112
113impl AerodromeSlipstreamsState {
114 #[allow(clippy::too_many_arguments)]
131 pub fn new(
132 id: String,
133 execution_block_timestamp: u64,
134 liquidity: u128,
135 sqrt_price: U256,
136 observation_index: u16,
137 observation_cardinality: u16,
138 default_fee: u32,
139 tick_spacing: i32,
140 tick: i32,
141 ticks: Vec<TickInfo>,
142 observations: Vec<Observation>,
143 dfc: DynamicFeeConfig,
144 ) -> Result<Self, SimulationError> {
145 let tick_list = TickList::from(tick_spacing as u16, ticks)?;
146 Ok(AerodromeSlipstreamsState {
147 id,
148 execution_block_timestamp,
149 liquidity,
150 sqrt_price,
151 observation_index,
152 observation_cardinality,
153 default_fee,
154 tick_spacing,
155 tick,
156 ticks: Arc::new(tick_list),
157 step_cache: SwapStepCache::default(),
158 observations: Observations::new(observations),
159 dfc,
160 position_assumption: BlockPositionAssumption::default(),
161 })
162 }
163
164 pub fn with_position_assumption(mut self, assumption: BlockPositionAssumption) -> Self {
168 self.position_assumption = assumption;
169 self
170 }
171
172 fn get_fee(&self) -> Result<ResolvedFee, SimulationError> {
173 get_dynamic_fee(
174 &self.dfc,
175 self.default_fee,
176 self.tick,
177 self.liquidity,
178 self.observation_index,
179 self.observation_cardinality,
180 &self.observations,
181 self.execution_block_timestamp as u32,
182 self.position_assumption == BlockPositionAssumption::First,
183 )
184 }
185
186 fn record_observation(&mut self, post_swap_tick: i32) -> Result<(), SimulationError> {
193 if post_swap_tick == self.tick {
194 return Ok(());
195 }
196 self.observation_index = self.observations.write(
197 self.observation_index,
198 self.execution_block_timestamp as u32,
199 self.tick,
200 self.liquidity,
201 self.observation_cardinality,
202 )?;
203 Ok(())
204 }
205
206 fn swap_to_target_price(
210 &self,
211 params: &QueryPoolSwapParams,
212 target: &Price,
213 tolerance: f64,
214 ) -> Result<PoolSwap, SimulationError> {
215 let token_in = params.token_in();
216 let token_out = params.token_out();
217 let target_f64 = price_to_f64_with_decimals(target, token_in.decimals, token_out.decimals)?;
218 if target_f64 == self.spot_price(token_in, token_out)? {
219 return Ok(PoolSwap::new(BigUint::ZERO, BigUint::ZERO, Box::new(self.clone()), None));
220 }
221 let Some(limit) = raw_target_price(target, tolerance, self.get_fee()?.fee) else {
222 return crate::evm::query_pool_swap::query_pool_swap(self, params);
223 };
224 let Ok((amount_in, amount_out, result)) = clmm_swap_to_price(
226 self.sqrt_price,
227 &token_in.address,
228 &token_out.address,
229 &limit,
230 0,
231 Sign::Positive,
232 |zero_for_one, amount_specified, sqrt_price_limit| {
233 self.swap(zero_for_one, amount_specified, Some(sqrt_price_limit))
234 },
235 ) else {
236 return crate::evm::query_pool_swap::query_pool_swap(self, params);
237 };
238
239 let new_state = if amount_in.is_zero() {
240 self.clone()
241 } else {
242 let (state, observation) =
243 self.clone_post_swap(result.sqrt_price, result.tick.value(), result.liquidity);
244 observation?;
245 state
246 };
247 let final_spot = new_state.spot_price(token_in, token_out)?;
248 if final_spot < target_f64 || final_spot > target_f64 * (1.0 + tolerance) {
249 return crate::evm::query_pool_swap::query_pool_swap(self, params);
250 }
251 Ok(PoolSwap::new(amount_in, amount_out, Box::new(new_state), None))
252 }
253
254 fn clone_post_swap(
258 &self,
259 sqrt_price: U256,
260 tick: i32,
261 liquidity: u128,
262 ) -> (Self, Result<(), SimulationError>) {
263 let mut state = self.clone();
264 state.step_cache = SwapStepCache::default();
265 let observation = state.record_observation(tick);
266 state.liquidity = liquidity;
267 state.tick = tick;
268 state.sqrt_price = sqrt_price;
269 (state, observation)
270 }
271
272 fn swap(
273 &self,
274 zero_for_one: bool,
275 amount_specified: I256,
276 sqrt_price_limit: Option<U256>,
277 ) -> Result<SwapResults, SimulationError> {
278 if self.liquidity == 0 {
279 return Err(SimulationError::RecoverableError("No liquidity".to_string()));
280 }
281 let price_limit = if let Some(limit) = sqrt_price_limit {
282 limit
283 } else if zero_for_one {
284 safe_add_u256(MIN_SQRT_RATIO, U256::from(1u64))?
285 } else {
286 safe_sub_u256(MAX_SQRT_RATIO, U256::from(1u64))?
287 };
288
289 let price_limit_valid = if zero_for_one {
290 price_limit > MIN_SQRT_RATIO && price_limit < self.sqrt_price
291 } else {
292 price_limit < MAX_SQRT_RATIO && price_limit > self.sqrt_price
293 };
294 if !price_limit_valid {
295 return Err(SimulationError::InvalidInput("Price limit out of range".into(), None));
296 }
297
298 let exact_input = amount_specified > I256::from_raw(U256::from(0u64));
299
300 let mut state = SwapState {
301 amount_remaining: amount_specified,
302 amount_calculated: I256::from_raw(U256::from(0u64)),
303 sqrt_price: self.sqrt_price,
304 tick: self.tick.into(),
305 liquidity: self.liquidity,
306 };
307 let resolved_fee = self.get_fee()?;
308 let twap_overhead = if resolved_fee.observed_twap { TWAP_FEE_OVERHEAD } else { 0 };
309 let mut gas_used = U256::from((SWAP_BASE_GAS + twap_overhead) as u64);
310 let mut n_loops = 0;
311
312 let fee = resolved_fee.fee;
313 let cache_key = if exact_input && sqrt_price_limit.is_none() {
317 StepCacheKey::new(self.sqrt_price, state.tick.value(), self.liquidity, fee, gas_used)
318 } else {
319 None
320 };
321 let mut cache_run = StepCacheRun::new(
322 &self.step_cache,
323 zero_for_one,
324 cache_key,
325 amount_specified.unsigned_abs(),
326 );
327 if let Some(progress) = cache_run.apply_cached_progress(&mut state) {
328 gas_used = progress.gas;
329 n_loops = progress.steps_taken;
331 }
332 while state.amount_remaining != I256::from_raw(U256::from(0u64)) &&
333 state.sqrt_price != price_limit
334 {
335 let (mut next_tick, next_tick_info) = match self
336 .ticks
337 .next_initialized_tick_within_one_word(state.tick.value(), zero_for_one)
338 {
339 Ok((tick, info)) => (tick, info),
340 Err(tick_err) => match tick_err.kind {
341 TickListErrorKind::TicksExeeded => {
342 let (new_state, observation) = self.clone_post_swap(
343 state.sqrt_price,
344 state.tick.value(),
345 state.liquidity,
346 );
347 if let Err(record_err) = observation {
351 trace!(%record_err, "skipping observation write on partial result");
352 }
353 return Err(SimulationError::InvalidInput(
354 "Ticks exceeded".into(),
355 Some(GetAmountOutResult::new(
356 u256_to_biguint(state.amount_calculated.abs().into_raw()),
357 u256_to_biguint(gas_used),
358 Box::new(new_state),
359 )),
360 ));
361 }
362 _ => return Err(SimulationError::FatalError("Unknown error".to_string())),
363 },
364 };
365
366 next_tick = next_tick.clamp(MIN_TICK, MAX_TICK);
367
368 let sqrt_price_start = state.sqrt_price;
369 let sqrt_price_next = match next_tick_info {
370 Some(info) => info.sqrt_price,
371 None => get_sqrt_ratio_at_tick_cached(next_tick)?,
372 };
373 let SwapStepResult { sqrt_price, amount_in_with_fee, amount_out } =
374 swap_math::compute_swap_step(
375 state.sqrt_price,
376 AerodromeSlipstreamsState::get_sqrt_ratio_target(
377 sqrt_price_next,
378 price_limit,
379 zero_for_one,
380 ),
381 state.liquidity,
382 state.amount_remaining,
383 fee,
384 )?;
385 state.sqrt_price = sqrt_price;
386
387 let step = StepComputation {
388 sqrt_price_start,
389 tick_next: next_tick,
390 sqrt_price_next,
391 amount_in_with_fee,
392 amount_out,
393 };
394 if exact_input {
395 state.amount_remaining -=
396 I256::checked_from_sign_and_abs(Sign::Positive, step.amount_in_with_fee)
397 .unwrap();
398 state.amount_calculated -=
399 I256::checked_from_sign_and_abs(Sign::Positive, step.amount_out).unwrap();
400 } else {
401 state.amount_remaining +=
402 I256::checked_from_sign_and_abs(Sign::Positive, step.amount_out).unwrap();
403 state.amount_calculated +=
404 I256::checked_from_sign_and_abs(Sign::Positive, step.amount_in_with_fee)
405 .unwrap();
406 }
407 if state.sqrt_price == step.sqrt_price_next {
408 if let Some(info) = next_tick_info {
409 let liquidity_raw = info.net_liquidity;
410 let liquidity_net = if zero_for_one { -liquidity_raw } else { liquidity_raw };
411 state.liquidity =
412 liquidity_math::add_liquidity_delta(state.liquidity, liquidity_net)?;
413 gas_used = safe_add_u256(gas_used, U256::from(TICK_CROSSING_GAS_COST))?;
414 }
415 state.tick =
416 PoolTick::from(if zero_for_one { step.tick_next - 1 } else { step.tick_next });
417 } else if state.sqrt_price != step.sqrt_price_start {
418 state.tick = PoolTick::at_sqrt_price(state.sqrt_price)?;
419 }
420 gas_used = safe_add_u256(gas_used, U256::from(LOOP_GAS_COST))?;
421 if n_loops == 0 {
422 gas_used = safe_add_u256(gas_used, U256::from(FIRST_LOOP_OVERHEAD))?;
423 }
424 n_loops += 1;
425 cache_run.cache_progress(&step, &state, gas_used);
426 }
427 Ok(SwapResults {
428 amount_calculated: state.amount_calculated,
429 amount_specified,
430 amount_remaining: state.amount_remaining,
431 sqrt_price: state.sqrt_price,
432 liquidity: state.liquidity,
433 tick: state.tick,
434 gas_used,
435 })
436 }
437
438 fn get_sqrt_ratio_target(
439 sqrt_price_next: U256,
440 sqrt_price_limit: U256,
441 zero_for_one: bool,
442 ) -> U256 {
443 let cond1 = if zero_for_one {
444 sqrt_price_next < sqrt_price_limit
445 } else {
446 sqrt_price_next > sqrt_price_limit
447 };
448
449 if cond1 {
450 sqrt_price_limit
451 } else {
452 sqrt_price_next
453 }
454 }
455}
456
457#[typetag::serde]
458impl ProtocolSim for AerodromeSlipstreamsState {
459 fn fee(&self) -> f64 {
460 match self.get_fee() {
461 Ok(resolved) => resolved.fee as f64 / 1_000_000.0,
462 Err(err) => {
463 error!(
464 pool = %self.id,
465 execution_block_timestamp = self.execution_block_timestamp,
466 %err,
467 "Error while calculating dynamic fee"
468 );
469 f64::MAX / 1_000_000.0
470 }
471 }
472 }
473
474 fn spot_price(&self, a: &Token, b: &Token) -> Result<f64, SimulationError> {
475 let price = if a < b {
476 sqrt_price_q96_to_f64(self.sqrt_price, a.decimals, b.decimals)?
477 } else {
478 1.0f64 / sqrt_price_q96_to_f64(self.sqrt_price, b.decimals, a.decimals)?
479 };
480 Ok(add_fee_markup(price, self.get_fee()?.fee as f64 / 1_000_000.0))
481 }
482
483 fn get_amount_out(
484 &self,
485 amount_in: BigUint,
486 token_a: &Token,
487 token_b: &Token,
488 ) -> Result<GetAmountOutResult, SimulationError> {
489 let zero_for_one = token_a < token_b;
490 let amount_specified = I256::checked_from_sign_and_abs(
491 Sign::Positive,
492 U256::from_be_slice(&amount_in.to_bytes_be()),
493 )
494 .ok_or_else(|| {
495 SimulationError::InvalidInput("I256 overflow: amount_in".to_string(), None)
496 })?;
497
498 let result = self.swap(zero_for_one, amount_specified, None)?;
499
500 trace!(?amount_in, ?token_a, ?token_b, ?zero_for_one, ?result, "SLIPSTREAMS SWAP");
501 let (new_state, observation) =
502 self.clone_post_swap(result.sqrt_price, result.tick.value(), result.liquidity);
503 observation?;
504
505 Ok(GetAmountOutResult::new(
506 u256_to_biguint(
507 result
508 .amount_calculated
509 .abs()
510 .into_raw(),
511 ),
512 u256_to_biguint(result.gas_used),
513 Box::new(new_state),
514 ))
515 }
516
517 fn get_limits(
518 &self,
519 token_in: Bytes,
520 token_out: Bytes,
521 ) -> Result<(BigUint, BigUint), SimulationError> {
522 if self.liquidity == 0 {
524 return Ok((BigUint::zero(), BigUint::zero()));
525 }
526
527 let zero_for_one = token_in < token_out;
528 let mut current_tick = self.tick;
529 let mut current_sqrt_price = self.sqrt_price;
530 let mut current_liquidity = self.liquidity;
531 let mut total_amount_in = U256::from(0u64);
532 let mut total_amount_out = U256::from(0u64);
533
534 let mut ticks_crossed: u64 = 0;
537 while let Ok((tick, next_tick_info)) = self
538 .ticks
539 .next_initialized_tick_within_one_word(current_tick, zero_for_one)
540 {
541 if ticks_crossed >= MAX_TICKS_CROSSED {
542 break;
543 }
544 ticks_crossed += 1;
545 let next_tick = tick.clamp(MIN_TICK, MAX_TICK);
547
548 let sqrt_price_next = match next_tick_info {
549 Some(info) => info.sqrt_price,
550 None => get_sqrt_ratio_at_tick_cached(next_tick)?,
551 };
552
553 let (amount_in, amount_out) = if zero_for_one {
556 let amount0 = get_amount0_delta(
557 sqrt_price_next,
558 current_sqrt_price,
559 current_liquidity,
560 true,
561 )?;
562 let amount1 = get_amount1_delta(
563 sqrt_price_next,
564 current_sqrt_price,
565 current_liquidity,
566 false,
567 )?;
568 (amount0, amount1)
569 } else {
570 let amount0 = get_amount0_delta(
571 sqrt_price_next,
572 current_sqrt_price,
573 current_liquidity,
574 false,
575 )?;
576 let amount1 = get_amount1_delta(
577 sqrt_price_next,
578 current_sqrt_price,
579 current_liquidity,
580 true,
581 )?;
582 (amount1, amount0)
583 };
584
585 total_amount_in = safe_add_u256(total_amount_in, amount_in)?;
587 total_amount_out = safe_add_u256(total_amount_out, amount_out)?;
588
589 if let Some(info) = next_tick_info {
594 let liquidity_raw = info.net_liquidity;
595 let liquidity_delta = if zero_for_one { -liquidity_raw } else { liquidity_raw };
596 current_liquidity =
597 liquidity_math::add_liquidity_delta(current_liquidity, liquidity_delta)?;
598 }
599
600 current_tick = if zero_for_one { next_tick - 1 } else { next_tick };
602 current_sqrt_price = sqrt_price_next;
603 }
604
605 Ok((u256_to_biguint(total_amount_in), u256_to_biguint(total_amount_out)))
606 }
607
608 fn delta_transition(
609 &mut self,
610 delta: ProtocolStateDelta,
611 _tokens: &HashMap<Bytes, Token>,
612 _balances: &Balances,
613 ) -> Result<(), TransitionError> {
614 self.step_cache = SwapStepCache::default();
616 if let Some(liquidity) = delta
618 .updated_attributes
619 .get("liquidity")
620 {
621 let liq_16_bytes = if liquidity.len() == 32 {
625 if liquidity == &Bytes::zero(32) {
627 Bytes::from([0; 16])
628 } else {
629 return Err(TransitionError::DecodeError(format!(
630 "Liquidity bytes too long for {liquidity}, expected 16",
631 )));
632 }
633 } else {
634 liquidity.clone()
635 };
636
637 self.liquidity = u128::from(liq_16_bytes);
638 }
639 if let Some(sqrt_price) = delta
640 .updated_attributes
641 .get("sqrt_price_x96")
642 {
643 self.sqrt_price = U256::from_be_slice(sqrt_price);
644 }
645 if let Some(observation_index) = delta
646 .updated_attributes
647 .get("observationIndex")
648 {
649 self.observation_index = u16::from(observation_index.clone());
650 }
651 if let Some(observation_cardinality) = delta
652 .updated_attributes
653 .get("observationCardinality")
654 {
655 self.observation_cardinality = u16::from(observation_cardinality.clone());
656 }
657 if let Some(default_fee) = delta
658 .updated_attributes
659 .get("default_fee")
660 {
661 self.default_fee = u32::from(default_fee.clone());
662 }
663 self.dfc
664 .update_from_attributes(&delta.updated_attributes)
665 .map_err(|err| {
666 TransitionError::DecodeError(format!(
667 "Failed to update dynamic fee module config: {err}"
668 ))
669 })?;
670 if let Some(tick) = delta.updated_attributes.get("tick") {
671 let ticks_4_bytes = if tick.len() == 32 {
675 if tick == &Bytes::zero(32) {
677 Bytes::from([0; 4])
678 } else {
679 return Err(TransitionError::DecodeError(format!(
680 "Tick bytes too long for {tick}, expected 4"
681 )));
682 }
683 } else {
684 tick.clone()
685 };
686 self.tick = i24_be_bytes_to_i32(&ticks_4_bytes);
687 }
688
689 for (key, value) in delta.updated_attributes.iter() {
691 if key.starts_with("ticks/") {
693 let parts: Vec<&str> = key.split('/').collect();
694 Arc::make_mut(&mut self.ticks)
695 .set_tick_liquidity(
696 parts[1]
697 .parse::<i32>()
698 .map_err(|err| TransitionError::DecodeError(err.to_string()))?,
699 i128::from(value.clone()),
700 )
701 .map_err(|err| TransitionError::DecodeError(err.to_string()))?;
702 }
703
704 if let Some(idx_str) = key.strip_prefix("observations/") {
706 if let Ok(idx) = idx_str.parse::<i32>() {
707 let _ = self
708 .observations
709 .upsert_observation(idx, value);
710 }
711 }
712 }
713 for key in delta.deleted_attributes.iter() {
715 if key.starts_with("ticks/") {
717 let parts: Vec<&str> = key.split('/').collect();
718 Arc::make_mut(&mut self.ticks)
719 .set_tick_liquidity(
720 parts[1]
721 .parse::<i32>()
722 .map_err(|err| TransitionError::DecodeError(err.to_string()))?,
723 0,
724 )
725 .map_err(|err| TransitionError::DecodeError(err.to_string()))?;
726 }
727
728 if let Some(idx_str) = key.strip_prefix("observations/") {
730 if let Ok(idx) = idx_str.parse::<i32>() {
731 let _ = self
732 .observations
733 .upsert_observation(idx, &[]);
734 }
735 }
736 }
737 Ok(())
738 }
739
740 fn apply_block(&mut self, block: &BlockContext) -> bool {
744 let timestamp = block.timestamp();
745 if timestamp == self.execution_block_timestamp {
746 return false;
747 }
748 let fee_before = self.get_fee().ok();
749 self.execution_block_timestamp = timestamp;
750 fee_before != self.get_fee().ok()
751 }
752
753 fn clone_box(&self) -> Box<dyn ProtocolSim> {
754 Box::new(self.clone())
755 }
756
757 fn as_any(&self) -> &dyn Any {
758 self
759 }
760
761 fn as_any_mut(&mut self) -> &mut dyn Any {
762 self
763 }
764
765 fn eq(&self, other: &dyn ProtocolSim) -> bool {
766 if let Some(other_state) = other
767 .as_any()
768 .downcast_ref::<AerodromeSlipstreamsState>()
769 {
770 let self_fee = match self.get_fee() {
771 Ok(fee) => fee,
772 Err(_) => return false,
773 };
774 let other_fee = match other_state.get_fee() {
775 Ok(fee) => fee,
776 Err(_) => return false,
777 };
778
779 self.liquidity == other_state.liquidity &&
780 self.sqrt_price == other_state.sqrt_price &&
781 self_fee == other_fee &&
782 self.tick == other_state.tick &&
783 self.ticks == other_state.ticks
784 } else {
785 false
786 }
787 }
788
789 fn query_pool_swap(&self, params: &QueryPoolSwapParams) -> Result<PoolSwap, SimulationError> {
792 match params.swap_constraint() {
793 SwapConstraint::TradeLimitPrice { .. } => {
794 crate::evm::query_pool_swap::query_pool_swap(self, params)
795 }
796 SwapConstraint::PoolTargetPrice {
797 target,
798 tolerance,
799 min_amount_in: _,
800 max_amount_in: _,
801 } => {
802 let swap = self.swap_to_target_price(params, target, *tolerance)?;
803 let target = price_to_f64_with_decimals(
804 target,
805 params.token_in().decimals,
806 params.token_out().decimals,
807 )?;
808 let spot = swap
809 .new_state()
810 .spot_price(params.token_in(), params.token_out())?;
811 if !tolerance.is_finite() ||
812 *tolerance < 0.0 ||
813 !(spot >= target && spot <= target * (1.0 + tolerance))
814 {
815 return Err(SimulationError::RecoverableError(
816 "Aerodrome target-price search ended outside the requested tolerance"
817 .into(),
818 ));
819 }
820 Ok(swap)
821 }
822 }
823 }
824}
825
826#[cfg(test)]
827mod tests {
828 use std::str::FromStr;
829
830 use alloy::primitives::{Sign, I256, U256};
831 use rstest::rstest;
832 use tycho_common::{models::Chain, simulation::errors::SimulationError};
833
834 use super::*;
835 use crate::evm::{
836 protocol::utils::{
837 slipstreams::{
838 dynamic_fee_module::{DynamicFeeConfig, ZERO_FEE_INDICATOR},
839 observations::Observation,
840 },
841 uniswap::{
842 tick_list::TickInfo,
843 tick_math::{
844 get_sqrt_ratio_at_tick, get_tick_at_sqrt_ratio, MAX_SQRT_RATIO, MIN_SQRT_RATIO,
845 MIN_TICK,
846 },
847 },
848 },
849 query_pool_swap::test_helpers::{target_price_params, to_price},
850 };
851
852 fn create_basic_test_pool() -> AerodromeSlipstreamsState {
853 let sqrt_price = get_sqrt_ratio_at_tick(0).expect("Failed to calculate sqrt price");
854 let ticks = vec![TickInfo::new(-120, 0).unwrap(), TickInfo::new(120, 0).unwrap()];
855 AerodromeSlipstreamsState::new(
856 "test-pool".to_string(),
857 1_000_000,
858 100_000_000_000_000_000_000u128,
859 sqrt_price,
860 0,
861 1,
862 3000,
863 1,
864 0,
865 ticks,
866 vec![Observation::default()],
867 DynamicFeeConfig::new(3000, 10_000, 1, false, 0),
868 )
869 .expect("Failed to create pool")
870 }
871
872 fn dynamic_fee_delta(dynamic_fee_module: [u8; 20]) -> ProtocolStateDelta {
873 ProtocolStateDelta {
874 component_id: "test-pool".to_string(),
875 updated_attributes: HashMap::from([
876 ("dynamic_fee_module".to_string(), Bytes::from(dynamic_fee_module)),
877 ("dfc_baseFee".to_string(), Bytes::from(500_u32.to_be_bytes())),
878 ("dfc_scalingFactor".to_string(), Bytes::from(0_u64.to_be_bytes())),
879 ("dfc_feeCap".to_string(), Bytes::from(700_u32.to_be_bytes())),
880 ("dfc_initialFeeEnabled".to_string(), Bytes::from([0_u8])),
881 ("dfc_initialFee".to_string(), Bytes::from(0_u32.to_be_bytes())),
882 ]),
883 ..Default::default()
884 }
885 }
886
887 fn initial_fee_pool(last_observation_ts: u32) -> AerodromeSlipstreamsState {
893 let mut pool = create_basic_test_pool();
894 pool.dfc = DynamicFeeConfig::new(2700, 30_000, 0, true, 750);
895 pool.position_assumption = BlockPositionAssumption::First;
896 pool.observations = Observations::new(vec![Observation {
897 block_timestamp: last_observation_ts,
898 initialized: true,
899 index: 0,
900 ..Default::default()
901 }]);
902 pool
903 }
904
905 #[test]
918 fn replays_base_block_50166683_swap_pair_wei_exact() {
919 let mut observations: Vec<Observation> = (0..213)
920 .map(|index| Observation { index, ..Default::default() })
921 .collect();
922 observations.push(Observation {
923 block_timestamp: 1_787_122_711, tick_cumulative: -18_995_710_863_218,
925 seconds_per_liquidity_cumulative_x128: U256::from_str(
926 "42501948193164408449462610706599523891176959",
927 )
928 .unwrap(),
929 initialized: true,
930 index: 213,
931 });
932
933 let mut pool = AerodromeSlipstreamsState::new(
934 "0xdFe5F275020def30993f042174Fc2D335678b626".to_string(),
935 1_787_122_711, 1_128_781_556_759_264_064u128,
937 U256::from_str("1979649713595747421731").unwrap(),
938 213,
939 360,
940 2700, 200,
942 -350_116,
943 vec![TickInfo::new(-351_000, 0).unwrap(), TickInfo::new(-349_000, 0).unwrap()],
946 observations,
947 DynamicFeeConfig::new(2700, 0, 0, true, 750),
948 )
949 .expect("state should build")
950 .with_position_assumption(BlockPositionAssumption::First);
952
953 assert!(pool.apply_block(&BlockContext::new(50_166_683, 1_787_122_713)));
955
956 let aero = Token::new(
957 &Bytes::from_str("0x940181a94A35A4569E4529A3CDfB74e38FD98631").unwrap(),
958 "AERO",
959 18,
960 0,
961 &[Some(10_000)],
962 Chain::Base,
963 100,
964 );
965 let cbbtc = Token::new(
966 &Bytes::from_str("0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf").unwrap(),
967 "cbBTC",
968 8,
969 0,
970 &[Some(10_000)],
971 Chain::Base,
972 100,
973 );
974
975 assert_eq!(pool.fee(), 750.0 / 1_000_000.0);
976 let first = pool
977 .get_amount_out(BigUint::from(1_688_626u32), &cbbtc, &aero)
978 .expect("first swap should succeed");
979 assert_eq!(first.amount, BigUint::from(2_702_489_253_591_513_843_346u128));
980
981 assert_eq!(first.new_state.fee(), 2700.0 / 1_000_000.0);
982 let second = first
983 .new_state
984 .get_amount_out(BigUint::from(450_733u32), &cbbtc, &aero)
985 .expect("second swap should succeed");
986 assert_eq!(second.amount, BigUint::from(719_894_300_964_297_656_776u128));
987
988 let replayed = first
989 .new_state
990 .as_any()
991 .downcast_ref::<AerodromeSlipstreamsState>()
992 .expect("state type");
993 assert_eq!(replayed.observation_index, 214, "chain slot0 shows 214 after the block");
994 assert_eq!(
995 replayed
996 .observations
997 .timestamp_at(214, 360)
998 .unwrap(),
999 1_787_122_713
1000 );
1001 }
1002
1003 #[test]
1004 fn ticks_exceeded_partial_result_still_records_the_observation() {
1005 let mut pool = initial_fee_pool(1_000);
1008 pool.apply_block(&BlockContext::new(101, 1_002));
1009 let token_a =
1010 Token::new(&Bytes::from([0x11; 20]), "A", 18, 0, &[Some(10_000)], Chain::Base, 100);
1011 let token_b =
1012 Token::new(&Bytes::from([0x22; 20]), "B", 18, 0, &[Some(10_000)], Chain::Base, 100);
1013
1014 let err = pool
1015 .get_amount_out(
1016 BigUint::from(1_000_000_000_000_000_000_000_000u128),
1017 &token_a,
1018 &token_b,
1019 )
1020 .expect_err("swap must exhaust the tick list");
1021 let SimulationError::InvalidInput(_, Some(partial)) = err else {
1022 panic!("expected a partial result, got {err:?}");
1023 };
1024
1025 assert_eq!(partial.new_state.fee(), 2700.0 / 1_000_000.0);
1026 }
1027
1028 #[test]
1029 fn default_quotes_the_worse_fee_when_position_is_unknown() {
1030 let mut pool = initial_fee_pool(1_000);
1034 pool.position_assumption = BlockPositionAssumption::WorstCase;
1035 pool.apply_block(&BlockContext::new(101, 1_002));
1036
1037 assert_eq!(
1038 pool.get_fee()
1039 .expect("fee should be computable")
1040 .fee,
1041 2700
1042 );
1043 }
1044
1045 #[test]
1046 fn worst_case_picks_the_initial_fee_when_it_is_the_higher_one() {
1047 let mut pool = initial_fee_pool(1_000);
1050 pool.dfc = DynamicFeeConfig::new(500, 30_000, 0, true, 4_000);
1051 pool.position_assumption = BlockPositionAssumption::WorstCase;
1052 pool.apply_block(&BlockContext::new(101, 1_002));
1053
1054 assert_eq!(
1055 pool.get_fee()
1056 .expect("fee should be computable")
1057 .fee,
1058 4_000
1059 );
1060 }
1061
1062 #[test]
1063 fn worst_case_keeps_a_flat_fee_pool_quiet_across_blocks() {
1064 let mut pool = initial_fee_pool(1_000);
1067 pool.position_assumption = BlockPositionAssumption::WorstCase;
1068 pool.apply_block(&BlockContext::new(100, 1_000));
1069
1070 assert!(!pool.apply_block(&BlockContext::new(101, 1_002)));
1071 assert!(!pool.apply_block(&BlockContext::new(102, 1_004)));
1072 }
1073
1074 #[test]
1075 fn apply_block_reports_a_fee_flip_and_is_idempotent() {
1076 let mut pool = initial_fee_pool(1_000);
1079 pool.apply_block(&BlockContext::new(100, 1_000));
1080
1081 assert!(pool.apply_block(&BlockContext::new(101, 1_002)), "branch flip must re-emit");
1082 assert!(!pool.apply_block(&BlockContext::new(101, 1_002)), "repeat block is a no-op");
1083 }
1084
1085 #[test]
1086 fn apply_block_stays_quiet_while_the_fee_does_not_move() {
1087 let mut pool = initial_fee_pool(1_000);
1090 pool.apply_block(&BlockContext::new(101, 1_002));
1091
1092 assert!(!pool.apply_block(&BlockContext::new(102, 1_004)));
1093 assert!(!pool.apply_block(&BlockContext::new(103, 1_006)));
1094 }
1095
1096 #[test]
1097 fn quotes_initial_fee_for_the_next_block_after_the_pool_traded() {
1098 let mut pool = initial_fee_pool(1_000);
1102 pool.apply_block(&BlockContext::new(101, 1_002));
1103
1104 assert_eq!(
1105 pool.get_fee()
1106 .expect("fee should be computable")
1107 .fee,
1108 750
1109 );
1110 }
1111
1112 #[test]
1113 fn quotes_dynamic_fee_when_targeting_a_block_the_pool_already_traded_in() {
1114 let mut pool = initial_fee_pool(1_000);
1117 pool.apply_block(&BlockContext::new(100, 1_000));
1118
1119 assert_eq!(
1120 pool.get_fee()
1121 .expect("fee should be computable")
1122 .fee,
1123 2700
1124 );
1125 }
1126
1127 #[test]
1128 fn chained_swap_in_the_same_block_pays_the_dynamic_fee() {
1129 let mut pool = initial_fee_pool(1_000);
1130 pool.apply_block(&BlockContext::new(101, 1_002));
1131 let token_a =
1132 Token::new(&Bytes::from([0x11; 20]), "A", 18, 0, &[Some(10_000)], Chain::Base, 100);
1133 let token_b =
1134 Token::new(&Bytes::from([0x22; 20]), "B", 18, 0, &[Some(10_000)], Chain::Base, 100);
1135
1136 assert_eq!(pool.fee(), 750.0 / 1_000_000.0);
1137
1138 let result = pool
1139 .get_amount_out(BigUint::from(100_000_000_000_000_000u128), &token_a, &token_b)
1140 .expect("first swap should succeed");
1141
1142 assert_eq!(result.new_state.fee(), 2700.0 / 1_000_000.0);
1145 }
1146
1147 #[test]
1148 fn swap_that_does_not_move_the_tick_leaves_the_initial_fee_available() {
1149 let mut pool = initial_fee_pool(1_000);
1152 pool.apply_block(&BlockContext::new(101, 1_002));
1153
1154 pool.record_observation(pool.tick)
1155 .expect("no-op write should succeed");
1156
1157 assert_eq!(
1158 pool.get_fee()
1159 .expect("fee should be computable")
1160 .fee,
1161 750
1162 );
1163 }
1164
1165 #[test]
1166 fn initial_fee_branch_does_not_charge_the_twap_gas_overhead() {
1167 let mut pool = initial_fee_pool(1_000);
1168 pool.dfc = DynamicFeeConfig::new(2700, 30_000, 6_000_000, true, 750);
1169 pool.apply_block(&BlockContext::new(101, 1_002));
1170
1171 let resolved = pool
1172 .get_fee()
1173 .expect("fee should be computable");
1174
1175 assert_eq!(resolved, ResolvedFee { fee: 750, observed_twap: false });
1176 }
1177
1178 #[test]
1179 fn dynamic_fee_update_applies_for_supported_module() {
1180 let mut pool = create_basic_test_pool();
1181 pool.dfc = DynamicFeeConfig::new(4500, 10_000, 1, false, 0);
1182 let delta =
1183 dynamic_fee_delta(hex_literal::hex!("090b2A6bb475c00e2256e2095A60887cD710803b"));
1184
1185 pool.delta_transition(delta, &HashMap::new(), &Balances::default())
1186 .expect("dynamic fee update should be valid");
1187
1188 assert_eq!(
1189 pool.get_fee()
1190 .expect("fee should be computable")
1191 .fee,
1192 500
1193 );
1194 }
1195
1196 #[test]
1197 fn dynamic_fee_update_falls_back_to_default_for_unsupported_module() {
1198 let mut pool = create_basic_test_pool();
1201 pool.dfc = DynamicFeeConfig::new(4500, 10_000, 1, false, 0);
1202 let delta =
1203 dynamic_fee_delta(hex_literal::hex!("DB45818A6db280ecfeB33cbeBd445423d0216b5D"));
1204
1205 pool.delta_transition(delta, &HashMap::new(), &Balances::default())
1206 .expect("unsupported module delta should decode to the default config");
1207
1208 assert_eq!(pool.dfc, DynamicFeeConfig::default());
1209 assert_eq!(
1210 pool.get_fee()
1211 .expect("fee should be computable")
1212 .fee,
1213 3000
1214 );
1215 }
1216
1217 #[test]
1218 fn applies_partial_dynamic_fee_updates_after_module_initialization() {
1219 let mut pool = create_basic_test_pool();
1220 pool.dfc = DynamicFeeConfig::new(4500, 10_000, 1, false, 0);
1221 let delta = ProtocolStateDelta {
1222 component_id: "test-pool".to_string(),
1223 updated_attributes: HashMap::from([(
1224 "dfc_baseFee".to_string(),
1225 Bytes::from(500_u32.to_be_bytes()),
1226 )]),
1227 ..Default::default()
1228 };
1229
1230 pool.delta_transition(delta, &HashMap::new(), &Balances::default())
1231 .expect("partial dynamic fee update should be valid");
1232
1233 assert_eq!(pool.dfc, DynamicFeeConfig::new(500, 10_000, 1, false, 0));
1234 }
1235
1236 #[test]
1237 fn test_partial_step_updates_tick_when_price_moves_without_crossing_initialized_tick() {
1238 let pool = create_basic_test_pool();
1239 let amount =
1240 I256::checked_from_sign_and_abs(Sign::Positive, U256::from(100_000_000_000_000_000u64))
1241 .unwrap();
1242
1243 let result = pool
1244 .swap(true, amount, None)
1245 .expect("swap should stay within the current liquidity range");
1246 let expected_tick =
1247 get_tick_at_sqrt_ratio(result.sqrt_price).expect("new sqrt price should map to a tick");
1248
1249 assert_ne!(result.sqrt_price, pool.sqrt_price);
1250 assert_ne!(result.sqrt_price, get_sqrt_ratio_at_tick(-120).unwrap());
1251 assert_ne!(expected_tick, pool.tick);
1252 assert_eq!(result.tick.value(), expected_tick);
1253 }
1254
1255 #[test]
1256 fn test_swap_keeps_boundary_tick_when_price_does_not_move() {
1257 let mut pool = create_basic_test_pool();
1258 pool.tick = -1;
1259 let amount = I256::checked_from_sign_and_abs(Sign::Positive, U256::from(1u64)).unwrap();
1260
1261 let result = pool
1262 .swap(true, amount, None)
1263 .expect("swap should consume the input as fee without moving price");
1264
1265 assert_eq!(result.sqrt_price, pool.sqrt_price);
1266 assert_eq!(get_tick_at_sqrt_ratio(result.sqrt_price).unwrap(), 0);
1267 assert_eq!(result.tick.value(), pool.tick);
1268 }
1269
1270 #[test]
1271 fn test_swap_price_limit_out_of_range_returns_error() {
1272 let pool = create_basic_test_pool();
1273 let amount = I256::checked_from_sign_and_abs(Sign::Positive, U256::from(1000u64)).unwrap();
1274
1275 let result = pool.swap(true, amount, Some(pool.sqrt_price));
1276 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
1277
1278 let result = pool.swap(true, amount, Some(MIN_SQRT_RATIO));
1279 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
1280
1281 let result = pool.swap(false, amount, Some(pool.sqrt_price));
1282 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
1283
1284 let result = pool.swap(false, amount, Some(MAX_SQRT_RATIO));
1285 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
1286 }
1287
1288 #[test]
1289 fn test_swap_at_extreme_price_returns_error() {
1290 let sqrt_price = MIN_SQRT_RATIO + U256::from(1u64);
1291 let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
1292 let ticks =
1293 vec![TickInfo::new(MIN_TICK, 0).unwrap(), TickInfo::new(MIN_TICK + 1, 0).unwrap()];
1294 let pool = AerodromeSlipstreamsState::new(
1295 "test-pool".to_string(),
1296 1_000_000,
1297 100_000_000_000_000_000_000u128,
1298 sqrt_price,
1299 0,
1300 1,
1301 3000,
1302 1,
1303 tick,
1304 ticks,
1305 vec![Observation::default()],
1306 DynamicFeeConfig::new(3000, 10_000, 1, false, 0),
1307 )
1308 .expect("Failed to create pool");
1309
1310 let amount = I256::checked_from_sign_and_abs(Sign::Positive, U256::from(1000u64)).unwrap();
1311 let result = pool.swap(true, amount, None);
1312 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
1313 }
1314
1315 fn token_pair() -> (Token, Token) {
1316 let token_a =
1317 Token::new(&Bytes::from([0x11; 20]), "A", 18, 0, &[Some(10_000)], Chain::Base, 100);
1318 let token_b =
1319 Token::new(&Bytes::from([0x22; 20]), "B", 18, 0, &[Some(10_000)], Chain::Base, 100);
1320 (token_a, token_b)
1321 }
1322
1323 #[rstest]
1324 fn test_query_pool_swap_target_price_lands_in_band(#[values(true, false)] sell_a: bool) {
1325 let mut pool = create_basic_test_pool();
1326 pool.dfc = DynamicFeeConfig::new(3000, 10_000, 0, false, 0);
1327 let (token_a, token_b) = token_pair();
1328 let (token_in, token_out) =
1329 if sell_a { (&token_a, &token_b) } else { (&token_b, &token_a) };
1330 let target = pool
1331 .spot_price(token_in, token_out)
1332 .unwrap() *
1333 0.99;
1334 let tolerance = 1e-4;
1335 let params = target_price_params(
1336 token_in,
1337 token_out,
1338 to_price(target, token_in, token_out),
1339 tolerance,
1340 );
1341
1342 let swap = pool.query_pool_swap(¶ms).unwrap();
1343
1344 let new_spot = swap
1345 .new_state()
1346 .spot_price(token_in, token_out)
1347 .unwrap();
1348 assert!(new_spot >= target && new_spot <= target * (1.0 + tolerance), "spot {new_spot}");
1349 assert!(swap.price_points().is_none(), "the native path returns no price points");
1350 let quote = pool
1351 .get_amount_out(swap.amount_in().clone(), token_in, token_out)
1352 .unwrap();
1353 assert_eq!("e.amount, swap.amount_out());
1354 }
1355
1356 #[test]
1357 fn target_price_revalidates_after_initial_fee_changes() {
1358 let mut pool = initial_fee_pool(999_999);
1359 pool.ticks = Arc::new(
1360 TickList::from(
1361 1,
1362 vec![TickInfo::new(-1200, 0).unwrap(), TickInfo::new(1200, 0).unwrap()],
1363 )
1364 .unwrap(),
1365 );
1366 let (token_in, token_out) = token_pair();
1367 let target = 0.99;
1368 let tolerance = 1e-4;
1369 let params = target_price_params(
1370 &token_in,
1371 &token_out,
1372 to_price(target, &token_in, &token_out),
1373 tolerance,
1374 );
1375 let swap = pool
1376 .query_pool_swap(¶ms)
1377 .expect("search reaches the target using the final fee");
1378 let spot = swap
1379 .new_state()
1380 .spot_price(&token_in, &token_out)
1381 .unwrap();
1382 assert!(spot >= target && spot <= target * (1.0 + tolerance), "final spot {spot}");
1383 assert!(swap.price_points().is_some(), "fee change should trigger numerical search");
1384 }
1385
1386 #[test]
1388 fn test_query_pool_swap_target_price_falls_back_to_search() {
1389 let pool = create_basic_test_pool();
1390 let (token_a, token_b) = token_pair();
1391 let spot = pool
1392 .spot_price(&token_a, &token_b)
1393 .unwrap();
1394 let params = target_price_params(
1395 &token_a,
1396 &token_b,
1397 to_price(spot * (1.0 - 1e-5), &token_a, &token_b),
1398 1e-4,
1399 );
1400
1401 let swap = pool.query_pool_swap(¶ms).unwrap();
1402
1403 assert!(swap.price_points().is_some(), "the numerical search returns price points");
1404 }
1405
1406 #[test]
1407 fn test_query_pool_swap_target_price_at_spot() {
1408 let mut pool = create_basic_test_pool();
1409 pool.dfc = DynamicFeeConfig::new(ZERO_FEE_INDICATOR, 10_000, 0, false, 0);
1410 let (token_a, token_b) = token_pair();
1411 let params = target_price_params(
1412 &token_a,
1413 &token_b,
1414 Price::new(BigUint::from(1u64), BigUint::from(1u64)),
1415 1e-4,
1416 );
1417
1418 let swap = pool.query_pool_swap(¶ms).unwrap();
1419
1420 assert!(swap.amount_in().is_zero());
1421 assert!(swap.amount_out().is_zero());
1422 assert!(swap.new_state().eq(&pool));
1423 }
1424
1425 #[test]
1426 fn test_query_pool_swap_target_price_above_spot() {
1427 let pool = create_basic_test_pool();
1428 let (token_a, token_b) = token_pair();
1429 let spot = pool
1430 .spot_price(&token_a, &token_b)
1431 .unwrap();
1432 let params = target_price_params(
1433 &token_a,
1434 &token_b,
1435 to_price(spot * 1.01, &token_a, &token_b),
1436 1e-4,
1437 );
1438
1439 let result = pool.query_pool_swap(¶ms);
1440
1441 assert!(matches!(result, Err(SimulationError::InvalidInput(..))), "got {result:?}");
1442 }
1443}
1444
1445#[cfg(test)]
1446mod tick_list_sharing_tests {
1447 use std::collections::{HashMap, HashSet};
1448
1449 use tycho_common::{dto::ProtocolStateDelta, hex_bytes::Bytes};
1450
1451 use super::*;
1452 use crate::evm::protocol::utils::uniswap::tick_math::get_sqrt_ratio_at_tick;
1453
1454 #[test]
1455 fn test_delta_transition_leaves_clones_ticks_unchanged() {
1456 let original = AerodromeSlipstreamsState::new(
1457 "test-pool".to_string(),
1458 1_000_000,
1459 100_000_000_000_000_000_000u128,
1460 get_sqrt_ratio_at_tick(0).unwrap(),
1461 0,
1462 1,
1463 3000,
1464 1,
1465 0,
1466 vec![TickInfo::new(-120, 10000).unwrap(), TickInfo::new(120, -10000).unwrap()],
1467 vec![Observation::default()],
1468 DynamicFeeConfig::new(3000, 10_000, 1, false, 0),
1469 )
1470 .unwrap();
1471 let mut updated = original.clone();
1472 let delta = ProtocolStateDelta {
1473 component_id: "State1".to_owned(),
1474 updated_attributes: HashMap::from([(
1475 "ticks/-120/net_liquidity".to_string(),
1476 Bytes::from(20000_i128.to_be_bytes().to_vec()),
1477 )]),
1478 deleted_attributes: HashSet::new(),
1479 };
1480
1481 updated
1482 .delta_transition(delta, &HashMap::new(), &Balances::default())
1483 .unwrap();
1484
1485 assert_eq!(
1486 updated
1487 .ticks
1488 .get_tick(-120)
1489 .unwrap()
1490 .net_liquidity,
1491 20000
1492 );
1493 assert_eq!(
1494 original
1495 .ticks
1496 .get_tick(-120)
1497 .unwrap()
1498 .net_liquidity,
1499 10000
1500 );
1501 }
1502}
1503
1504#[cfg(test)]
1505mod step_cache_tests {
1506 use std::{
1507 collections::{HashMap, HashSet},
1508 str::FromStr,
1509 };
1510
1511 use tycho_common::{dto::ProtocolStateDelta, hex_bytes::Bytes};
1512
1513 use super::*;
1514 use crate::evm::protocol::utils::uniswap::swap_step_cache::test_fixtures::{
1515 amount_orders, test_amounts, test_ticks, wbtc_weth, LIQUIDITY, SQRT_PRICE, TICK,
1516 };
1517
1518 fn test_pool() -> AerodromeSlipstreamsState {
1519 AerodromeSlipstreamsState::new(
1520 "test-pool".to_string(),
1521 1_000_000,
1522 LIQUIDITY,
1523 U256::from_str(SQRT_PRICE).unwrap(),
1524 0,
1525 1,
1526 500,
1527 10,
1528 TICK,
1529 test_ticks(),
1530 vec![Observation::default()],
1531 DynamicFeeConfig::new(500, 10_000, 1, false, 0),
1532 )
1533 .unwrap()
1534 }
1535
1536 fn format_quote_result(
1538 pool: &AerodromeSlipstreamsState,
1539 amount: &BigUint,
1540 sell: &Token,
1541 buy: &Token,
1542 ) -> String {
1543 match pool.get_amount_out(amount.clone(), sell, buy) {
1544 Ok(result) => {
1545 let state = result
1546 .new_state
1547 .as_any()
1548 .downcast_ref::<AerodromeSlipstreamsState>()
1549 .unwrap();
1550 format!(
1551 "{} {} {} {} {}",
1552 result.amount, result.gas, state.sqrt_price, state.tick, state.liquidity
1553 )
1554 }
1555 Err(error) => format!("error {error}"),
1556 }
1557 }
1558
1559 #[test]
1560 fn test_step_cache_quotes_match_a_fresh_state_in_any_order() {
1561 let (wbtc, weth) = wbtc_weth();
1562 for (sell, buy, smallest) in [(&wbtc, &weth, 1_000u64), (&weth, &wbtc, 1_000_000_000)] {
1563 for order in amount_orders(&test_amounts(smallest)) {
1564 let shared = test_pool();
1565 for amount in &order {
1566 assert_eq!(
1567 format_quote_result(&shared, amount, sell, buy),
1568 format_quote_result(&test_pool(), amount, sell, buy),
1569 "selling {amount} {}",
1570 sell.symbol
1571 );
1572 }
1573 let steps = shared
1574 .step_cache
1575 .min_inputs(sell < buy)
1576 .len();
1577 assert!(steps > 2, "only {steps} steps cached");
1578 }
1579 }
1580 }
1581
1582 #[test]
1583 fn test_step_cache_quotes_match_a_fresh_state_at_each_cached_step() {
1584 let (wbtc, weth) = wbtc_weth();
1585 for (sell, buy, doublings) in [(&wbtc, &weth, 30u32), (&weth, &wbtc, 70)] {
1588 let cached = test_pool();
1589 let _ = cached.get_amount_out(BigUint::from(10u32) << doublings, sell, buy);
1590 let boundaries = cached.step_cache.min_inputs(sell < buy);
1591 assert!(boundaries.len() > 2, "only {} steps cached", boundaries.len());
1592 for boundary in boundaries.into_iter().skip(1) {
1593 for amount in [boundary - U256::from(1u64), boundary, boundary + U256::from(1u64)] {
1594 let amount = u256_to_biguint(amount);
1595 assert_eq!(
1596 format_quote_result(&cached, &amount, sell, buy),
1597 format_quote_result(&test_pool(), &amount, sell, buy),
1598 "selling {amount} {}",
1599 sell.symbol
1600 );
1601 }
1602 }
1603 }
1604 }
1605
1606 #[test]
1607 fn test_step_cache_of_a_swapped_state() {
1608 let (wbtc, weth) = wbtc_weth();
1609 let pool = test_pool();
1610 let swapped = pool
1611 .get_amount_out(BigUint::from(300_000_000u64), &wbtc, &weth)
1612 .unwrap()
1613 .new_state;
1614 let swapped = swapped
1615 .as_any()
1616 .downcast_ref::<AerodromeSlipstreamsState>()
1617 .unwrap();
1618 let fresh: AerodromeSlipstreamsState =
1620 serde_json::from_value(serde_json::to_value(swapped).unwrap()).unwrap();
1621
1622 for amount in test_amounts(1_000).iter().rev() {
1623 assert_eq!(
1624 format_quote_result(swapped, amount, &wbtc, &weth),
1625 format_quote_result(&fresh, amount, &wbtc, &weth),
1626 "selling {amount} WBTC"
1627 );
1628 }
1629 }
1630
1631 #[test]
1632 fn test_step_cache_restarts_after_delta_transition() {
1633 let (wbtc, weth) = wbtc_weth();
1634 let large = BigUint::from(3_000_000_000u64);
1635 let delta = || ProtocolStateDelta {
1638 component_id: "State1".to_owned(),
1639 updated_attributes: HashMap::from([(
1640 "ticks/255820/net_liquidity".to_string(),
1641 Bytes::from(
1642 1_000_000_000_000_000i128
1643 .to_be_bytes()
1644 .to_vec(),
1645 ),
1646 )]),
1647 deleted_attributes: HashSet::new(),
1648 };
1649 let mut cached = test_pool();
1650 cached
1651 .get_amount_out(large.clone(), &wbtc, &weth)
1652 .unwrap();
1653 let mut fresh = test_pool();
1654
1655 cached
1656 .delta_transition(delta(), &HashMap::new(), &Balances::default())
1657 .unwrap();
1658 fresh
1659 .delta_transition(delta(), &HashMap::new(), &Balances::default())
1660 .unwrap();
1661
1662 assert_eq!(
1663 format_quote_result(&cached, &large, &wbtc, &weth),
1664 format_quote_result(&fresh, &large, &wbtc, &weth)
1665 );
1666 }
1667}