Skip to main content

tycho_simulation/evm/protocol/vm/
decoder.rs

1use 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    /// Decodes a `ComponentWithState`, block `BlockHeader` and HashMap of all available tokens into
28    /// an `EVMPoolState`.
29    ///
30    /// Errors with a `InvalidSnapshotError`.
31    #[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        // Decode involved contracts
43        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                // Stateless contracts address are UTF-8 encoded
55                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        // Tokens whose protocol does not emit token contract storage. Simulation handles their
104        // transfers entirely in the proxy's local bookkeeping (custom approval + recipient balance)
105        // so they never delegate to an implementation another protocol may have left in the shared
106        // DB.
107        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        // Decode balances
124        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        // A protocol may override only one block env field. In that case the VM call will use the
188        // overridden field together with the current block's other field, so block.number and
189        // block.timestamp may not correspond to the same real chain block.
190        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
258/// The caller (`tx.origin`) for a protocol's adapter `price()` query.
259///
260/// Balancer V3's price path calls `querySwapExactIn`, which reverts with `NotStaticCall()` unless
261/// `tx.origin == address(0)` (its off-chain query-mode check). Every other adapter uses the
262/// default `EXTERNAL_ACCOUNT` caller (`None`).
263fn 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(); //Sample timestamp
296
297        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        // Initialize engine with balancer storage
380        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        // vm_component() is vm:balancer_v2, so the price query uses the default caller.
442        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        // Balancer V3 must query with tx.origin == address(0); everything else uses the default.
452        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}