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