tycho_simulation/evm/protocol/aerodrome_v1/
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::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 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}