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)]
33pub 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 pub fn balances(mut self, balances: HashMap<Address, U256>) -> Self {
142 self.balances = balances;
143 self
144 }
145
146 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 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 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 for tokens_pair in self.tokens.iter().permutations(2) {
342 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 let max_capabilities = capabilities
360 .iter()
361 .map(|c| c.len())
362 .max()
363 .unwrap_or(0);
364
365 let common_capabilities: HashSet<_> = capabilities
367 .iter()
368 .fold(capabilities[0].clone(), |acc, cap| acc.intersection(cap).cloned().collect());
369
370 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 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}