Skip to main content

tycho_simulation/evm/protocol/aerodrome_v1/
decoder.rs

1use std::collections::HashMap;
2
3use alloy::primitives::U256;
4use tycho_client::feed::{synchronizer::ComponentWithState, BlockHeader};
5use tycho_common::{models::token::Token, Bytes};
6
7use crate::{
8    evm::protocol::aerodrome_v1::state::AerodromeV1State,
9    protocol::{
10        errors::InvalidSnapshotError,
11        models::{DecoderContext, TryFromWithBlock},
12    },
13};
14
15impl TryFromWithBlock<ComponentWithState, BlockHeader> for AerodromeV1State {
16    type Error = InvalidSnapshotError;
17
18    /// Decodes a `ComponentWithState` into an `AerodromeV1State`. Errors with an
19    /// `InvalidSnapshotError` if any required attribute is missing.
20    async fn try_from_with_header(
21        snapshot: ComponentWithState,
22        _block: BlockHeader,
23        _account_balances: &HashMap<Bytes, HashMap<Bytes, Bytes>>,
24        all_tokens: &HashMap<Bytes, Token>,
25        _decoder_context: &DecoderContext,
26    ) -> Result<Self, Self::Error> {
27        let reserve0 = U256::from_be_slice(
28            snapshot
29                .state
30                .attributes
31                .get("reserve0")
32                .ok_or(InvalidSnapshotError::MissingAttribute("reserve0".to_string()))?,
33        );
34        let reserve1 = U256::from_be_slice(
35            snapshot
36                .state
37                .attributes
38                .get("reserve1")
39                .ok_or(InvalidSnapshotError::MissingAttribute("reserve1".to_string()))?,
40        );
41        let stable = snapshot
42            .component
43            .static_attributes
44            .get("is_stable")
45            .ok_or(InvalidSnapshotError::MissingAttribute("is_stable".to_string()))?
46            .first() ==
47            Some(&1);
48
49        let fee = snapshot
50            .state
51            .attributes
52            .get("fee")
53            .map(|fee| u32::from(fee.clone()))
54            .unwrap_or(0);
55
56        if fee > 10_000 {
57            return Err(InvalidSnapshotError::ValueError(format!(
58                "Invalid fee value {fee}, expected <= 10000 bps"
59            )));
60        }
61
62        let token0 = all_tokens
63            .get(&snapshot.component.tokens[0])
64            .ok_or_else(|| InvalidSnapshotError::ValueError("Token0 not found".to_string()))?;
65        let token1 = all_tokens
66            .get(&snapshot.component.tokens[1])
67            .ok_or_else(|| InvalidSnapshotError::ValueError("Token1 not found".to_string()))?;
68
69        Ok(Self::new(reserve0, reserve1, stable, fee, token0.decimals as u8, token1.decimals as u8))
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use std::collections::HashMap;
76
77    use alloy::primitives::U256;
78    use rstest::rstest;
79    use tycho_client::feed::synchronizer::ComponentWithState;
80    use tycho_common::{
81        dto::ResponseProtocolState,
82        models::{token::Token, Chain},
83        simulation::protocol_sim::ProtocolSim,
84        Bytes,
85    };
86
87    use super::super::state::AerodromeV1State;
88    use crate::protocol::{errors::InvalidSnapshotError, models::TryFromWithBlock};
89
90    fn token_keys() -> (Bytes, Bytes) {
91        let token0 = Bytes::from([0_u8; 20]);
92        let mut addr = [0_u8; 20];
93        addr[19] = 1;
94        let token1 = Bytes::from(addr);
95        (token0, token1)
96    }
97
98    fn tokens() -> HashMap<Bytes, Token> {
99        let (token0_addr, token1_addr) = token_keys();
100        let token0 = Token::new(&token0_addr, "T0", 18, 0, &[Some(10_000)], Chain::Ethereum, 100);
101        let token1 = Token::new(&token1_addr, "T1", 6, 0, &[Some(10_000)], Chain::Ethereum, 100);
102        HashMap::from([(token0.address.clone(), token0), (token1.address.clone(), token1)])
103    }
104
105    #[tokio::test]
106    async fn test_aerodrome_v1_try_from() {
107        let (token0, token1) = token_keys();
108        let snapshot = ComponentWithState {
109            state: ResponseProtocolState {
110                component_id: "State1".to_owned(),
111                attributes: HashMap::from([
112                    ("reserve0".to_string(), Bytes::from(vec![0; 32])),
113                    ("reserve1".to_string(), Bytes::from(vec![0; 32])),
114                ]),
115                balances: HashMap::new(),
116            },
117            component: tycho_common::dto::ProtocolComponent {
118                tokens: vec![token0, token1],
119                static_attributes: HashMap::from([("is_stable".to_string(), Bytes::from(vec![0]))]),
120                ..Default::default()
121            },
122            component_tvl: None,
123            entrypoints: Vec::new(),
124        };
125
126        let result = AerodromeV1State::try_from_with_header(
127            snapshot,
128            Default::default(),
129            &HashMap::default(),
130            &tokens(),
131            &Default::default(),
132        )
133        .await;
134
135        assert!(result.is_ok());
136        assert_eq!(
137            result.unwrap(),
138            AerodromeV1State::new(U256::from(0u64), U256::from(0u64), false, 0, 18, 6)
139        );
140    }
141
142    #[tokio::test]
143    #[rstest]
144    #[case::missing_reserve0("reserve0")]
145    #[case::missing_reserve1("reserve1")]
146    #[case::missing_is_stable("is_stable")]
147    async fn test_aerodrome_v1_try_from_missing_attribute(#[case] missing_attribute: &str) {
148        let (token0, token1) = token_keys();
149        let mut attributes = HashMap::from([
150            ("reserve0".to_string(), Bytes::from(vec![0; 32])),
151            ("reserve1".to_string(), Bytes::from(vec![0; 32])),
152        ]);
153        let mut static_attributes =
154            HashMap::from([("is_stable".to_string(), Bytes::from(vec![0]))]);
155        match missing_attribute {
156            "reserve0" | "reserve1" => {
157                attributes.remove(missing_attribute);
158            }
159            "is_stable" => {
160                static_attributes.remove(missing_attribute);
161            }
162            _ => unreachable!("unexpected attribute under test: {missing_attribute}"),
163        }
164
165        let snapshot = ComponentWithState {
166            state: ResponseProtocolState {
167                component_id: "State1".to_owned(),
168                attributes,
169                balances: HashMap::new(),
170            },
171            component: tycho_common::dto::ProtocolComponent {
172                tokens: vec![token0, token1],
173                static_attributes,
174                ..Default::default()
175            },
176            component_tvl: None,
177            entrypoints: Vec::new(),
178        };
179
180        let result = AerodromeV1State::try_from_with_header(
181            snapshot,
182            Default::default(),
183            &HashMap::default(),
184            &tokens(),
185            &Default::default(),
186        )
187        .await;
188
189        assert!(result.is_err());
190        assert!(matches!(
191            result.unwrap_err(),
192            InvalidSnapshotError::MissingAttribute(ref x) if x == missing_attribute
193        ));
194    }
195
196    #[tokio::test]
197    async fn test_aerodrome_v1_try_from_invalid_fee() {
198        let (token0, token1) = token_keys();
199        let snapshot = ComponentWithState {
200            state: ResponseProtocolState {
201                component_id: "State1".to_owned(),
202                attributes: HashMap::from([
203                    ("reserve0".to_string(), Bytes::from(vec![0; 32])),
204                    ("reserve1".to_string(), Bytes::from(vec![0; 32])),
205                    ("fee".to_string(), Bytes::from(10_001_u32.to_be_bytes().to_vec())),
206                ]),
207                balances: HashMap::new(),
208            },
209            component: tycho_common::dto::ProtocolComponent {
210                tokens: vec![token0, token1],
211                static_attributes: HashMap::from([("is_stable".to_string(), Bytes::from(vec![0]))]),
212                ..Default::default()
213            },
214            component_tvl: None,
215            entrypoints: Vec::new(),
216        };
217
218        let result = AerodromeV1State::try_from_with_header(
219            snapshot,
220            Default::default(),
221            &HashMap::default(),
222            &tokens(),
223            &Default::default(),
224        )
225        .await;
226        assert!(matches!(result, Err(InvalidSnapshotError::ValueError(_))));
227    }
228
229    #[tokio::test]
230    async fn test_aerodrome_v1_try_from_zero_fee_indicator() {
231        let (token0, token1) = token_keys();
232        let snapshot = ComponentWithState {
233            state: ResponseProtocolState {
234                component_id: "State1".to_owned(),
235                attributes: HashMap::from([
236                    ("reserve0".to_string(), Bytes::from(vec![0; 32])),
237                    ("reserve1".to_string(), Bytes::from(vec![0; 32])),
238                    ("fee".to_string(), Bytes::from(420_u32.to_be_bytes().to_vec())),
239                ]),
240                balances: HashMap::new(),
241            },
242            component: tycho_common::dto::ProtocolComponent {
243                tokens: vec![token0, token1],
244                static_attributes: HashMap::from([("is_stable".to_string(), Bytes::from(vec![1]))]),
245                ..Default::default()
246            },
247            component_tvl: None,
248            entrypoints: Vec::new(),
249        };
250
251        let result = AerodromeV1State::try_from_with_header(
252            snapshot,
253            Default::default(),
254            &HashMap::default(),
255            &tokens(),
256            &Default::default(),
257        )
258        .await;
259
260        assert!(result.is_ok());
261        let state = result.unwrap();
262        assert_eq!(state.fee, 420);
263        assert!(state.stable);
264        assert_eq!(state.fee(), 0.0);
265        assert_eq!(state.decimals0, 18);
266        assert_eq!(state.decimals1, 6);
267    }
268}