1use std::{any::Any, collections::HashMap};
2
3use alloy::primitives::U256;
4use num_bigint::BigUint;
5use num_traits::Zero;
6use serde::{Deserialize, Serialize};
7use tycho_common::{
8 dto::ProtocolStateDelta,
9 models::token::Token,
10 simulation::{
11 errors::{SimulationError, TransitionError},
12 protocol_sim::{
13 Balances, GetAmountOutResult, PoolSwap, ProtocolSim, QueryPoolSwapParams,
14 SwapConstraint,
15 },
16 },
17 Bytes,
18};
19
20use super::solidly_stable::{
21 get_amount_out as solidly_stable_get_amount_out, get_limits as solidly_stable_get_limits,
22};
23use crate::evm::protocol::{
24 cpmm::protocol::{cpmm_fee, cpmm_get_limits, cpmm_spot_price, cpmm_swap_to_price, ProtocolFee},
25 safe_math::{safe_add_u256, safe_div_u256, safe_mul_u256, safe_sub_u256},
26 u256_num::{biguint_to_u256, u256_to_biguint},
27 utils::add_fee_markup,
28};
29
30const FEE_PRECISION_BPS: u32 = 10_000;
31const FEE_PRECISION: U256 = U256::from_limbs([10_000, 0, 0, 0]);
32const AERODROME_V1_STABLE_FEE_BPS: u32 = 5;
33const AERODROME_V1_VOLATILE_FEE_BPS: u32 = 30;
34const AERODROME_V1_ZERO_FEE_INDICATOR_BPS: u32 = 420;
35
36#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
37pub struct AerodromeV1State {
38 pub reserve0: U256,
39 pub reserve1: U256,
40 pub stable: bool,
41 pub fee: u32,
42 pub decimals0: u8,
43 pub decimals1: u8,
44}
45
46impl AerodromeV1State {
47 pub fn new(
49 reserve0: U256,
50 reserve1: U256,
51 stable: bool,
52 fee: u32,
53 decimals0: u8,
54 decimals1: u8,
55 ) -> Self {
56 Self { reserve0, reserve1, stable, fee, decimals0, decimals1 }
57 }
58
59 fn default_fee_bps(&self) -> u32 {
60 if self.stable {
61 AERODROME_V1_STABLE_FEE_BPS
62 } else {
63 AERODROME_V1_VOLATILE_FEE_BPS
64 }
65 }
66
67 fn resolved_fee_bps(&self) -> u32 {
68 if self.fee == AERODROME_V1_ZERO_FEE_INDICATOR_BPS {
69 0
70 } else if self.fee != 0 {
71 self.fee
72 } else {
73 self.default_fee_bps()
74 }
75 }
76
77 fn protocol_fee(&self) -> Result<ProtocolFee, SimulationError> {
78 let fee_bps = self.resolved_fee_bps();
79
80 if fee_bps > FEE_PRECISION_BPS {
81 return Err(SimulationError::FatalError(format!(
82 "Invalid resolved fee value {}, expected <= {} bps",
83 fee_bps, FEE_PRECISION_BPS
84 )));
85 }
86
87 Ok(ProtocolFee::new(U256::from(FEE_PRECISION_BPS - fee_bps), FEE_PRECISION))
88 }
89
90 fn volatile_get_amount_out(
99 &self,
100 amount_in: U256,
101 reserve_in: U256,
102 reserve_out: U256,
103 ) -> Result<U256, SimulationError> {
104 if amount_in == U256::ZERO {
105 return Err(SimulationError::InvalidInput("Amount in cannot be zero".to_string(), None));
106 }
107
108 if reserve_in == U256::ZERO || reserve_out == U256::ZERO {
109 return Err(SimulationError::RecoverableError("No liquidity".to_string()));
110 }
111
112 let fee_bps = self.resolved_fee_bps();
113 let fee_amount =
114 safe_div_u256(safe_mul_u256(amount_in, U256::from(fee_bps))?, FEE_PRECISION)?;
115 let amount_in_after_fee = safe_sub_u256(amount_in, fee_amount)?;
116 let numerator = safe_mul_u256(amount_in_after_fee, reserve_out)?;
117 let denominator = safe_add_u256(reserve_in, amount_in_after_fee)?;
118
119 safe_div_u256(numerator, denominator)
120 }
121}
122
123#[typetag::serde]
124impl ProtocolSim for AerodromeV1State {
125 fn fee(&self) -> f64 {
126 cpmm_fee(self.resolved_fee_bps())
127 }
128
129 fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
130 let price = cpmm_spot_price(base, quote, self.reserve0, self.reserve1)?;
131 Ok(add_fee_markup(price, self.fee()))
132 }
133
134 fn get_amount_out(
135 &self,
136 amount_in: BigUint,
137 token_in: &Token,
138 token_out: &Token,
139 ) -> Result<GetAmountOutResult, SimulationError> {
140 let amount_in = biguint_to_u256(&amount_in);
141 let zero2one = token_in.address < token_out.address;
142 let amount_out = if self.stable {
143 solidly_stable_get_amount_out(
144 amount_in,
145 zero2one,
146 self.reserve0,
147 self.reserve1,
148 self.resolved_fee_bps(),
149 if zero2one { token_in.decimals as u8 } else { token_out.decimals as u8 },
150 if zero2one { token_out.decimals as u8 } else { token_in.decimals as u8 },
151 )?
152 } else {
153 let (reserve_in, reserve_out) = if zero2one {
154 (self.reserve0, self.reserve1)
155 } else {
156 (self.reserve1, self.reserve0)
157 };
158 self.volatile_get_amount_out(amount_in, reserve_in, reserve_out)?
159 };
160 let mut new_state = self.clone();
161 let (reserve0_mut, reserve1_mut) = (&mut new_state.reserve0, &mut new_state.reserve1);
162 if zero2one {
163 *reserve0_mut = safe_add_u256(self.reserve0, amount_in)?;
164 *reserve1_mut = safe_sub_u256(self.reserve1, amount_out)?;
165 } else {
166 *reserve0_mut = safe_sub_u256(self.reserve0, amount_out)?;
167 *reserve1_mut = safe_add_u256(self.reserve1, amount_in)?;
168 };
169 Ok(GetAmountOutResult::new(
170 u256_to_biguint(amount_out),
171 BigUint::from(120_000u32),
172 Box::new(new_state),
173 ))
174 }
175
176 fn get_limits(
177 &self,
178 sell_token: Bytes,
179 buy_token: Bytes,
180 ) -> Result<(BigUint, BigUint), SimulationError> {
181 if self.stable {
182 solidly_stable_get_limits(
183 sell_token,
184 buy_token,
185 self.reserve0,
186 self.reserve1,
187 self.decimals0,
188 self.decimals1,
189 )
190 } else {
191 cpmm_get_limits(
192 sell_token,
193 buy_token,
194 self.reserve0,
195 self.reserve1,
196 self.resolved_fee_bps(),
197 )
198 }
199 }
200
201 fn delta_transition(
202 &mut self,
203 delta: ProtocolStateDelta,
204 _tokens: &HashMap<Bytes, Token>,
205 _balances: &Balances,
206 ) -> Result<(), TransitionError> {
207 if let Some(reserve0) = delta.updated_attributes.get("reserve0") {
208 self.reserve0 = U256::from_be_slice(reserve0);
209 }
210 if let Some(reserve1) = delta.updated_attributes.get("reserve1") {
211 self.reserve1 = U256::from_be_slice(reserve1);
212 }
213 if let Some(fee) = delta.updated_attributes.get("fee") {
214 self.fee = u32::from(fee.clone());
215 let resolved_fee_bps = self.resolved_fee_bps();
216 if resolved_fee_bps > FEE_PRECISION_BPS {
217 return Err(TransitionError::DecodeError(format!(
218 "Invalid resolved fee value {}, expected <= {} bps",
219 resolved_fee_bps, FEE_PRECISION_BPS
220 )));
221 }
222 }
223 Ok(())
224 }
225
226 fn query_pool_swap(&self, params: &QueryPoolSwapParams) -> Result<PoolSwap, SimulationError> {
227 match params.swap_constraint() {
228 SwapConstraint::PoolTargetPrice {
229 target: price,
230 tolerance: _,
231 min_amount_in: _,
232 max_amount_in: _,
233 } => {
234 let zero2one = params.token_in().address < params.token_out().address;
235 let (reserve_in, reserve_out) = if zero2one {
236 (self.reserve0, self.reserve1)
237 } else {
238 (self.reserve1, self.reserve0)
239 };
240
241 let (amount_in, _) =
242 cpmm_swap_to_price(reserve_in, reserve_out, price, self.protocol_fee()?)?;
243 if amount_in.is_zero() {
244 return Ok(PoolSwap::new(
245 BigUint::ZERO,
246 BigUint::ZERO,
247 Box::new(self.clone()),
248 None,
249 ));
250 }
251
252 let res =
253 self.get_amount_out(amount_in.clone(), params.token_in(), params.token_out())?;
254 Ok(PoolSwap::new(amount_in, res.amount, res.new_state, None))
255 }
256 SwapConstraint::TradeLimitPrice { .. } => Err(SimulationError::InvalidInput(
257 "AerodromeV1State does not support TradeLimitPrice constraint in query_pool_swap"
258 .to_string(),
259 None,
260 )),
261 }
262 }
263
264 fn clone_box(&self) -> Box<dyn ProtocolSim> {
265 Box::new(self.clone())
266 }
267
268 fn as_any(&self) -> &dyn Any {
269 self
270 }
271
272 fn as_any_mut(&mut self) -> &mut dyn Any {
273 self
274 }
275
276 fn eq(&self, other: &dyn ProtocolSim) -> bool {
277 if let Some(other_state) = other.as_any().downcast_ref::<Self>() {
278 self.reserve0 == other_state.reserve0 &&
279 self.reserve1 == other_state.reserve1 &&
280 self.stable == other_state.stable &&
281 self.fee == other_state.fee &&
282 self.decimals0 == other_state.decimals0 &&
283 self.decimals1 == other_state.decimals1
284 } else {
285 false
286 }
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use std::{
293 collections::{HashMap, HashSet},
294 str::FromStr,
295 };
296
297 use alloy::primitives::U256;
298 use num_bigint::BigUint;
299 use num_traits::One;
300 use tycho_common::{
301 dto::ProtocolStateDelta,
302 hex_bytes::Bytes,
303 models::{token::Token, Chain},
304 simulation::{
305 errors::TransitionError,
306 protocol_sim::{Balances, ProtocolSim},
307 },
308 };
309
310 use super::{AerodromeV1State, AERODROME_V1_ZERO_FEE_INDICATOR_BPS};
311
312 fn token_0() -> Token {
313 Token::new(&Bytes::from([0_u8; 20]), "T0", 18, 0, &[Some(10_000)], Chain::Ethereum, 100)
314 }
315
316 fn token_1() -> Token {
317 let mut addr = [0_u8; 20];
318 addr[19] = 1;
319 Token::new(&Bytes::from(addr), "T1", 18, 0, &[Some(10_000)], Chain::Ethereum, 100)
320 }
321
322 fn base_usdc() -> Token {
323 Token::new(
324 &Bytes::from_str("0x833589fcd6edb6e08f4c7c32d4f71b54bda02913").unwrap(),
325 "USDC",
326 6,
327 0,
328 &[Some(10_000)],
329 Chain::Base,
330 100,
331 )
332 }
333
334 fn base_usdt() -> Token {
335 Token::new(
336 &Bytes::from_str("0xfde4c96c8593536e31f229ea8f37b2ada2699bb2").unwrap(),
337 "USDT",
338 6,
339 0,
340 &[Some(10_000)],
341 Chain::Base,
342 100,
343 )
344 }
345
346 fn base_aero() -> Token {
347 Token::new(
348 &Bytes::from_str("0x940181a94a35a4569e4529a3cdfb74e38fd98631").unwrap(),
349 "AERO",
350 18,
351 0,
352 &[Some(10_000)],
353 Chain::Base,
354 100,
355 )
356 }
357
358 #[test]
359 fn test_get_amount_out_matches_real_volatile_pool_on_chain() {
360 let state = AerodromeV1State::new(
366 U256::from_str("12130133468200").unwrap(),
367 U256::from_str("33517464576714176786208401").unwrap(),
368 false,
369 30,
370 6,
371 18,
372 );
373 let out = state
374 .get_amount_out(BigUint::from_str("26225348558").unwrap(), &base_usdc(), &base_aero())
375 .expect("swap should succeed");
376
377 assert_eq!(out.amount, BigUint::from_str("72091968892551547616192").unwrap());
378 }
379
380 #[test]
381 fn test_delta_transition_supports_fee_only_update() {
382 let mut state = AerodromeV1State::new(
383 U256::from(2_000_000u32),
384 U256::from(1_000_000u32),
385 false,
386 30,
387 18,
388 18,
389 );
390 let delta = ProtocolStateDelta {
391 component_id: "pool".to_string(),
392 updated_attributes: HashMap::from([(
393 "fee".to_string(),
394 Bytes::from(5_u32.to_be_bytes().to_vec()),
395 )]),
396 deleted_attributes: HashSet::new(),
397 };
398
399 state
400 .delta_transition(delta, &HashMap::new(), &Balances::default())
401 .expect("fee-only update should succeed");
402 assert_eq!(state.fee, 5);
403 assert_eq!(state.reserve0, U256::from(2_000_000u32));
404 assert_eq!(state.reserve1, U256::from(1_000_000u32));
405 }
406
407 #[test]
408 fn test_delta_transition_rejects_invalid_fee() {
409 let mut state = AerodromeV1State::new(U256::ONE, U256::ONE, false, 30, 18, 18);
410 let delta = ProtocolStateDelta {
411 component_id: "pool".to_string(),
412 updated_attributes: HashMap::from([(
413 "fee".to_string(),
414 Bytes::from(10_101_u32.to_be_bytes().to_vec()),
415 )]),
416 deleted_attributes: HashSet::new(),
417 };
418
419 let err = state
420 .delta_transition(delta, &HashMap::new(), &Balances::default())
421 .expect_err("invalid fee should fail");
422 assert!(matches!(err, TransitionError::DecodeError(_)));
423 }
424
425 #[test]
426 fn test_fee_fn_returns_fraction() {
427 let state = AerodromeV1State::new(U256::ONE, U256::ONE, false, 30, 18, 18);
428 assert_eq!(state.fee(), 0.003);
429 let state = AerodromeV1State::new(U256::ONE, U256::ONE, true, 5, 18, 18);
430 assert_eq!(state.fee(), 0.0005);
431 }
432
433 #[test]
434 fn test_protocol_fee_accepts_zero_fee_indicator() {
435 let state = AerodromeV1State::new(
436 U256::ONE,
437 U256::ONE,
438 true,
439 AERODROME_V1_ZERO_FEE_INDICATOR_BPS,
440 18,
441 18,
442 );
443 assert!(state.protocol_fee().is_ok());
444 assert_eq!(state.fee(), 0.0);
445 }
446
447 #[test]
448 fn test_protocol_fee_rejects_out_of_range() {
449 let state = AerodromeV1State::new(U256::ONE, U256::ONE, false, 10_001, 18, 18);
450 assert!(state.protocol_fee().is_err());
451 }
452
453 #[test]
454 fn test_fee_defaults_when_custom_fee_missing() {
455 let state = AerodromeV1State::new(U256::ONE, U256::ONE, false, 0, 18, 18);
456 assert_eq!(state.fee(), 0.003);
457 let stable_state = AerodromeV1State::new(U256::ONE, U256::ONE, true, 0, 18, 18);
458 assert_eq!(stable_state.fee(), 0.0005);
459 }
460
461 #[test]
462 fn test_get_amount_out_no_fee() {
463 let state = AerodromeV1State::new(
464 U256::from(10_000u32),
465 U256::from(10_000u32),
466 false,
467 AERODROME_V1_ZERO_FEE_INDICATOR_BPS,
468 18,
469 18,
470 );
471 let out = state
472 .get_amount_out(BigUint::one(), &token_0(), &token_1())
473 .expect("swap should succeed");
474 assert_eq!(
475 out.amount,
476 BigUint::one() * BigUint::from(10_000u32) / BigUint::from(10_001u32)
477 );
478 }
479
480 #[test]
481 fn test_get_amount_out_stable_uses_cfmm_curve() {
482 let state = AerodromeV1State::new(
483 U256::from(2_642_455_102_346_776_307_825u128),
484 U256::from(3_320_301_880_379_841_502_303u128),
485 true,
486 5,
487 18,
488 18,
489 );
490
491 let out = state
492 .get_amount_out(BigUint::from(2_000_000_000_000_000_000u128), &token_0(), &token_1())
493 .expect("stable swap should succeed");
494
495 assert_eq!(out.amount, BigUint::from(2_004_830_151_166_915_124u128));
496 }
497
498 #[test]
499 fn test_get_amount_out_matches_real_stable_pool_on_chain() {
500 let state = AerodromeV1State::new(
506 U256::from(2_170_141_538u32),
507 U256::from(2_029_164_659u32),
508 true,
509 5,
510 6,
511 6,
512 );
513
514 let out = state
515 .get_amount_out(BigUint::from(123_456_789u32), &base_usdc(), &base_usdt())
516 .expect("stable swap should succeed");
517
518 assert_eq!(out.amount, BigUint::from(123_320_126u32));
519 }
520}