1use std::collections::BTreeMap;
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6use std::time::{SystemTime, UNIX_EPOCH};
7
8use alloy::network::AnyNetwork;
9use alloy::primitives::{Address, Bytes, Log, TxKind, B256, U256};
10use alloy::providers::Provider;
11use alloy::rpc::types::trace::geth::pre_state::{AccountState, DiffMode};
12use serde::Serialize;
13use foundry_fork_db::{cache::BlockchainDbMeta, BlockchainDb, SharedBackend};
14use revm::context::TxEnv;
15use revm::database::CacheDB;
16use revm::primitives::hardfork::SpecId;
17use revm::state::EvmState;
18use revm::Database;
19use revm::{Context, ExecuteEvm, MainBuilder, MainContext};
20use revm_inspectors::tracing::{TracingInspector, TracingInspectorConfig};
21
22pub use revm_inspectors::tracing::CallTraceArena;
23
24use crate::error::{Error, Result};
25use crate::types::Operation;
26
27#[derive(Debug, Clone)]
29pub struct SimulationResult {
30 pub success: bool,
32 pub gas_used: u64,
34 pub return_data: Bytes,
36 pub logs: Vec<Log>,
38 pub revert_reason: Option<String>,
40 pub state_diff: DiffMode,
42 pub traces: Option<CallTraceArena>,
44}
45
46impl SimulationResult {
47 pub fn is_success(&self) -> bool {
49 self.success
50 }
51
52 pub fn error_message(&self) -> Option<&str> {
54 self.revert_reason.as_deref()
55 }
56
57 pub fn format_traces(&self) -> Option<String> {
61 use revm_inspectors::tracing::TraceWriter;
62
63 let traces = self.traces.as_ref()?;
64 let mut writer = TraceWriter::new(Vec::<u8>::new());
65 writer.write_arena(traces).ok()?;
66 String::from_utf8(writer.into_writer()).ok()
67 }
68}
69
70#[derive(Debug, Serialize)]
72pub struct SimulationDebugOutput {
73 pub timestamp: String,
75 pub chain_id: u64,
77 pub account_address: Address,
79 pub call: CallDebugInfo,
81 pub result: SimulationResultDebug,
83}
84
85#[derive(Debug, Serialize)]
87pub struct CallDebugInfo {
88 pub to: Address,
90 pub value: String,
92 pub data: String,
94 pub operation: String,
96}
97
98#[derive(Debug, Serialize)]
100pub struct SimulationResultDebug {
101 pub success: bool,
103 pub gas_used: u64,
105 pub revert_reason: Option<String>,
107 pub return_data: String,
109 pub logs: Vec<LogDebug>,
111 pub state_diff: StateDiffDebug,
113 pub traces: Option<String>,
115}
116
117#[derive(Debug, Serialize)]
119pub struct LogDebug {
120 pub address: Address,
122 pub topics: Vec<String>,
124 pub data: String,
126}
127
128#[derive(Debug, Serialize)]
130pub struct StateDiffDebug {
131 pub pre: BTreeMap<Address, AccountStateDebug>,
133 pub post: BTreeMap<Address, AccountStateDebug>,
135}
136
137#[derive(Debug, Serialize)]
139pub struct AccountStateDebug {
140 #[serde(skip_serializing_if = "Option::is_none")]
142 pub balance: Option<String>,
143 #[serde(skip_serializing_if = "Option::is_none")]
145 pub nonce: Option<u64>,
146 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
148 pub storage: BTreeMap<String, String>,
149}
150
151impl SimulationDebugOutput {
152 pub fn new(
154 chain_id: u64,
155 account_address: Address,
156 to: Address,
157 value: U256,
158 data: &Bytes,
159 operation: &crate::types::Operation,
160 result: &SimulationResult,
161 ) -> Self {
162 let timestamp = chrono::Utc::now().to_rfc3339();
163
164 let call = CallDebugInfo {
165 to,
166 value: value.to_string(),
167 data: format!("0x{}", alloy::primitives::hex::encode(data)),
168 operation: format!("{:?}", operation),
169 };
170
171 let logs = result
172 .logs
173 .iter()
174 .map(|log| LogDebug {
175 address: log.address,
176 topics: log
177 .topics()
178 .iter()
179 .map(|t| format!("0x{}", alloy::primitives::hex::encode(t)))
180 .collect(),
181 data: format!("0x{}", alloy::primitives::hex::encode(log.data.data.as_ref())),
182 })
183 .collect();
184
185 let state_diff = StateDiffDebug {
186 pre: result
187 .state_diff
188 .pre
189 .iter()
190 .map(|(addr, state)| (*addr, AccountStateDebug::from(state)))
191 .collect(),
192 post: result
193 .state_diff
194 .post
195 .iter()
196 .map(|(addr, state)| (*addr, AccountStateDebug::from(state)))
197 .collect(),
198 };
199
200 let result_debug = SimulationResultDebug {
201 success: result.success,
202 gas_used: result.gas_used,
203 revert_reason: result.revert_reason.clone(),
204 return_data: format!("0x{}", alloy::primitives::hex::encode(&result.return_data)),
205 logs,
206 state_diff,
207 traces: result.format_traces(),
208 };
209
210 Self {
211 timestamp,
212 chain_id,
213 account_address,
214 call,
215 result: result_debug,
216 }
217 }
218
219 pub fn write_to_dir(&self, dir: &Path) -> std::io::Result<PathBuf> {
225 std::fs::create_dir_all(dir)?;
227
228 let timestamp = SystemTime::now()
230 .duration_since(UNIX_EPOCH)
231 .unwrap_or_default()
232 .as_secs();
233 let filename = format!(
234 "{}-{}-{}.json",
235 self.chain_id,
236 self.account_address.to_string().to_lowercase(),
237 timestamp
238 );
239 let path = dir.join(filename);
240
241 let json = serde_json::to_string_pretty(self)
243 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
244 std::fs::write(&path, json)?;
245
246 Ok(path)
247 }
248}
249
250impl From<&AccountState> for AccountStateDebug {
251 fn from(state: &AccountState) -> Self {
252 Self {
253 balance: state.balance.map(|b| b.to_string()),
254 nonce: state.nonce,
255 storage: state
256 .storage
257 .iter()
258 .map(|(k, v)| {
259 (
260 format!("0x{}", alloy::primitives::hex::encode(k)),
261 format!("0x{}", alloy::primitives::hex::encode(v)),
262 )
263 })
264 .collect(),
265 }
266 }
267}
268
269fn build_state_diff(state: &EvmState) -> DiffMode {
274 let mut pre = BTreeMap::new();
275 let mut post = BTreeMap::new();
276
277 for (address, account) in state.iter() {
278 if !account.is_touched() {
280 continue;
281 }
282
283 let mut pre_storage = BTreeMap::new();
285 let mut post_storage = BTreeMap::new();
286
287 for (key, slot) in account.storage.iter() {
288 if slot.is_changed() {
289 pre_storage.insert(B256::from(*key), B256::from(slot.original_value));
290 post_storage.insert(B256::from(*key), B256::from(slot.present_value));
291 }
292 }
293
294 let pre_state = AccountState {
296 balance: Some(account.original_info.balance),
297 nonce: Some(account.original_info.nonce),
298 code: account
299 .original_info
300 .code
301 .as_ref()
302 .map(|c| Bytes::from(c.original_bytes().to_vec())),
303 storage: pre_storage,
304 };
305
306 let post_state = AccountState {
308 balance: Some(account.info.balance),
309 nonce: Some(account.info.nonce),
310 code: account
311 .info
312 .code
313 .as_ref()
314 .map(|c| Bytes::from(c.original_bytes().to_vec())),
315 storage: post_storage,
316 };
317
318 pre.insert(*address, pre_state);
319 post.insert(*address, post_state);
320 }
321
322 DiffMode { pre, post }
323}
324
325pub struct ForkSimulator<P> {
327 provider: P,
328 chain_id: u64,
329 block_number: Option<u64>,
330 tracing: bool,
331 caller_balance: Option<U256>,
332 debug_output_dir: Option<PathBuf>,
333 debug_account_address: Option<Address>,
335}
336
337impl<P> ForkSimulator<P>
338where
339 P: Provider<AnyNetwork> + Clone + 'static,
340{
341 pub fn new(provider: P, chain_id: u64) -> Self {
343 Self {
344 provider,
345 chain_id,
346 block_number: None,
347 tracing: false,
348 caller_balance: None,
349 debug_output_dir: None,
350 debug_account_address: None,
351 }
352 }
353
354 pub fn with_debug_output_dir(mut self, dir: impl Into<PathBuf>, account_address: Address) -> Self {
362 self.debug_output_dir = Some(dir.into());
363 self.debug_account_address = Some(account_address);
364 self
365 }
366
367 pub fn at_block(mut self, block: u64) -> Self {
369 self.block_number = Some(block);
370 self
371 }
372
373 pub fn with_tracing(mut self, enable: bool) -> Self {
379 self.tracing = enable;
380 self
381 }
382
383 pub fn with_caller_balance(mut self, balance: U256) -> Self {
387 self.caller_balance = Some(balance);
388 self
389 }
390
391 pub async fn create_fork_db(&self) -> Result<CacheDB<SharedBackend>> {
393 let block = match self.block_number {
394 Some(b) => b,
395 None => self
396 .provider
397 .get_block_number()
398 .await
399 .map_err(|e| Error::ForkDb(e.to_string()))?,
400 };
401
402 let meta = BlockchainDbMeta::new(
403 Default::default(), format!("fork-{}", self.chain_id),
405 );
406
407 let db = BlockchainDb::new(meta, None);
408 let backend = SharedBackend::spawn_backend_thread(
409 Arc::new(self.provider.clone()),
410 db,
411 Some(block.into()),
412 );
413
414 Ok(CacheDB::new(backend))
415 }
416
417 pub async fn simulate_call(
419 &self,
420 from: Address,
421 to: Address,
422 value: U256,
423 data: Bytes,
424 operation: Operation,
425 ) -> Result<SimulationResult> {
426 let mut db = self.create_fork_db().await?;
427
428 if let Some(balance) = self.caller_balance {
431 let existing_account = db
432 .load_account(from)
433 .map_err(|e| Error::ForkDb(format!("Failed to load caller account: {:?}", e)))?;
434 existing_account.info.balance = balance;
435 }
436
437 let caller_nonce = db
439 .basic(from)
440 .map_err(|e| Error::ForkDb(format!("Failed to fetch caller info: {:?}", e)))?
441 .map(|info| info.nonce)
442 .unwrap_or(0);
443
444 let (call_to, call_data) = match operation {
446 Operation::Call => (to, data.to_vec()),
447 Operation::DelegateCall => {
448 (to, data.to_vec())
451 }
452 };
453
454 let tx = TxEnv {
455 caller: from,
456 gas_limit: 30_000_000,
457 gas_price: 0,
458 kind: TxKind::Call(call_to),
459 value,
460 data: call_data.into(),
461 nonce: caller_nonce,
462 chain_id: Some(self.chain_id),
463 ..Default::default()
464 };
465
466 let ctx = Context::mainnet()
468 .with_db(db)
469 .modify_cfg_chained(|cfg| {
470 cfg.spec = SpecId::CANCUN;
471 cfg.chain_id = self.chain_id;
472 cfg.disable_eip3607 = true;
474 })
475 .modify_block_chained(|block| {
476 block.basefee = 0;
477 })
478 .with_tx(tx.clone());
479
480 let sim_result = if self.tracing {
481 let config = TracingInspectorConfig::default_parity();
483 let mut inspector = TracingInspector::new(config);
484
485 let mut evm = ctx.build_mainnet_with_inspector(&mut inspector);
487 let result = evm.transact(tx).map_err(|e| Error::Revm(format!("{:?}", e)))?;
488
489 let traces = Some(inspector.into_traces());
491
492 let mut sim_result = self.process_result(result);
493 sim_result.traces = traces;
494 sim_result
495 } else {
496 let mut evm = ctx.build_mainnet();
498 let result = evm.transact(tx).map_err(|e| Error::Revm(format!("{:?}", e)))?;
499
500 self.process_result(result)
501 };
502
503 if !sim_result.success {
505 if let (Some(dir), Some(account_address)) =
506 (&self.debug_output_dir, self.debug_account_address)
507 {
508 let debug_output = SimulationDebugOutput::new(
509 self.chain_id,
510 account_address,
511 to,
512 value,
513 &data,
514 &operation,
515 &sim_result,
516 );
517 let _ = debug_output.write_to_dir(dir);
519 }
520 }
521
522 Ok(sim_result)
523 }
524
525 pub async fn estimate_safe_tx_gas(
529 &self,
530 from: Address,
531 to: Address,
532 value: U256,
533 data: Bytes,
534 operation: Operation,
535 ) -> Result<U256> {
536 let result = self.simulate_call(from, to, value, data, operation).await?;
537
538 if !result.success {
539 return Err(Error::GasEstimation(format!(
540 "Simulation failed: {}",
541 result.revert_reason.unwrap_or_else(|| "unknown".to_string())
542 )));
543 }
544
545 let gas_with_buffer = result.gas_used + (result.gas_used / 10);
547 Ok(U256::from(gas_with_buffer))
548 }
549
550 fn process_result<H>(
551 &self,
552 result: revm::context::result::ExecResultAndState<revm::context::result::ExecutionResult<H>>,
553 ) -> SimulationResult
554 where
555 H: std::fmt::Debug,
556 {
557 use revm::context::result::{ExecutionResult, Output};
558
559 let state_diff = build_state_diff(&result.state);
561
562 match result.result {
563 ExecutionResult::Success {
564 gas_used,
565 output,
566 logs,
567 ..
568 } => {
569 let return_data = match output {
570 Output::Call(data) => Bytes::from(data.to_vec()),
571 Output::Create(_, _) => Bytes::new(),
572 };
573
574 let logs = logs
575 .into_iter()
576 .filter_map(|log| {
577 Log::new(log.address, log.topics().to_vec(), log.data.data.clone())
578 })
579 .collect();
580
581 SimulationResult {
582 success: true,
583 gas_used,
584 return_data,
585 logs,
586 revert_reason: None,
587 state_diff,
588 traces: None,
589 }
590 }
591 ExecutionResult::Revert { gas_used, output } => {
592 let revert_reason = Self::decode_revert_reason(&output);
593 SimulationResult {
594 success: false,
595 gas_used,
596 return_data: Bytes::from(output.to_vec()),
597 logs: vec![],
598 revert_reason: Some(revert_reason),
599 state_diff,
600 traces: None,
601 }
602 }
603 ExecutionResult::Halt { gas_used, reason } => SimulationResult {
604 success: false,
605 gas_used,
606 return_data: Bytes::new(),
607 logs: vec![],
608 revert_reason: Some(format!("Halted: {:?}", reason)),
609 state_diff,
610 traces: None,
611 },
612 }
613 }
614
615 fn decode_revert_reason(output: &revm::primitives::Bytes) -> String {
616 if output.len() < 4 {
617 return "Unknown revert".to_string();
618 }
619
620 if output[0..4] == [0x08, 0xc3, 0x79, 0xa0] && output.len() >= 68 {
622 let offset = 4 + 32;
624 if output.len() > offset + 32 {
625 let len = u32::from_be_bytes([
626 output[offset + 28],
627 output[offset + 29],
628 output[offset + 30],
629 output[offset + 31],
630 ]) as usize;
631
632 let str_start = offset + 32;
633 if output.len() >= str_start + len {
634 if let Ok(s) = String::from_utf8(output[str_start..str_start + len].to_vec()) {
635 return s;
636 }
637 }
638 }
639 }
640
641 if output[0..4] == [0x4e, 0x48, 0x7b, 0x71] && output.len() >= 36 {
643 let panic_code =
644 u32::from_be_bytes([output[32], output[33], output[34], output[35]]) as usize;
645 return match panic_code {
646 0x00 => "Panic: generic/compiler panic",
647 0x01 => "Panic: assertion failed",
648 0x11 => "Panic: arithmetic overflow/underflow",
649 0x12 => "Panic: division by zero",
650 0x21 => "Panic: invalid enum value",
651 0x22 => "Panic: access to incorrectly encoded storage",
652 0x31 => "Panic: pop on empty array",
653 0x32 => "Panic: array out of bounds",
654 0x41 => "Panic: memory overflow",
655 0x51 => "Panic: call to zero-initialized function",
656 _ => "Panic: unknown code",
657 }
658 .to_string();
659 }
660
661 format!("Revert: 0x{}", alloy::primitives::hex::encode(output))
662 }
663}
664
665#[cfg(test)]
666mod tests {
667 use super::*;
668
669 #[test]
670 fn test_simulation_result() {
671 let result = SimulationResult {
672 success: true,
673 gas_used: 21000,
674 return_data: Bytes::new(),
675 logs: vec![],
676 revert_reason: None,
677 state_diff: DiffMode::default(),
678 traces: None,
679 };
680
681 assert!(result.is_success());
682 assert!(result.error_message().is_none());
683 assert!(result.format_traces().is_none());
684 }
685
686 #[test]
687 fn test_simulation_result_revert() {
688 let result = SimulationResult {
689 success: false,
690 gas_used: 21000,
691 return_data: Bytes::new(),
692 logs: vec![],
693 revert_reason: Some("ERC20: insufficient balance".to_string()),
694 state_diff: DiffMode::default(),
695 traces: None,
696 };
697
698 assert!(!result.is_success());
699 assert_eq!(result.error_message(), Some("ERC20: insufficient balance"));
700 }
701
702 #[test]
703 fn test_state_diff_with_balance_change() {
704 let mut pre = BTreeMap::new();
705 let mut post = BTreeMap::new();
706
707 let addr = Address::ZERO;
708
709 pre.insert(
710 addr,
711 AccountState {
712 balance: Some(U256::from(1000)),
713 nonce: Some(0),
714 code: None,
715 storage: BTreeMap::new(),
716 },
717 );
718
719 post.insert(
720 addr,
721 AccountState {
722 balance: Some(U256::from(500)),
723 nonce: Some(1),
724 code: None,
725 storage: BTreeMap::new(),
726 },
727 );
728
729 let state_diff = DiffMode { pre, post };
730
731 let result = SimulationResult {
732 success: true,
733 gas_used: 21000,
734 return_data: Bytes::new(),
735 logs: vec![],
736 revert_reason: None,
737 state_diff,
738 traces: None,
739 };
740
741 assert!(result.is_success());
742 assert_eq!(result.state_diff.pre.len(), 1);
743 assert_eq!(result.state_diff.post.len(), 1);
744
745 let pre_account = result.state_diff.pre.get(&addr).unwrap();
746 let post_account = result.state_diff.post.get(&addr).unwrap();
747
748 assert_eq!(pre_account.balance, Some(U256::from(1000)));
749 assert_eq!(post_account.balance, Some(U256::from(500)));
750 assert_eq!(pre_account.nonce, Some(0));
751 assert_eq!(post_account.nonce, Some(1));
752 }
753
754 #[test]
755 fn test_state_diff_with_storage_change() {
756 let mut pre = BTreeMap::new();
757 let mut post = BTreeMap::new();
758
759 let addr = Address::ZERO;
760 let storage_key = B256::ZERO;
761
762 let pre_value = B256::from(U256::from(100));
764 let post_value = B256::from(U256::from(200));
765
766 let mut pre_storage = BTreeMap::new();
767 pre_storage.insert(storage_key, pre_value);
768
769 let mut post_storage = BTreeMap::new();
770 post_storage.insert(storage_key, post_value);
771
772 pre.insert(
773 addr,
774 AccountState {
775 balance: Some(U256::ZERO),
776 nonce: Some(0),
777 code: None,
778 storage: pre_storage,
779 },
780 );
781
782 post.insert(
783 addr,
784 AccountState {
785 balance: Some(U256::ZERO),
786 nonce: Some(0),
787 code: None,
788 storage: post_storage,
789 },
790 );
791
792 let state_diff = DiffMode { pre, post };
793
794 let result = SimulationResult {
795 success: true,
796 gas_used: 50000,
797 return_data: Bytes::new(),
798 logs: vec![],
799 revert_reason: None,
800 state_diff,
801 traces: None,
802 };
803
804 let pre_account = result.state_diff.pre.get(&addr).unwrap();
805 let post_account = result.state_diff.post.get(&addr).unwrap();
806
807 assert_eq!(pre_account.storage.get(&storage_key), Some(&pre_value));
808 assert_eq!(post_account.storage.get(&storage_key), Some(&post_value));
809 }
810}