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