Skip to main content

tycho_simulation/evm/protocol/ring_swap_v2/
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::{
9        cpmm::protocol::cpmm_try_from_with_header, ring_swap_v2::state::RingSwapV2State,
10    },
11    protocol::{
12        errors::InvalidSnapshotError,
13        models::{DecoderContext, TryFromWithBlock},
14    },
15};
16
17impl TryFromWithBlock<ComponentWithState, BlockHeader> for RingSwapV2State {
18    type Error = InvalidSnapshotError;
19
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, reserve1) = cpmm_try_from_with_header(snapshot.clone())?;
28        let component_tokens = &snapshot.component.tokens;
29        if component_tokens.len() != 2 {
30            return Err(InvalidSnapshotError::ValueError(format!(
31                "RingSwapV2 component {} has {} tokens, expected 2",
32                snapshot.component.id,
33                component_tokens.len()
34            )));
35        }
36
37        let underlying_token0 = static_attribute(&snapshot, "underlying_token0")?;
38        let underlying_token1 = static_attribute(&snapshot, "underlying_token1")?;
39        let reserves_inverted = static_attribute(&snapshot, "reserves_inverted")?
40            .last()
41            .copied()
42            .unwrap_or_default() ==
43            1;
44
45        let (expected_component0, expected_component1) = if reserves_inverted {
46            (underlying_token1, underlying_token0)
47        } else {
48            (underlying_token0, underlying_token1)
49        };
50
51        if component_tokens[0] != expected_component0 || component_tokens[1] != expected_component1
52        {
53            return Err(InvalidSnapshotError::ValueError(format!(
54                "RingSwapV2 component {} token order does not match its FewToken metadata",
55                snapshot.component.id
56            )));
57        }
58
59        let backing0 = component_balance(&snapshot, &component_tokens[0])?;
60        let backing1 = component_balance(&snapshot, &component_tokens[1])?;
61
62        Ok(RingSwapV2State::new(
63            snapshot.component.id,
64            reserve0,
65            reserve1,
66            backing0,
67            backing1,
68            component_tokens[0].clone(),
69            component_tokens[1].clone(),
70        ))
71    }
72}
73
74fn static_attribute(
75    snapshot: &ComponentWithState,
76    name: &str,
77) -> Result<Bytes, InvalidSnapshotError> {
78    snapshot
79        .component
80        .static_attributes
81        .get(name)
82        .cloned()
83        .ok_or_else(|| InvalidSnapshotError::MissingAttribute(name.to_string()))
84}
85
86fn component_balance(
87    snapshot: &ComponentWithState,
88    token: &Bytes,
89) -> Result<U256, InvalidSnapshotError> {
90    snapshot
91        .state
92        .balances
93        .get(token)
94        .map(|balance| U256::from_be_slice(balance))
95        .ok_or_else(|| {
96            InvalidSnapshotError::ValueError(format!(
97                "Missing RingSwapV2 component balance for component {} and token {token:?}",
98                snapshot.component.id
99            ))
100        })
101}
102
103#[cfg(test)]
104mod tests {
105    use std::collections::HashMap;
106
107    use alloy::primitives::U256;
108    use tycho_client::feed::{synchronizer::ComponentWithState, BlockHeader};
109    use tycho_common::{
110        models::protocol::{ProtocolComponent, ProtocolComponentState},
111        Bytes,
112    };
113
114    use super::*;
115    use crate::protocol::{errors::InvalidSnapshotError, models::TryFromWithBlock};
116
117    fn address(value: u8) -> Bytes {
118        Bytes::from(vec![value; 20])
119    }
120
121    fn snapshot() -> ComponentWithState {
122        let token0 = address(1);
123        let token1 = address(2);
124        ComponentWithState {
125            state: ProtocolComponentState {
126                component_id: "ring".to_string(),
127                attributes: HashMap::from([
128                    ("reserve0".to_string(), Bytes::from(vec![10])),
129                    ("reserve1".to_string(), Bytes::from(vec![20])),
130                ]),
131                balances: HashMap::from([
132                    (token0.clone(), Bytes::from(vec![7])),
133                    (token1.clone(), Bytes::from(vec![8])),
134                ]),
135            },
136            component: ProtocolComponent {
137                id: "ring".to_string(),
138                tokens: vec![token0.clone(), token1.clone()],
139                static_attributes: HashMap::from([
140                    ("fw_token0".to_string(), address(3)),
141                    ("fw_token1".to_string(), address(4)),
142                    ("underlying_token0".to_string(), token0),
143                    ("underlying_token1".to_string(), token1),
144                    ("reserves_inverted".to_string(), Bytes::from(vec![0])),
145                ]),
146                ..Default::default()
147            },
148            component_tvl: None,
149            entrypoints: Vec::new(),
150        }
151    }
152
153    #[tokio::test]
154    async fn decodes_component_balances_as_available_backing() {
155        let state = RingSwapV2State::try_from_with_header(
156            snapshot(),
157            BlockHeader::default(),
158            &HashMap::new(),
159            &HashMap::new(),
160            &Default::default(),
161        )
162        .await
163        .unwrap();
164
165        assert_eq!(state.component_id, "ring");
166        assert_eq!(state.backing0, U256::from(7));
167        assert_eq!(state.backing1, U256::from(8));
168    }
169
170    #[tokio::test]
171    async fn decodes_inverted_pair_metadata_in_component_order() {
172        let mut inverted_snapshot = snapshot();
173        inverted_snapshot
174            .component
175            .static_attributes = HashMap::from([
176            ("fw_token0".to_string(), address(3)),
177            ("fw_token1".to_string(), address(4)),
178            ("underlying_token0".to_string(), address(2)),
179            ("underlying_token1".to_string(), address(1)),
180            ("reserves_inverted".to_string(), Bytes::from(vec![1])),
181        ]);
182
183        let state = RingSwapV2State::try_from_with_header(
184            inverted_snapshot,
185            BlockHeader::default(),
186            &HashMap::new(),
187            &HashMap::new(),
188            &Default::default(),
189        )
190        .await
191        .unwrap();
192
193        assert_eq!(state.backing0, U256::from(7));
194        assert_eq!(state.backing1, U256::from(8));
195    }
196
197    #[tokio::test]
198    async fn rejects_snapshot_without_component_balance() {
199        let mut missing_balance = snapshot();
200        missing_balance
201            .state
202            .balances
203            .remove(&address(2));
204        let result = RingSwapV2State::try_from_with_header(
205            missing_balance,
206            BlockHeader::default(),
207            &HashMap::new(),
208            &HashMap::new(),
209            &Default::default(),
210        )
211        .await;
212
213        assert!(matches!(result, Err(InvalidSnapshotError::ValueError(_))));
214    }
215}