tycho_simulation/evm/protocol/ekubo/
decoder.rs

1use 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        // See extension ID encoding in tycho-protocol-sdk
42        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, /* For the purpose of quote computation it isn't
134                                            * required to track actual timestamps */
135                },
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}