tycho_simulation/evm/protocol/vm/
tycho_simulation_contract.rs1use 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#[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 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 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(¶ms)
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 Ok(())
214 }
215
216 fn clear_temp_storage(&mut self) -> Result<(), <Self as EngineDatabaseInterface>::Error> {
217 Ok(())
219 }
220
221 fn get_current_block(&self) -> Option<tycho_client::feed::BlockHeader> {
222 None }
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 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 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"); let expected_buy_token =
265 hex!("0000000000000000000000000000000000000000000000000000000000000003"); assert_eq!(&encoded[4..36], &expected_pool_id); assert_eq!(&encoded[36..68], &expected_sell_token); assert_eq!(&encoded[68..100], &expected_buy_token); }
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 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") .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); 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}