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::{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    ) -> Result<TychoSimulationResponse, SimulationError> {
134        let call_data = self.encode_input(selector, args);
135        let params = SimulationParameters {
136            data: call_data,
137            to: self.address,
138            overrides,
139            caller: caller.unwrap_or(*EXTERNAL_ACCOUNT),
140            value,
141            gas_limit: None,
142            transient_storage,
143        };
144
145        let sim_result = self.simulate(params)?;
146
147        Ok(TychoSimulationResponse {
148            return_value: sim_result.result.to_vec(),
149            simulation_result: sim_result,
150        })
151    }
152
153    fn simulate(&self, params: SimulationParameters) -> Result<SimulationResult, SimulationError> {
154        self.engine
155            .simulate(&params)
156            .map_err(|e| coerce_error(&e, "pool_state", params.gas_limit))
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use std::str::FromStr;
163
164    use alloy::primitives::{hex, Bytes};
165    use tycho_client::feed::BlockHeader;
166
167    use super::*;
168    use crate::evm::{
169        engine_db::{
170            create_engine,
171            engine_db_interface::EngineDatabaseInterface,
172            simulation_db::SimulationDB,
173            tycho_db::PreCachedDBError,
174            utils::{get_client, get_runtime},
175        },
176        protocol::vm::{constants::BALANCER_V2, utils::string_to_bytes32},
177    };
178
179    #[derive(Debug, Clone)]
180    struct MockDatabase;
181
182    impl DatabaseRef for MockDatabase {
183        type Error = PreCachedDBError;
184
185        fn basic_ref(&self, _address: Address) -> Result<Option<AccountInfo>, Self::Error> {
186            Ok(Some(AccountInfo::default()))
187        }
188
189        fn code_by_hash_ref(&self, _code_hash: B256) -> Result<Bytecode, Self::Error> {
190            Ok(Bytecode::new())
191        }
192
193        fn storage_ref(&self, _address: Address, _index: U256) -> Result<U256, Self::Error> {
194            Ok(U256::from(0))
195        }
196
197        fn block_hash_ref(&self, _number: u64) -> Result<B256, Self::Error> {
198            Ok(B256::default())
199        }
200    }
201
202    impl EngineDatabaseInterface for MockDatabase {
203        type Error = String;
204
205        fn init_account(
206            &self,
207            _address: Address,
208            _account: AccountInfo,
209            _permanent_storage: Option<HashMap<U256, U256>>,
210            _mocked: bool,
211        ) -> Result<(), <Self as EngineDatabaseInterface>::Error> {
212            // Do nothing
213            Ok(())
214        }
215
216        fn clear_temp_storage(&mut self) -> Result<(), <Self as EngineDatabaseInterface>::Error> {
217            // Do nothing
218            Ok(())
219        }
220
221        fn get_current_block(&self) -> Option<tycho_client::feed::BlockHeader> {
222            None // Mock database doesn't have a real block
223        }
224    }
225
226    fn create_mock_engine() -> SimulationEngine<MockDatabase> {
227        SimulationEngine::new(MockDatabase, false)
228    }
229
230    fn create_contract() -> TychoSimulationContract<MockDatabase> {
231        let address = Address::ZERO;
232        let engine = create_mock_engine();
233        TychoSimulationContract::new_contract(
234            address,
235            Bytecode::new_raw(BALANCER_V2.into()),
236            engine,
237        )
238        .unwrap()
239    }
240
241    #[test]
242    fn test_encode_input_get_capabilities() {
243        let contract = create_contract();
244
245        // Arguments for the 'getCapabilities' function
246        let pool_id =
247            "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".to_string();
248        let sell_token = Address::from_str("0000000000000000000000000000000000000002").unwrap();
249        let buy_token = Address::from_str("0000000000000000000000000000000000000003").unwrap();
250
251        let encoded = contract.encode_input(
252            "getCapabilities(bytes32,address,address)",
253            (string_to_bytes32(&pool_id).unwrap(), sell_token, buy_token),
254        );
255
256        // The expected selector for "getCapabilities(bytes32,address,address)"
257        let expected_selector = hex!("48bd7dfd");
258        assert_eq!(&encoded[..4], &expected_selector[..]);
259
260        let expected_pool_id =
261            hex!("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef");
262        let expected_sell_token =
263            hex!("0000000000000000000000000000000000000000000000000000000000000002"); // padded to 32 bytes
264        let expected_buy_token =
265            hex!("0000000000000000000000000000000000000000000000000000000000000003"); // padded to 32 bytes
266
267        assert_eq!(&encoded[4..36], &expected_pool_id); // 32 bytes for poolId
268        assert_eq!(&encoded[36..68], &expected_sell_token); // 32 bytes for address (padded)
269        assert_eq!(&encoded[68..100], &expected_buy_token); // 32 bytes for address (padded)
270    }
271
272    #[test]
273    fn test_transient_storage() {
274        let db = SimulationDB::new(
275            get_client(None).expect("Failed to create Tycho RPC client"),
276            get_runtime().expect("Failed to create Tokio runtime"),
277            None,
278        );
279        let mut engine = create_engine(db, true).expect("Failed to create simulation engine");
280
281        // Dummy block (irrelevant for this test)
282        let block = BlockHeader {
283            number: 1,
284            hash: tycho_common::Bytes::from_str(
285                "0x0000000000000000000000000000000000000000000000000000000000000000",
286            )
287            .unwrap(),
288            timestamp: 1748397011,
289            ..Default::default()
290        };
291        engine.state.set_block(Some(block));
292
293        let contract_address = Address::from_str("0x0010d0d5db05933fa0d9f7038d365e1541a41888") // Irrelevant address
294            .expect("Invalid address");
295        let storage_slot: U256 =
296            U256::from_str("0xc090fc4683624cfc3884e9d8de5eca132f2d0ec062aff75d43c0465d5ceeab23")
297                .expect("Invalid storage slot");
298        let storage_value: U256 = U256::from(42); // Example value to store
299
300        // Bytecode retrieved by running `forge inspect TLoadTest deployedBytecode` on the following
301        // contract (must be converted to a Solidity file):
302        //
303        // // SPDX-License-Identifier: UNLICENSED
304        // pragma solidity ^0.8.26;
305        //
306        // contract TLoadTest {
307        //    bytes32 constant SLOT =
308        // 0xc090fc4683624cfc3884e9d8de5eca132f2d0ec062aff75d43c0465d5ceeab23;
309        //
310        //    function test() public view returns (bool) {
311        //        assembly {
312        //            let x := tload(SLOT)
313        //            mstore(0x0, x)
314        //            return(0x0, 0x20)
315        //        }
316        //    }
317        // }
318
319        let bytecode = Bytecode::new_raw(Bytes::from_str("0x6004361015600b575f80fd5b5f3560e01c63f8a8fd6d14601d575f80fd5b346054575f3660031901126054577fc090fc4683624cfc3884e9d8de5eca132f2d0ec062aff75d43c0465d5ceeab235c5f5260205ff35b5f80fdfea2646970667358221220f176684ab08659ff85817601a5398286c6029cf53bde9b1cce1a0c9bace67dad64736f6c634300081c0033").unwrap());
320        let contract = TychoSimulationContract::new_contract(contract_address, bytecode, engine)
321            .expect("Failed to create GenericVMHookHandler");
322
323        let transient_storage_params =
324            HashMap::from([(contract_address, HashMap::from([(storage_slot, storage_value)]))]);
325        let args = ();
326        let selector = "test()";
327
328        let res = contract
329            .call(selector, args, None, None, U256::from(0u64), Some(transient_storage_params))
330            .unwrap();
331
332        let decoded: U256 = U256::abi_decode(&res.return_value)
333            .map_err(|e| {
334                SimulationError::FatalError(format!("Failed to decode test return value: {e:?}"))
335            })
336            .unwrap();
337
338        assert_eq!(decoded, storage_value);
339    }
340}