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