tycho_simulation/evm/protocol/ekubo/
decoder.rs1use std::collections::HashMap;
2
3use evm_ekubo_sdk::{
4 math::uint::U256,
5 quoting::{
6 full_range_pool::FullRangePoolState,
7 oracle_pool::OraclePoolState,
8 twamm_pool::TwammPoolState,
9 types::{Config, NodeKey},
10 },
11};
12use itertools::Itertools;
13use num_traits::Zero;
14use tycho_client::feed::{synchronizer::ComponentWithState, BlockHeader};
15use tycho_common::{models::token::Token, Bytes};
16
17use super::{
18 attributes::{sale_rate_deltas_from_attributes, ticks_from_attributes},
19 pool::{base::BasePool, full_range::FullRangePool, oracle::OraclePool, twamm::TwammPool},
20 state::EkuboState,
21};
22use crate::{
23 evm::protocol::ekubo::pool::mev_resist::MevResistPool,
24 protocol::{
25 errors::InvalidSnapshotError,
26 models::{DecoderContext, TryFromWithBlock},
27 },
28};
29
30enum EkuboExtension {
31 Base,
32 Oracle,
33 Twamm,
34 MevResist,
35}
36
37impl TryFrom<Bytes> for EkuboExtension {
38 type Error = InvalidSnapshotError;
39
40 fn try_from(value: Bytes) -> Result<Self, Self::Error> {
41 match i32::from(value) {
43 0 => Err(InvalidSnapshotError::ValueError("Unknown Ekubo extension".to_string())),
44 1 => Ok(Self::Base),
45 2 => Ok(Self::Oracle),
46 3 => Ok(Self::Twamm),
47 4 => Ok(Self::MevResist),
48 discriminant => Err(InvalidSnapshotError::ValueError(format!(
49 "Unknown Ekubo extension discriminant {discriminant}"
50 ))),
51 }
52 }
53}
54
55impl TryFromWithBlock<ComponentWithState, BlockHeader> for EkuboState {
56 type Error = InvalidSnapshotError;
57
58 async fn try_from_with_header(
59 snapshot: ComponentWithState,
60 _block: BlockHeader,
61 _account_balances: &HashMap<Bytes, HashMap<Bytes, Bytes>>,
62 _all_tokens: &HashMap<Bytes, Token>,
63 _decoder_context: &DecoderContext,
64 ) -> Result<Self, Self::Error> {
65 let static_attrs = snapshot.component.static_attributes;
66 let state_attrs = snapshot.state.attributes;
67
68 let extension_id = attribute(&static_attrs, "extension_id")?
69 .clone()
70 .try_into()?;
71
72 let (token0, token1) = (
73 U256::from_big_endian(attribute(&static_attrs, "token0")?),
74 U256::from_big_endian(attribute(&static_attrs, "token1")?),
75 );
76
77 let fee = u64::from_be_bytes(
78 attribute(&static_attrs, "fee")?
79 .as_ref()
80 .try_into()
81 .map_err(|err| {
82 InvalidSnapshotError::ValueError(format!("fee length mismatch: {err:?}"))
83 })?,
84 );
85
86 let tick_spacing = u32::from_be_bytes(
87 attribute(&static_attrs, "tick_spacing")?
88 .as_ref()
89 .try_into()
90 .map_err(|err| {
91 InvalidSnapshotError::ValueError(format!(
92 "tick_spacing length mismatch: {err:?}"
93 ))
94 })?,
95 );
96
97 let extension = U256::from_big_endian(attribute(&static_attrs, "extension")?);
98
99 let config = Config { fee, tick_spacing, extension };
100
101 let liquidity = attribute(&state_attrs, "liquidity")?
102 .clone()
103 .into();
104
105 let sqrt_ratio = U256::from_big_endian(attribute(&state_attrs, "sqrt_ratio")?);
106
107 let key = NodeKey { token0, token1, config };
108
109 Ok(match extension_id {
110 EkuboExtension::Base => {
111 if tick_spacing.is_zero() {
112 Self::FullRange(FullRangePool::new(
113 key,
114 FullRangePoolState { sqrt_ratio, liquidity },
115 )?)
116 } else {
117 let tick = attribute(&state_attrs, "tick")?
118 .clone()
119 .into();
120
121 let mut ticks = ticks_from_attributes(state_attrs)
122 .map_err(InvalidSnapshotError::ValueError)?;
123
124 ticks.sort_unstable_by_key(|tick| tick.index);
125
126 Self::Base(BasePool::new(key, ticks, sqrt_ratio, liquidity, tick)?)
127 }
128 }
129 EkuboExtension::Oracle => Self::Oracle(OraclePool::new(
130 &key,
131 OraclePoolState {
132 full_range_pool_state: FullRangePoolState { sqrt_ratio, liquidity },
133 last_snapshot_time: 0, },
136 )?),
137 EkuboExtension::Twamm => {
138 let (token0_sale_rate, token1_sale_rate) = (
139 attribute(&state_attrs, "token0_sale_rate")?
140 .clone()
141 .into(),
142 attribute(&state_attrs, "token1_sale_rate")?
143 .clone()
144 .into(),
145 );
146
147 let last_execution_time: u64 = attribute(&state_attrs, "last_execution_time")?
148 .clone()
149 .into();
150
151 let mut virtual_order_deltas =
152 sale_rate_deltas_from_attributes(state_attrs, last_execution_time)
153 .map_err(InvalidSnapshotError::ValueError)?
154 .collect_vec();
155
156 virtual_order_deltas.sort_unstable_by_key(|delta| delta.time);
157
158 Self::Twamm(TwammPool::new(
159 &key,
160 TwammPoolState {
161 full_range_pool_state: FullRangePoolState { sqrt_ratio, liquidity },
162 token0_sale_rate,
163 token1_sale_rate,
164 last_execution_time,
165 },
166 virtual_order_deltas,
167 )?)
168 }
169 EkuboExtension::MevResist => {
170 let tick = attribute(&state_attrs, "tick")?
171 .clone()
172 .into();
173
174 let mut ticks =
175 ticks_from_attributes(state_attrs).map_err(InvalidSnapshotError::ValueError)?;
176
177 ticks.sort_unstable_by_key(|tick| tick.index);
178
179 Self::MevResist(MevResistPool::new(key, ticks, sqrt_ratio, liquidity, tick)?)
180 }
181 })
182 }
183}
184
185fn attribute<'a>(
186 map: &'a HashMap<String, Bytes>,
187 key: &str,
188) -> Result<&'a Bytes, InvalidSnapshotError> {
189 map.get(key)
190 .ok_or_else(|| InvalidSnapshotError::MissingAttribute(key.to_string()))
191}
192
193#[cfg(test)]
194mod tests {
195 use rstest::*;
196 use rstest_reuse::apply;
197 use tycho_common::dto::ResponseProtocolState;
198
199 use super::*;
200 use crate::evm::protocol::{
201 ekubo::test_cases::*, test_utils::try_decode_snapshot_with_defaults,
202 };
203
204 #[apply(all_cases)]
205 #[tokio::test]
206 async fn test_try_from_with_header(case: TestCase) {
207 let snapshot = ComponentWithState {
208 state: ResponseProtocolState {
209 attributes: case.state_attributes,
210 ..Default::default()
211 },
212 component: case.component,
213 component_tvl: None,
214 entrypoints: Vec::new(),
215 };
216
217 let result = try_decode_snapshot_with_defaults::<EkuboState>(snapshot)
218 .await
219 .expect("reconstructing state");
220
221 assert_eq!(result, case.state_before_transition);
222 }
223
224 #[apply(all_cases)]
225 #[tokio::test]
226 async fn test_try_from_invalid(case: TestCase) {
227 for missing_attribute in case.required_attributes {
228 let mut component = case.component.clone();
229 let mut attributes = case.state_attributes.clone();
230
231 component
232 .static_attributes
233 .remove(&missing_attribute);
234 attributes.remove(&missing_attribute);
235
236 let snapshot = ComponentWithState {
237 state: ResponseProtocolState {
238 attributes,
239 component_id: Default::default(),
240 balances: Default::default(),
241 },
242 component,
243 component_tvl: None,
244 entrypoints: Vec::new(),
245 };
246
247 let result = EkuboState::try_from_with_header(
248 snapshot,
249 BlockHeader::default(),
250 &HashMap::default(),
251 &HashMap::default(),
252 &DecoderContext::new(),
253 )
254 .await;
255
256 assert!(result.is_err());
257 }
258 }
259}