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 super::state::{JoinEscrows, SkyComponentKind, SkyState};
8use crate::protocol::{
9 errors::InvalidSnapshotError,
10 models::{DecoderContext, TryFromWithBlock},
11};
12
13impl TryFromWithBlock<ComponentWithState, BlockHeader> for SkyState {
14 type Error = InvalidSnapshotError;
15
16 async fn try_from_with_header(
19 snapshot: ComponentWithState,
20 _block: BlockHeader,
21 _account_balances: &HashMap<Bytes, HashMap<Bytes, Bytes>>,
22 all_tokens: &HashMap<Bytes, Token>,
23 _decoder_context: &DecoderContext,
24 ) -> Result<Self, Self::Error> {
25 let kind = match snapshot
26 .component
27 .static_attributes
28 .get("component_type")
29 .ok_or_else(|| InvalidSnapshotError::MissingAttribute("component_type".to_string()))?
30 .as_ref()
31 {
32 b"psm" => SkyComponentKind::Psm,
33 b"psm_wrapper" => SkyComponentKind::PsmWrapper,
34 b"converter" => SkyComponentKind::Converter,
35 other => {
36 return Err(InvalidSnapshotError::ValueError(format!(
37 "unknown sky component_type: {}",
38 String::from_utf8_lossy(other)
39 )))
40 }
41 };
42
43 let tokens = snapshot
44 .component
45 .tokens
46 .iter()
47 .map(|address| {
48 all_tokens
49 .get(address)
50 .cloned()
51 .ok_or_else(|| {
52 InvalidSnapshotError::ValueError(format!("unknown token {address}"))
53 })
54 })
55 .collect::<Result<Vec<_>, _>>()?;
56 let [a, b] = tokens.try_into().map_err(|_| {
57 InvalidSnapshotError::ValueError("sky components have exactly two tokens".to_string())
58 })?;
59
60 let gem_address = snapshot
63 .component
64 .static_attributes
65 .get("gem")
66 .ok_or_else(|| InvalidSnapshotError::MissingAttribute("gem".to_string()))?;
67 let (stable, gem) = if b.address == *gem_address {
68 (a, b)
69 } else if a.address == *gem_address {
70 (b, a)
71 } else {
72 return Err(InvalidSnapshotError::ValueError(format!(
73 "gem attribute {gem_address} is not among the component tokens"
74 )));
75 };
76 if gem.decimals > stable.decimals {
80 return Err(InvalidSnapshotError::ValueError(format!(
81 "gem decimals ({}) exceed stable decimals ({})",
82 gem.decimals, stable.decimals
83 )));
84 }
85
86 let get_fee = |name: &str| -> Result<U256, InvalidSnapshotError> {
87 match kind {
88 SkyComponentKind::Converter => Ok(U256::ZERO),
90 SkyComponentKind::Psm | SkyComponentKind::PsmWrapper => snapshot
91 .state
92 .attributes
93 .get(name)
94 .map(|value| U256::from_be_slice(value))
95 .ok_or_else(|| InvalidSnapshotError::MissingAttribute(name.to_string())),
96 }
97 };
98 let get_balance = |token: &Token| -> Result<U256, InvalidSnapshotError> {
99 snapshot
100 .state
101 .balances
102 .get(&token.address)
103 .map(|balance| U256::from_be_slice(balance))
104 .ok_or_else(|| {
105 InvalidSnapshotError::ValueError(format!(
109 "missing balance for token {} on component {}",
110 token.address, snapshot.component.id
111 ))
112 })
113 };
114
115 let (stable_balance, gem_balance) = (get_balance(&stable)?, get_balance(&gem)?);
116
117 let get_escrow = |name: &str| -> Result<U256, InvalidSnapshotError> {
118 snapshot
119 .state
120 .attributes
121 .get(name)
122 .map(|value| U256::from_be_slice(value))
123 .ok_or_else(|| InvalidSnapshotError::MissingAttribute(name.to_string()))
124 };
125 let escrows = match kind {
128 SkyComponentKind::PsmWrapper => Some(JoinEscrows {
129 dai: get_escrow("dai_escrow")?,
130 usds: get_escrow("usds_escrow")?,
131 }),
132 SkyComponentKind::Psm | SkyComponentKind::Converter => None,
133 };
134
135 Ok(SkyState::new(
136 snapshot.component.id.to_string(),
137 kind,
138 stable,
139 gem,
140 get_fee("tin")?,
141 get_fee("tout")?,
142 stable_balance,
143 gem_balance,
144 escrows,
145 ))
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use std::str::FromStr;
152
153 use rstest::rstest;
154 use tycho_common::{
155 models::{
156 protocol::{ProtocolComponent, ProtocolComponentState},
157 Chain,
158 },
159 Bytes,
160 };
161
162 use super::*;
163
164 async fn decode(
165 snapshot: ComponentWithState,
166 all_tokens: &HashMap<Bytes, Token>,
167 ) -> Result<SkyState, InvalidSnapshotError> {
168 SkyState::try_from_with_header(
169 snapshot,
170 Default::default(),
171 &HashMap::default(),
172 all_tokens,
173 &Default::default(),
174 )
175 .await
176 }
177
178 const PSM_ID: &str = "0xf6e72db5454dd049d0788e411b06cfaf16853042";
179 const DAI: &str = "0x6b175474e89094c44da98b954eedeac495271d0f";
180 const USDC: &str = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48";
181 const USDS: &str = "0xdc035d45d973e3ec169d2276ddab16f1e407384f";
182
183 fn token(address: &str, symbol: &str, decimals: u32) -> Token {
184 Token::new(
185 &Bytes::from_str(address).unwrap(),
186 symbol,
187 decimals,
188 0,
189 &[Some(50_000)],
190 Chain::Ethereum,
191 100,
192 )
193 }
194
195 fn all_tokens() -> HashMap<Bytes, Token> {
196 [token(DAI, "DAI", 18), token(USDC, "USDC", 6), token(USDS, "USDS", 18)]
197 .into_iter()
198 .map(|t| (t.address.clone(), t))
199 .collect()
200 }
201
202 fn snapshot(
203 id: &str,
204 component_type: &str,
205 gem: &str,
206 tokens: Vec<&str>,
207 with_fees: bool,
208 ) -> ComponentWithState {
209 let attributes = if with_fees {
210 HashMap::from([
211 ("tin".to_string(), Bytes::from(U256::ZERO.to_be_bytes_vec())),
212 ("tout".to_string(), Bytes::from(U256::from(2u8).to_be_bytes_vec())),
213 ])
214 } else {
215 HashMap::new()
216 };
217 ComponentWithState {
218 state: ProtocolComponentState {
219 component_id: id.to_string(),
220 attributes,
221 balances: HashMap::from([
222 (
223 Bytes::from_str(tokens[0]).unwrap(),
224 Bytes::from(U256::from(1000u64).to_be_bytes_vec()),
225 ),
226 (
227 Bytes::from_str(tokens[1]).unwrap(),
228 Bytes::from(U256::from(2000u64).to_be_bytes_vec()),
229 ),
230 ]),
231 },
232 component: ProtocolComponent {
233 id: id.to_string(),
234 tokens: tokens
235 .iter()
236 .map(|t| Bytes::from_str(t).unwrap())
237 .collect(),
238 static_attributes: HashMap::from([
239 ("component_type".to_string(), Bytes::from(component_type.as_bytes().to_vec())),
240 ("gem".to_string(), Bytes::from_str(gem).unwrap()),
241 ]),
242 ..Default::default()
243 },
244 component_tvl: None,
245 entrypoints: Vec::new(),
246 }
247 }
248
249 #[rstest]
250 #[case::stable_first(vec![DAI, USDC])]
251 #[case::gem_first(vec![USDC, DAI])]
252 #[tokio::test]
253 async fn decodes_psm_regardless_of_token_order(#[case] tokens: Vec<&str>) {
254 let snap = snapshot(PSM_ID, "psm", USDC, tokens, true);
255 let state = decode(snap, &all_tokens())
256 .await
257 .unwrap();
258 assert_eq!(state.kind, SkyComponentKind::Psm);
259 }
260
261 #[tokio::test]
262 async fn decodes_converter_without_fee_attributes() {
263 let snap = snapshot(
264 "0x3225737a9bbb6473cb4a45b7244aca2befdb276a",
265 "converter",
266 USDS,
267 vec![DAI, USDS],
268 false,
269 );
270 let state = decode(snap, &all_tokens())
271 .await
272 .unwrap();
273 assert_eq!(state.kind, SkyComponentKind::Converter);
274 }
275
276 fn wrapper_snapshot() -> ComponentWithState {
277 let mut snap = snapshot(
278 "0xa188eec8f81263234da3622a406892f3d630f98c",
279 "psm_wrapper",
280 USDC,
281 vec![USDS, USDC],
282 true,
283 );
284 snap.state.attributes.extend([
285 ("dai_escrow".to_string(), Bytes::from(U256::from(7u8).to_be_bytes_vec())),
286 ("usds_escrow".to_string(), Bytes::from(U256::from(9u8).to_be_bytes_vec())),
287 ]);
288 snap
289 }
290
291 #[tokio::test]
292 async fn decodes_wrapper_with_join_escrows() {
293 let state = decode(wrapper_snapshot(), &all_tokens())
294 .await
295 .unwrap();
296 assert_eq!(state.kind, SkyComponentKind::PsmWrapper);
297 }
298
299 #[rstest]
300 #[case::dai_escrow("dai_escrow")]
301 #[case::usds_escrow("usds_escrow")]
302 #[tokio::test]
303 async fn missing_escrow_attribute_errors_for_wrapper(#[case] name: &str) {
304 let mut snap = wrapper_snapshot();
305 snap.state.attributes.remove(name);
306 let result = decode(snap, &all_tokens()).await;
307 assert!(matches!(result, Err(InvalidSnapshotError::MissingAttribute(_))));
308 }
309
310 #[tokio::test]
311 async fn missing_fee_attribute_errors_for_psm() {
312 let snap = snapshot(PSM_ID, "psm", USDC, vec![DAI, USDC], false);
313 let result = decode(snap, &all_tokens()).await;
314 assert!(matches!(result, Err(InvalidSnapshotError::MissingAttribute(_))));
315 }
316
317 #[tokio::test]
318 async fn unknown_component_type_errors() {
319 let snap = snapshot(PSM_ID, "mystery", USDC, vec![DAI, USDC], true);
320 let result = decode(snap, &all_tokens()).await;
321 assert!(matches!(result, Err(InvalidSnapshotError::ValueError(_))));
322 }
323
324 #[tokio::test]
325 async fn gem_decimals_above_stable_decimals_errors() {
326 let tokens = [token(DAI, "DAI", 6), token(USDC, "USDC", 18)]
329 .into_iter()
330 .map(|t| (t.address.clone(), t))
331 .collect();
332 let snap = snapshot(PSM_ID, "psm", USDC, vec![DAI, USDC], true);
333 let result = decode(snap, &tokens).await;
334 assert!(matches!(result, Err(InvalidSnapshotError::ValueError(_))));
335 }
336
337 #[tokio::test]
338 async fn missing_gem_attribute_errors() {
339 let mut snap = snapshot(PSM_ID, "psm", USDC, vec![DAI, USDC], true);
340 snap.component
341 .static_attributes
342 .remove("gem");
343 let result = decode(snap, &all_tokens()).await;
344 assert!(matches!(result, Err(InvalidSnapshotError::MissingAttribute(_))));
345 }
346
347 #[tokio::test]
348 async fn missing_balance_errors() {
349 let mut snap = snapshot(PSM_ID, "psm", USDC, vec![DAI, USDC], true);
350 snap.state
351 .balances
352 .remove(&Bytes::from_str(DAI).unwrap());
353 let result = decode(snap, &all_tokens()).await;
354 assert!(matches!(result, Err(InvalidSnapshotError::ValueError(_))));
355 }
356}