tycho_simulation/evm/protocol/ring_swap_v2/
decoder.rs1use 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}