Skip to main content

tycho_simulation/evm/protocol/vm/
tycho_simulation_contract.rs

1use std::{collections::HashMap, fmt::Debug};
2
3use alloy::{
4    primitives::{keccak256, Address, Keccak256, B256, U256},
5    sol_types::SolValue,
6};
7use revm::{
8    state::{AccountInfo, Bytecode},
9    DatabaseRef,
10};
11use tycho_common::simulation::errors::SimulationError;
12
13use super::{
14    constants::{EXTERNAL_ACCOUNT, MAX_BALANCE},
15    utils::coerce_error,
16};
17use crate::evm::{
18    engine_db::engine_db_interface::EngineDatabaseInterface,
19    simulation::{BlockEnvOverrides, SimulationEngine, SimulationParameters, SimulationResult},
20};
21
22#[derive(Debug, Clone)]
23pub struct TychoSimulationResponse {
24    pub return_value: Vec<u8>,
25    pub simulation_result: SimulationResult,
26}
27
28/// Represents a contract interface that interacts with the tycho_simulation environment to perform
29/// simulations on Ethereum smart contracts.
30///
31/// `TychoSimulationContract` is a wrapper around the low-level details of encoding and decoding
32/// inputs and outputs, simulating transactions, and handling ABI interactions specific to the Tycho
33/// environment. It is designed to be used by applications requiring smart contract simulations
34/// and includes methods for encoding function calls, decoding transaction results, and interacting
35/// with the `SimulationEngine`.
36///
37/// # Type Parameters
38/// - `D`: A database reference that implements `DatabaseRef` and `Clone`, which the simulation
39///   engine uses to access blockchain state.
40///
41/// # Fields
42/// - `abi`: The Application Binary Interface of the contract, which defines its functions and event
43///   signatures.
44/// - `address`: The address of the contract being simulated.
45/// - `engine`: The `SimulationEngine` instance responsible for simulating transactions and managing
46///   the contract's state.
47///
48/// # Errors
49/// Returns errors of type `SimulationError` when encoding, decoding, or simulation operations
50/// fail. These errors provide detailed feedback on potential issues.
51#[derive(Clone, Debug)]
52pub struct TychoSimulationContract<D: EngineDatabaseInterface + Clone + Debug>
53where
54    <D as DatabaseRef>::Error: Debug,
55    <D as EngineDatabaseInterface>::Error: Debug,
56{
57    pub(crate) address: Address,
58    pub(crate) engine: SimulationEngine<D>,
59}
60
61impl<D: EngineDatabaseInterface + Clone + Debug> TychoSimulationContract<D>
62where
63    <D as DatabaseRef>::Error: Debug,
64    <D as EngineDatabaseInterface>::Error: Debug,
65{
66    pub fn new(address: Address, engine: SimulationEngine<D>) -> Result<Self, SimulationError> {
67        Ok(Self { address, engine })
68    }
69
70    // Creates a new instance with the ISwapAdapter ABI
71    pub fn new_contract(
72        address: Address,
73        adapter_contract_bytecode: Bytecode,
74        engine: SimulationEngine<D>,
75    ) -> Result<Self, SimulationError> {
76        engine
77            .state
78            .init_account(
79                address,
80                AccountInfo {
81                    balance: *MAX_BALANCE,
82                    nonce: 0,
83                    code_hash: B256::from(keccak256(
84                        adapter_contract_bytecode
85                            .clone()
86                            .bytes(),
87                    )),
88                    code: Some(adapter_contract_bytecode),
89                },
90                None,
91                false,
92            )
93            .map_err(|err| {
94                SimulationError::FatalError(format!(
95                    "Failed to init contract account in simulation engine: {err:?}"
96                ))
97            })?;
98
99        Ok(Self { address, engine })
100    }
101
102    fn encode_input(&self, selector: &str, args: impl SolValue) -> Vec<u8> {
103        let mut hasher = Keccak256::new();
104        hasher.update(selector.as_bytes());
105        let selector_bytes = &hasher.finalize()[..4];
106        let mut call_data = selector_bytes.to_vec();
107        let mut encoded_args = args.abi_encode();
108        // Remove extra prefix if present (32 bytes for dynamic data)
109        // Alloy encoding is including a prefix for dynamic data indicating the offset or length
110        // but at this point we don't want that
111        if encoded_args.len() > 32 &&
112            encoded_args[..32] ==
113                [0u8; 31]
114                    .into_iter()
115                    .chain([32].to_vec())
116                    .collect::<Vec<u8>>()
117        {
118            encoded_args = encoded_args[32..].to_vec();
119        }
120        call_data.extend(encoded_args);
121        call_data
122    }
123
124    #[allow(clippy::too_many_arguments)]
125    pub fn call(
126        &self,
127        selector: &str,
128        args: impl SolValue,
129        overrides: Option<HashMap<Address, HashMap<U256, U256>>>,
130        caller: Option<Address>,
131        value: U256,
132        transient_storage: Option<HashMap<Address, HashMap<U256, U256>>>,
133        block_overrides: Option<BlockEnvOverrides>,
134    ) -> Result<TychoSimulationResponse, SimulationError> {
135        let call_data = self.encode_input(selector, args);
136        let params = SimulationParameters {
137            data: call_data,
138            to: self.address,
139            overrides,
140            caller: caller.unwrap_or(*EXTERNAL_ACCOUNT),
141            value,
142            gas_limit: None,
143            transient_storage,
144            block_overrides,
145        };
146
147        let sim_result = self.simulate(params)?;
148
149        Ok(TychoSimulationResponse {
150            return_value: sim_result.result.to_vec(),
151            simulation_result: sim_result,
152        })
153    }
154
155    fn simulate(&self, params: SimulationParameters) -> Result<SimulationResult, SimulationError> {
156        self.engine
157            .simulate(&params)
158            .map_err(|e| coerce_error(&e, "pool_state", params.gas_limit))
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use std::str::FromStr;
165
166    use alloy::primitives::{hex, Bytes};
167    use tycho_client::feed::BlockHeader;
168
169    use super::*;
170    use crate::evm::{
171        engine_db::{
172            create_engine,
173            engine_db_interface::EngineDatabaseInterface,
174            simulation_db::SimulationDB,
175            tycho_db::PreCachedDBError,
176            utils::{get_client, get_runtime},
177        },
178        protocol::vm::{constants::BALANCER_V2, utils::string_to_bytes32},
179    };
180
181    #[derive(Debug, Clone)]
182    struct MockDatabase;
183
184    impl DatabaseRef for MockDatabase {
185        type Error = PreCachedDBError;
186
187        fn basic_ref(&self, _address: Address) -> Result<Option<AccountInfo>, Self::Error> {
188            Ok(Some(AccountInfo::default()))
189        }
190
191        fn code_by_hash_ref(&self, _code_hash: B256) -> Result<Bytecode, Self::Error> {
192            Ok(Bytecode::new())
193        }
194
195        fn storage_ref(&self, _address: Address, _index: U256) -> Result<U256, Self::Error> {
196            Ok(U256::from(0))
197        }
198
199        fn block_hash_ref(&self, _number: u64) -> Result<B256, Self::Error> {
200            Ok(B256::default())
201        }
202    }
203
204    impl EngineDatabaseInterface for MockDatabase {
205        type Error = String;
206
207        fn init_account(
208            &self,
209            _address: Address,
210            _account: AccountInfo,
211            _permanent_storage: Option<HashMap<U256, U256>>,
212            _mocked: bool,
213        ) -> Result<(), <Self as EngineDatabaseInterface>::Error> {
214            // Do nothing
215            Ok(())
216        }
217
218        fn clear_temp_storage(&mut self) -> Result<(), <Self as EngineDatabaseInterface>::Error> {
219            // Do nothing
220            Ok(())
221        }
222
223        fn get_current_block(&self) -> Option<tycho_client::feed::BlockHeader> {
224            None // Mock database doesn't have a real block
225        }
226    }
227
228    fn create_mock_engine() -> SimulationEngine<MockDatabase> {
229        SimulationEngine::new(MockDatabase, false)
230    }
231
232    fn create_contract() -> TychoSimulationContract<MockDatabase> {
233        let address = Address::ZERO;
234        let engine = create_mock_engine();
235        TychoSimulationContract::new_contract(
236            address,
237            Bytecode::new_raw(BALANCER_V2.into()),
238            engine,
239        )
240        .unwrap()
241    }
242
243    #[test]
244    fn test_encode_input_get_capabilities() {
245        let contract = create_contract();
246
247        // Arguments for the 'getCapabilities' function
248        let pool_id =
249            "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".to_string();
250        let sell_token = Address::from_str("0000000000000000000000000000000000000002").unwrap();
251        let buy_token = Address::from_str("0000000000000000000000000000000000000003").unwrap();
252
253        let encoded = contract.encode_input(
254            "getCapabilities(bytes32,address,address)",
255            (string_to_bytes32(&pool_id).unwrap(), sell_token, buy_token),
256        );
257
258        // The expected selector for "getCapabilities(bytes32,address,address)"
259        let expected_selector = hex!("48bd7dfd");
260        assert_eq!(&encoded[..4], &expected_selector[..]);
261
262        let expected_pool_id =
263            hex!("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef");
264        let expected_sell_token =
265            hex!("0000000000000000000000000000000000000000000000000000000000000002"); // padded to 32 bytes
266        let expected_buy_token =
267            hex!("0000000000000000000000000000000000000000000000000000000000000003"); // padded to 32 bytes
268
269        assert_eq!(&encoded[4..36], &expected_pool_id); // 32 bytes for poolId
270        assert_eq!(&encoded[36..68], &expected_sell_token); // 32 bytes for address (padded)
271        assert_eq!(&encoded[68..100], &expected_buy_token); // 32 bytes for address (padded)
272    }
273
274    #[test]
275    fn test_transient_storage() {
276        let db = SimulationDB::new(
277            get_client(None).expect("Failed to create Tycho RPC client"),
278            get_runtime().expect("Failed to create Tokio runtime"),
279            None,
280        );
281        let mut engine = create_engine(db, true).expect("Failed to create simulation engine");
282
283        // Dummy block (irrelevant for this test)
284        let block = BlockHeader {
285            number: 1,
286            hash: tycho_common::Bytes::from_str(
287                "0x0000000000000000000000000000000000000000000000000000000000000000",
288            )
289            .unwrap(),
290            timestamp: 1748397011,
291            ..Default::default()
292        };
293        engine.state.set_block(Some(block));
294
295        let contract_address = Address::from_str("0x0010d0d5db05933fa0d9f7038d365e1541a41888") // Irrelevant address
296            .expect("Invalid address");
297        let storage_slot: U256 =
298            U256::from_str("0xc090fc4683624cfc3884e9d8de5eca132f2d0ec062aff75d43c0465d5ceeab23")
299                .expect("Invalid storage slot");
300        let storage_value: U256 = U256::from(42); // Example value to store
301
302        // Bytecode retrieved by running `forge inspect TLoadTest deployedBytecode` on the following
303        // contract (must be converted to a Solidity file):
304        //
305        // // SPDX-License-Identifier: UNLICENSED
306        // pragma solidity ^0.8.26;
307        //
308        // contract TLoadTest {
309        //    bytes32 constant SLOT =
310        // 0xc090fc4683624cfc3884e9d8de5eca132f2d0ec062aff75d43c0465d5ceeab23;
311        //
312        //    function test() public view returns (bool) {
313        //        assembly {
314        //            let x := tload(SLOT)
315        //            mstore(0x0, x)
316        //            return(0x0, 0x20)
317        //        }
318        //    }
319        // }
320
321        let bytecode = Bytecode::new_raw(Bytes::from_str("0x6004361015600b575f80fd5b5f3560e01c63f8a8fd6d14601d575f80fd5b346054575f3660031901126054577fc090fc4683624cfc3884e9d8de5eca132f2d0ec062aff75d43c0465d5ceeab235c5f5260205ff35b5f80fdfea2646970667358221220f176684ab08659ff85817601a5398286c6029cf53bde9b1cce1a0c9bace67dad64736f6c634300081c0033").unwrap());
322        let contract = TychoSimulationContract::new_contract(contract_address, bytecode, engine)
323            .expect("Failed to create GenericVMHookHandler");
324
325        let transient_storage_params =
326            HashMap::from([(contract_address, HashMap::from([(storage_slot, storage_value)]))]);
327        let args = ();
328        let selector = "test()";
329
330        let res = contract
331            .call(
332                selector,
333                args,
334                None,
335                None,
336                U256::from(0u64),
337                Some(transient_storage_params),
338                None,
339            )
340            .unwrap();
341
342        let decoded: U256 = U256::abi_decode(&res.return_value)
343            .map_err(|e| {
344                SimulationError::FatalError(format!("Failed to decode test return value: {e:?}"))
345            })
346            .unwrap();
347
348        assert_eq!(decoded, storage_value);
349    }
350
351    #[test]
352    fn test_call_applies_block_overrides() {
353        let db = SimulationDB::new(
354            get_client(None).expect("Failed to create Tycho RPC client"),
355            get_runtime().expect("Failed to create Tokio runtime"),
356            None,
357        );
358        let mut engine = create_engine(db, false).expect("Failed to create simulation engine");
359        engine
360            .state
361            .set_block(Some(BlockHeader {
362                number: 1,
363                hash: tycho_common::Bytes::from_str(
364                    "0x0000000000000000000000000000000000000000000000000000000000000000",
365                )
366                .unwrap(),
367                timestamp: 2,
368                ..Default::default()
369            }));
370
371        let contract_address = Address::from_str("0x0000000000000000000000000000000000001234")
372            .expect("Invalid contract address");
373        // Minimal runtime bytecode equivalent to the following Solidity contract:
374        //
375        // // SPDX-License-Identifier: UNLICENSED
376        // pragma solidity ^0.8.26;
377        //
378        // contract BlockNumberTest {
379        //     function test() external view returns (uint256) {
380        //         return block.number;
381        //     }
382        // }
383        let bytecode = Bytecode::new_raw(Bytes::from_static(&[
384            0x43, // NUMBER
385            0x60, 0x00, // PUSH1 0
386            0x52, // MSTORE
387            0x60, 0x20, // PUSH1 32
388            0x60, 0x00, // PUSH1 0
389            0xf3, // RETURN
390        ]));
391        let contract = TychoSimulationContract::new_contract(contract_address, bytecode, engine)
392            .expect("Failed to create test contract");
393
394        let res = contract
395            .call(
396                "test()",
397                (),
398                None,
399                None,
400                U256::ZERO,
401                None,
402                Some(BlockEnvOverrides { number: Some(123), timestamp: Some(456) }),
403            )
404            .expect("contract call should apply block overrides");
405
406        assert_eq!(U256::from_be_slice(&res.return_value), U256::from(123));
407    }
408}