Skip to main content

tycho_simulation/evm/protocol/ekubo_v3/
state.rs

1use std::{
2    any::Any,
3    collections::{HashMap, HashSet},
4    fmt::Debug,
5};
6
7use ekubo_sdk::{
8    chain::evm::{EvmPoolKey, EvmTokenAmount},
9    U256,
10};
11use num_bigint::BigUint;
12use revm::primitives::Address;
13use serde::{Deserialize, Serialize};
14use tycho_common::{
15    dto::ProtocolStateDelta,
16    models::token::Token,
17    simulation::{
18        errors::{SimulationError, TransitionError},
19        protocol_sim::{Balances, GetAmountOutResult, ProtocolSim},
20    },
21    Bytes,
22};
23
24use super::pool::{
25    concentrated::ConcentratedPool, full_range::FullRangePool, oracle::OraclePool,
26    twamm::TwammPool, EkuboPool,
27};
28use crate::evm::protocol::{
29    ekubo_v3::pool::{
30        boosted_fees::BoostedFeesPool, mev_capture::MevCapturePool, stableswap::StableswapPool,
31    },
32    u256_num::u256_to_f64,
33};
34
35#[enum_delegate::implement(EkuboPool)]
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37pub enum EkuboV3State {
38    Concentrated(ConcentratedPool),
39    FullRange(FullRangePool),
40    Stableswap(StableswapPool),
41    Oracle(OraclePool),
42    Twamm(TwammPool),
43    MevCapture(MevCapturePool),
44    BoostedFees(BoostedFeesPool),
45}
46
47fn sqrt_price_q128_to_f64(
48    x: U256,
49    (token0_decimals, token1_decimals): (usize, usize),
50) -> Result<f64, SimulationError> {
51    let token_correction = 10f64.powi(token0_decimals as i32 - token1_decimals as i32);
52
53    let price = u256_to_f64(x)? / 2.0f64.powi(128);
54    Ok(price.powi(2) * token_correction)
55}
56
57#[typetag::serde]
58impl ProtocolSim for EkuboV3State {
59    fn fee(&self) -> f64 {
60        self.key().config.fee as f64 / (2f64.powi(64))
61    }
62
63    fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
64        let sqrt_ratio = self.sqrt_ratio();
65        let (base_decimals, quote_decimals) = (base.decimals as usize, quote.decimals as usize);
66
67        if base < quote {
68            sqrt_price_q128_to_f64(sqrt_ratio, (base_decimals, quote_decimals))
69        } else {
70            sqrt_price_q128_to_f64(sqrt_ratio, (quote_decimals, base_decimals))
71                .map(|price| 1.0f64 / price)
72        }
73    }
74
75    fn get_amount_out(
76        &self,
77        amount_in: BigUint,
78        token_in: &Token,
79        _token_out: &Token,
80    ) -> Result<GetAmountOutResult, SimulationError> {
81        let token_amount = EvmTokenAmount {
82            token: Address::try_from(&token_in.address[..]).map_err(|err| {
83                SimulationError::InvalidInput(format!("token_in invalid: {err}"), None)
84            })?,
85            amount: amount_in.try_into().map_err(|_| {
86                SimulationError::InvalidInput("amount in must fit into a i128".to_string(), None)
87            })?,
88        };
89
90        let quote = self.quote(token_amount)?;
91
92        if quote.calculated_amount > i128::MAX as u128 {
93            return Err(SimulationError::RecoverableError(
94                "calculated amount exceeds i128::MAX".to_string(),
95            ));
96        }
97
98        let res = GetAmountOutResult {
99            amount: BigUint::from(quote.calculated_amount),
100            gas: quote.gas.into(),
101            new_state: Box::new(quote.new_state),
102        };
103
104        if quote.consumed_amount != token_amount.amount {
105            return Err(SimulationError::InvalidInput(
106                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),
107                Some(res),
108            ));
109        }
110
111        Ok(res)
112    }
113
114    fn delta_transition(
115        &mut self,
116        delta: ProtocolStateDelta,
117        _tokens: &HashMap<Bytes, Token>,
118        _balances: &Balances,
119    ) -> Result<(), TransitionError> {
120        if let Some(liquidity) = delta
121            .updated_attributes
122            .get("liquidity")
123        {
124            self.set_liquidity(liquidity.clone().into());
125        }
126
127        if let Some(sqrt_price) = delta
128            .updated_attributes
129            .get("sqrt_ratio")
130        {
131            self.set_sqrt_ratio(U256::try_from_be_slice(sqrt_price).ok_or_else(|| {
132                TransitionError::DecodeError("failed to parse updated pool price".to_string())
133            })?);
134        }
135
136        self.finish_transition(delta.updated_attributes, delta.deleted_attributes)
137    }
138
139    fn query_pool_swap(
140        &self,
141        params: &tycho_common::simulation::protocol_sim::QueryPoolSwapParams,
142    ) -> Result<tycho_common::simulation::protocol_sim::PoolSwap, SimulationError> {
143        crate::evm::query_pool_swap::query_pool_swap(self, params)
144    }
145
146    fn clone_box(&self) -> Box<dyn ProtocolSim> {
147        Box::new(self.clone())
148    }
149
150    fn as_any(&self) -> &dyn Any {
151        self
152    }
153
154    fn as_any_mut(&mut self) -> &mut dyn Any {
155        self
156    }
157
158    fn eq(&self, other: &dyn ProtocolSim) -> bool {
159        other
160            .as_any()
161            .downcast_ref::<EkuboV3State>()
162            .is_some_and(|other_state| self == other_state)
163    }
164
165    fn get_limits(
166        &self,
167        sell_token: Bytes,
168        _buy_token: Bytes,
169    ) -> Result<(BigUint, BigUint), SimulationError> {
170        let consumed_amount =
171            self.get_limit(Address::try_from(&sell_token[..]).map_err(|err| {
172                SimulationError::InvalidInput(format!("sell_token invalid: {err}"), None)
173            })?)?;
174
175        // TODO Update once exact out is supported
176        Ok((
177            BigUint::try_from(consumed_amount).map_err(|_| {
178                SimulationError::FatalError(format!(
179                    "Failed to convert consumed amount `{consumed_amount}` into BigUint"
180                ))
181            })?,
182            BigUint::ZERO,
183        ))
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use rstest::*;
190    use rstest_reuse::apply;
191
192    use super::*;
193    use crate::evm::protocol::ekubo_v3::test_cases::*;
194
195    #[apply(all_cases)]
196    fn test_delta_transition(case: TestCase) {
197        let mut state = case.state_before_transition;
198
199        state
200            .delta_transition(
201                ProtocolStateDelta {
202                    updated_attributes: case.transition_attributes,
203                    ..Default::default()
204                },
205                &HashMap::default(),
206                &Balances::default(),
207            )
208            .expect("executing transition");
209
210        assert_eq!(state, case.state_after_transition);
211    }
212
213    #[apply(all_cases)]
214    fn test_get_amount_out(case: TestCase) {
215        let (token0, token1) = (case.token0(), case.token1());
216        let (amount_in, expected_out) = case.swap_token0;
217
218        let res = case
219            .state_after_transition
220            .get_amount_out(amount_in, &token0, &token1)
221            .expect("computing quote");
222
223        assert_eq!(res.amount, expected_out);
224    }
225
226    #[apply(all_cases)]
227    fn test_get_limits(case: TestCase) {
228        use std::ops::Deref;
229
230        let (token0, token1) = (case.token0(), case.token1());
231        let state = case.state_after_transition;
232
233        let max_amount_in = state
234            .get_limits(token0.address.deref().into(), token1.address.deref().into())
235            .expect("computing limits for token0")
236            .0;
237
238        assert_eq!(max_amount_in, case.expected_limit_token0);
239
240        state
241            .get_amount_out(max_amount_in, &token0, &token1)
242            .expect("quoting with limit");
243    }
244}