1use std::{any::Any, collections::HashMap};
2
3use alloy::primitives::{Sign, I256, U256};
4use num_bigint::BigUint;
5use num_traits::Zero;
6use serde::{Deserialize, Serialize};
7use tracing::{error, trace};
8use tycho_common::{
9 dto::ProtocolStateDelta,
10 models::token::Token,
11 simulation::{
12 errors::{SimulationError, TransitionError},
13 protocol_sim::{Balances, BlockContext, GetAmountOutResult, ProtocolSim},
14 },
15 Bytes,
16};
17
18use crate::{
19 evm::protocol::{
20 safe_math::{safe_add_u256, safe_sub_u256},
21 u256_num::u256_to_biguint,
22 utils::{
23 add_fee_markup,
24 slipstreams::{
25 dynamic_fee_module::{get_dynamic_fee, DynamicFeeConfig, ResolvedFee},
26 observations::{Observation, Observations},
27 },
28 uniswap::{
29 i24_be_bytes_to_i32, liquidity_math,
30 sqrt_price_math::{get_amount0_delta, get_amount1_delta, sqrt_price_q96_to_f64},
31 swap_math,
32 tick_list::{TickInfo, TickList, TickListErrorKind},
33 tick_math::{
34 get_sqrt_ratio_at_tick, get_tick_at_sqrt_ratio, MAX_SQRT_RATIO, MAX_TICK,
35 MIN_SQRT_RATIO, MIN_TICK,
36 },
37 StepComputation, SwapResults, SwapState,
38 },
39 },
40 },
41 protocol::models::BlockPositionAssumption,
42};
43
44const FIRST_LOOP_OVERHEAD: i32 = 15_000;
48const LOOP_GAS_COST: i32 = 12_500;
51const TICK_CROSSING_GAS_COST: i32 = 70_000;
55const TWAP_FEE_OVERHEAD: i32 = 65_000;
59const SWAP_BASE_GAS: i32 = 125_000;
61const MAX_SWAP_GAS: u64 = 16_700_000;
63const MAX_TICKS_CROSSED: u64 =
65 (MAX_SWAP_GAS - SWAP_BASE_GAS as u64) / TICK_CROSSING_GAS_COST as u64;
66
67#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
68pub struct AerodromeSlipstreamsState {
69 id: String,
70 execution_block_timestamp: u64,
77 liquidity: u128,
78 sqrt_price: U256,
79 observation_index: u16,
80 observation_cardinality: u16,
81 default_fee: u32,
82 tick_spacing: i32,
83 tick: i32,
84 ticks: TickList,
85 observations: Observations,
86 dfc: DynamicFeeConfig,
87 position_assumption: BlockPositionAssumption,
90}
91
92impl AerodromeSlipstreamsState {
93 #[allow(clippy::too_many_arguments)]
110 pub fn new(
111 id: String,
112 execution_block_timestamp: u64,
113 liquidity: u128,
114 sqrt_price: U256,
115 observation_index: u16,
116 observation_cardinality: u16,
117 default_fee: u32,
118 tick_spacing: i32,
119 tick: i32,
120 ticks: Vec<TickInfo>,
121 observations: Vec<Observation>,
122 dfc: DynamicFeeConfig,
123 ) -> Result<Self, SimulationError> {
124 let tick_list = TickList::from(tick_spacing as u16, ticks)?;
125 Ok(AerodromeSlipstreamsState {
126 id,
127 execution_block_timestamp,
128 liquidity,
129 sqrt_price,
130 observation_index,
131 observation_cardinality,
132 default_fee,
133 tick_spacing,
134 tick,
135 ticks: tick_list,
136 observations: Observations::new(observations),
137 dfc,
138 position_assumption: BlockPositionAssumption::default(),
139 })
140 }
141
142 pub fn with_position_assumption(mut self, assumption: BlockPositionAssumption) -> Self {
146 self.position_assumption = assumption;
147 self
148 }
149
150 fn get_fee(&self) -> Result<ResolvedFee, SimulationError> {
151 get_dynamic_fee(
152 &self.dfc,
153 self.default_fee,
154 self.tick,
155 self.liquidity,
156 self.observation_index,
157 self.observation_cardinality,
158 &self.observations,
159 self.execution_block_timestamp as u32,
160 self.position_assumption == BlockPositionAssumption::First,
161 )
162 }
163
164 fn record_observation(&mut self, post_swap_tick: i32) -> Result<(), SimulationError> {
171 if post_swap_tick == self.tick {
172 return Ok(());
173 }
174 self.observation_index = self.observations.write(
175 self.observation_index,
176 self.execution_block_timestamp as u32,
177 self.tick,
178 self.liquidity,
179 self.observation_cardinality,
180 )?;
181 Ok(())
182 }
183
184 fn swap(
185 &self,
186 zero_for_one: bool,
187 amount_specified: I256,
188 sqrt_price_limit: Option<U256>,
189 ) -> Result<SwapResults, SimulationError> {
190 if self.liquidity == 0 {
191 return Err(SimulationError::RecoverableError("No liquidity".to_string()));
192 }
193 let price_limit = if let Some(limit) = sqrt_price_limit {
194 limit
195 } else if zero_for_one {
196 safe_add_u256(MIN_SQRT_RATIO, U256::from(1u64))?
197 } else {
198 safe_sub_u256(MAX_SQRT_RATIO, U256::from(1u64))?
199 };
200
201 let price_limit_valid = if zero_for_one {
202 price_limit > MIN_SQRT_RATIO && price_limit < self.sqrt_price
203 } else {
204 price_limit < MAX_SQRT_RATIO && price_limit > self.sqrt_price
205 };
206 if !price_limit_valid {
207 return Err(SimulationError::InvalidInput("Price limit out of range".into(), None));
208 }
209
210 let exact_input = amount_specified > I256::from_raw(U256::from(0u64));
211
212 let mut state = SwapState {
213 amount_remaining: amount_specified,
214 amount_calculated: I256::from_raw(U256::from(0u64)),
215 sqrt_price: self.sqrt_price,
216 tick: self.tick,
217 liquidity: self.liquidity,
218 };
219 let resolved_fee = self.get_fee()?;
220 let twap_overhead = if resolved_fee.observed_twap { TWAP_FEE_OVERHEAD } else { 0 };
221 let mut gas_used = U256::from((SWAP_BASE_GAS + twap_overhead) as u64);
222 let mut n_loops = 0;
223
224 let fee = resolved_fee.fee;
225 while state.amount_remaining != I256::from_raw(U256::from(0u64)) &&
226 state.sqrt_price != price_limit
227 {
228 let (mut next_tick, initialized) = match self
229 .ticks
230 .next_initialized_tick_within_one_word(state.tick, zero_for_one)
231 {
232 Ok((tick, init)) => (tick, init),
233 Err(tick_err) => match tick_err.kind {
234 TickListErrorKind::TicksExeeded => {
235 let mut new_state = self.clone();
236 if let Err(record_err) = new_state.record_observation(state.tick) {
240 trace!(%record_err, "skipping observation write on partial result");
241 }
242 new_state.liquidity = state.liquidity;
243 new_state.tick = state.tick;
244 new_state.sqrt_price = state.sqrt_price;
245 return Err(SimulationError::InvalidInput(
246 "Ticks exceeded".into(),
247 Some(GetAmountOutResult::new(
248 u256_to_biguint(state.amount_calculated.abs().into_raw()),
249 u256_to_biguint(gas_used),
250 Box::new(new_state),
251 )),
252 ));
253 }
254 _ => return Err(SimulationError::FatalError("Unknown error".to_string())),
255 },
256 };
257
258 next_tick = next_tick.clamp(MIN_TICK, MAX_TICK);
259
260 let sqrt_price_start = state.sqrt_price;
261 let sqrt_price_next = get_sqrt_ratio_at_tick(next_tick)?;
262 let (sqrt_price, amount_in, amount_out, fee_amount) = swap_math::compute_swap_step(
263 state.sqrt_price,
264 AerodromeSlipstreamsState::get_sqrt_ratio_target(
265 sqrt_price_next,
266 price_limit,
267 zero_for_one,
268 ),
269 state.liquidity,
270 state.amount_remaining,
271 fee,
272 )?;
273 state.sqrt_price = sqrt_price;
274
275 let step = StepComputation {
276 sqrt_price_start,
277 tick_next: next_tick,
278 initialized,
279 sqrt_price_next,
280 amount_in,
281 amount_out,
282 fee_amount,
283 };
284 if exact_input {
285 state.amount_remaining -= I256::checked_from_sign_and_abs(
286 Sign::Positive,
287 safe_add_u256(step.amount_in, step.fee_amount)?,
288 )
289 .unwrap();
290 state.amount_calculated -=
291 I256::checked_from_sign_and_abs(Sign::Positive, step.amount_out).unwrap();
292 } else {
293 state.amount_remaining +=
294 I256::checked_from_sign_and_abs(Sign::Positive, step.amount_out).unwrap();
295 state.amount_calculated += I256::checked_from_sign_and_abs(
296 Sign::Positive,
297 safe_add_u256(step.amount_in, step.fee_amount)?,
298 )
299 .unwrap();
300 }
301 if state.sqrt_price == step.sqrt_price_next {
302 if step.initialized {
303 let liquidity_raw = self
304 .ticks
305 .get_tick(step.tick_next)
306 .unwrap()
307 .net_liquidity;
308 let liquidity_net = if zero_for_one { -liquidity_raw } else { liquidity_raw };
309 state.liquidity =
310 liquidity_math::add_liquidity_delta(state.liquidity, liquidity_net)?;
311 gas_used = safe_add_u256(gas_used, U256::from(TICK_CROSSING_GAS_COST))?;
312 }
313 state.tick = if zero_for_one { step.tick_next - 1 } else { step.tick_next };
314 } else if state.sqrt_price != step.sqrt_price_start {
315 state.tick = get_tick_at_sqrt_ratio(state.sqrt_price)?;
316 }
317 gas_used = safe_add_u256(gas_used, U256::from(LOOP_GAS_COST))?;
318 if n_loops == 0 {
319 gas_used = safe_add_u256(gas_used, U256::from(FIRST_LOOP_OVERHEAD))?;
320 }
321 n_loops += 1;
322 }
323 Ok(SwapResults {
324 amount_calculated: state.amount_calculated,
325 amount_specified,
326 amount_remaining: state.amount_remaining,
327 sqrt_price: state.sqrt_price,
328 liquidity: state.liquidity,
329 tick: state.tick,
330 gas_used,
331 })
332 }
333
334 fn get_sqrt_ratio_target(
335 sqrt_price_next: U256,
336 sqrt_price_limit: U256,
337 zero_for_one: bool,
338 ) -> U256 {
339 let cond1 = if zero_for_one {
340 sqrt_price_next < sqrt_price_limit
341 } else {
342 sqrt_price_next > sqrt_price_limit
343 };
344
345 if cond1 {
346 sqrt_price_limit
347 } else {
348 sqrt_price_next
349 }
350 }
351}
352
353#[typetag::serde]
354impl ProtocolSim for AerodromeSlipstreamsState {
355 fn fee(&self) -> f64 {
356 match self.get_fee() {
357 Ok(resolved) => resolved.fee as f64 / 1_000_000.0,
358 Err(err) => {
359 error!(
360 pool = %self.id,
361 execution_block_timestamp = self.execution_block_timestamp,
362 %err,
363 "Error while calculating dynamic fee"
364 );
365 f64::MAX / 1_000_000.0
366 }
367 }
368 }
369
370 fn spot_price(&self, a: &Token, b: &Token) -> Result<f64, SimulationError> {
371 let price = if a < b {
372 sqrt_price_q96_to_f64(self.sqrt_price, a.decimals, b.decimals)?
373 } else {
374 1.0f64 / sqrt_price_q96_to_f64(self.sqrt_price, b.decimals, a.decimals)?
375 };
376 Ok(add_fee_markup(price, self.get_fee()?.fee as f64 / 1_000_000.0))
377 }
378
379 fn get_amount_out(
380 &self,
381 amount_in: BigUint,
382 token_a: &Token,
383 token_b: &Token,
384 ) -> Result<GetAmountOutResult, SimulationError> {
385 let zero_for_one = token_a < token_b;
386 let amount_specified = I256::checked_from_sign_and_abs(
387 Sign::Positive,
388 U256::from_be_slice(&amount_in.to_bytes_be()),
389 )
390 .ok_or_else(|| {
391 SimulationError::InvalidInput("I256 overflow: amount_in".to_string(), None)
392 })?;
393
394 let result = self.swap(zero_for_one, amount_specified, None)?;
395
396 trace!(?amount_in, ?token_a, ?token_b, ?zero_for_one, ?result, "SLIPSTREAMS SWAP");
397 let mut new_state = self.clone();
398 new_state.record_observation(result.tick)?;
399 new_state.liquidity = result.liquidity;
400 new_state.tick = result.tick;
401 new_state.sqrt_price = result.sqrt_price;
402
403 Ok(GetAmountOutResult::new(
404 u256_to_biguint(
405 result
406 .amount_calculated
407 .abs()
408 .into_raw(),
409 ),
410 u256_to_biguint(result.gas_used),
411 Box::new(new_state),
412 ))
413 }
414
415 fn get_limits(
416 &self,
417 token_in: Bytes,
418 token_out: Bytes,
419 ) -> Result<(BigUint, BigUint), SimulationError> {
420 if self.liquidity == 0 {
422 return Ok((BigUint::zero(), BigUint::zero()));
423 }
424
425 let zero_for_one = token_in < token_out;
426 let mut current_tick = self.tick;
427 let mut current_sqrt_price = self.sqrt_price;
428 let mut current_liquidity = self.liquidity;
429 let mut total_amount_in = U256::from(0u64);
430 let mut total_amount_out = U256::from(0u64);
431
432 let mut ticks_crossed: u64 = 0;
435 while let Ok((tick, initialized)) = self
436 .ticks
437 .next_initialized_tick_within_one_word(current_tick, zero_for_one)
438 {
439 if ticks_crossed >= MAX_TICKS_CROSSED {
440 break;
441 }
442 ticks_crossed += 1;
443 let next_tick = tick.clamp(MIN_TICK, MAX_TICK);
445
446 let sqrt_price_next = get_sqrt_ratio_at_tick(next_tick)?;
448
449 let (amount_in, amount_out) = if zero_for_one {
452 let amount0 = get_amount0_delta(
453 sqrt_price_next,
454 current_sqrt_price,
455 current_liquidity,
456 true,
457 )?;
458 let amount1 = get_amount1_delta(
459 sqrt_price_next,
460 current_sqrt_price,
461 current_liquidity,
462 false,
463 )?;
464 (amount0, amount1)
465 } else {
466 let amount0 = get_amount0_delta(
467 sqrt_price_next,
468 current_sqrt_price,
469 current_liquidity,
470 false,
471 )?;
472 let amount1 = get_amount1_delta(
473 sqrt_price_next,
474 current_sqrt_price,
475 current_liquidity,
476 true,
477 )?;
478 (amount1, amount0)
479 };
480
481 total_amount_in = safe_add_u256(total_amount_in, amount_in)?;
483 total_amount_out = safe_add_u256(total_amount_out, amount_out)?;
484
485 if initialized {
490 let liquidity_raw = self
491 .ticks
492 .get_tick(next_tick)
493 .unwrap()
494 .net_liquidity;
495 let liquidity_delta = if zero_for_one { -liquidity_raw } else { liquidity_raw };
496 current_liquidity =
497 liquidity_math::add_liquidity_delta(current_liquidity, liquidity_delta)?;
498 }
499
500 current_tick = if zero_for_one { next_tick - 1 } else { next_tick };
502 current_sqrt_price = sqrt_price_next;
503 }
504
505 Ok((u256_to_biguint(total_amount_in), u256_to_biguint(total_amount_out)))
506 }
507
508 fn delta_transition(
509 &mut self,
510 delta: ProtocolStateDelta,
511 _tokens: &HashMap<Bytes, Token>,
512 _balances: &Balances,
513 ) -> Result<(), TransitionError> {
514 if let Some(liquidity) = delta
516 .updated_attributes
517 .get("liquidity")
518 {
519 let liq_16_bytes = if liquidity.len() == 32 {
523 if liquidity == &Bytes::zero(32) {
525 Bytes::from([0; 16])
526 } else {
527 return Err(TransitionError::DecodeError(format!(
528 "Liquidity bytes too long for {liquidity}, expected 16",
529 )));
530 }
531 } else {
532 liquidity.clone()
533 };
534
535 self.liquidity = u128::from(liq_16_bytes);
536 }
537 if let Some(sqrt_price) = delta
538 .updated_attributes
539 .get("sqrt_price_x96")
540 {
541 self.sqrt_price = U256::from_be_slice(sqrt_price);
542 }
543 if let Some(observation_index) = delta
544 .updated_attributes
545 .get("observationIndex")
546 {
547 self.observation_index = u16::from(observation_index.clone());
548 }
549 if let Some(observation_cardinality) = delta
550 .updated_attributes
551 .get("observationCardinality")
552 {
553 self.observation_cardinality = u16::from(observation_cardinality.clone());
554 }
555 if let Some(default_fee) = delta
556 .updated_attributes
557 .get("default_fee")
558 {
559 self.default_fee = u32::from(default_fee.clone());
560 }
561 self.dfc
562 .update_from_attributes(&delta.updated_attributes)
563 .map_err(|err| {
564 TransitionError::DecodeError(format!(
565 "Failed to update dynamic fee module config: {err}"
566 ))
567 })?;
568 if let Some(tick) = delta.updated_attributes.get("tick") {
569 let ticks_4_bytes = if tick.len() == 32 {
573 if tick == &Bytes::zero(32) {
575 Bytes::from([0; 4])
576 } else {
577 return Err(TransitionError::DecodeError(format!(
578 "Tick bytes too long for {tick}, expected 4"
579 )));
580 }
581 } else {
582 tick.clone()
583 };
584 self.tick = i24_be_bytes_to_i32(&ticks_4_bytes);
585 }
586
587 for (key, value) in delta.updated_attributes.iter() {
589 if key.starts_with("ticks/") {
591 let parts: Vec<&str> = key.split('/').collect();
592 self.ticks
593 .set_tick_liquidity(
594 parts[1]
595 .parse::<i32>()
596 .map_err(|err| TransitionError::DecodeError(err.to_string()))?,
597 i128::from(value.clone()),
598 )
599 .map_err(|err| TransitionError::DecodeError(err.to_string()))?;
600 }
601
602 if let Some(idx_str) = key.strip_prefix("observations/") {
604 if let Ok(idx) = idx_str.parse::<i32>() {
605 let _ = self
606 .observations
607 .upsert_observation(idx, value);
608 }
609 }
610 }
611 for key in delta.deleted_attributes.iter() {
613 if key.starts_with("ticks/") {
615 let parts: Vec<&str> = key.split('/').collect();
616 self.ticks
617 .set_tick_liquidity(
618 parts[1]
619 .parse::<i32>()
620 .map_err(|err| TransitionError::DecodeError(err.to_string()))?,
621 0,
622 )
623 .map_err(|err| TransitionError::DecodeError(err.to_string()))?;
624 }
625
626 if let Some(idx_str) = key.strip_prefix("observations/") {
628 if let Ok(idx) = idx_str.parse::<i32>() {
629 let _ = self
630 .observations
631 .upsert_observation(idx, &[]);
632 }
633 }
634 }
635 Ok(())
636 }
637
638 fn apply_block(&mut self, block: &BlockContext) -> bool {
642 let timestamp = block.timestamp();
643 if timestamp == self.execution_block_timestamp {
644 return false;
645 }
646 let fee_before = self.get_fee().ok();
647 self.execution_block_timestamp = timestamp;
648 fee_before != self.get_fee().ok()
649 }
650
651 fn clone_box(&self) -> Box<dyn ProtocolSim> {
652 Box::new(self.clone())
653 }
654
655 fn as_any(&self) -> &dyn Any {
656 self
657 }
658
659 fn as_any_mut(&mut self) -> &mut dyn Any {
660 self
661 }
662
663 fn eq(&self, other: &dyn ProtocolSim) -> bool {
664 if let Some(other_state) = other
665 .as_any()
666 .downcast_ref::<AerodromeSlipstreamsState>()
667 {
668 let self_fee = match self.get_fee() {
669 Ok(fee) => fee,
670 Err(_) => return false,
671 };
672 let other_fee = match other_state.get_fee() {
673 Ok(fee) => fee,
674 Err(_) => return false,
675 };
676
677 self.liquidity == other_state.liquidity &&
678 self.sqrt_price == other_state.sqrt_price &&
679 self_fee == other_fee &&
680 self.tick == other_state.tick &&
681 self.ticks == other_state.ticks
682 } else {
683 false
684 }
685 }
686
687 fn query_pool_swap(
688 &self,
689 params: &tycho_common::simulation::protocol_sim::QueryPoolSwapParams,
690 ) -> Result<tycho_common::simulation::protocol_sim::PoolSwap, SimulationError> {
691 crate::evm::query_pool_swap::query_pool_swap(self, params)
692 }
693}
694
695#[cfg(test)]
696mod tests {
697 use std::str::FromStr;
698
699 use alloy::primitives::{Sign, I256, U256};
700 use tycho_common::{models::Chain, simulation::errors::SimulationError};
701
702 use super::*;
703 use crate::evm::protocol::utils::{
704 slipstreams::{dynamic_fee_module::DynamicFeeConfig, observations::Observation},
705 uniswap::{
706 tick_list::TickInfo,
707 tick_math::{
708 get_sqrt_ratio_at_tick, get_tick_at_sqrt_ratio, MAX_SQRT_RATIO, MIN_SQRT_RATIO,
709 MIN_TICK,
710 },
711 },
712 };
713
714 fn create_basic_test_pool() -> AerodromeSlipstreamsState {
715 let sqrt_price = get_sqrt_ratio_at_tick(0).expect("Failed to calculate sqrt price");
716 let ticks = vec![TickInfo::new(-120, 0).unwrap(), TickInfo::new(120, 0).unwrap()];
717 AerodromeSlipstreamsState::new(
718 "test-pool".to_string(),
719 1_000_000,
720 100_000_000_000_000_000_000u128,
721 sqrt_price,
722 0,
723 1,
724 3000,
725 1,
726 0,
727 ticks,
728 vec![Observation::default()],
729 DynamicFeeConfig::new(3000, 10_000, 1, false, 0),
730 )
731 .expect("Failed to create pool")
732 }
733
734 fn dynamic_fee_delta(dynamic_fee_module: [u8; 20]) -> ProtocolStateDelta {
735 ProtocolStateDelta {
736 component_id: "test-pool".to_string(),
737 updated_attributes: HashMap::from([
738 ("dynamic_fee_module".to_string(), Bytes::from(dynamic_fee_module)),
739 ("dfc_baseFee".to_string(), Bytes::from(500_u32.to_be_bytes())),
740 ("dfc_scalingFactor".to_string(), Bytes::from(0_u64.to_be_bytes())),
741 ("dfc_feeCap".to_string(), Bytes::from(700_u32.to_be_bytes())),
742 ("dfc_initialFeeEnabled".to_string(), Bytes::from([0_u8])),
743 ("dfc_initialFee".to_string(), Bytes::from(0_u32.to_be_bytes())),
744 ]),
745 ..Default::default()
746 }
747 }
748
749 fn initial_fee_pool(last_observation_ts: u32) -> AerodromeSlipstreamsState {
755 let mut pool = create_basic_test_pool();
756 pool.dfc = DynamicFeeConfig::new(2700, 30_000, 0, true, 750);
757 pool.position_assumption = BlockPositionAssumption::First;
758 pool.observations = Observations::new(vec![Observation {
759 block_timestamp: last_observation_ts,
760 initialized: true,
761 index: 0,
762 ..Default::default()
763 }]);
764 pool
765 }
766
767 #[test]
780 fn replays_base_block_50166683_swap_pair_wei_exact() {
781 let mut observations: Vec<Observation> = (0..213)
782 .map(|index| Observation { index, ..Default::default() })
783 .collect();
784 observations.push(Observation {
785 block_timestamp: 1_787_122_711, tick_cumulative: -18_995_710_863_218,
787 seconds_per_liquidity_cumulative_x128: U256::from_str(
788 "42501948193164408449462610706599523891176959",
789 )
790 .unwrap(),
791 initialized: true,
792 index: 213,
793 });
794
795 let mut pool = AerodromeSlipstreamsState::new(
796 "0xdFe5F275020def30993f042174Fc2D335678b626".to_string(),
797 1_787_122_711, 1_128_781_556_759_264_064u128,
799 U256::from_str("1979649713595747421731").unwrap(),
800 213,
801 360,
802 2700, 200,
804 -350_116,
805 vec![TickInfo::new(-351_000, 0).unwrap(), TickInfo::new(-349_000, 0).unwrap()],
808 observations,
809 DynamicFeeConfig::new(2700, 0, 0, true, 750),
810 )
811 .expect("state should build")
812 .with_position_assumption(BlockPositionAssumption::First);
814
815 assert!(pool.apply_block(&BlockContext::new(50_166_683, 1_787_122_713)));
817
818 let aero = Token::new(
819 &Bytes::from_str("0x940181a94A35A4569E4529A3CDfB74e38FD98631").unwrap(),
820 "AERO",
821 18,
822 0,
823 &[Some(10_000)],
824 Chain::Base,
825 100,
826 );
827 let cbbtc = Token::new(
828 &Bytes::from_str("0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf").unwrap(),
829 "cbBTC",
830 8,
831 0,
832 &[Some(10_000)],
833 Chain::Base,
834 100,
835 );
836
837 assert_eq!(pool.fee(), 750.0 / 1_000_000.0);
838 let first = pool
839 .get_amount_out(BigUint::from(1_688_626u32), &cbbtc, &aero)
840 .expect("first swap should succeed");
841 assert_eq!(first.amount, BigUint::from(2_702_489_253_591_513_843_346u128));
842
843 assert_eq!(first.new_state.fee(), 2700.0 / 1_000_000.0);
844 let second = first
845 .new_state
846 .get_amount_out(BigUint::from(450_733u32), &cbbtc, &aero)
847 .expect("second swap should succeed");
848 assert_eq!(second.amount, BigUint::from(719_894_300_964_297_656_776u128));
849
850 let replayed = first
851 .new_state
852 .as_any()
853 .downcast_ref::<AerodromeSlipstreamsState>()
854 .expect("state type");
855 assert_eq!(replayed.observation_index, 214, "chain slot0 shows 214 after the block");
856 assert_eq!(
857 replayed
858 .observations
859 .timestamp_at(214, 360)
860 .unwrap(),
861 1_787_122_713
862 );
863 }
864
865 #[test]
866 fn ticks_exceeded_partial_result_still_records_the_observation() {
867 let mut pool = initial_fee_pool(1_000);
870 pool.apply_block(&BlockContext::new(101, 1_002));
871 let token_a =
872 Token::new(&Bytes::from([0x11; 20]), "A", 18, 0, &[Some(10_000)], Chain::Base, 100);
873 let token_b =
874 Token::new(&Bytes::from([0x22; 20]), "B", 18, 0, &[Some(10_000)], Chain::Base, 100);
875
876 let err = pool
877 .get_amount_out(
878 BigUint::from(1_000_000_000_000_000_000_000_000u128),
879 &token_a,
880 &token_b,
881 )
882 .expect_err("swap must exhaust the tick list");
883 let SimulationError::InvalidInput(_, Some(partial)) = err else {
884 panic!("expected a partial result, got {err:?}");
885 };
886
887 assert_eq!(partial.new_state.fee(), 2700.0 / 1_000_000.0);
888 }
889
890 #[test]
891 fn default_quotes_the_worse_fee_when_position_is_unknown() {
892 let mut pool = initial_fee_pool(1_000);
896 pool.position_assumption = BlockPositionAssumption::WorstCase;
897 pool.apply_block(&BlockContext::new(101, 1_002));
898
899 assert_eq!(
900 pool.get_fee()
901 .expect("fee should be computable")
902 .fee,
903 2700
904 );
905 }
906
907 #[test]
908 fn worst_case_picks_the_initial_fee_when_it_is_the_higher_one() {
909 let mut pool = initial_fee_pool(1_000);
912 pool.dfc = DynamicFeeConfig::new(500, 30_000, 0, true, 4_000);
913 pool.position_assumption = BlockPositionAssumption::WorstCase;
914 pool.apply_block(&BlockContext::new(101, 1_002));
915
916 assert_eq!(
917 pool.get_fee()
918 .expect("fee should be computable")
919 .fee,
920 4_000
921 );
922 }
923
924 #[test]
925 fn worst_case_keeps_a_flat_fee_pool_quiet_across_blocks() {
926 let mut pool = initial_fee_pool(1_000);
929 pool.position_assumption = BlockPositionAssumption::WorstCase;
930 pool.apply_block(&BlockContext::new(100, 1_000));
931
932 assert!(!pool.apply_block(&BlockContext::new(101, 1_002)));
933 assert!(!pool.apply_block(&BlockContext::new(102, 1_004)));
934 }
935
936 #[test]
937 fn apply_block_reports_a_fee_flip_and_is_idempotent() {
938 let mut pool = initial_fee_pool(1_000);
941 pool.apply_block(&BlockContext::new(100, 1_000));
942
943 assert!(pool.apply_block(&BlockContext::new(101, 1_002)), "branch flip must re-emit");
944 assert!(!pool.apply_block(&BlockContext::new(101, 1_002)), "repeat block is a no-op");
945 }
946
947 #[test]
948 fn apply_block_stays_quiet_while_the_fee_does_not_move() {
949 let mut pool = initial_fee_pool(1_000);
952 pool.apply_block(&BlockContext::new(101, 1_002));
953
954 assert!(!pool.apply_block(&BlockContext::new(102, 1_004)));
955 assert!(!pool.apply_block(&BlockContext::new(103, 1_006)));
956 }
957
958 #[test]
959 fn quotes_initial_fee_for_the_next_block_after_the_pool_traded() {
960 let mut pool = initial_fee_pool(1_000);
964 pool.apply_block(&BlockContext::new(101, 1_002));
965
966 assert_eq!(
967 pool.get_fee()
968 .expect("fee should be computable")
969 .fee,
970 750
971 );
972 }
973
974 #[test]
975 fn quotes_dynamic_fee_when_targeting_a_block_the_pool_already_traded_in() {
976 let mut pool = initial_fee_pool(1_000);
979 pool.apply_block(&BlockContext::new(100, 1_000));
980
981 assert_eq!(
982 pool.get_fee()
983 .expect("fee should be computable")
984 .fee,
985 2700
986 );
987 }
988
989 #[test]
990 fn chained_swap_in_the_same_block_pays_the_dynamic_fee() {
991 let mut pool = initial_fee_pool(1_000);
992 pool.apply_block(&BlockContext::new(101, 1_002));
993 let token_a =
994 Token::new(&Bytes::from([0x11; 20]), "A", 18, 0, &[Some(10_000)], Chain::Base, 100);
995 let token_b =
996 Token::new(&Bytes::from([0x22; 20]), "B", 18, 0, &[Some(10_000)], Chain::Base, 100);
997
998 assert_eq!(pool.fee(), 750.0 / 1_000_000.0);
999
1000 let result = pool
1001 .get_amount_out(BigUint::from(100_000_000_000_000_000u128), &token_a, &token_b)
1002 .expect("first swap should succeed");
1003
1004 assert_eq!(result.new_state.fee(), 2700.0 / 1_000_000.0);
1007 }
1008
1009 #[test]
1010 fn swap_that_does_not_move_the_tick_leaves_the_initial_fee_available() {
1011 let mut pool = initial_fee_pool(1_000);
1014 pool.apply_block(&BlockContext::new(101, 1_002));
1015
1016 pool.record_observation(pool.tick)
1017 .expect("no-op write should succeed");
1018
1019 assert_eq!(
1020 pool.get_fee()
1021 .expect("fee should be computable")
1022 .fee,
1023 750
1024 );
1025 }
1026
1027 #[test]
1028 fn initial_fee_branch_does_not_charge_the_twap_gas_overhead() {
1029 let mut pool = initial_fee_pool(1_000);
1030 pool.dfc = DynamicFeeConfig::new(2700, 30_000, 6_000_000, true, 750);
1031 pool.apply_block(&BlockContext::new(101, 1_002));
1032
1033 let resolved = pool
1034 .get_fee()
1035 .expect("fee should be computable");
1036
1037 assert_eq!(resolved, ResolvedFee { fee: 750, observed_twap: false });
1038 }
1039
1040 #[test]
1041 fn dynamic_fee_update_applies_for_supported_module() {
1042 let mut pool = create_basic_test_pool();
1043 pool.dfc = DynamicFeeConfig::new(4500, 10_000, 1, false, 0);
1044 let delta =
1045 dynamic_fee_delta(hex_literal::hex!("090b2A6bb475c00e2256e2095A60887cD710803b"));
1046
1047 pool.delta_transition(delta, &HashMap::new(), &Balances::default())
1048 .expect("dynamic fee update should be valid");
1049
1050 assert_eq!(
1051 pool.get_fee()
1052 .expect("fee should be computable")
1053 .fee,
1054 500
1055 );
1056 }
1057
1058 #[test]
1059 fn dynamic_fee_update_falls_back_to_default_for_unsupported_module() {
1060 let mut pool = create_basic_test_pool();
1063 pool.dfc = DynamicFeeConfig::new(4500, 10_000, 1, false, 0);
1064 let delta =
1065 dynamic_fee_delta(hex_literal::hex!("DB45818A6db280ecfeB33cbeBd445423d0216b5D"));
1066
1067 pool.delta_transition(delta, &HashMap::new(), &Balances::default())
1068 .expect("unsupported module delta should decode to the default config");
1069
1070 assert_eq!(pool.dfc, DynamicFeeConfig::default());
1071 assert_eq!(
1072 pool.get_fee()
1073 .expect("fee should be computable")
1074 .fee,
1075 3000
1076 );
1077 }
1078
1079 #[test]
1080 fn applies_partial_dynamic_fee_updates_after_module_initialization() {
1081 let mut pool = create_basic_test_pool();
1082 pool.dfc = DynamicFeeConfig::new(4500, 10_000, 1, false, 0);
1083 let delta = ProtocolStateDelta {
1084 component_id: "test-pool".to_string(),
1085 updated_attributes: HashMap::from([(
1086 "dfc_baseFee".to_string(),
1087 Bytes::from(500_u32.to_be_bytes()),
1088 )]),
1089 ..Default::default()
1090 };
1091
1092 pool.delta_transition(delta, &HashMap::new(), &Balances::default())
1093 .expect("partial dynamic fee update should be valid");
1094
1095 assert_eq!(pool.dfc, DynamicFeeConfig::new(500, 10_000, 1, false, 0));
1096 }
1097
1098 #[test]
1099 fn test_partial_step_updates_tick_when_price_moves_without_crossing_initialized_tick() {
1100 let pool = create_basic_test_pool();
1101 let amount =
1102 I256::checked_from_sign_and_abs(Sign::Positive, U256::from(100_000_000_000_000_000u64))
1103 .unwrap();
1104
1105 let result = pool
1106 .swap(true, amount, None)
1107 .expect("swap should stay within the current liquidity range");
1108 let expected_tick =
1109 get_tick_at_sqrt_ratio(result.sqrt_price).expect("new sqrt price should map to a tick");
1110
1111 assert_ne!(result.sqrt_price, pool.sqrt_price);
1112 assert_ne!(result.sqrt_price, get_sqrt_ratio_at_tick(-120).unwrap());
1113 assert_ne!(expected_tick, pool.tick);
1114 assert_eq!(result.tick, expected_tick);
1115 }
1116
1117 #[test]
1118 fn test_swap_keeps_boundary_tick_when_price_does_not_move() {
1119 let mut pool = create_basic_test_pool();
1120 pool.tick = -1;
1121 let amount = I256::checked_from_sign_and_abs(Sign::Positive, U256::from(1u64)).unwrap();
1122
1123 let result = pool
1124 .swap(true, amount, None)
1125 .expect("swap should consume the input as fee without moving price");
1126
1127 assert_eq!(result.sqrt_price, pool.sqrt_price);
1128 assert_eq!(get_tick_at_sqrt_ratio(result.sqrt_price).unwrap(), 0);
1129 assert_eq!(result.tick, pool.tick);
1130 }
1131
1132 #[test]
1133 fn test_swap_price_limit_out_of_range_returns_error() {
1134 let pool = create_basic_test_pool();
1135 let amount = I256::checked_from_sign_and_abs(Sign::Positive, U256::from(1000u64)).unwrap();
1136
1137 let result = pool.swap(true, amount, Some(pool.sqrt_price));
1138 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
1139
1140 let result = pool.swap(true, amount, Some(MIN_SQRT_RATIO));
1141 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
1142
1143 let result = pool.swap(false, amount, Some(pool.sqrt_price));
1144 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
1145
1146 let result = pool.swap(false, amount, Some(MAX_SQRT_RATIO));
1147 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
1148 }
1149
1150 #[test]
1151 fn test_swap_at_extreme_price_returns_error() {
1152 let sqrt_price = MIN_SQRT_RATIO + U256::from(1u64);
1153 let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
1154 let ticks =
1155 vec![TickInfo::new(MIN_TICK, 0).unwrap(), TickInfo::new(MIN_TICK + 1, 0).unwrap()];
1156 let pool = AerodromeSlipstreamsState::new(
1157 "test-pool".to_string(),
1158 1_000_000,
1159 100_000_000_000_000_000_000u128,
1160 sqrt_price,
1161 0,
1162 1,
1163 3000,
1164 1,
1165 tick,
1166 ticks,
1167 vec![Observation::default()],
1168 DynamicFeeConfig::new(3000, 10_000, 1, false, 0),
1169 )
1170 .expect("Failed to create pool");
1171
1172 let amount = I256::checked_from_sign_and_abs(Sign::Positive, U256::from(1000u64)).unwrap();
1173 let result = pool.swap(true, amount, None);
1174 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
1175 }
1176}