1use std::{
2 any::Any,
3 collections::{HashMap, HashSet},
4 fmt::Debug,
5};
6
7use evm_ekubo_sdk::{
8 math::{
9 tick::{MAX_SQRT_RATIO, MIN_SQRT_RATIO},
10 uint::U256,
11 },
12 quoting::types::{NodeKey, TokenAmount},
13};
14use num_bigint::BigUint;
15use serde::{Deserialize, Serialize};
16use tycho_common::{
17 dto::ProtocolStateDelta,
18 models::token::Token,
19 simulation::{
20 errors::{SimulationError, TransitionError},
21 protocol_sim::{
22 Balances, GetAmountOutResult, PoolSwap, ProtocolSim, QueryPoolSwapParams,
23 SwapConstraint,
24 },
25 },
26 Bytes,
27};
28
29use super::pool::{
30 base::BasePool, full_range::FullRangePool, oracle::OraclePool, twamm::TwammPool, EkuboPool,
31};
32use crate::evm::protocol::{
33 ekubo::pool::mev_resist::MevResistPool,
34 ekubo_common::{swap_to_target_price, EkuboSwapToPrice},
35 u256_num::u256_to_f64,
36 utils::add_fee_markup,
37};
38
39#[enum_delegate::implement(EkuboPool)]
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41pub enum EkuboState {
42 Base(BasePool),
43 FullRange(FullRangePool),
44 Oracle(OraclePool),
45 Twamm(TwammPool),
46 MevResist(MevResistPool),
47}
48
49fn sqrt_price_q128_to_f64(
50 x: U256,
51 (token0_decimals, token1_decimals): (usize, usize),
52) -> Result<f64, SimulationError> {
53 let token_correction = 10f64.powi(token0_decimals as i32 - token1_decimals as i32);
54
55 let price = u256_to_f64(alloy::primitives::U256::from_limbs(x.0))? / 2.0f64.powi(128);
56 Ok(price.powi(2) * token_correction)
57}
58
59impl EkuboSwapToPrice for EkuboState {
60 type SqrtRatio = U256;
61
62 fn sqrt_ratio_in_range(sqrt_ratio: &BigUint) -> Option<U256> {
63 if sqrt_ratio.bits() > 256 {
64 return None;
65 }
66 let sqrt_ratio = U256::from_big_endian(&sqrt_ratio.to_bytes_be());
67 (MIN_SQRT_RATIO..=MAX_SQRT_RATIO)
68 .contains(&sqrt_ratio)
69 .then_some(sqrt_ratio)
70 }
71
72 fn current_sqrt_ratio(&self) -> U256 {
73 self.sqrt_ratio()
74 }
75
76 fn quote_to_limit(
77 &self,
78 token_in: &Token,
79 amount: i128,
80 sqrt_ratio_limit: Option<U256>,
81 ) -> Result<(i128, u128, Self), SimulationError> {
82 let token_amount = TokenAmount { token: U256::from_big_endian(&token_in.address), amount };
83 let quote = self.quote(token_amount, sqrt_ratio_limit)?;
84 Ok((quote.consumed_amount, quote.calculated_amount, quote.new_state))
85 }
86}
87
88#[typetag::serde]
89impl ProtocolSim for EkuboState {
90 fn fee(&self) -> f64 {
91 self.key().config.fee as f64 / (2f64.powi(64))
92 }
93
94 fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
95 let sqrt_ratio = self.sqrt_ratio();
96 let (base_decimals, quote_decimals) = (base.decimals as usize, quote.decimals as usize);
97
98 let price = if base < quote {
99 sqrt_price_q128_to_f64(sqrt_ratio, (base_decimals, quote_decimals))?
100 } else {
101 1.0f64 / sqrt_price_q128_to_f64(sqrt_ratio, (quote_decimals, base_decimals))?
102 };
103 Ok(add_fee_markup(price, self.fee()))
104 }
105
106 fn get_amount_out(
107 &self,
108 amount_in: BigUint,
109 token_in: &Token,
110 _token_out: &Token,
111 ) -> Result<GetAmountOutResult, SimulationError> {
112 let token_amount = TokenAmount {
113 token: U256::from_big_endian(&token_in.address),
114 amount: amount_in.try_into().map_err(|_| {
115 SimulationError::InvalidInput("amount in must fit into a i128".to_string(), None)
116 })?,
117 };
118
119 let quote = self.quote(token_amount, None)?;
120
121 if quote.calculated_amount > i128::MAX as u128 {
122 return Err(SimulationError::RecoverableError(
123 "calculated amount exceeds i128::MAX".to_string(),
124 ));
125 }
126
127 let res = GetAmountOutResult {
128 amount: BigUint::from(quote.calculated_amount),
129 gas: quote.gas.into(),
130 new_state: Box::new(quote.new_state),
131 };
132
133 if quote.consumed_amount != token_amount.amount {
134 return Err(SimulationError::InvalidInput(
135 format!("pool does not have enough liquidity to support complete swap. input amount: {input_amount}, consumed amount: {consumed_amount}", input_amount = token_amount.amount, consumed_amount = quote.consumed_amount),
136 Some(res),
137 ));
138 }
139
140 Ok(res)
141 }
142
143 fn delta_transition(
144 &mut self,
145 delta: ProtocolStateDelta,
146 _tokens: &HashMap<Bytes, Token>,
147 _balances: &Balances,
148 ) -> Result<(), TransitionError> {
149 if let Some(liquidity) = delta
150 .updated_attributes
151 .get("liquidity")
152 {
153 self.set_liquidity(liquidity.clone().into());
154 }
155
156 if let Some(sqrt_price) = delta
157 .updated_attributes
158 .get("sqrt_ratio")
159 {
160 self.set_sqrt_ratio(U256::from_big_endian(sqrt_price));
161 }
162
163 self.finish_transition(delta.updated_attributes, delta.deleted_attributes)
164 }
165
166 fn clone_box(&self) -> Box<dyn ProtocolSim> {
167 Box::new(self.clone())
168 }
169
170 fn as_any(&self) -> &dyn Any {
171 self
172 }
173
174 fn as_any_mut(&mut self) -> &mut dyn Any {
175 self
176 }
177
178 fn eq(&self, other: &dyn ProtocolSim) -> bool {
179 other
180 .as_any()
181 .downcast_ref::<EkuboState>()
182 .is_some_and(|other_state| self == other_state)
183 }
184
185 fn get_limits(
186 &self,
187 sell_token: Bytes,
188 _buy_token: Bytes,
189 ) -> Result<(BigUint, BigUint), SimulationError> {
190 let consumed_amount = self.get_limit(U256::from_big_endian(&sell_token))?;
191
192 Ok((
194 BigUint::try_from(consumed_amount).map_err(|_| {
195 SimulationError::FatalError(format!(
196 "Failed to convert consumed amount `{consumed_amount}` into BigUint"
197 ))
198 })?,
199 BigUint::ZERO,
200 ))
201 }
202
203 fn query_pool_swap(&self, params: &QueryPoolSwapParams) -> Result<PoolSwap, SimulationError> {
206 match params.swap_constraint() {
207 SwapConstraint::TradeLimitPrice { .. } => {
208 crate::evm::query_pool_swap::query_pool_swap(self, params)
209 }
210 SwapConstraint::PoolTargetPrice { target, .. } => {
211 swap_to_target_price(self, params, target, self.key().config.fee)
212 }
213 }
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use evm_ekubo_sdk::{
220 math::{tick::MIN_SQRT_RATIO, uint::U256},
221 quoting::types::{Config, NodeKey, Tick},
222 };
223 use rstest::*;
224 use rstest_reuse::apply;
225
226 use super::*;
227 use crate::evm::protocol::{
228 ekubo::{pool::base::BasePool, test_cases::*},
229 ekubo_common::test_helpers::*,
230 };
231
232 #[apply(all_cases)]
233 fn test_delta_transition(case: TestCase) {
234 let mut state = case.state_before_transition;
235
236 state
237 .delta_transition(
238 ProtocolStateDelta {
239 updated_attributes: case.transition_attributes,
240 ..Default::default()
241 },
242 &HashMap::default(),
243 &Balances::default(),
244 )
245 .expect("executing transition");
246
247 assert_eq!(state, case.state_after_transition);
248 }
249
250 #[apply(all_cases)]
251 fn test_get_amount_out(case: TestCase) {
252 let (token0, token1) = (case.token0(), case.token1());
253 let (amount_in, expected_out) = case.swap_token0;
254
255 let res = case
256 .state_after_transition
257 .get_amount_out(amount_in, &token0, &token1)
258 .expect("computing quote");
259
260 assert_eq!(res.amount, expected_out);
261 }
262
263 #[apply(all_cases)]
264 fn test_get_limits(case: TestCase) {
265 use std::ops::Deref;
266
267 let (token0, token1) = (case.token0(), case.token1());
268 let state = case.state_after_transition;
269
270 let max_amount_in = state
271 .get_limits(token0.address.deref().into(), token1.address.deref().into())
272 .expect("computing limits for token0")
273 .0;
274
275 assert_eq!(max_amount_in, case.expected_limit_token0);
276
277 state
278 .get_amount_out(max_amount_in, &token0, &token1)
279 .expect("quoting with limit");
280 }
281
282 #[test]
283 fn test_get_limits_negative_consumed_amount() {
284 let eth_address = U256::zero();
288 let usdt_address_bytes =
289 hex::decode("dac17f958d2ee523a2206206994597c13d831ec7").expect("valid hex");
290 let usdt_address = U256::from_big_endian(&usdt_address_bytes);
291
292 let pool_key = NodeKey {
293 token0: eth_address,
294 token1: usdt_address,
295 config: Config { fee: 0, tick_spacing: 1000, extension: U256::zero() },
296 };
297
298 let state = EkuboState::Base(
301 BasePool::new(
302 pool_key,
303 vec![
304 Tick { index: 1000, liquidity_delta: 1 },
305 Tick { index: 2000, liquidity_delta: -1 },
306 ],
307 MIN_SQRT_RATIO, 0, -887272, )
311 .unwrap(),
312 );
313
314 let (limit, _) = state
315 .get_limits(
316 pool_key.token0.to_big_endian().into(),
317 pool_key.token1.to_big_endian().into(),
318 )
319 .unwrap();
320
321 assert_eq!(limit, BigUint::ZERO);
323 }
324
325 #[rstest]
326 #[case::full_range(full_range(), 0.95)]
327 #[case::mev_resist_with_fee(mev_resist(), 0.999_995)]
328 #[case::oracle(oracle(), 0.99)]
329 #[case::twamm(twamm(), 0.99)]
330 fn test_query_pool_swap_target_price_lands_in_band(
331 #[case] case: TestCase,
332 #[case] multiplier: f64,
333 ) {
334 assert_lands_in_band(
335 &case.state_after_transition,
336 &case.token0(),
337 &case.token1(),
338 multiplier,
339 );
340 }
341
342 #[rstest]
343 fn test_query_pool_swap_target_price_above_spot(full_range: TestCase) {
344 let state = &full_range.state_after_transition;
345 assert_target_above_spot_rejected(state, &full_range.token0(), &full_range.token1());
346 }
347
348 #[rstest]
349 fn test_query_pool_swap_target_price_at_spot(full_range: TestCase) {
350 let state = &full_range.state_after_transition;
351 assert_target_at_spot_gives_zero_swap(state, &full_range.token0(), &full_range.token1());
352 }
353
354 #[rstest]
355 fn test_query_pool_swap_target_price_out_of_range(full_range: TestCase) {
356 let state = &full_range.state_after_transition;
357 assert_out_of_range_falls_back(state, &full_range.token0(), &full_range.token1());
358 }
359
360 #[rstest]
361 fn test_query_pool_swap_target_price_empty_pool(full_range: TestCase) {
362 let state = empty_full_range_state();
363 assert_missed_limit_falls_back(&state, &full_range.token0(), &full_range.token1());
364 }
365
366 #[test]
367 fn test_query_pool_swap_target_price_virtual_orders_past_target() {
368 let case = twamm();
369 let state = &case.state_after_transition;
370 assert_virtual_orders_applied_before_direction_check(state, &case.token0(), &case.token1());
371 }
372}