tycho_simulation/evm/protocol/vm/
decoder.rs1use std::{
2 collections::{HashMap, HashSet},
3 str::FromStr,
4};
5
6use alloy::primitives::{Address, U256};
7use revm::state::Bytecode;
8use tycho_client::feed::{synchronizer::ComponentWithState, BlockHeader};
9use tycho_common::{models::token::Token, simulation::errors::SimulationError, Bytes};
10
11use super::{state::EVMPoolState, state_builder::EVMPoolStateBuilder};
12use crate::{
13 evm::{
14 engine_db::{tycho_db::PreCachedDB, SHARED_TYCHO_DB},
15 protocol::vm::{constants::get_adapter_file, utils::json_deserialize_address_list},
16 simulation::BlockEnvOverrides,
17 },
18 protocol::{
19 errors::InvalidSnapshotError,
20 models::{DecoderContext, TryFromWithBlock},
21 },
22};
23
24impl TryFromWithBlock<ComponentWithState, BlockHeader> for EVMPoolState<PreCachedDB> {
25 type Error = InvalidSnapshotError;
26
27 #[allow(deprecated)]
32 async fn try_from_with_header(
33 snapshot: 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 id = snapshot.component.id.clone();
40 let tokens = snapshot.component.tokens.clone();
41
42 let mut stateless_contracts = HashMap::new();
44 let mut index = 0;
45
46 loop {
47 let address_key = format!("stateless_contract_addr_{index}");
48 if let Some(encoded_address_bytes) = snapshot
49 .state
50 .attributes
51 .get(&address_key)
52 {
53 let encoded_address = hex::encode(encoded_address_bytes);
54 let address_hex = encoded_address
56 .strip_prefix("0x")
57 .unwrap_or(&encoded_address);
58
59 let decoded = match hex::decode(address_hex) {
60 Ok(decoded_bytes) => match String::from_utf8(decoded_bytes) {
61 Ok(decoded_string) => decoded_string,
62 Err(_) => continue,
63 },
64 Err(_) => continue,
65 };
66
67 let code_key = format!("stateless_contract_code_{index}");
68 let code = snapshot
69 .state
70 .attributes
71 .get(&code_key)
72 .map(|value| value.to_vec());
73
74 stateless_contracts.insert(decoded, code);
75 index += 1;
76 } else {
77 break;
78 }
79 }
80 let involved_contracts = snapshot
81 .component
82 .contract_addresses
83 .iter()
84 .map(|bytes: &Bytes| Address::from_slice(bytes.as_ref()))
85 .collect::<HashSet<Address>>();
86
87 let potential_rebase_tokens: HashSet<Address> = if let Some(bytes) = snapshot
88 .component
89 .static_attributes
90 .get("rebase_tokens")
91 {
92 if let Ok(vecs) = json_deserialize_address_list(bytes) {
93 vecs.into_iter()
94 .map(|addr| Address::from_slice(&addr))
95 .collect()
96 } else {
97 HashSet::new()
98 }
99 } else {
100 HashSet::new()
101 };
102
103 let self_contained_tokens: HashSet<Address> = if let Some(bytes) = snapshot
108 .component
109 .static_attributes
110 .get("self_contained_tokens")
111 {
112 if let Ok(vecs) = json_deserialize_address_list(bytes) {
113 vecs.into_iter()
114 .map(|addr| Address::from_slice(&addr))
115 .collect()
116 } else {
117 HashSet::new()
118 }
119 } else {
120 HashSet::new()
121 };
122
123 let balance_owner = snapshot
125 .state
126 .attributes
127 .get("balance_owner")
128 .map(|owner| Address::from_slice(owner.as_ref()));
129 let component_balances = snapshot
130 .state
131 .balances
132 .iter()
133 .map(|(k, v)| (Address::from_slice(k), U256::from_be_slice(v)))
134 .collect::<HashMap<_, _>>();
135 let account_balances = account_balances
136 .iter()
137 .filter(|(k, _)| involved_contracts.contains(&Address::from_slice(k)))
138 .map(|(k, v)| {
139 let addr = Address::from_slice(k);
140 let balances = v
141 .iter()
142 .map(|(k, v)| (Address::from_slice(k), U256::from_be_slice(v)))
143 .collect();
144 (addr, balances)
145 })
146 .collect::<HashMap<_, _>>();
147
148 let manual_updates = snapshot
149 .component
150 .static_attributes
151 .contains_key("manual_updates");
152
153 let protocol_name = snapshot
154 .component
155 .protocol_system
156 .strip_prefix("vm:")
157 .unwrap_or({
158 snapshot
159 .component
160 .protocol_system
161 .as_str()
162 });
163 let adapter_bytecode;
164 if let Some(adapter_bytecode_path) = &decoder_context.adapter_path {
165 let bytecode_bytes = std::fs::read(adapter_bytecode_path).map_err(|e| {
166 SimulationError::FatalError(format!(
167 "Failed to read adapter bytecode from {adapter_bytecode_path}: {e}"
168 ))
169 })?;
170 adapter_bytecode = Bytecode::new_raw(bytecode_bytes.into());
171 } else {
172 adapter_bytecode = Bytecode::new_raw(get_adapter_file(protocol_name)?.into());
173 }
174 let adapter_contract_address = Address::from_str(&format!(
175 "{hex_protocol_name:0>40}",
176 hex_protocol_name = hex::encode(protocol_name)
177 ))
178 .map_err(|_| {
179 InvalidSnapshotError::ValueError(
180 "Error converting protocol name to address".to_string(),
181 )
182 })?;
183 let mut vm_traces = false;
184 if let Some(trace) = &decoder_context.vm_traces {
185 vm_traces = *trace;
186 }
187 let block_number = snapshot
191 .state
192 .attributes
193 .get("override_block_number")
194 .map(|block_number| {
195 <[u8; 8]>::try_from(block_number.as_ref())
196 .map(u64::from_be_bytes)
197 .map_err(|_| {
198 InvalidSnapshotError::ValueError(
199 "override_block_number attribute must be an 8-byte big-endian u64"
200 .to_string(),
201 )
202 })
203 })
204 .transpose()?;
205 let block_timestamp = snapshot
206 .state
207 .attributes
208 .get("override_block_timestamp")
209 .map(|block_timestamp| {
210 <[u8; 8]>::try_from(block_timestamp.as_ref())
211 .map(u64::from_be_bytes)
212 .map_err(|_| {
213 InvalidSnapshotError::ValueError(
214 "override_block_timestamp attribute must be an 8-byte big-endian u64"
215 .to_string(),
216 )
217 })
218 })
219 .transpose()?;
220 let block_overrides = if block_number.is_some() || block_timestamp.is_some() {
221 Some(BlockEnvOverrides { number: block_number, timestamp: block_timestamp })
222 } else {
223 None
224 };
225 let mut pool_state_builder =
226 EVMPoolStateBuilder::new(id.clone(), tokens.clone(), adapter_contract_address)
227 .balances(component_balances)
228 .disable_overwrite_tokens(potential_rebase_tokens)
229 .self_contained_tokens(self_contained_tokens)
230 .account_balances(account_balances)
231 .adapter_contract_bytecode(adapter_bytecode)
232 .involved_contracts(involved_contracts)
233 .stateless_contracts(stateless_contracts)
234 .manual_updates(manual_updates)
235 .trace(vm_traces)
236 .block_overrides(block_overrides)
237 .spot_price_caller(spot_price_caller(protocol_name));
238
239 if let Some(balance_owner) = balance_owner {
240 pool_state_builder = pool_state_builder.balance_owner(balance_owner)
241 };
242
243 let mut pool_state = pool_state_builder
244 .build(SHARED_TYCHO_DB.clone())
245 .await
246 .map_err(InvalidSnapshotError::VMError)?;
247
248 if let Some(receiver) = decoder_context.live_override.clone() {
249 pool_state.set_live_overrides(receiver);
250 }
251
252 pool_state.set_spot_prices(all_tokens)?;
253
254 Ok(pool_state)
255 }
256}
257
258fn spot_price_caller(protocol_name: &str) -> Option<Address> {
264 (protocol_name == "balancer_v3").then_some(Address::ZERO)
265}
266
267#[cfg(test)]
268mod tests {
269 use std::{collections::HashSet, fs, path::Path};
270
271 use chrono::DateTime;
272 use revm::{primitives::KECCAK_EMPTY, state::AccountInfo};
273 use serde_json::Value;
274 use tycho_common::models::{
275 protocol::{ProtocolComponent, ProtocolComponentState},
276 Chain, ChangeType,
277 };
278
279 use super::*;
280 use crate::evm::{
281 engine_db::{create_engine, engine_db_interface::EngineDatabaseInterface},
282 protocol::vm::constants::{BALANCER_V2, CURVE},
283 tycho_models::AccountUpdate,
284 };
285
286 #[test]
287 fn test_to_adapter_file_name() {
288 assert_eq!(get_adapter_file("balancer_v2").unwrap(), BALANCER_V2);
289 assert_eq!(get_adapter_file("curve").unwrap(), CURVE);
290 }
291
292 fn vm_component() -> ProtocolComponent {
293 let creation_time = DateTime::from_timestamp(1622526000, 0)
294 .unwrap()
295 .naive_utc(); let mut static_attributes: HashMap<String, Bytes> = HashMap::new();
298 static_attributes.insert("manual_updates".to_string(), Bytes::from_str("0x01").unwrap());
299
300 let dai_addr = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
301 let bal_addr = Bytes::from_str("0xba100000625a3754423978a60c9317c58a424e3d").unwrap();
302 let tokens = vec![dai_addr, bal_addr];
303
304 ProtocolComponent {
305 id: "0x4626d81b3a1711beb79f4cecff2413886d461677000200000000000000000011".to_string(),
306 protocol_system: "vm:balancer_v2".to_string(),
307 protocol_type_name: "balancer_v2_pool".to_string(),
308 chain: Chain::Ethereum,
309 tokens,
310 contract_addresses: vec![
311 Bytes::from_str("0xBA12222222228d8Ba445958a75a0704d566BF2C8").unwrap()
312 ],
313 static_attributes,
314 change: ChangeType::Creation,
315 creation_tx: Bytes::from_str("0x0000").unwrap(),
316 created_at: creation_time,
317 }
318 }
319
320 fn load_balancer_account_data() -> Vec<AccountUpdate> {
321 let project_root = env!("CARGO_MANIFEST_DIR");
322 let asset_path =
323 Path::new(project_root).join("tests/assets/decoder/balancer_v2_snapshot.json");
324 let json_data = fs::read_to_string(asset_path).expect("Failed to read test asset");
325 let data: Value = serde_json::from_str(&json_data).expect("Failed to parse JSON");
326
327 let accounts: Vec<AccountUpdate> = serde_json::from_value(data["accounts"].clone())
328 .expect("Expected accounts to match AccountUpdate structure");
329 accounts
330 }
331
332 #[tokio::test]
333 async fn test_try_from_with_header() {
334 let attributes: HashMap<String, Bytes> = vec![
335 (
336 "balance_owner".to_string(),
337 Bytes::from_str("0xBA12222222228d8Ba445958a75a0704d566BF2C8").unwrap(),
338 ),
339 ("override_block_number".to_string(), Bytes::from(123_u64.to_be_bytes().to_vec())),
340 ("override_block_timestamp".to_string(), Bytes::from(456_u64.to_be_bytes().to_vec())),
341 ("reserve1".to_string(), Bytes::from(200_u64.to_le_bytes().to_vec())),
342 ]
343 .into_iter()
344 .collect();
345 let tokens = [
346 Token::new(
347 &Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(),
348 "DAI",
349 18,
350 0,
351 &[Some(10_000)],
352 tycho_common::models::Chain::Ethereum,
353 100,
354 ),
355 Token::new(
356 &Bytes::from_str("0xba100000625a3754423978a60c9317c58a424e3d").unwrap(),
357 "BAL",
358 18,
359 0,
360 &[Some(10_000)],
361 tycho_common::models::Chain::Ethereum,
362 100,
363 ),
364 ]
365 .into_iter()
366 .map(|t| (t.address.clone(), t))
367 .collect::<HashMap<_, _>>();
368 let snapshot = ComponentWithState {
369 state: ProtocolComponentState {
370 component_id: "0x4626d81b3a1711beb79f4cecff2413886d461677000200000000000000000011"
371 .to_owned(),
372 attributes,
373 balances: HashMap::new(),
374 },
375 component: vm_component(),
376 component_tvl: None,
377 entrypoints: Vec::new(),
378 };
379 let block = BlockHeader::default();
381 let accounts = load_balancer_account_data();
382 let db = SHARED_TYCHO_DB.clone();
383 let engine = create_engine(db.clone(), false).unwrap();
384 for account in accounts.clone() {
385 engine
386 .state
387 .init_account(
388 account.address,
389 AccountInfo {
390 balance: account.balance.unwrap_or_default(),
391 nonce: 0u64,
392 code_hash: KECCAK_EMPTY,
393 code: account
394 .code
395 .clone()
396 .map(|arg0: Vec<u8>| Bytecode::new_raw(arg0.into())),
397 },
398 None,
399 false,
400 )
401 .expect("Failed to init account");
402 }
403 db.update(accounts, Some(block.clone()))
404 .unwrap();
405 let account_balances = HashMap::from([(
406 Bytes::from("0xBA12222222228d8Ba445958a75a0704d566BF2C8"),
407 HashMap::from([
408 (
409 Bytes::from("0x6b175474e89094c44da98b954eedeac495271d0f"),
410 Bytes::from(100_u64.to_le_bytes().to_vec()),
411 ),
412 (
413 Bytes::from("0xba100000625a3754423978a60c9317c58a424e3d"),
414 Bytes::from(100_u64.to_le_bytes().to_vec()),
415 ),
416 ]),
417 )]);
418
419 let decoder_context = DecoderContext::new();
420 let res = EVMPoolState::try_from_with_header(
421 snapshot,
422 block,
423 &account_balances,
424 &tokens,
425 &decoder_context,
426 )
427 .await
428 .unwrap();
429
430 let res_pool = res;
431
432 assert_eq!(
433 res_pool.get_balance_owner(),
434 Some(Address::from_str("0xBA12222222228d8Ba445958a75a0704d566BF2C8").unwrap())
435 );
436 let mut exp_involved_contracts = HashSet::new();
437 exp_involved_contracts
438 .insert(Address::from_str("0xBA12222222228d8Ba445958a75a0704d566BF2C8").unwrap());
439 assert_eq!(res_pool.get_involved_contracts(), exp_involved_contracts);
440 assert!(res_pool.get_manual_updates());
441 assert_eq!(res_pool.get_spot_price_caller(), None);
443 assert_eq!(
444 res_pool.get_block_overrides(),
445 Some(BlockEnvOverrides { number: Some(123), timestamp: Some(456) })
446 );
447 }
448
449 #[test]
450 fn test_spot_price_caller() {
451 assert_eq!(spot_price_caller("balancer_v3"), Some(Address::ZERO));
453 assert_eq!(spot_price_caller("balancer_v2"), None);
454 assert_eq!(spot_price_caller("curve"), None);
455 }
456}