tycho_simulation/evm/protocol/ekubo_v3/
state.rs1use 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, ve33::Ve33Pool, EkuboPool, EkuboPoolQuote,
27};
28use crate::evm::protocol::{
29 ekubo_v3::{
30 addresses::SIGNED_EXCLUSIVE_SWAP_DEPLOYMENTS,
31 pool::{
32 boosted_fees::BoostedFeesPool, mev_capture::MevCapturePool, stableswap::StableswapPool,
33 },
34 },
35 u256_num::u256_to_f64,
36};
37
38const SIGNED_EXCLUSIVE_SWAP_GAS: u64 = 62_373;
45
46#[enum_delegate::implement(EkuboPool)]
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48pub enum EkuboV3State {
49 Concentrated(ConcentratedPool),
50 FullRange(FullRangePool),
51 Stableswap(StableswapPool),
52 Oracle(OraclePool),
53 Twamm(TwammPool),
54 MevCapture(MevCapturePool),
55 BoostedFees(BoostedFeesPool),
56 Ve33(Ve33Pool),
57}
58
59fn sqrt_price_q128_to_f64(
60 x: U256,
61 (token0_decimals, token1_decimals): (usize, usize),
62) -> Result<f64, SimulationError> {
63 let token_correction = 10f64.powi(token0_decimals as i32 - token1_decimals as i32);
64
65 let price = u256_to_f64(x)? / 2.0f64.powi(128);
66 Ok(price.powi(2) * token_correction)
67}
68
69impl EkuboV3State {
70 fn forward_overhead_gas(&self) -> u64 {
74 let extension = self.key().config.extension;
75 if SIGNED_EXCLUSIVE_SWAP_DEPLOYMENTS
76 .iter()
77 .any(|(_, deployment)| *deployment == extension)
78 {
79 SIGNED_EXCLUSIVE_SWAP_GAS
80 } else {
81 0
82 }
83 }
84}
85
86#[typetag::serde]
87impl ProtocolSim for EkuboV3State {
88 fn fee(&self) -> f64 {
89 let fee = match self {
90 Self::Ve33(pool) => pool.swap_fee(),
91 _ => self.key().config.fee,
92 };
93 fee as f64 / (2f64.powi(64))
94 }
95
96 fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
97 let sqrt_ratio = self.sqrt_ratio();
98 let (base_decimals, quote_decimals) = (base.decimals as usize, quote.decimals as usize);
99
100 if base < quote {
101 sqrt_price_q128_to_f64(sqrt_ratio, (base_decimals, quote_decimals))
102 } else {
103 sqrt_price_q128_to_f64(sqrt_ratio, (quote_decimals, base_decimals))
104 .map(|price| 1.0f64 / price)
105 }
106 }
107
108 fn get_amount_out(
109 &self,
110 amount_in: BigUint,
111 token_in: &Token,
112 _token_out: &Token,
113 ) -> Result<GetAmountOutResult, SimulationError> {
114 let token_amount = EvmTokenAmount {
115 token: Address::try_from(&token_in.address[..]).map_err(|err| {
116 SimulationError::InvalidInput(format!("token_in invalid: {err}"), None)
117 })?,
118 amount: amount_in.try_into().map_err(|_| {
119 SimulationError::InvalidInput("amount in must fit into a i128".to_string(), None)
120 })?,
121 };
122
123 let quote = self.quote(token_amount)?;
124
125 if quote.calculated_amount > i128::MAX as u128 {
126 return Err(SimulationError::RecoverableError(
127 "calculated amount exceeds i128::MAX".to_string(),
128 ));
129 }
130
131 let res = GetAmountOutResult {
132 amount: BigUint::from(quote.calculated_amount),
133 gas: BigUint::from(quote.gas) + BigUint::from(self.forward_overhead_gas()),
134 new_state: Box::new(quote.new_state),
135 };
136
137 if quote.consumed_amount != token_amount.amount {
138 return Err(SimulationError::InvalidInput(
139 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),
140 Some(res),
141 ));
142 }
143
144 Ok(res)
145 }
146
147 fn delta_transition(
148 &mut self,
149 delta: ProtocolStateDelta,
150 _tokens: &HashMap<Bytes, Token>,
151 _balances: &Balances,
152 ) -> Result<(), TransitionError> {
153 if let Some(liquidity) = delta
154 .updated_attributes
155 .get("liquidity")
156 {
157 self.set_liquidity(liquidity.clone().into());
158 }
159
160 if let Some(sqrt_price) = delta
161 .updated_attributes
162 .get("sqrt_ratio")
163 {
164 self.set_sqrt_ratio(U256::try_from_be_slice(sqrt_price).ok_or_else(|| {
165 TransitionError::DecodeError("failed to parse updated pool price".to_string())
166 })?);
167 }
168
169 self.finish_transition(delta.updated_attributes, delta.deleted_attributes)
170 }
171
172 fn query_pool_swap(
173 &self,
174 params: &tycho_common::simulation::protocol_sim::QueryPoolSwapParams,
175 ) -> Result<tycho_common::simulation::protocol_sim::PoolSwap, SimulationError> {
176 crate::evm::query_pool_swap::query_pool_swap(self, params)
177 }
178
179 fn clone_box(&self) -> Box<dyn ProtocolSim> {
180 Box::new(self.clone())
181 }
182
183 fn as_any(&self) -> &dyn Any {
184 self
185 }
186
187 fn as_any_mut(&mut self) -> &mut dyn Any {
188 self
189 }
190
191 fn eq(&self, other: &dyn ProtocolSim) -> bool {
192 other
193 .as_any()
194 .downcast_ref::<EkuboV3State>()
195 .is_some_and(|other_state| self == other_state)
196 }
197
198 fn get_limits(
199 &self,
200 sell_token: Bytes,
201 _buy_token: Bytes,
202 ) -> Result<(BigUint, BigUint), SimulationError> {
203 let consumed_amount =
204 self.get_limit(Address::try_from(&sell_token[..]).map_err(|err| {
205 SimulationError::InvalidInput(format!("sell_token invalid: {err}"), None)
206 })?)?;
207
208 Ok((
210 BigUint::try_from(consumed_amount).map_err(|_| {
211 SimulationError::FatalError(format!(
212 "Failed to convert consumed amount `{consumed_amount}` into BigUint"
213 ))
214 })?,
215 BigUint::ZERO,
216 ))
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use rstest::*;
223 use rstest_reuse::apply;
224
225 use super::*;
226 use crate::evm::protocol::ekubo_v3::test_cases::*;
227
228 #[rstest]
230 fn test_signed_exclusive_swap_gas_includes_the_forward_overhead() {
231 let signed = signed_exclusive_swap();
232 let (token0, token1) = (signed.token0(), signed.token1());
233 let (amount_in, _) = signed.swap_token0.clone();
234
235 let signed_gas = signed
236 .state_after_transition
237 .get_amount_out(amount_in.clone(), &token0, &token1)
238 .expect("signed pool quotes")
239 .gas;
240
241 let plain = concentrated();
242 let plain_gas = plain
243 .state_after_transition
244 .get_amount_out(amount_in, &plain.token0(), &plain.token1())
245 .expect("plain pool quotes")
246 .gas;
247
248 assert_eq!(
249 signed_gas - plain_gas,
250 BigUint::from(SIGNED_EXCLUSIVE_SWAP_GAS),
251 "the signed pool must carry exactly the forward overhead over an equivalent plain pool"
252 );
253 }
254
255 #[rstest]
257 fn test_other_pools_carry_no_forward_overhead() {
258 for case in [concentrated(), full_range(), mev_capture()] {
259 assert_eq!(
260 case.state_after_transition
261 .forward_overhead_gas(),
262 0,
263 "only a signed-exclusive pool is surcharged"
264 );
265 }
266 }
267
268 #[apply(all_cases)]
269 fn test_delta_transition(case: TestCase) {
270 let mut state = case.state_before_transition;
271
272 state
273 .delta_transition(
274 ProtocolStateDelta {
275 updated_attributes: case.transition_attributes,
276 ..Default::default()
277 },
278 &HashMap::default(),
279 &Balances::default(),
280 )
281 .expect("executing transition");
282
283 assert_eq!(state, case.state_after_transition);
284 }
285
286 #[apply(all_cases)]
287 fn test_get_amount_out(case: TestCase) {
288 let (token0, token1) = (case.token0(), case.token1());
289 let (amount_in, expected_out) = case.swap_token0;
290
291 let res = case
292 .state_after_transition
293 .get_amount_out(amount_in, &token0, &token1)
294 .expect("computing quote");
295
296 assert_eq!(res.amount, expected_out);
297 }
298
299 #[apply(all_cases)]
300 fn test_get_limits(case: TestCase) {
301 use std::ops::Deref;
302
303 let (token0, token1) = (case.token0(), case.token1());
304 let state = case.state_after_transition;
305
306 let max_amount_in = state
307 .get_limits(token0.address.deref().into(), token1.address.deref().into())
308 .expect("computing limits for token0")
309 .0;
310
311 assert_eq!(max_amount_in, case.expected_limit_token0);
312
313 state
314 .get_amount_out(max_amount_in, &token0, &token1)
315 .expect("quoting with limit");
316 }
317}