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::{adapter::build_pool, 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 raw = vm::read_raw_pool_state(
84 &engine,
85 &pool_alloy,
86 resolved,
87 &decimals,
88 &Default::default(),
89 )?;
90 let pool = build_pool(&raw).map_err(|e| {
91 InvalidSnapshotError::ValueError(format!("curve build_pool failed: {e}"))
92 })?;
93 Ok(CurveState::new(pool_address, coins, decimals, resolved, pool, raw.admin_fee))
94 }
95}
96
97fn parse_coins(
100 static_attributes: &HashMap<String, Bytes>,
101) -> Result<Vec<Bytes>, InvalidSnapshotError> {
102 let raw = static_attributes
103 .get("coins")
104 .ok_or_else(|| {
105 InvalidSnapshotError::ValueError("Missing `coins` static attribute".to_string())
106 })?;
107 let text = std::str::from_utf8(raw.as_ref()).map_err(|e| {
108 InvalidSnapshotError::ValueError(format!("`coins` attribute is not valid UTF-8: {e}"))
109 })?;
110 let addresses: Vec<String> = serde_json::from_str(text).map_err(|e| {
111 InvalidSnapshotError::ValueError(format!("Failed to parse `coins` attribute: {e}"))
112 })?;
113 addresses
114 .iter()
115 .map(|address| {
116 Bytes::from_str(address)
117 .map(normalize_eth)
118 .map_err(|e| {
119 InvalidSnapshotError::ValueError(format!("Invalid coin address {address}: {e}"))
120 })
121 })
122 .collect()
123}
124
125fn normalize_eth(address: Bytes) -> Bytes {
127 if address.as_ref() == ETH_SENTINEL {
128 Bytes::from(vec![0u8; 20])
129 } else {
130 address
131 }
132}
133
134fn coin_decimals(
137 coin: &Bytes,
138 all_tokens: &HashMap<Bytes, Token>,
139 pool_address: &Bytes,
140) -> Result<u8, InvalidSnapshotError> {
141 if let Some(token) = all_tokens.get(coin) {
142 return Ok(token.decimals as u8);
143 }
144 if coin.iter().all(|b| *b == 0) {
145 return Ok(18);
146 }
147 Err(InvalidSnapshotError::ValueError(format!(
148 "Missing token {coin} in state for curve pool {pool_address}"
149 )))
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 fn attrs_with_coins(json: &str) -> HashMap<String, Bytes> {
157 let mut m = HashMap::new();
158 m.insert("coins".to_string(), Bytes::from(json.as_bytes().to_vec()));
159 m
160 }
161
162 #[test]
163 fn parse_coins_preserves_on_chain_order_and_normalizes_eth() {
164 let json = r#"["0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48","0x2260fac5e5542a773aa44fbcfedf7c193bc2c599","0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"]"#;
166 let coins = parse_coins(&attrs_with_coins(json)).unwrap();
167 assert_eq!(coins.len(), 3);
168 assert_eq!(
169 coins[0],
170 Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap()
171 );
172 assert_eq!(
173 coins[1],
174 Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap()
175 );
176 assert_eq!(coins[2], Bytes::from(vec![0u8; 20]), "ETH sentinel -> zero address");
177 }
178
179 #[test]
180 fn parse_coins_missing_attribute_errors() {
181 let err = parse_coins(&HashMap::new()).unwrap_err();
182 assert!(matches!(err, InvalidSnapshotError::ValueError(_)));
183 }
184}