tycho_simulation/evm/protocol/curve/
decoder.rs1use std::{collections::HashMap, str::FromStr};
2
3use alloy::primitives::Address as AlloyAddress;
4use tycho_client::feed::{synchronizer::ComponentWithState, BlockHeader};
5use tycho_common::{models::token::Token, Bytes};
6
7use crate::{
8 evm::{
9 engine_db::{create_engine, SHARED_TYCHO_DB},
10 protocol::{
11 curve::{state::CurveState, variant, vm},
12 vm::utils::load_stateless_contracts,
13 },
14 },
15 protocol::{
16 errors::InvalidSnapshotError,
17 models::{DecoderContext, TryFromWithBlock},
18 },
19};
20
21const ETH_SENTINEL: [u8; 20] = [0xEE; 20];
25
26impl TryFromWithBlock<ComponentWithState, BlockHeader> for CurveState {
27 type Error = InvalidSnapshotError;
28
29 async fn try_from_with_header(
36 value: ComponentWithState,
37 _block: BlockHeader,
38 _account_balances: &HashMap<Bytes, HashMap<Bytes, Bytes>>,
39 all_tokens: &HashMap<Bytes, Token>,
40 decoder_context: &DecoderContext,
41 ) -> Result<Self, Self::Error> {
42 let pool_address = Bytes::from_str(value.component.id.as_str()).map_err(|e| {
43 InvalidSnapshotError::ValueError(format!(
44 "Expected curve component id to be the pool address: {e}"
45 ))
46 })?;
47
48 let coins = parse_coins(&value.component.static_attributes)?;
49 if coins.len() < 2 {
50 return Err(InvalidSnapshotError::ValueError(format!(
51 "Curve pool {pool_address} has fewer than 2 coins"
52 )));
53 }
54 let decimals = coins
55 .iter()
56 .map(|coin| coin_decimals(coin, all_tokens, &pool_address))
57 .collect::<Result<Vec<u8>, _>>()?;
58
59 let engine = create_engine(
60 SHARED_TYCHO_DB.clone(),
61 decoder_context
62 .vm_traces
63 .unwrap_or_default(),
64 )
65 .expect("Infallible");
66
67 load_stateless_contracts(&engine, &value.state.attributes).await?;
70
71 let pool_alloy = AlloyAddress::from_slice(pool_address.as_ref());
72 vm::load_math_contract(&engine, &pool_alloy).await?;
76
77 let resolved = variant::resolve_variant(
78 &value.component.static_attributes,
79 &pool_alloy,
80 coins.len(),
81 &engine,
82 )?;
83 let pool = vm::decode_from_vm(&engine, &pool_alloy, resolved, &decimals)?;
84
85 Ok(CurveState::new(pool_address, coins, decimals, resolved, pool))
86 }
87}
88
89fn parse_coins(
92 static_attributes: &HashMap<String, Bytes>,
93) -> Result<Vec<Bytes>, InvalidSnapshotError> {
94 let raw = static_attributes
95 .get("coins")
96 .ok_or_else(|| {
97 InvalidSnapshotError::ValueError("Missing `coins` static attribute".to_string())
98 })?;
99 let text = std::str::from_utf8(raw.as_ref()).map_err(|e| {
100 InvalidSnapshotError::ValueError(format!("`coins` attribute is not valid UTF-8: {e}"))
101 })?;
102 let addresses: Vec<String> = serde_json::from_str(text).map_err(|e| {
103 InvalidSnapshotError::ValueError(format!("Failed to parse `coins` attribute: {e}"))
104 })?;
105 addresses
106 .iter()
107 .map(|address| {
108 Bytes::from_str(address)
109 .map(normalize_eth)
110 .map_err(|e| {
111 InvalidSnapshotError::ValueError(format!("Invalid coin address {address}: {e}"))
112 })
113 })
114 .collect()
115}
116
117fn normalize_eth(address: Bytes) -> Bytes {
119 if address.as_ref() == ETH_SENTINEL {
120 Bytes::from(vec![0u8; 20])
121 } else {
122 address
123 }
124}
125
126fn coin_decimals(
129 coin: &Bytes,
130 all_tokens: &HashMap<Bytes, Token>,
131 pool_address: &Bytes,
132) -> Result<u8, InvalidSnapshotError> {
133 if let Some(token) = all_tokens.get(coin) {
134 return Ok(token.decimals as u8);
135 }
136 if coin.iter().all(|b| *b == 0) {
137 return Ok(18);
138 }
139 Err(InvalidSnapshotError::ValueError(format!(
140 "Missing token {coin} in state for curve pool {pool_address}"
141 )))
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 fn attrs_with_coins(json: &str) -> HashMap<String, Bytes> {
149 let mut m = HashMap::new();
150 m.insert("coins".to_string(), Bytes::from(json.as_bytes().to_vec()));
151 m
152 }
153
154 #[test]
155 fn parse_coins_preserves_on_chain_order_and_normalizes_eth() {
156 let json = r#"["0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48","0x2260fac5e5542a773aa44fbcfedf7c193bc2c599","0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"]"#;
158 let coins = parse_coins(&attrs_with_coins(json)).unwrap();
159 assert_eq!(coins.len(), 3);
160 assert_eq!(
161 coins[0],
162 Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap()
163 );
164 assert_eq!(
165 coins[1],
166 Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap()
167 );
168 assert_eq!(coins[2], Bytes::from(vec![0u8; 20]), "ETH sentinel -> zero address");
169 }
170
171 #[test]
172 fn parse_coins_missing_attribute_errors() {
173 let err = parse_coins(&HashMap::new()).unwrap_err();
174 assert!(matches!(err, InvalidSnapshotError::ValueError(_)));
175 }
176}