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        models::{protocol::ProtocolComponentState, token::Token, Chain},
82        simulation::protocol_sim::ProtocolSim,
83        Bytes,
84    };
85
86    use super::super::state::AerodromeV1State;
87    use crate::protocol::{errors::InvalidSnapshotError, models::TryFromWithBlock};
88
89    fn token_keys() -> (Bytes, Bytes) {
90        let token0 = Bytes::from([0_u8; 20]);
91        let mut addr = [0_u8; 20];
92        addr[19] = 1;
93        let token1 = Bytes::from(addr);
94        (token0, token1)
95    }
96
97    fn tokens() -> HashMap<Bytes, Token> {
98        let (token0_addr, token1_addr) = token_keys();
99        let token0 = Token::new(&token0_addr, "T0", 18, 0, &[Some(10_000)], Chain::Ethereum, 100);
100        let token1 = Token::new(&token1_addr, "T1", 6, 0, &[Some(10_000)], Chain::Ethereum, 100);
101        HashMap::from([(token0.address.clone(), token0), (token1.address.clone(), token1)])
102    }
103
104    #[tokio::test]
105    async fn test_aerodrome_v1_try_from() {
106        let (token0, token1) = token_keys();
107        let snapshot = ComponentWithState {
108            state: ProtocolComponentState {
109                component_id: "State1".to_owned(),
110                attributes: HashMap::from([
111                    ("reserve0".to_string(), Bytes::from(vec![0; 32])),
112                    ("reserve1".to_string(), Bytes::from(vec![0; 32])),
113                ]),
114                balances: HashMap::new(),
115            },
116            component: tycho_common::models::protocol::ProtocolComponent {
117                tokens: vec![token0, token1],
118                static_attributes: HashMap::from([("is_stable".to_string(), Bytes::from(vec![0]))]),
119                ..Default::default()
120            },
121            component_tvl: None,
122            entrypoints: Vec::new(),
123        };
124
125        let result = AerodromeV1State::try_from_with_header(
126            snapshot,
127            Default::default(),
128            &HashMap::default(),
129            &tokens(),
130            &Default::default(),
131        )
132        .await;
133
134        assert!(result.is_ok());
135        assert_eq!(
136            result.unwrap(),
137            AerodromeV1State::new(U256::from(0u64), U256::from(0u64), false, 0, 18, 6)
138        );
139    }
140
141    #[tokio::test]
142    #[rstest]
143    #[case::missing_reserve0("reserve0")]
144    #[case::missing_reserve1("reserve1")]
145    #[case::missing_is_stable("is_stable")]
146    async fn test_aerodrome_v1_try_from_missing_attribute(#[case] missing_attribute: &str) {
147        let (token0, token1) = token_keys();
148        let mut attributes = HashMap::from([
149            ("reserve0".to_string(), Bytes::from(vec![0; 32])),
150            ("reserve1".to_string(), Bytes::from(vec![0; 32])),
151        ]);
152        let mut static_attributes =
153            HashMap::from([("is_stable".to_string(), Bytes::from(vec![0]))]);
154        match missing_attribute {
155            "reserve0" | "reserve1" => {
156                attributes.remove(missing_attribute);
157            }
158            "is_stable" => {
159                static_attributes.remove(missing_attribute);
160            }
161            _ => unreachable!("unexpected attribute under test: {missing_attribute}"),
162        }
163
164        let snapshot = ComponentWithState {
165            state: ProtocolComponentState {
166                component_id: "State1".to_owned(),
167                attributes,
168                balances: HashMap::new(),
169            },
170            component: tycho_common::models::protocol::ProtocolComponent {
171                tokens: vec![token0, token1],
172                static_attributes,
173                ..Default::default()
174            },
175            component_tvl: None,
176            entrypoints: Vec::new(),
177        };
178
179        let result = AerodromeV1State::try_from_with_header(
180            snapshot,
181            Default::default(),
182            &HashMap::default(),
183            &tokens(),
184            &Default::default(),
185        )
186        .await;
187
188        assert!(result.is_err());
189        assert!(matches!(
190            result.unwrap_err(),
191            InvalidSnapshotError::MissingAttribute(ref x) if x == missing_attribute
192        ));
193    }
194
195    #[tokio::test]
196    async fn test_aerodrome_v1_try_from_invalid_fee() {
197        let (token0, token1) = token_keys();
198        let snapshot = ComponentWithState {
199            state: ProtocolComponentState {
200                component_id: "State1".to_owned(),
201                attributes: HashMap::from([
202                    ("reserve0".to_string(), Bytes::from(vec![0; 32])),
203                    ("reserve1".to_string(), Bytes::from(vec![0; 32])),
204                    ("fee".to_string(), Bytes::from(10_001_u32.to_be_bytes().to_vec())),
205                ]),
206                balances: HashMap::new(),
207            },
208            component: tycho_common::models::protocol::ProtocolComponent {
209                tokens: vec![token0, token1],
210                static_attributes: HashMap::from([("is_stable".to_string(), Bytes::from(vec![0]))]),
211                ..Default::default()
212            },
213            component_tvl: None,
214            entrypoints: Vec::new(),
215        };
216
217        let result = AerodromeV1State::try_from_with_header(
218            snapshot,
219            Default::default(),
220            &HashMap::default(),
221            &tokens(),
222            &Default::default(),
223        )
224        .await;
225        assert!(matches!(result, Err(InvalidSnapshotError::ValueError(_))));
226    }
227
228    #[tokio::test]
229    async fn test_aerodrome_v1_try_from_zero_fee_indicator() {
230        let (token0, token1) = token_keys();
231        let snapshot = ComponentWithState {
232            state: ProtocolComponentState {
233                component_id: "State1".to_owned(),
234                attributes: HashMap::from([
235                    ("reserve0".to_string(), Bytes::from(vec![0; 32])),
236                    ("reserve1".to_string(), Bytes::from(vec![0; 32])),
237                    ("fee".to_string(), Bytes::from(420_u32.to_be_bytes().to_vec())),
238                ]),
239                balances: HashMap::new(),
240            },
241            component: tycho_common::models::protocol::ProtocolComponent {
242                tokens: vec![token0, token1],
243                static_attributes: HashMap::from([("is_stable".to_string(), Bytes::from(vec![1]))]),
244                ..Default::default()
245            },
246            component_tvl: None,
247            entrypoints: Vec::new(),
248        };
249
250        let result = AerodromeV1State::try_from_with_header(
251            snapshot,
252            Default::default(),
253            &HashMap::default(),
254            &tokens(),
255            &Default::default(),
256        )
257        .await;
258
259        assert!(result.is_ok());
260        let state = result.unwrap();
261        assert_eq!(state.fee, 420);
262        assert!(state.stable);
263        assert_eq!(state.fee(), 0.0);
264        assert_eq!(state.decimals0, 18);
265        assert_eq!(state.decimals1, 6);
266    }
267}