Skip to main content

tycho_simulation/evm/protocol/vm/
state_builder.rs

1use std::{
2    collections::{HashMap, HashSet},
3    fmt::Debug,
4};
5
6use alloy::{
7    primitives::{Address, Bytes, Keccak256, U256},
8    sol_types::SolValue,
9};
10use itertools::Itertools;
11use revm::{
12    primitives::KECCAK_EMPTY,
13    state::{AccountInfo, Bytecode},
14    DatabaseRef,
15};
16use tracing::warn;
17use tycho_common::{simulation::errors::SimulationError, Bytes as TychoBytes};
18
19use super::{
20    constants::{EXTERNAL_ACCOUNT, MAX_BALANCE},
21    models::Capability,
22    state::EVMPoolState,
23    tycho_simulation_contract::TychoSimulationContract,
24    utils::get_code_for_contract,
25};
26use crate::evm::{
27    engine_db::{create_engine, engine_db_interface::EngineDatabaseInterface},
28    protocol::utils::bytes_to_address,
29    simulation::{BlockEnvOverrides, SimulationEngine, SimulationParameters},
30};
31
32#[derive(Debug)]
33/// `EVMPoolStateBuilder` is a builder pattern implementation for creating instances of
34/// `EVMPoolState`.
35///
36/// This struct provides a flexible way to construct `EVMPoolState` objects with
37/// multiple optional parameters. It handles the validation of required fields and applies default
38/// values for optional parameters where necessary.
39/// # Example
40/// Constructing a `EVMPoolState` with only the required parameters:
41/// ```rust
42/// use alloy::primitives::Address;
43/// use std::path::PathBuf;
44/// use tycho_common::Bytes;
45/// use tycho_simulation::evm::engine_db::SHARED_TYCHO_DB;
46/// use tycho_simulation::evm::protocol::vm::state_builder::EVMPoolStateBuilder;
47/// use tycho_simulation::evm::protocol::vm::constants::BALANCER_V2;
48/// /// use tycho_common::simulation::errors::SimulationError;
49/// use revm::state::Bytecode;
50///
51/// #[tokio::main]
52/// async fn main() -> Result<(), tycho_common::simulation::errors::SimulationError> {
53///     use tycho_client::feed::BlockHeader;
54///
55///     let pool_id: String = "0x4626d81b3a1711beb79f4cecff2413886d461677000200000000000000000011".into();
56///
57///     let tokens = vec![
58///         Bytes::from("0x6b175474e89094c44da98b954eedeac495271d0f"),
59///         Bytes::from("0xba100000625a3754423978a60c9317c58a424e3d"),
60///     ];
61///
62///     // Set up the block for the database
63///     let block = BlockHeader {
64///         number: 1,
65///         hash: Default::default(),
66///         timestamp: 1632456789,
67///         ..Default::default()
68///     };
69///     SHARED_TYCHO_DB.update(vec![], Some(block)).unwrap();
70///
71///     // Build the EVMPoolState
72///     let pool_state = EVMPoolStateBuilder::new(pool_id, tokens, Address::random())
73///         .adapter_contract_bytecode(Bytecode::new_raw(BALANCER_V2.into()))
74///         .build(SHARED_TYCHO_DB.clone())
75///         .await?;
76///     Ok(())
77/// }
78/// ```
79pub struct EVMPoolStateBuilder<D: EngineDatabaseInterface + Clone + Debug>
80where
81    <D as DatabaseRef>::Error: Debug,
82    <D as EngineDatabaseInterface>::Error: Debug,
83{
84    id: String,
85    tokens: Vec<TychoBytes>,
86    balances: HashMap<Address, U256>,
87    adapter_address: Address,
88    balance_owner: Option<Address>,
89    capabilities: Option<HashSet<Capability>>,
90    involved_contracts: Option<HashSet<Address>>,
91    contract_balances: HashMap<Address, HashMap<Address, U256>>,
92    stateless_contracts: Option<HashMap<String, Option<Vec<u8>>>>,
93    manual_updates: Option<bool>,
94    trace: Option<bool>,
95    engine: Option<SimulationEngine<D>>,
96    adapter_contract: Option<TychoSimulationContract<D>>,
97    adapter_contract_bytecode: Option<Bytecode>,
98    disable_overwrite_tokens: HashSet<Address>,
99    self_contained_tokens: HashSet<Address>,
100    block_overrides: Option<BlockEnvOverrides>,
101}
102
103impl<D> EVMPoolStateBuilder<D>
104where
105    D: EngineDatabaseInterface + Clone + Debug + 'static,
106    <D as DatabaseRef>::Error: Debug,
107    <D as EngineDatabaseInterface>::Error: Debug,
108{
109    pub fn new(id: String, tokens: Vec<TychoBytes>, adapter_address: Address) -> Self {
110        Self {
111            id,
112            tokens,
113            balances: HashMap::new(),
114            adapter_address,
115            balance_owner: None,
116            capabilities: None,
117            involved_contracts: None,
118            contract_balances: HashMap::new(),
119            stateless_contracts: None,
120            manual_updates: None,
121            trace: None,
122            engine: None,
123            adapter_contract: None,
124            adapter_contract_bytecode: None,
125            disable_overwrite_tokens: HashSet::new(),
126            self_contained_tokens: HashSet::new(),
127            block_overrides: None,
128        }
129    }
130
131    #[deprecated(note = "Use account balances instead")]
132    pub fn balance_owner(mut self, balance_owner: Address) -> Self {
133        self.balance_owner = Some(balance_owner);
134        self
135    }
136
137    /// Set component balances. This balance belongs to the 'balance_owner' if one is set,
138    /// otherwise it belongs to the pool itself.
139    pub fn balances(mut self, balances: HashMap<Address, U256>) -> Self {
140        self.balances = balances;
141        self
142    }
143
144    /// Set contract balances
145    pub fn account_balances(
146        mut self,
147        account_balances: HashMap<Address, HashMap<Address, U256>>,
148    ) -> Self {
149        self.contract_balances = account_balances;
150        self
151    }
152
153    pub fn capabilities(mut self, capabilities: HashSet<Capability>) -> Self {
154        self.capabilities = Some(capabilities);
155        self
156    }
157
158    pub fn involved_contracts(mut self, involved_contracts: HashSet<Address>) -> Self {
159        self.involved_contracts = Some(involved_contracts);
160        self
161    }
162
163    pub fn stateless_contracts(
164        mut self,
165        stateless_contracts: HashMap<String, Option<Vec<u8>>>,
166    ) -> Self {
167        self.stateless_contracts = Some(stateless_contracts);
168        self
169    }
170    pub fn manual_updates(mut self, manual_updates: bool) -> Self {
171        self.manual_updates = Some(manual_updates);
172        self
173    }
174
175    pub fn trace(mut self, trace: bool) -> Self {
176        self.trace = Some(trace);
177        self
178    }
179
180    pub fn engine(mut self, engine: SimulationEngine<D>) -> Self {
181        self.engine = Some(engine);
182        self
183    }
184
185    pub fn adapter_contract(mut self, adapter_contract: TychoSimulationContract<D>) -> Self {
186        self.adapter_contract = Some(adapter_contract);
187        self
188    }
189
190    pub fn adapter_contract_bytecode(mut self, adapter_contract_bytecode: Bytecode) -> Self {
191        self.adapter_contract_bytecode = Some(adapter_contract_bytecode);
192        self
193    }
194
195    pub fn disable_overwrite_tokens(mut self, disable_overwrite_tokens: HashSet<Address>) -> Self {
196        self.disable_overwrite_tokens = disable_overwrite_tokens;
197        self
198    }
199
200    pub fn self_contained_tokens(mut self, self_contained_tokens: HashSet<Address>) -> Self {
201        self.self_contained_tokens = self_contained_tokens;
202        self
203    }
204
205    pub fn block_overrides(mut self, block_overrides: Option<BlockEnvOverrides>) -> Self {
206        self.block_overrides = block_overrides;
207        self
208    }
209
210    /// Build the final EVMPoolState object
211    pub async fn build(mut self, db: D) -> Result<EVMPoolState<D>, SimulationError> {
212        let engine = if let Some(engine) = &self.engine {
213            engine.clone()
214        } else {
215            self.engine = Some(self.get_default_engine(db).await?);
216            self.engine.clone().ok_or_else(|| {
217                SimulationError::FatalError(
218                    "Failed to get build engine: Engine not initialized".to_string(),
219                )
220            })?
221        };
222
223        if self.adapter_contract.is_none() {
224            self.adapter_contract = Some(TychoSimulationContract::new_contract(
225                self.adapter_address,
226                self.adapter_contract_bytecode
227                    .clone()
228                    .ok_or_else(|| {
229                        SimulationError::FatalError("Adapter contract bytecode not set".to_string())
230                    })?,
231                engine.clone(),
232            )?)
233        };
234
235        let capabilities = if let Some(capabilities) = &self.capabilities {
236            capabilities.clone()
237        } else {
238            self.get_default_capabilities()?
239        };
240
241        let adapter_contract = self.adapter_contract.ok_or_else(|| {
242            SimulationError::FatalError(
243                "Failed to get build engine: Adapter contract not initialized".to_string(),
244            )
245        })?;
246
247        Ok(EVMPoolState::new(
248            self.id,
249            self.tokens,
250            self.balances,
251            self.balance_owner,
252            self.contract_balances,
253            HashMap::new(),
254            capabilities,
255            HashMap::new(),
256            self.involved_contracts
257                .unwrap_or_default(),
258            self.manual_updates.unwrap_or(false),
259            adapter_contract,
260            self.disable_overwrite_tokens,
261            self.self_contained_tokens,
262            self.block_overrides,
263        ))
264    }
265
266    async fn get_default_engine(&self, db: D) -> Result<SimulationEngine<D>, SimulationError> {
267        let engine = create_engine(db, self.trace.unwrap_or(false))?;
268
269        engine
270            .state
271            .init_account(
272                *EXTERNAL_ACCOUNT,
273                AccountInfo {
274                    balance: *MAX_BALANCE,
275                    nonce: 0,
276                    code_hash: KECCAK_EMPTY,
277                    code: None,
278                },
279                None,
280                false,
281            )
282            .map_err(|err| {
283                SimulationError::FatalError(format!(
284                    "Failed to get default engine: Failed to init external account: {err:?}"
285                ))
286            })?;
287
288        if let Some(stateless_contracts) = &self.stateless_contracts {
289            for (address, bytecode) in stateless_contracts.iter() {
290                let mut addr_str = address.clone();
291                let (code, code_hash) = if bytecode.is_none() {
292                    if addr_str.starts_with("call") {
293                        addr_str = self
294                            .get_address_from_call(&engine, &addr_str)?
295                            .to_string();
296                    }
297                    let code = get_code_for_contract(&addr_str, None).await?;
298                    (Some(code.clone()), code.hash_slow())
299                } else {
300                    let code =
301                        Bytecode::new_raw(Bytes::from(bytecode.clone().ok_or_else(|| {
302                            SimulationError::FatalError(
303                                "Failed to get default engine: Byte code from stateless contracts is None".into(),
304                            )
305                        })?));
306                    (Some(code.clone()), code.hash_slow())
307                };
308                let account_address: Address = addr_str.parse().map_err(|_| {
309                    SimulationError::FatalError(format!(
310                        "Failed to get default engine: Couldn't parse address string {address}"
311                    ))
312                })?;
313                engine.state.init_account(
314                    Address(*account_address),
315                    AccountInfo { balance: Default::default(), nonce: 0, code_hash, code },
316                    None,
317                    false,
318                ).map_err(|err| {
319                    SimulationError::FatalError(format!(
320                        "Failed to get default engine: Failed to init stateless contract account: {err:?}"
321                    ))
322                })?;
323            }
324        }
325        Ok(engine)
326    }
327
328    fn get_default_capabilities(&mut self) -> Result<HashSet<Capability>, SimulationError> {
329        let mut capabilities = Vec::new();
330
331        // Generate all permutations of tokens and retrieve capabilities
332        for tokens_pair in self.tokens.iter().permutations(2) {
333            // Manually unpack the inner vector
334            if let [t0, t1] = tokens_pair[..] {
335                let caps = self
336                    .adapter_contract
337                    .clone()
338                    .ok_or_else(|| {
339                        SimulationError::FatalError(
340                            "Failed to get default capabilities: Adapter contract not initialized"
341                                .to_string(),
342                        )
343                    })?
344                    .get_capabilities(&self.id, bytes_to_address(t0)?, bytes_to_address(t1)?)?;
345                capabilities.push(caps);
346            }
347        }
348
349        // Find the maximum capabilities length
350        let max_capabilities = capabilities
351            .iter()
352            .map(|c| c.len())
353            .max()
354            .unwrap_or(0);
355
356        // Intersect all capability sets
357        let common_capabilities: HashSet<_> = capabilities
358            .iter()
359            .fold(capabilities[0].clone(), |acc, cap| acc.intersection(cap).cloned().collect());
360
361        // Check for mismatches in capabilities
362        if common_capabilities.len() < max_capabilities {
363            warn!(
364                "Warning: Pool {} has different capabilities depending on the token pair!",
365                self.id
366            );
367        }
368        Ok(common_capabilities)
369    }
370
371    /// Gets the address of the code - mostly used for dynamic proxy implementations. For example,
372    /// some protocols have some dynamic math implementation that is given by the factory. When
373    /// we swap on the pools for such protocols, it will call the factory to get the implementation
374    /// and use it for the swap.
375    /// This method simulates the call to the pool, which gives us the address of the
376    /// implementation.
377    ///
378    /// # See Also
379    /// [Dynamic Address Resolution Example](https://github.com/propeller-heads/propeller-protocol-lib/blob/main/docs/indexing/reserved-attributes.md#description-2)
380    fn get_address_from_call(
381        &self,
382        engine: &SimulationEngine<D>,
383        decoded: &str,
384    ) -> Result<Address, SimulationError> {
385        let method_name = decoded
386            .split(':')
387            .next_back()
388            .ok_or_else(|| {
389                SimulationError::FatalError(
390                    "Failed to get address from call: Could not decode method name from call"
391                        .into(),
392                )
393            })?;
394
395        let selector = {
396            let mut hasher = Keccak256::new();
397            hasher.update(method_name.as_bytes());
398            let result = hasher.finalize();
399            result[..4].to_vec()
400        };
401
402        let to_address = decoded
403            .split(':')
404            .nth(1)
405            .ok_or_else(|| {
406                SimulationError::FatalError(
407                    "Failed to get address from call: Could not decode to_address from call".into(),
408                )
409            })?;
410
411        let parsed_address: Address = to_address.parse().map_err(|_| {
412            SimulationError::FatalError(format!(
413                "Failed to get address from call: Invalid address format: {to_address}"
414            ))
415        })?;
416
417        let sim_params = SimulationParameters {
418            data: selector.to_vec(),
419            to: parsed_address,
420            overrides: Some(HashMap::new()),
421            caller: *EXTERNAL_ACCOUNT,
422            value: U256::from(0u64),
423            gas_limit: None,
424            transient_storage: None,
425            block_overrides: None,
426        };
427
428        let sim_result = engine
429            .simulate(&sim_params)
430            .map_err(|err| SimulationError::FatalError(err.to_string()))?;
431
432        let address: Address = Address::abi_decode(&sim_result.result).map_err(|e| {
433            SimulationError::FatalError(format!("Failed to get address from call: Failed to decode address list from simulation result {e:?}"))
434        })?;
435
436        Ok(address)
437    }
438}
439
440#[cfg(test)]
441mod tests {
442    use std::str::FromStr;
443
444    use super::*;
445    use crate::evm::engine_db::{tycho_db::PreCachedDB, SHARED_TYCHO_DB};
446
447    #[test]
448    fn test_build_without_required_fields() {
449        let id = "pool_1".to_string();
450        let tokens =
451            vec![TychoBytes::from_str("0000000000000000000000000000000000000000").unwrap()];
452        let balances = HashMap::new();
453        let adapter_address =
454            Address::from_str("0xA2C5C98A892fD6656a7F39A2f63228C0Bc846270").unwrap();
455        let result = tokio_test::block_on(
456            EVMPoolStateBuilder::<PreCachedDB>::new(id, tokens, adapter_address)
457                .balances(balances)
458                .build(SHARED_TYCHO_DB.clone()),
459        );
460
461        assert!(result.is_err());
462        match result.unwrap_err() {
463            SimulationError::FatalError(field) => {
464                assert_eq!(field, "Adapter contract bytecode not set")
465            }
466            _ => panic!("Unexpected error type"),
467        }
468    }
469
470    #[test]
471    fn test_engine_setup() {
472        let id = "pool_1".to_string();
473        let token2 = TychoBytes::from_str("0000000000000000000000000000000000000002").unwrap();
474        let token3 = TychoBytes::from_str("0000000000000000000000000000000000000003").unwrap();
475        let tokens = vec![token2.clone(), token3.clone()];
476        let balances = HashMap::new();
477        let adapter_address =
478            Address::from_str("0xA2C5C98A892fD6656a7F39A2f63228C0Bc846270").unwrap();
479        let builder =
480            EVMPoolStateBuilder::<PreCachedDB>::new(id, tokens, adapter_address).balances(balances);
481
482        let engine = tokio_test::block_on(builder.get_default_engine(SHARED_TYCHO_DB.clone()));
483
484        assert!(engine.is_ok());
485        let engine = engine.unwrap();
486        assert!(engine
487            .state
488            .get_account_storage()
489            .expect("Failed to get account storage")
490            .account_present(&EXTERNAL_ACCOUNT));
491    }
492}