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