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 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}