Skip to main content

tycho_simulation/evm/protocol/rocketpool/
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};
6use tycho_ethereum::BytesCodec;
7
8use super::state::RocketpoolState;
9use crate::protocol::{
10    errors::InvalidSnapshotError,
11    models::{DecoderContext, TryFromWithBlock},
12};
13
14impl TryFromWithBlock<ComponentWithState, BlockHeader> for RocketpoolState {
15    type Error = InvalidSnapshotError;
16
17    /// Decodes a `ComponentWithState` into a `RocketpoolState`. Errors with a
18    /// `InvalidSnapshotError` if any required attribute is missing.
19    async fn try_from_with_header(
20        snapshot: ComponentWithState,
21        _block: BlockHeader,
22        _account_balances: &HashMap<Bytes, HashMap<Bytes, Bytes>>,
23        _all_tokens: &HashMap<Bytes, Token>,
24        _decoder_context: &DecoderContext,
25    ) -> Result<Self, Self::Error> {
26        let total_eth = snapshot
27            .state
28            .attributes
29            .get("total_eth")
30            .map(U256::from_bytes)
31            .ok_or_else(|| InvalidSnapshotError::MissingAttribute("total_eth".to_string()))?;
32        let reth_supply = snapshot
33            .state
34            .attributes
35            .get("reth_supply")
36            .map(U256::from_bytes)
37            .ok_or_else(|| InvalidSnapshotError::MissingAttribute("reth_supply".to_string()))?;
38
39        let deposit_contract_balance = snapshot
40            .state
41            .attributes
42            .get("deposit_contract_balance")
43            .map(U256::from_bytes)
44            .ok_or_else(|| {
45                InvalidSnapshotError::MissingAttribute("deposit_contract_balance".to_string())
46            })?;
47
48        let reth_contract_liquidity = snapshot
49            .state
50            .attributes
51            .get("reth_contract_liquidity")
52            .map(U256::from_bytes)
53            .ok_or_else(|| {
54                InvalidSnapshotError::MissingAttribute("reth_contract_liquidity".to_string())
55            })?;
56
57        let deposits_enabled = snapshot
58            .state
59            .attributes
60            .get("deposits_enabled")
61            .map(|val| !U256::from_bytes(val).is_zero())
62            .ok_or_else(|| {
63                InvalidSnapshotError::MissingAttribute("deposits_enabled".to_string())
64            })?;
65
66        let deposit_assigning_enabled = snapshot
67            .state
68            .attributes
69            .get("deposit_assigning_enabled")
70            .map(|val| !U256::from_bytes(val).is_zero())
71            .ok_or_else(|| {
72                InvalidSnapshotError::MissingAttribute("deposit_assigning_enabled".to_string())
73            })?;
74
75        let deposit_fee = snapshot
76            .state
77            .attributes
78            .get("deposit_fee")
79            .map(U256::from_bytes)
80            .ok_or_else(|| InvalidSnapshotError::MissingAttribute("deposit_fee".to_string()))?;
81
82        let min_deposit_amount = snapshot
83            .state
84            .attributes
85            .get("min_deposit_amount")
86            .map(U256::from_bytes)
87            .ok_or_else(|| {
88                InvalidSnapshotError::MissingAttribute("min_deposit_amount".to_string())
89            })?;
90
91        let max_deposit_pool_size = snapshot
92            .state
93            .attributes
94            .get("max_deposit_pool_size")
95            .map(U256::from_bytes)
96            .ok_or_else(|| {
97                InvalidSnapshotError::MissingAttribute("max_deposit_pool_size".to_string())
98            })?;
99
100        let deposit_assign_maximum = snapshot
101            .state
102            .attributes
103            .get("deposit_assign_maximum")
104            .map(U256::from_bytes)
105            .ok_or_else(|| {
106                InvalidSnapshotError::MissingAttribute("deposit_assign_maximum".to_string())
107            })?;
108
109        let deposit_assign_socialised_maximum = snapshot
110            .state
111            .attributes
112            .get("deposit_assign_socialised_maximum")
113            .map(U256::from_bytes)
114            .ok_or_else(|| {
115                InvalidSnapshotError::MissingAttribute(
116                    "deposit_assign_socialised_maximum".to_string(),
117                )
118            })?;
119
120        let queue_variable_start = snapshot
121            .state
122            .attributes
123            .get("queue_variable_start")
124            .map(U256::from_bytes)
125            .ok_or_else(|| {
126                InvalidSnapshotError::MissingAttribute("queue_variable_start".to_string())
127            })?;
128
129        let queue_variable_end = snapshot
130            .state
131            .attributes
132            .get("queue_variable_end")
133            .map(U256::from_bytes)
134            .ok_or_else(|| {
135                InvalidSnapshotError::MissingAttribute("queue_variable_end".to_string())
136            })?;
137
138        Ok(RocketpoolState::new(
139            reth_supply,
140            total_eth,
141            deposit_contract_balance,
142            reth_contract_liquidity,
143            deposit_fee,
144            deposits_enabled,
145            min_deposit_amount,
146            max_deposit_pool_size,
147            deposit_assigning_enabled,
148            deposit_assign_maximum,
149            deposit_assign_socialised_maximum,
150            queue_variable_start,
151            queue_variable_end,
152        ))
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use std::collections::HashMap;
159
160    use alloy::primitives::U256;
161    use rstest::rstest;
162    use tycho_client::feed::synchronizer::ComponentWithState;
163    use tycho_common::{dto::ResponseProtocolState, Bytes};
164
165    use super::super::state::RocketpoolState;
166    use crate::{
167        evm::protocol::{test_utils, test_utils::try_decode_snapshot_with_defaults},
168        protocol::errors::InvalidSnapshotError,
169    };
170
171    fn create_test_snapshot() -> ComponentWithState {
172        ComponentWithState {
173            state: ResponseProtocolState {
174                component_id: "Rocketpool".to_owned(),
175                attributes: HashMap::from([
176                    (
177                        "total_eth".to_string(),
178                        Bytes::from(U256::from(100_000_000_000_000_000_000u128).to_be_bytes_vec()),
179                    ),
180                    (
181                        "reth_supply".to_string(),
182                        Bytes::from(U256::from(95_000_000_000_000_000_000u128).to_be_bytes_vec()),
183                    ),
184                    (
185                        "deposit_contract_balance".to_string(),
186                        Bytes::from(U256::from(50_000_000_000_000_000_000u128).to_be_bytes_vec()),
187                    ), // 50 ETH in deposit contract
188                    (
189                        "reth_contract_liquidity".to_string(),
190                        Bytes::from(U256::from(10_000_000_000_000_000_000u128).to_be_bytes_vec()),
191                    ), // 10 ETH in rETH contract
192                    ("deposits_enabled".to_string(), Bytes::from(vec![0x01])),
193                    ("deposit_assigning_enabled".to_string(), Bytes::from(vec![0x01])),
194                    (
195                        "deposit_fee".to_string(),
196                        Bytes::from(U256::from(5_000_000_000_000_000u128).to_be_bytes_vec()),
197                    ), // 0.5%
198                    (
199                        "min_deposit_amount".to_string(),
200                        Bytes::from(U256::from(10_000_000_000_000_000u128).to_be_bytes_vec()),
201                    ), // 0.01 ETH
202                    (
203                        "max_deposit_pool_size".to_string(),
204                        Bytes::from(
205                            U256::from(5_000_000_000_000_000_000_000u128).to_be_bytes_vec(),
206                        ),
207                    ), // 5000 ETH
208                    (
209                        "deposit_assign_maximum".to_string(),
210                        Bytes::from(U256::from(10u64).to_be_bytes_vec()),
211                    ),
212                    (
213                        "deposit_assign_socialised_maximum".to_string(),
214                        Bytes::from(U256::from(2u64).to_be_bytes_vec()),
215                    ),
216                    (
217                        "queue_variable_start".to_string(),
218                        Bytes::from(U256::from(100u64).to_be_bytes_vec()),
219                    ),
220                    (
221                        "queue_variable_end".to_string(),
222                        Bytes::from(U256::from(105u64).to_be_bytes_vec()),
223                    ),
224                ]),
225                balances: HashMap::new(),
226            },
227            component: Default::default(),
228            component_tvl: None,
229            entrypoints: Vec::new(),
230        }
231    }
232
233    #[tokio::test]
234    async fn test_rocketpool_try_from() {
235        let snapshot = create_test_snapshot();
236
237        let result =
238            test_utils::try_decode_snapshot_with_defaults::<RocketpoolState>(snapshot).await;
239
240        assert!(result.is_ok());
241        let state = result.unwrap();
242        assert_eq!(state.total_eth, U256::from(100_000_000_000_000_000_000u128));
243        assert_eq!(state.reth_supply, U256::from(95_000_000_000_000_000_000u128));
244        assert_eq!(state.deposit_contract_balance, U256::from(50_000_000_000_000_000_000u128));
245        assert_eq!(state.reth_contract_liquidity, U256::from(10_000_000_000_000_000_000u128));
246        assert!(state.deposits_enabled);
247        assert!(state.deposit_assigning_enabled);
248        assert_eq!(state.min_deposit_amount, U256::from(10_000_000_000_000_000u128));
249        assert_eq!(state.max_deposit_pool_size, U256::from(5_000_000_000_000_000_000_000u128));
250        assert_eq!(state.queue_variable_start, U256::from(100u64));
251        assert_eq!(state.queue_variable_end, U256::from(105u64));
252    }
253
254    #[tokio::test]
255    async fn test_rocketpool_try_from_deposits_disabled() {
256        let eth_address = Bytes::from(vec![0u8; 20]);
257
258        let snapshot = ComponentWithState {
259            state: ResponseProtocolState {
260                component_id: "Rocketpool".to_owned(),
261                attributes: HashMap::from([
262                    ("total_eth".to_string(), Bytes::from(U256::from(100u64).to_be_bytes_vec())),
263                    ("reth_supply".to_string(), Bytes::from(U256::from(100u64).to_be_bytes_vec())),
264                    (
265                        "deposit_contract_balance".to_string(),
266                        Bytes::from(U256::from(50u64).to_be_bytes_vec()),
267                    ),
268                    (
269                        "reth_contract_liquidity".to_string(),
270                        Bytes::from(U256::from(10u64).to_be_bytes_vec()),
271                    ),
272                    ("deposits_enabled".to_string(), Bytes::from(vec![0x00])), // disabled
273                    ("deposit_assigning_enabled".to_string(), Bytes::from(vec![0x00])), // disabled
274                    ("deposit_fee".to_string(), Bytes::from(U256::from(0u64).to_be_bytes_vec())),
275                    (
276                        "min_deposit_amount".to_string(),
277                        Bytes::from(U256::from(0u64).to_be_bytes_vec()),
278                    ),
279                    (
280                        "max_deposit_pool_size".to_string(),
281                        Bytes::from(U256::from(1000u64).to_be_bytes_vec()),
282                    ),
283                    (
284                        "deposit_assign_maximum".to_string(),
285                        Bytes::from(U256::from(0u64).to_be_bytes_vec()),
286                    ),
287                    (
288                        "deposit_assign_socialised_maximum".to_string(),
289                        Bytes::from(U256::from(0u64).to_be_bytes_vec()),
290                    ),
291                    (
292                        "queue_full_start".to_string(),
293                        Bytes::from(U256::from(0u64).to_be_bytes_vec()),
294                    ),
295                    ("queue_full_end".to_string(), Bytes::from(U256::from(0u64).to_be_bytes_vec())),
296                    (
297                        "queue_half_start".to_string(),
298                        Bytes::from(U256::from(0u64).to_be_bytes_vec()),
299                    ),
300                    ("queue_half_end".to_string(), Bytes::from(U256::from(0u64).to_be_bytes_vec())),
301                    (
302                        "queue_variable_start".to_string(),
303                        Bytes::from(U256::from(0u64).to_be_bytes_vec()),
304                    ),
305                    (
306                        "queue_variable_end".to_string(),
307                        Bytes::from(U256::from(0u64).to_be_bytes_vec()),
308                    ),
309                ]),
310                balances: HashMap::from([(
311                    eth_address,
312                    Bytes::from(U256::from(50u64).to_be_bytes_vec()),
313                )]),
314            },
315            component: Default::default(),
316            component_tvl: None,
317            entrypoints: Vec::new(),
318        };
319
320        let result = try_decode_snapshot_with_defaults::<RocketpoolState>(snapshot).await;
321
322        assert!(result.is_ok());
323        let state = result.unwrap();
324        assert!(!state.deposits_enabled);
325        assert!(!state.deposit_assigning_enabled);
326    }
327
328    #[tokio::test]
329    #[rstest]
330    #[case::missing_total_eth("total_eth")]
331    #[case::missing_reth_supply("reth_supply")]
332    #[case::missing_deposit_contract_balance("deposit_contract_balance")]
333    #[case::missing_reth_contract_liquidity("reth_contract_liquidity")]
334    #[case::missing_deposits_enabled("deposits_enabled")]
335    #[case::missing_deposit_assigning_enabled("deposit_assigning_enabled")]
336    #[case::missing_deposit_fee("deposit_fee")]
337    #[case::missing_min_deposit_amount("min_deposit_amount")]
338    #[case::missing_max_deposit_pool_size("max_deposit_pool_size")]
339    #[case::missing_deposit_assign_maximum("deposit_assign_maximum")]
340    #[case::missing_deposit_assign_socialised_maximum("deposit_assign_socialised_maximum")]
341    #[case::missing_queue_variable_start("queue_variable_start")]
342    #[case::missing_queue_variable_end("queue_variable_end")]
343    async fn test_rocketpool_try_from_missing_attribute(#[case] missing_attribute: &str) {
344        let eth_address = Bytes::from(vec![0u8; 20]);
345
346        let mut attributes = HashMap::from([
347            ("total_eth".to_string(), Bytes::from(U256::from(100u64).to_be_bytes_vec())),
348            ("reth_supply".to_string(), Bytes::from(U256::from(100u64).to_be_bytes_vec())),
349            (
350                "deposit_contract_balance".to_string(),
351                Bytes::from(U256::from(50u64).to_be_bytes_vec()),
352            ),
353            (
354                "reth_contract_liquidity".to_string(),
355                Bytes::from(U256::from(10u64).to_be_bytes_vec()),
356            ),
357            ("deposits_enabled".to_string(), Bytes::from(vec![0x01])),
358            ("deposit_assigning_enabled".to_string(), Bytes::from(vec![0x01])),
359            ("deposit_fee".to_string(), Bytes::from(U256::from(0u64).to_be_bytes_vec())),
360            ("min_deposit_amount".to_string(), Bytes::from(U256::from(0u64).to_be_bytes_vec())),
361            (
362                "max_deposit_pool_size".to_string(),
363                Bytes::from(U256::from(1000u64).to_be_bytes_vec()),
364            ),
365            ("deposit_assign_maximum".to_string(), Bytes::from(U256::from(0u64).to_be_bytes_vec())),
366            (
367                "deposit_assign_socialised_maximum".to_string(),
368                Bytes::from(U256::from(0u64).to_be_bytes_vec()),
369            ),
370            ("queue_full_start".to_string(), Bytes::from(U256::from(0u64).to_be_bytes_vec())),
371            ("queue_full_end".to_string(), Bytes::from(U256::from(0u64).to_be_bytes_vec())),
372            ("queue_half_start".to_string(), Bytes::from(U256::from(0u64).to_be_bytes_vec())),
373            ("queue_half_end".to_string(), Bytes::from(U256::from(0u64).to_be_bytes_vec())),
374            ("queue_variable_start".to_string(), Bytes::from(U256::from(0u64).to_be_bytes_vec())),
375            ("queue_variable_end".to_string(), Bytes::from(U256::from(0u64).to_be_bytes_vec())),
376        ]);
377        attributes.remove(missing_attribute);
378
379        let snapshot = ComponentWithState {
380            state: ResponseProtocolState {
381                component_id: "Rocketpool".to_owned(),
382                attributes,
383                balances: HashMap::from([(
384                    eth_address,
385                    Bytes::from(U256::from(50u64).to_be_bytes_vec()),
386                )]),
387            },
388            component: Default::default(),
389            component_tvl: None,
390            entrypoints: Vec::new(),
391        };
392
393        let result = try_decode_snapshot_with_defaults::<RocketpoolState>(snapshot).await;
394
395        assert!(result.is_err());
396        assert!(matches!(
397            result.unwrap_err(),
398            InvalidSnapshotError::MissingAttribute(ref x) if x == missing_attribute
399        ));
400    }
401}