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::ekubo::test_cases::*;
201
202 #[apply(all_cases)]
203 #[tokio::test]
204 async fn test_try_from_with_header(case: TestCase) {
205 let snapshot = ComponentWithState {
206 state: ResponseProtocolState {
207 attributes: case.state_attributes,
208 ..Default::default()
209 },
210 component: case.component,
211 component_tvl: None,
212 entrypoints: Vec::new(),
213 };
214
215 let result = EkuboState::try_from_with_header(
216 snapshot,
217 BlockHeader::default(),
218 &HashMap::new(),
219 &HashMap::new(),
220 &DecoderContext::new(),
221 )
222 .await
223 .expect("reconstructing state");
224
225 assert_eq!(result, case.state_before_transition);
226 }
227
228 #[apply(all_cases)]
229 #[tokio::test]
230 async fn test_try_from_invalid(case: TestCase) {
231 for missing_attribute in case.required_attributes {
232 let mut component = case.component.clone();
233 let mut attributes = case.state_attributes.clone();
234
235 component
236 .static_attributes
237 .remove(&missing_attribute);
238 attributes.remove(&missing_attribute);
239
240 let snapshot = ComponentWithState {
241 state: ResponseProtocolState {
242 attributes,
243 component_id: Default::default(),
244 balances: Default::default(),
245 },
246 component,
247 component_tvl: None,
248 entrypoints: Vec::new(),
249 };
250
251 let result = EkuboState::try_from_with_header(
252 snapshot,
253 BlockHeader::default(),
254 &HashMap::default(),
255 &HashMap::default(),
256 &DecoderContext::new(),
257 )
258 .await;
259
260 assert!(result.is_err());
261 }
262 }
263}