diff --git a/Cargo.lock b/Cargo.lock index 03435f14e27ca..60d072d6e4ed1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -563,7 +563,7 @@ version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da95d038ede1a964ce99f49cbe27a7fb538d1da595e4b4f70b8c8f338d17bf16" dependencies = [ - "heck", + "heck 0.4.0", "proc-macro-error", "proc-macro2", "quote", @@ -702,6 +702,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "comfy-table" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b103d85ca6e209388771bfb7aa6b68a7aeec4afbf6f0a0264bfbf50360e5212e" +dependencies = [ + "crossterm", + "strum", + "strum_macros", + "unicode-width", +] + [[package]] name = "command-group" version = "1.0.8" @@ -831,6 +843,31 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "crossterm" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77b75a27dc8d220f1f8521ea69cd55a34d720a200ebb3a624d9aa19193d3b432" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio 0.7.14", + "parking_lot 0.12.0", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" +dependencies = [ + "winapi", +] + [[package]] name = "crunchy" version = "0.2.2" @@ -1488,7 +1525,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" name = "forge" version = "0.1.0" dependencies = [ + "ansi_term", "bytes", + "comfy-table", "ethers", "eyre", "foundry-utils", @@ -1880,6 +1919,15 @@ dependencies = [ "ahash", ] +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "heck" version = "0.4.0" @@ -3471,7 +3519,7 @@ dependencies = [ [[package]] name = "revm" version = "1.2.0" -source = "git+https://github.com/onbjerg/revm?branch=onbjerg/blockhashes#fd8f390daca7e60b97798423a053a4509b54a569" +source = "git+https://github.com/onbjerg/revm?branch=onbjerg/tracer-ends#7a243f84a4ea9f5a969e9546bbf3d6b6096c845e" dependencies = [ "arrayref", "auto_impl", @@ -3487,7 +3535,7 @@ dependencies = [ [[package]] name = "revm_precompiles" version = "0.4.0" -source = "git+https://github.com/onbjerg/revm?branch=onbjerg/blockhashes#fd8f390daca7e60b97798423a053a4509b54a569" +source = "git+https://github.com/onbjerg/revm?branch=onbjerg/tracer-ends#7a243f84a4ea9f5a969e9546bbf3d6b6096c845e" dependencies = [ "bytes", "k256", @@ -3901,6 +3949,27 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "signal-hook" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "647c97df271007dcea485bb74ffdb57f2e683f1306c854f468a0c244badabf2d" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29fd5867f1c4f2c5be079aee7a2adf1152ebb04a4bc4d341f504b7dece607ed4" +dependencies = [ + "libc", + "mio 0.7.14", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -4010,6 +4079,25 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strum" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cae14b91c7d11c9a851d3fbc80a963198998c2a64eec840477fa92d8ce9b70bb" + +[[package]] +name = "strum_macros" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bb0dc7ee9c15cea6199cde9a127fa16a4c5819af85395457ad72d68edc85a38" +dependencies = [ + "heck 0.3.3", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "substrate-bn" version = "0.6.0" @@ -4519,6 +4607,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" + [[package]] name = "unicode-width" version = "0.1.9" diff --git a/cli/src/cmd/test.rs b/cli/src/cmd/test.rs index 62d6f7e482e5b..f9d6a5d7db89c 100644 --- a/cli/src/cmd/test.rs +++ b/cli/src/cmd/test.rs @@ -8,8 +8,11 @@ use ansi_term::Colour; use clap::{AppSettings, Parser}; use ethers::solc::{ArtifactOutput, ProjectCompileOutput}; use forge::{ - decode::decode_console_logs, executor::opts::EvmOpts, MultiContractRunnerBuilder, TestFilter, - TestResult, + decode::decode_console_logs, + executor::opts::EvmOpts, + gas_report::GasReport, + trace::{identifier::LocalTraceIdentifier, CallTraceDecoder, TraceKind}, + MultiContractRunnerBuilder, TestFilter, TestResult, }; use foundry_config::{figment::Figment, Config}; use regex::Regex; @@ -219,7 +222,7 @@ pub struct Test { impl Test { pub fn gas_used(&self) -> u64 { - self.result.gas_used + self.result.kind.gas_used().gas() } } @@ -267,7 +270,6 @@ impl TestOutcome { if !self.allow_failure { let failures = self.failures().count(); if failures > 0 { - println!(); println!("Failed tests:"); for (name, result) in self.failures() { short_test_result(name, result); @@ -318,133 +320,99 @@ fn test( filter: Filter, json: bool, allow_failure: bool, - gas_reports: (bool, Vec), + (gas_reporting, gas_reports): (bool, Vec), ) -> eyre::Result { let verbosity = evm_opts.verbosity; - let gas_reporting = gas_reports.0; if gas_reporting && evm_opts.verbosity < 3 { - // force evm to do tracing, but don't hit the verbosity print path + // Enable tracing without hitting the verbosity print path evm_opts.verbosity = 3; } let mut runner = builder.build(output, evm_opts)?; if json { let results = runner.test(&filter, None)?; - let res = serde_json::to_string(&results)?; // TODO: Make this work normally - println!("{}", res); + println!("{}", serde_json::to_string(&results)?); Ok(TestOutcome::new(results, allow_failure)) } else { - // TODO: Re-enable when ported - //let mut gas_report = GasReport::new(gas_reports.1); + let local_identifier = LocalTraceIdentifier::new(&runner.known_contracts); let (tx, rx) = channel::<(String, BTreeMap)>(); - //let known_contracts = runner.known_contracts.clone(); - // TODO: Re-enable when ported - //let execution_info = runner.execution_info.clone(); - let handle = thread::spawn(move || { - while let Ok((contract_name, tests)) = rx.recv() { - println!(); - if !tests.is_empty() { - let term = if tests.len() > 1 { "tests" } else { "test" }; - println!("Running {} {} for {}", tests.len(), term, contract_name); + thread::spawn(move || runner.test(&filter, Some(tx)).unwrap()); + + let mut results: BTreeMap> = BTreeMap::new(); + let mut gas_report = GasReport::new(gas_reports); + for (contract_name, mut tests) in rx { + println!(); + if !tests.is_empty() { + let term = if tests.len() > 1 { "tests" } else { "test" }; + println!("Running {} {} for {}", tests.len(), term, contract_name); + } + for (name, result) in &mut tests { + short_test_result(name, result); + + // We only display logs at level 2 and above + if verbosity < 2 { + continue } - for (name, result) in tests { - short_test_result(&name, &result); - // adds a linebreak only if there were any traces or logs, so that the - // output does not look like 1 big block. - let add_newline = false; - if verbosity > 1 && !result.logs.is_empty() { - // We only decode logs from Hardhat and DS-style console events - let console_logs = decode_console_logs(&result.logs); - if !console_logs.is_empty() { - println!("Logs:"); - for log in console_logs { - println!(" {}", log); - } - } - } - // TODO: Re-enable this when traces are ported - /*if verbosity > 2 { - if let (Some(traces), Some(identified_contracts)) = - (&result.traces, &result.identified_contracts) - { - if !result.success && verbosity == 3 || verbosity > 3 { - // add a new line if any logs were printed & to separate them from - // the traces to be printed - if !result.logs.is_empty() { - println!(); - } - let mut ident = identified_contracts.clone(); - let (funcs, events, errors) = &execution_info; - let mut exec_info = ExecutionInfo::new( - // &runner.known_contracts, - &known_contracts, - &mut ident, - &result.labeled_addresses, - funcs, - events, - errors, - ); - let vm = vm(); - let mut trace_string = "".to_string(); - if verbosity > 4 || !result.success { - add_newline = true; - println!("Traces:"); - // print setup calls as well - traces.iter().for_each(|trace| { - trace.construct_trace_string( - 0, - &mut exec_info, - &vm, - " ", - &mut trace_string, - ); - }); - } else if !traces.is_empty() { - add_newline = true; - println!("Traces:"); - traces - .last() - .expect("no last but not empty") - .construct_trace_string( - 0, - &mut exec_info, - &vm, - " ", - &mut trace_string, - ); - } - if !trace_string.is_empty() { - println!("{}", trace_string); - } - } - } - }*/ - if add_newline { - println!(); + + // We only decode logs from Hardhat and DS-style console events + let console_logs = decode_console_logs(&result.logs); + if !console_logs.is_empty() { + println!("Logs:"); + for log in console_logs { + println!(" {}", log); } + println!(); } - } - }); - let results = runner.test(&filter, Some(tx))?; + if !result.traces.is_empty() { + // We only display traces at verbosity level 3 and above + if verbosity < 3 { + continue + } - handle.join().unwrap(); + // At verbosity level 3, we only display traces for failed tests + if verbosity == 3 && result.success { + continue + } + + // Identify addresses in each trace + let mut decoder = + CallTraceDecoder::new_with_labels(result.labeled_addresses.clone()); + + println!("Traces:"); + for (kind, trace) in &mut result.traces { + decoder.identify(trace, &local_identifier); + + let should_include = match kind { + // At verbosity level 4, we also display the setup trace for failed + // tests At verbosity level 5, we display + // all traces for all tests + TraceKind::Setup => { + (verbosity >= 5) || (verbosity == 4 && !result.success) + } + TraceKind::Execution => verbosity > 3 || !result.success, + _ => false, + }; + + if should_include { + decoder.decode(trace); + println!("{}", trace); + } + } - // TODO: Re-enable when ported - /*if gas_reporting { - for tests in results.values() { - for result in tests.values() { - if let (Some(traces), Some(identified_contracts)) = - (&result.traces, &result.identified_contracts) - { - gas_report.analyze(traces, identified_contracts); + if gas_reporting { + gas_report.analyze(&result.traces); } } } - gas_report.finalize(); - println!("{}", gas_report); - }*/ + results.insert(contract_name, tests); + } + + if gas_reporting { + println!("{}", gas_report.finalize()); + } + Ok(TestOutcome::new(results, allow_failure)) } } diff --git a/forge/Cargo.toml b/forge/Cargo.toml index 46673f95b1b4c..dd4d84ad906ca 100644 --- a/forge/Cargo.toml +++ b/forge/Cargo.toml @@ -25,11 +25,13 @@ rlp = "0.5.1" bytes = "1.1.0" thiserror = "1.0.29" -revm = { package = "revm", git = "https://github.com/onbjerg/revm", branch = "onbjerg/blockhashes", default-features = false, features = ["std", "k256"] } +revm = { package = "revm", git = "https://github.com/onbjerg/revm", branch = "onbjerg/tracer-ends", default-features = false, features = ["std", "k256"] } hashbrown = "0.12" once_cell = "1.9.0" parking_lot = "0.12.0" futures = "0.3.21" +ansi_term = "0.12.1" +comfy-table = "5.0.0" [dev-dependencies] ethers = { git = "https://github.com/gakonst/ethers-rs", default-features = false, features = ["solc-full", "solc-tests"] } diff --git a/forge/src/executor/abi.rs b/forge/src/executor/abi.rs index e02b65d8feea6..dc5c7d284b68e 100644 --- a/forge/src/executor/abi.rs +++ b/forge/src/executor/abi.rs @@ -72,7 +72,7 @@ ethers::contract::abigen!( event log_named_string (string key, string val) ]"# ); -pub use console_mod::CONSOLE_ABI; +pub use console_mod::{ConsoleEvents, CONSOLE_ABI}; // Bindings for Hardhat console ethers::contract::abigen!(HardhatConsole, "./abi/console.json",); diff --git a/forge/src/executor/builder.rs b/forge/src/executor/builder.rs index b3effd1c1a5c4..bda7961ae845c 100644 --- a/forge/src/executor/builder.rs +++ b/forge/src/executor/builder.rs @@ -98,6 +98,14 @@ impl ExecutorBuilder { self } + /// Enables tracing + #[must_use] + pub fn with_tracing(mut self) -> Self { + self.inspector_config.tracing = true; + self + } + + /// Sets the EVM spec to use #[must_use] pub fn with_spec(mut self, spec: SpecId) -> Self { self.env.cfg.spec_id = spec; @@ -124,6 +132,5 @@ impl ExecutorBuilder { Executor::new(db, self.env, self.inspector_config) } - // TODO: add with_traces // TODO: add with_debug(ger?) } diff --git a/forge/src/executor/fuzz/mod.rs b/forge/src/executor/fuzz/mod.rs index c3ca85a9c8d36..f185aa97a4a85 100644 --- a/forge/src/executor/fuzz/mod.rs +++ b/forge/src/executor/fuzz/mod.rs @@ -1,17 +1,20 @@ mod strategies; -use crate::executor::{Executor, RawCallResult}; +pub use proptest::test_runner::{Config as FuzzConfig, Reason}; + +use crate::{ + executor::{Executor, RawCallResult}, + trace::CallTraceArena, +}; use ethers::{ - abi::{Abi, Function}, + abi::{Abi, Function, RawLog, Token}, types::{Address, Bytes}, }; -use revm::{db::DatabaseRef, Return}; -use strategies::fuzz_calldata; - -pub use proptest::test_runner::{Config as FuzzConfig, Reason}; use proptest::test_runner::{TestCaseError, TestError, TestRunner}; +use revm::db::DatabaseRef; use serde::{Deserialize, Serialize}; -use std::cell::RefCell; +use std::{cell::RefCell, collections::BTreeMap, fmt}; +use strategies::fuzz_calldata; /// Magic return code for the `assume` cheatcode pub const ASSUME_MAGIC_RETURN_CODE: &[u8] = "FOUNDRY::ASSUME".as_bytes(); @@ -49,104 +52,122 @@ where func: &Function, address: Address, should_fail: bool, - abi: Option<&Abi>, + errors: Option<&Abi>, ) -> FuzzTestResult { let strat = fuzz_calldata(func); // Stores the consumed gas and calldata of every successful fuzz call - let fuzz_cases: RefCell> = RefCell::new(Default::default()); + let cases: RefCell> = RefCell::new(Default::default()); - // Stores the latest return and revert reason of a test call - let return_reason: RefCell> = RefCell::new(None); - let revert_reason = RefCell::new(None); + // Stores the result of the last call + let call: RefCell = RefCell::new(Default::default()); - let mut runner = self.runner.clone(); tracing::debug!(func = ?func.name, should_fail, "fuzzing"); - let test_error = runner - .run(&strat, |calldata| { - let RawCallResult { status, result, gas, state_changeset, .. } = self - .executor - .call_raw(self.sender, address, calldata.0.clone(), 0.into()) - .expect("could not make raw evm call"); - - // When assume cheat code is triggered return a special string "FOUNDRY::ASSUME" - if result.as_ref() == ASSUME_MAGIC_RETURN_CODE { - *return_reason.borrow_mut() = Some(status); - let err = "ASSUME: Too many rejects"; - *revert_reason.borrow_mut() = Some(err.to_string()); - return Err(TestCaseError::Reject(err.into())) - } - - let success = self.executor.is_success( - address, - status, - state_changeset.expect("we should have a state changeset"), - should_fail, - ); - - // Store the result of this test case - let _ = return_reason.borrow_mut().insert(status); - if !success { - let revert = - foundry_utils::decode_revert(result.as_ref(), abi).unwrap_or_default(); - let _ = revert_reason.borrow_mut().insert(revert); - } - - // This will panic and get caught by the executor - proptest::prop_assert!( - success, - "{}, expected failure: {}, reason: '{}'", - func.name, - should_fail, - match foundry_utils::decode_revert(result.as_ref(), abi) { + let run_result = self.runner.clone().run(&strat, |calldata| { + *call.borrow_mut() = self + .executor + .call_raw(self.sender, address, calldata.0.clone(), 0.into()) + .expect("could not make raw evm call"); + let call = call.borrow(); + + // When assume cheat code is triggered return a special string "FOUNDRY::ASSUME" + if call.result.as_ref() == ASSUME_MAGIC_RETURN_CODE { + return Err(TestCaseError::reject("ASSUME: Too many rejects")) + } + + let success = self.executor.is_success( + address, + call.status, + call.state_changeset.clone().expect("we should have a state changeset"), + should_fail, + ); + + if success { + cases.borrow_mut().push(FuzzCase { calldata, gas: call.gas }); + Ok(()) + } else { + Err(TestCaseError::fail( + match foundry_utils::decode_revert(call.result.as_ref(), errors) { Ok(e) => e, - Err(e) => e.to_string(), - } - ); + Err(_) => "".to_string(), + }, + )) + } + }); + + let call = call.into_inner(); + let mut result = FuzzTestResult { + cases: FuzzedCases::new(cases.into_inner()), + success: run_result.is_ok(), + reason: None, + counterexample: None, + logs: call.logs, + traces: call.traces, + labeled_addresses: call.labels, + }; + + match run_result { + Err(TestError::Abort(reason)) => { + result.reason = Some(reason.to_string()); + } + Err(TestError::Fail(reason, calldata)) => { + let reason = reason.to_string(); + result.reason = if reason.is_empty() { None } else { Some(reason) }; + + let args = func + .decode_input(&calldata.as_ref()[4..]) + .expect("could not decode fuzzer inputs"); + result.counterexample = Some(CounterExample { calldata, args }); + } + _ => (), + } - // Push test case to the case set - fuzz_cases.borrow_mut().push(FuzzCase { calldata, gas }); - Ok(()) - }) - .err() - .map(|test_error| FuzzError { - test_error, - return_reason: return_reason.into_inner().expect("Reason must be set"), - revert_reason: revert_reason.into_inner().expect("Revert error string must be set"), - }); - - FuzzTestResult { cases: FuzzedCases::new(fuzz_cases.into_inner()), test_error } + result + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CounterExample { + pub calldata: Bytes, + + #[serde(skip)] + pub args: Vec, +} + +impl fmt::Display for CounterExample { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let args = foundry_utils::format_tokens(&self.args).collect::>().join(", "); + write!(f, "calldata=0x{}, args=[{}]", hex::encode(&self.calldata), args) } } /// The outcome of a fuzz test +#[derive(Debug)] pub struct FuzzTestResult { /// Every successful fuzz test case pub cases: FuzzedCases, - /// if there was a case that resulted in an error, this contains the error and the return - /// reason of the failed call - pub test_error: Option, -} -impl FuzzTestResult { - /// Returns `true` if all test cases succeeded - pub fn is_ok(&self) -> bool { - self.test_error.is_none() - } + /// Whether the test case was successful. This means that the transaction executed + /// properly, or that there was a revert and that the test was expected to fail + /// (prefixed with `testFail`) + pub success: bool, - /// Returns `true` if a test case failed - pub fn is_err(&self) -> bool { - self.test_error.is_some() - } -} + /// If there was a revert, this field will be populated. Note that the test can + /// still be successful (i.e self.success == true) when it's expected to fail. + pub reason: Option, + + /// Minimal reproduction test case for failing fuzz tests + pub counterexample: Option, -pub struct FuzzError { - /// The proptest error occurred as a result of a test case - pub test_error: TestError, - /// The return reason of the offending call - pub return_reason: Return, - /// The revert string of the offending call - pub revert_reason: String, + /// Any captured & parsed as strings logs along the test's execution which should + /// be printed to the user. + pub logs: Vec, + + /// Traces + pub traces: Option, + + /// Labeled addresses + pub labeled_addresses: BTreeMap, } /// Container type for all successful test cases @@ -217,24 +238,27 @@ pub struct FuzzCase { #[cfg(test)] mod tests { - use crate::CALLER; - - use crate::test_helpers::{fuzz_executor, test_executor, COMPILED}; + use super::FuzzTestResult; + use crate::{ + executor::DeployResult, + test_helpers::{fuzz_executor, test_executor, COMPILED}, + CALLER, + }; #[test] fn prints_fuzzed_revert_reasons() { let mut executor = test_executor(); let compiled = COMPILED.find("FuzzTests").expect("could not find contract"); - let (addr, _, _, _) = + let DeployResult { address, .. } = executor.deploy(*CALLER, compiled.bytecode().unwrap().0.clone(), 0.into()).unwrap(); let executor = fuzz_executor(&executor); let func = compiled.abi.unwrap().function("testFuzzedRevert").unwrap(); - let res = executor.fuzz(func, addr, false, compiled.abi); - let error = res.test_error.unwrap(); - let revert_reason = error.revert_reason; - assert_eq!(revert_reason, "fuzztest-revert"); + let FuzzTestResult { reason, success, .. } = + executor.fuzz(func, address, false, compiled.abi); + assert!(!success, "test did not revert"); + assert_eq!(reason, Some("fuzztest-revert".to_string())); } } diff --git a/forge/src/executor/inspector/cheatcodes/expect.rs b/forge/src/executor/inspector/cheatcodes/expect.rs index 23227274c214a..e5ab0f1898ce4 100644 --- a/forge/src/executor/inspector/cheatcodes/expect.rs +++ b/forge/src/executor/inspector/cheatcodes/expect.rs @@ -3,10 +3,10 @@ use crate::abi::HEVMCalls; use bytes::Bytes; use ethers::{ abi::{AbiEncode, RawLog}, - types::{Address, H256}, + types::Address, }; use once_cell::sync::Lazy; -use revm::{return_ok, Database, EVMData, Interpreter, Return}; +use revm::{return_ok, Database, EVMData, Return}; use std::str::FromStr; /// For some cheatcodes we may internally change the status of the call, i.e. in `expectRevert`. @@ -116,39 +116,23 @@ pub struct ExpectedEmit { pub found: bool, } -pub fn handle_expect_emit(state: &mut Cheatcodes, interpreter: &Interpreter, n: u8) { - // Decode the log - let (offset, len) = - (try_or_return!(interpreter.stack().peek(0)), try_or_return!(interpreter.stack().peek(1))); - let data = if len.is_zero() { - Vec::new() - } else { - interpreter.memory.get_slice(as_usize_or_return!(offset), as_usize_or_return!(len)).to_vec() - }; - - let n = n as usize; - let mut topics = Vec::with_capacity(n); - for i in 0..n { - let mut topic = H256::zero(); - try_or_return!(interpreter.stack.peek(2 + i)).to_big_endian(topic.as_bytes_mut()); - topics.push(topic); - } - +pub fn handle_expect_emit(state: &mut Cheatcodes, log: RawLog) { // Fill or check the expected emits if let Some(next_expect_to_fill) = state.expected_emits.iter_mut().find(|expect| expect.log.is_none()) { // We have unfilled expects, so we fill the first one - next_expect_to_fill.log = Some(RawLog { topics, data }); + next_expect_to_fill.log = Some(log); } else if let Some(next_expect) = state.expected_emits.iter_mut().find(|expect| !expect.found) { // We do not have unfilled expects, so we try to match this log with the first unfound // log that we expect let expected = next_expect.log.as_ref().expect("we should have a log to compare against here"); - if expected.topics[0] == topics[0] { + if expected.topics[0] == log.topics[0] { // Topic 0 matches so the amount of topics in the expected and actual log should // match here - let topics_match = topics + let topics_match = log + .topics .iter() .skip(1) .enumerate() @@ -157,7 +141,7 @@ pub fn handle_expect_emit(state: &mut Cheatcodes, interpreter: &Interpreter, n: // Maybe check data next_expect.found = if next_expect.checks[3] { - expected.data == data && topics_match + expected.data == log.data && topics_match } else { topics_match }; @@ -179,7 +163,7 @@ pub fn apply( } HEVMCalls::ExpectEmit(inner) => { state.expected_emits.push(ExpectedEmit { - depth: data.subroutine.depth() + 1, + depth: data.subroutine.depth(), checks: [inner.0, inner.1, inner.2, inner.3], ..Default::default() }); diff --git a/forge/src/executor/inspector/cheatcodes/mod.rs b/forge/src/executor/inspector/cheatcodes/mod.rs index 608aa95b6cba2..efca2167beb10 100644 --- a/forge/src/executor/inspector/cheatcodes/mod.rs +++ b/forge/src/executor/inspector/cheatcodes/mod.rs @@ -23,6 +23,8 @@ use revm::{ }; use std::collections::BTreeMap; +use super::logs::extract_log; + /// An inspector that handles calls to various cheatcodes, each with their own behavior. /// /// Cheatcodes can be called by contracts during execution to modify the VM environment, such as @@ -177,13 +179,8 @@ where // Match logs if `expectEmit` has been called if !self.expected_emits.is_empty() { - match interpreter.contract.code[interpreter.program_counter()] { - opcode::LOG0 => handle_expect_emit(self, interpreter, 0), - opcode::LOG1 => handle_expect_emit(self, interpreter, 1), - opcode::LOG2 => handle_expect_emit(self, interpreter, 2), - opcode::LOG3 => handle_expect_emit(self, interpreter, 3), - opcode::LOG4 => handle_expect_emit(self, interpreter, 4), - _ => (), + if let Some(log) = extract_log(interpreter) { + handle_expect_emit(self, log) } } diff --git a/forge/src/executor/inspector/logs.rs b/forge/src/executor/inspector/logs.rs index 2e4827d268d20..a00e7f6193f0d 100644 --- a/forge/src/executor/inspector/logs.rs +++ b/forge/src/executor/inspector/logs.rs @@ -21,31 +21,6 @@ impl LogCollector { Default::default() } - fn log(&mut self, interpreter: &Interpreter, n: u8) { - let (offset, len) = ( - try_or_return!(interpreter.stack().peek(0)), - try_or_return!(interpreter.stack().peek(1)), - ); - let data = if len.is_zero() { - Vec::new() - } else { - interpreter - .memory - .get_slice(as_usize_or_return!(offset), as_usize_or_return!(len)) - .to_vec() - }; - - let n = n as usize; - let mut topics = Vec::with_capacity(n); - for i in 0..n { - let mut topic = H256::zero(); - try_or_return!(interpreter.stack.peek(2 + i)).to_big_endian(topic.as_bytes_mut()); - topics.push(topic); - } - - self.logs.push(RawLog { topics, data }); - } - fn hardhat_log(&mut self, input: Vec) -> (Return, Bytes) { // Patch the Hardhat-style selectors let input = patch_hardhat_console_selector(input.to_vec()); @@ -76,13 +51,8 @@ where _: &mut EVMData<'_, DB>, _is_static: bool, ) -> Return { - match interpreter.contract.code[interpreter.program_counter()] { - opcode::LOG0 => self.log(interpreter, 0), - opcode::LOG1 => self.log(interpreter, 1), - opcode::LOG2 => self.log(interpreter, 2), - opcode::LOG3 => self.log(interpreter, 3), - opcode::LOG4 => self.log(interpreter, 4), - _ => (), + if let Some(log) = extract_log(interpreter) { + self.logs.push(log); } Return::Continue @@ -115,3 +85,46 @@ fn convert_hh_log_to_event(call: HardhatConsoleCalls) -> RawLog { data: ethers::abi::encode(&[Token::String(call.to_string())]), } } + +/// Extracts a log from the interpreter if there is any. +pub fn extract_log(interpreter: &Interpreter) -> Option { + let num_topics = match interpreter.contract.code[interpreter.program_counter()] { + opcode::LOG0 => 0, + opcode::LOG1 => 1, + opcode::LOG2 => 2, + opcode::LOG3 => 3, + opcode::LOG4 => 4, + _ => return None, + }; + + let (offset, len) = ( + as_usize_or_return!(interpreter.stack().peek(0).ok()?, None), + as_usize_or_return!(interpreter.stack().peek(1).ok()?, None), + ); + let data = if len == 0 { + Vec::new() + } else { + // If we're trying to access more memory than exists, we will pretend like that memory is + // zeroed. We could resize the memory here, but it would mess up the gas accounting REVM + // does for memory resizes. + if offset > interpreter.memory.len() { + vec![0; len] + } else if offset + len > interpreter.memory.len() { + let mut data = + Vec::from(interpreter.memory.get_slice(offset, interpreter.memory.len())); + data.resize(offset + len, 0); + data + } else { + interpreter.memory.get_slice(offset, len).to_vec() + } + }; + + let mut topics = Vec::with_capacity(num_topics); + for i in 0..num_topics { + let mut topic = H256::zero(); + interpreter.stack.peek(2 + i).ok()?.to_big_endian(topic.as_bytes_mut()); + topics.push(topic); + } + + Some(RawLog { topics, data }) +} diff --git a/forge/src/executor/inspector/macros.rs b/forge/src/executor/inspector/macros.rs index 03994cacd74cb..7b0e9e08f30db 100644 --- a/forge/src/executor/inspector/macros.rs +++ b/forge/src/executor/inspector/macros.rs @@ -1,16 +1,3 @@ -/// Returns from the function on an error, discarding the error. -/// -/// Useful for inspectors that read state that might be invalid, but do not want to emit -/// appropriate errors themselves. -macro_rules! try_or_return { - ($e:expr) => { - match $e { - Ok(v) => v, - Err(_) => return, - } - }; -} - /// Returns [Return::Continue] on an error, discarding the error. /// /// Useful for inspectors that read state that might be invalid, but do not want to emit @@ -36,4 +23,11 @@ macro_rules! as_usize_or_return { $v.0[0] as usize } }; + ($v:expr, $r:expr) => { + if $v.0[1] != 0 || $v.0[2] != 0 || $v.0[3] != 0 { + return $r + } else { + $v.0[0] as usize + } + }; } diff --git a/forge/src/executor/inspector/mod.rs b/forge/src/executor/inspector/mod.rs index 61511b07a0df6..5e6666c5efd7e 100644 --- a/forge/src/executor/inspector/mod.rs +++ b/forge/src/executor/inspector/mod.rs @@ -4,6 +4,9 @@ mod macros; mod logs; pub use logs::LogCollector; +mod tracer; +pub use tracer::Tracer; + mod stack; pub use stack::InspectorStack; @@ -16,6 +19,8 @@ pub struct InspectorStackConfig { pub cheatcodes: bool, /// Whether or not the FFI cheatcode is enabled pub ffi: bool, + /// Whether or not tracing is enabled + pub tracing: bool, } impl InspectorStackConfig { @@ -26,6 +31,9 @@ impl InspectorStackConfig { if self.cheatcodes { stack.cheatcodes = Some(Cheatcodes::new(self.ffi)); } + if self.tracing { + stack.tracer = Some(Tracer::new()); + } stack } } diff --git a/forge/src/executor/inspector/stack.rs b/forge/src/executor/inspector/stack.rs index 0940627e26630..514540ed38633 100644 --- a/forge/src/executor/inspector/stack.rs +++ b/forge/src/executor/inspector/stack.rs @@ -1,4 +1,4 @@ -use super::{Cheatcodes, LogCollector}; +use super::{Cheatcodes, LogCollector, Tracer}; use bytes::Bytes; use ethers::types::Address; use revm::{db::Database, CallInputs, CreateInputs, EVMData, Gas, Inspector, Interpreter, Return}; @@ -23,6 +23,7 @@ macro_rules! call_inspectors { /// remaining inspectors are not called. #[derive(Default)] pub struct InspectorStack { + pub tracer: Option, pub logs: Option, pub cheatcodes: Option, } @@ -43,7 +44,7 @@ where data: &mut EVMData<'_, DB>, is_static: bool, ) -> Return { - call_inspectors!(inspector, [&mut self.logs, &mut self.cheatcodes], { + call_inspectors!(inspector, [&mut self.tracer, &mut self.logs, &mut self.cheatcodes], { let status = inspector.initialize_interp(interpreter, data, is_static); // Allow inspectors to exit early @@ -61,7 +62,7 @@ where data: &mut EVMData<'_, DB>, is_static: bool, ) -> Return { - call_inspectors!(inspector, [&mut self.logs, &mut self.cheatcodes], { + call_inspectors!(inspector, [&mut self.tracer, &mut self.logs, &mut self.cheatcodes], { let status = inspector.step(interpreter, data, is_static); // Allow inspectors to exit early @@ -80,7 +81,7 @@ where is_static: bool, status: Return, ) -> Return { - call_inspectors!(inspector, [&mut self.logs, &mut self.cheatcodes], { + call_inspectors!(inspector, [&mut self.tracer, &mut self.logs, &mut self.cheatcodes], { let status = inspector.step_end(interpreter, data, is_static, status); // Allow inspectors to exit early @@ -98,7 +99,7 @@ where call: &CallInputs, is_static: bool, ) -> (Return, Gas, Bytes) { - call_inspectors!(inspector, [&mut self.logs, &mut self.cheatcodes], { + call_inspectors!(inspector, [&mut self.tracer, &mut self.logs, &mut self.cheatcodes], { let (status, gas, retdata) = inspector.call(data, call, is_static); // Allow inspectors to exit early @@ -119,7 +120,7 @@ where retdata: Bytes, is_static: bool, ) -> (Return, Gas, Bytes) { - call_inspectors!(inspector, [&mut self.logs, &mut self.cheatcodes], { + call_inspectors!(inspector, [&mut self.tracer, &mut self.logs, &mut self.cheatcodes], { let (new_status, new_gas, new_retdata) = inspector.call_end(data, call, remaining_gas, status, retdata.clone(), is_static); @@ -137,7 +138,7 @@ where data: &mut EVMData<'_, DB>, call: &CreateInputs, ) -> (Return, Option
, Gas, Bytes) { - call_inspectors!(inspector, [&mut self.logs, &mut self.cheatcodes], { + call_inspectors!(inspector, [&mut self.tracer, &mut self.logs, &mut self.cheatcodes], { let (status, addr, gas, retdata) = inspector.create(data, call); // Allow inspectors to exit early @@ -158,7 +159,7 @@ where remaining_gas: Gas, retdata: Bytes, ) -> (Return, Option
, Gas, Bytes) { - call_inspectors!(inspector, [&mut self.logs, &mut self.cheatcodes], { + call_inspectors!(inspector, [&mut self.tracer, &mut self.logs, &mut self.cheatcodes], { let (new_status, new_address, new_gas, new_retdata) = inspector.create_end(data, call, status, address, remaining_gas, retdata.clone()); @@ -171,7 +172,7 @@ where } fn selfdestruct(&mut self) { - call_inspectors!(inspector, [&mut self.logs, &mut self.cheatcodes], { + call_inspectors!(inspector, [&mut self.tracer, &mut self.logs, &mut self.cheatcodes], { Inspector::::selfdestruct(inspector); }); } diff --git a/forge/src/executor/inspector/tracer.rs b/forge/src/executor/inspector/tracer.rs new file mode 100644 index 0000000000000..236480ac406a7 --- /dev/null +++ b/forge/src/executor/inspector/tracer.rs @@ -0,0 +1,166 @@ +use super::logs::extract_log; +use crate::{ + executor::HARDHAT_CONSOLE_ADDRESS, + trace::{ + CallTrace, CallTraceArena, LogCallOrder, RawOrDecodedCall, RawOrDecodedLog, + RawOrDecodedReturnData, + }, +}; +use bytes::Bytes; +use ethers::{ + types::{Address, U256}, + utils::{get_contract_address, get_create2_address}, +}; +use revm::{ + return_ok, CallInputs, CreateInputs, CreateScheme, Database, EVMData, Gas, Inspector, + Interpreter, Return, +}; + +/// An inspector that collects call traces. +#[derive(Default, Debug)] +pub struct Tracer { + pub trace_stack: Vec, + pub traces: CallTraceArena, +} + +impl Tracer { + pub fn new() -> Self { + Default::default() + } + + pub fn start_trace( + &mut self, + depth: usize, + address: Address, + data: Vec, + value: U256, + created: bool, + ) { + self.trace_stack.push(self.traces.push_trace( + 0, + CallTrace { + depth, + address, + created, + data: RawOrDecodedCall::Raw(data), + value, + ..Default::default() + }, + )); + } + + pub fn fill_trace(&mut self, success: bool, cost: u64, output: Vec) { + let trace = &mut self.traces.arena + [self.trace_stack.pop().expect("more traces were filled than started")] + .trace; + trace.success = success; + trace.gas_cost = cost; + trace.output = RawOrDecodedReturnData::Raw(output); + } +} + +impl Inspector for Tracer +where + DB: Database, +{ + fn call( + &mut self, + data: &mut EVMData<'_, DB>, + call: &CallInputs, + _: bool, + ) -> (Return, Gas, Bytes) { + if call.contract != *HARDHAT_CONSOLE_ADDRESS { + self.start_trace( + data.subroutine.depth() as usize, + call.contract, + call.input.to_vec(), + call.transfer.value, + false, + ); + } + + (Return::Continue, Gas::new(call.gas_limit), Bytes::new()) + } + + fn step( + &mut self, + interpreter: &mut Interpreter, + _: &mut EVMData<'_, DB>, + _is_static: bool, + ) -> Return { + if let Some(log) = extract_log(interpreter) { + let node = &mut self.traces.arena[*self.trace_stack.last().expect("no ongoing trace")]; + node.ordering.push(LogCallOrder::Log(node.logs.len())); + node.logs.push(RawOrDecodedLog::Raw(log)); + } + + Return::Continue + } + + fn call_end( + &mut self, + _: &mut EVMData<'_, DB>, + call: &CallInputs, + gas: Gas, + status: Return, + retdata: Bytes, + _: bool, + ) -> (Return, Gas, Bytes) { + if call.contract != *HARDHAT_CONSOLE_ADDRESS { + self.fill_trace(matches!(status, return_ok!()), gas.spend(), retdata.to_vec()); + } + + (status, gas, retdata) + } + + fn create( + &mut self, + data: &mut EVMData<'_, DB>, + call: &CreateInputs, + ) -> (Return, Option
, Gas, Bytes) { + // TODO: Does this increase gas cost? + data.subroutine.load_account(call.caller, data.db); + let nonce = data.subroutine.account(call.caller).info.nonce; + + self.start_trace( + data.subroutine.depth() as usize, + match call.scheme { + CreateScheme::Create => get_contract_address(call.caller, nonce), + CreateScheme::Create2 { salt } => { + let mut buffer: [u8; 4 * 8] = [0; 4 * 8]; + salt.to_big_endian(&mut buffer); + get_create2_address(call.caller, buffer, call.init_code.clone()) + } + }, + call.init_code.to_vec(), + call.value, + true, + ); + + (Return::Continue, None, Gas::new(call.gas_limit), Bytes::new()) + } + + fn create_end( + &mut self, + data: &mut EVMData<'_, DB>, + _: &CreateInputs, + status: Return, + address: Option
, + gas: Gas, + retdata: Bytes, + ) -> (Return, Option
, Gas, Bytes) { + let code = match address { + Some(address) => data + .subroutine + .account(address) + .info + .code + .as_ref() + .map_or(vec![], |code| code.to_vec()), + None => vec![], + }; + self.fill_trace(matches!(status, return_ok!()), gas.spend(), code); + + (status, address, gas, retdata) + } +} diff --git a/forge/src/executor/mod.rs b/forge/src/executor/mod.rs index 8a2f5cdb7c246..eb08f91bde8c0 100644 --- a/forge/src/executor/mod.rs +++ b/forge/src/executor/mod.rs @@ -1,7 +1,5 @@ /// ABIs used internally in the executor pub mod abi; -use std::collections::BTreeMap; - pub use abi::{ patch_hardhat_console_selector, HardhatConsoleCalls, CHEATCODE_ADDRESS, CONSOLE_ABI, HARDHAT_CONSOLE_ABI, HARDHAT_CONSOLE_ADDRESS, @@ -27,7 +25,7 @@ pub mod fuzz; pub use revm::SpecId; use self::inspector::InspectorStackConfig; -use crate::CALLER; +use crate::{trace::CallTraceArena, CALLER}; use bytes::Bytes; use ethers::{ abi::{Abi, Detokenize, RawLog, Tokenize}, @@ -41,6 +39,7 @@ use revm::{ db::{CacheDB, DatabaseCommit, DatabaseRef, EmptyDB}, return_ok, Account, CreateScheme, Env, Return, TransactOut, TransactTo, TxEnv, EVM, }; +use std::collections::BTreeMap; #[derive(thiserror::Error, Debug)] pub enum EvmError { @@ -51,6 +50,8 @@ pub enum EvmError { reason: String, gas_used: u64, logs: Vec, + traces: Option, + labels: BTreeMap, state_changeset: Option>, }, /// Error which occurred during ABI encoding/decoding @@ -61,6 +62,19 @@ pub enum EvmError { Eyre(#[from] eyre::Error), } +/// The result of a deployment. +#[derive(Debug)] +pub struct DeployResult { + /// The address of the deployed contract + pub address: Address, + /// The gas cost of the deployment + pub gas: u64, + /// The logs emitted during the deployment + pub logs: Vec, + /// The traces of the deployment + pub traces: Option, +} + /// The result of a call. #[derive(Debug)] pub struct CallResult { @@ -74,6 +88,8 @@ pub struct CallResult { pub logs: Vec, /// The labels assigned to addresses during the call pub labels: BTreeMap, + /// The traces of the call + pub traces: Option, /// The changeset of the state. /// /// This is only present if the changed state was not committed to the database (i.e. if you @@ -94,6 +110,8 @@ pub struct RawCallResult { pub logs: Vec, /// The labels assigned to addresses during the call pub labels: BTreeMap, + /// The traces of the call + pub traces: Option, /// The changeset of the state. /// /// This is only present if the changed state was not committed to the database (i.e. if you @@ -101,6 +119,20 @@ pub struct RawCallResult { pub state_changeset: Option>, } +impl Default for RawCallResult { + fn default() -> Self { + Self { + status: Return::Continue, + result: Bytes::new(), + gas: 0, + logs: Vec::new(), + labels: BTreeMap::new(), + traces: None, + state_changeset: None, + } + } +} + pub struct Executor { // Note: We do not store an EVM here, since we are really // only interested in the database. REVM's `EVM` is a thin @@ -154,13 +186,8 @@ where } /// Calls the `setUp()` function on a contract. - pub fn setup( - &mut self, - address: Address, - ) -> std::result::Result<(Return, Vec), EvmError> { - let CallResult { status, logs, .. } = - self.call_committing::<(), _, _>(*CALLER, address, "setUp()", (), 0.into(), None)?; - Ok((status, logs)) + pub fn setup(&mut self, address: Address) -> std::result::Result, EvmError> { + self.call_committing::<(), _, _>(*CALLER, address, "setUp()", (), 0.into(), None) } /// Performs a call to an account on the current state of the VM. @@ -177,12 +204,12 @@ where ) -> std::result::Result, EvmError> { let func = func.into(); let calldata = Bytes::from(encode_function_data(&func, args)?.to_vec()); - let RawCallResult { result, status, gas, logs, labels, .. } = + let RawCallResult { result, status, gas, logs, labels, traces, .. } = self.call_raw_committing(from, to, calldata, value)?; match status { return_ok!() => { let result = decode_function_data(&func, result, false)?; - Ok(CallResult { status, result, gas, logs, labels, state_changeset: None }) + Ok(CallResult { status, result, gas, logs, labels, traces, state_changeset: None }) } _ => { let reason = foundry_utils::decode_revert(result.as_ref(), abi) @@ -192,6 +219,8 @@ where reason, gas_used: gas, logs, + traces, + labels, state_changeset: None, }) } @@ -219,9 +248,9 @@ where TransactOut::Call(data) => data, _ => Bytes::default(), }; - let (logs, labels) = collect_inspector_states(inspector); + let (logs, labels, traces) = collect_inspector_states(inspector); - Ok(RawCallResult { status, result, gas, logs, labels, state_changeset: None }) + Ok(RawCallResult { status, result, gas, logs, labels, traces, state_changeset: None }) } /// Performs a call to an account on the current state of the VM. @@ -238,17 +267,25 @@ where ) -> std::result::Result, EvmError> { let func = func.into(); let calldata = Bytes::from(encode_function_data(&func, args)?.to_vec()); - let RawCallResult { result, status, gas, logs, labels, state_changeset } = + let RawCallResult { result, status, gas, logs, labels, traces, state_changeset } = self.call_raw(from, to, calldata, value)?; match status { return_ok!() => { let result = decode_function_data(&func, result, false)?; - Ok(CallResult { status, result, gas, logs, labels, state_changeset }) + Ok(CallResult { status, result, gas, logs, labels, traces, state_changeset }) } _ => { let reason = foundry_utils::decode_revert(result.as_ref(), abi) .unwrap_or_else(|_| format!("{:?}", status)); - Err(EvmError::Execution { status, reason, gas_used: gas, logs, state_changeset }) + Err(EvmError::Execution { + status, + reason, + gas_used: gas, + logs, + traces, + labels, + state_changeset, + }) } } } @@ -275,42 +312,45 @@ where _ => Bytes::default(), }; - let (logs, labels) = collect_inspector_states(inspector); + let (logs, labels, traces) = collect_inspector_states(inspector); Ok(RawCallResult { status, result, gas, logs: logs.to_vec(), labels, + traces, state_changeset: Some(state_changeset), }) } /// Deploys a contract and commits the new state to the underlying database. - pub fn deploy( - &mut self, - from: Address, - code: Bytes, - value: U256, - ) -> Result<(Address, Return, u64, Vec)> { + pub fn deploy(&mut self, from: Address, code: Bytes, value: U256) -> Result { let mut evm = EVM::new(); evm.env = self.build_env(from, TransactTo::Create(CreateScheme::Create), code, value); evm.database(&mut self.db); let mut inspector = self.inspector_config.stack(); let (status, out, gas, _) = evm.inspect_commit(&mut inspector); - let addr = match out { + let address = match out { TransactOut::Create(_, Some(addr)) => addr, // TODO: We should have better error handling logic in the test runner // regarding deployments in general _ => eyre::bail!("deployment failed: {:?}", status), }; - let (logs, _) = collect_inspector_states(inspector); + let (logs, _, traces) = collect_inspector_states(inspector); - Ok((addr, status, gas, logs)) + Ok(DeployResult { address, gas, logs, traces }) } - /// Check if a call to a test contract was successful + /// Check if a call to a test contract was successful. + /// + /// This function checks both the VM status of the call and DSTest's `failed`. + /// + /// DSTest will not revert inside its `assertEq`-like functions which allows + /// to test multiple assertions in 1 test function while also preserving logs. + /// + /// Instead it sets `failed` to `true` which we must check. pub fn is_success( &self, address: Address, @@ -354,10 +394,13 @@ where } } -fn collect_inspector_states(stack: InspectorStack) -> (Vec, BTreeMap) { +fn collect_inspector_states( + stack: InspectorStack, +) -> (Vec, BTreeMap, Option) { let logs = if let Some(logs) = stack.logs { logs.logs } else { Vec::new() }; let labels = if let Some(cheatcodes) = stack.cheatcodes { cheatcodes.labels } else { BTreeMap::new() }; + let traces = stack.tracer.map(|tracer| tracer.traces); - (logs, labels) + (logs, labels, traces) } diff --git a/forge/src/gas_report.rs b/forge/src/gas_report.rs new file mode 100644 index 0000000000000..a4d1d47657dd0 --- /dev/null +++ b/forge/src/gas_report.rs @@ -0,0 +1,147 @@ +use crate::{ + executor::{CHEATCODE_ADDRESS, HARDHAT_CONSOLE_ADDRESS}, + trace::{CallTraceArena, RawOrDecodedCall, TraceKind}, +}; +use comfy_table::{modifiers::UTF8_ROUND_CORNERS, presets::UTF8_FULL, *}; +use ethers::types::U256; +use serde::{Deserialize, Serialize}; +use std::{collections::BTreeMap, fmt::Display}; + +#[derive(Default, Debug, Serialize, Deserialize)] +pub struct GasReport { + pub report_for: Vec, + pub contracts: BTreeMap, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct ContractInfo { + pub gas: U256, + pub size: U256, + pub functions: BTreeMap, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct GasInfo { + pub calls: Vec, + pub min: U256, + pub mean: U256, + pub median: U256, + pub max: U256, +} + +impl GasReport { + pub fn new(report_for: Vec) -> Self { + Self { report_for, ..Default::default() } + } + + pub fn analyze(&mut self, traces: &[(TraceKind, CallTraceArena)]) { + let report_for_all = self.report_for.is_empty() || self.report_for.iter().any(|s| s == "*"); + traces.iter().for_each(|(_, trace)| { + self.analyze_trace(trace, report_for_all); + }); + } + + fn analyze_trace(&mut self, trace: &CallTraceArena, report_for_all: bool) { + self.analyze_node(0, trace, report_for_all); + } + + fn analyze_node(&mut self, node_index: usize, arena: &CallTraceArena, report_for_all: bool) { + let node = &arena.arena[node_index]; + let trace = &node.trace; + + if trace.address == *CHEATCODE_ADDRESS || trace.address == *HARDHAT_CONSOLE_ADDRESS { + return + } + + if let Some(name) = &trace.contract { + let report_for = self.report_for.iter().any(|s| s == name); + if report_for || report_for_all { + let mut contract_report = + self.contracts.entry(name.to_string()).or_insert_with(Default::default); + + match &trace.data { + RawOrDecodedCall::Raw(bytes) if trace.created => { + contract_report.gas = trace.gas_cost.into(); + contract_report.size = bytes.len().into(); + } + // TODO: More robust test contract filtering + RawOrDecodedCall::Decoded(func, _) if !func.starts_with("test") => { + let function_report = contract_report + .functions + .entry(func.clone()) + .or_insert_with(Default::default); + function_report.calls.push(trace.gas_cost.into()); + } + _ => (), + } + } + } + + node.children.iter().for_each(|index| { + self.analyze_node(*index, arena, report_for_all); + }); + } + + #[must_use] + pub fn finalize(mut self) -> Self { + self.contracts.iter_mut().for_each(|(_, contract)| { + contract.functions.iter_mut().for_each(|(_, func)| { + func.calls.sort(); + func.min = func.calls.first().cloned().unwrap_or_default(); + func.max = func.calls.last().cloned().unwrap_or_default(); + func.mean = + func.calls.iter().fold(U256::zero(), |acc, x| acc + x) / func.calls.len(); + + let len = func.calls.len(); + func.median = if len > 0 { + if len % 2 == 0 { + (func.calls[len / 2 - 1] + func.calls[len / 2]) / 2 + } else { + func.calls[len / 2] + } + } else { + 0.into() + }; + }); + }); + self + } +} + +impl Display for GasReport { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + for (name, contract) in self.contracts.iter() { + let mut table = Table::new(); + table.load_preset(UTF8_FULL).apply_modifier(UTF8_ROUND_CORNERS); + table.set_header(vec![Cell::new(format!("{} contract", name)) + .add_attribute(Attribute::Bold) + .fg(Color::Green)]); + table.add_row(vec![ + Cell::new("Deployment Cost").add_attribute(Attribute::Bold).fg(Color::Cyan), + Cell::new("Deployment Size").add_attribute(Attribute::Bold).fg(Color::Cyan), + ]); + table.add_row(vec![contract.gas.to_string(), contract.size.to_string()]); + + table.add_row(vec![ + Cell::new("Function Name").add_attribute(Attribute::Bold).fg(Color::Magenta), + Cell::new("min").add_attribute(Attribute::Bold).fg(Color::Green), + Cell::new("avg").add_attribute(Attribute::Bold).fg(Color::Yellow), + Cell::new("median").add_attribute(Attribute::Bold).fg(Color::Yellow), + Cell::new("max").add_attribute(Attribute::Bold).fg(Color::Red), + Cell::new("# calls").add_attribute(Attribute::Bold), + ]); + contract.functions.iter().for_each(|(fname, function)| { + table.add_row(vec![ + Cell::new(fname.to_string()).add_attribute(Attribute::Bold), + Cell::new(function.min.to_string()).fg(Color::Green), + Cell::new(function.mean.to_string()).fg(Color::Yellow), + Cell::new(function.median.to_string()).fg(Color::Yellow), + Cell::new(function.max.to_string()).fg(Color::Red), + Cell::new(function.calls.len().to_string()), + ]); + }); + writeln!(f, "{}", table)? + } + Ok(()) + } +} diff --git a/forge/src/lib.rs b/forge/src/lib.rs index 611b065424ca7..dfad3ce55b746 100644 --- a/forge/src/lib.rs +++ b/forge/src/lib.rs @@ -1,5 +1,11 @@ pub mod decode; +/// Call trace arena, decoding and formatting +pub mod trace; + +/// Gas reports +pub mod gas_report; + /// The Forge test runner mod runner; pub use runner::{ContractRunner, TestKind, TestKindGas, TestResult}; diff --git a/forge/src/multi_runner.rs b/forge/src/multi_runner.rs index 3cde2019d59c1..9103972552619 100644 --- a/forge/src/multi_runner.rs +++ b/forge/src/multi_runner.rs @@ -3,20 +3,17 @@ use crate::{ runner::TestResult, ContractRunner, TestFilter, }; -use foundry_utils::PostLinkInput; -use revm::db::DatabaseRef; - use ethers::{ - abi::{Abi, Event, Function}, - prelude::{artifacts::CompactContractBytecode, Artifact, ArtifactId, ArtifactOutput}, - types::{Address, Bytes, H256, U256}, + abi::Abi, + prelude::{artifacts::CompactContractBytecode, ArtifactId, ArtifactOutput}, + solc::{Artifact, ProjectCompileOutput}, + types::{Address, Bytes, U256}, }; - -use proptest::test_runner::TestRunner; - -use ethers::solc::ProjectCompileOutput; use eyre::Result; +use foundry_utils::PostLinkInput; +use proptest::test_runner::TestRunner; use rayon::prelude::*; +use revm::db::DatabaseRef; use std::{collections::BTreeMap, marker::Sync, sync::mpsc::Sender}; /// Builder used for instantiating the multi-contract runner @@ -115,20 +112,15 @@ impl MultiContractRunnerBuilder { }, )?; - // TODO Add forge specific contracts - //known_contracts.insert("VM".to_string(), (HEVM_ABI.clone(), Vec::new())); - //known_contracts.insert("VM_CONSOLE".to_string(), (CONSOLE_ABI.clone(), Vec::new())); - let execution_info = foundry_utils::flatten_known_contracts(&known_contracts); Ok(MultiContractRunner { contracts: deployable_contracts, known_contracts, - identified_contracts: Default::default(), evm_opts, evm_spec: self.evm_spec.unwrap_or(SpecId::LONDON), sender: self.sender, fuzzer: self.fuzzer, - execution_info, + errors: Some(execution_info.2), source_paths, }) } @@ -166,14 +158,12 @@ pub struct MultiContractRunner { pub contracts: BTreeMap)>, /// Compiled contracts by name that have an Abi and runtime bytecode pub known_contracts: BTreeMap)>, - /// Identified contracts by test - pub identified_contracts: BTreeMap>, /// The EVM instance used in the test runner pub evm_opts: EvmOpts, /// The EVM spec pub evm_spec: SpecId, - /// All contract execution info, (functions, events, errors) - pub execution_info: (BTreeMap<[u8; 4], Function>, BTreeMap, Abi), + /// All known errors, used for decoding reverts + pub errors: Option, /// The fuzzer which will be used to run parametric tests (w/ non-0 solidity args) fuzzer: Option, /// The address which will be used as the `from` field in all EVM calls @@ -188,13 +178,14 @@ impl MultiContractRunner { filter: &(impl TestFilter + Send + Sync), stream_result: Option)>>, ) -> Result>> { - let source_paths = self.source_paths.clone(); let env = self.evm_opts.evm_env(); let results = self .contracts .par_iter() - .filter(|(name, _)| filter.matches_path(source_paths.get(*name).unwrap())) - .filter(|(name, _)| filter.matches_contract(name)) + .filter(|(name, _)| { + filter.matches_path(&self.source_paths.get(*name).unwrap()) && + filter.matches_contract(name) + }) .filter(|(_, (abi, _, _))| abi.functions().any(|func| filter.matches_test(&func.name))) .map(|(name, (abi, deploy_code, libs))| { let mut builder = ExecutorBuilder::new() @@ -208,15 +199,17 @@ impl MultiContractRunner { builder = builder.with_fork(fork); } + if self.evm_opts.verbosity >= 3 { + builder = builder.with_tracing(); + } + let executor = builder.build(); let result = self.run_tests(name, abi, executor, deploy_code.clone(), libs, filter)?; Ok((name.clone(), result)) }) - .filter_map(|x: Result<_>| x.ok()) - .filter_map( - |(name, result)| if result.is_empty() { None } else { Some((name, result)) }, - ) + .filter_map(Result::<_>::ok) + .filter(|(_, results)| !results.is_empty()) .map_with(stream_result, |stream_result, (name, result)| { if let Some(stream_result) = stream_result.as_ref() { stream_result.send((name.clone(), result.clone())).unwrap(); @@ -250,10 +243,10 @@ impl MultiContractRunner { deploy_code, self.evm_opts.initial_balance, self.sender, - Some((&self.execution_info.0, &self.execution_info.1, &self.execution_info.2)), + self.errors.as_ref(), libs, ); - runner.run_tests(filter, self.fuzzer.clone(), Some(&self.known_contracts)) + runner.run_tests(filter, self.fuzzer.clone()) } } diff --git a/forge/src/runner.rs b/forge/src/runner.rs index 46dabfce89537..c35135403b3fd 100644 --- a/forge/src/runner.rs +++ b/forge/src/runner.rs @@ -1,40 +1,21 @@ use crate::{ executor::{ - fuzz::{FuzzError, FuzzTestResult, FuzzedCases, FuzzedExecutor}, - CallResult, EvmError, Executor, RawCallResult, + fuzz::{CounterExample, FuzzedCases, FuzzedExecutor}, + CallResult, DeployResult, EvmError, Executor, }, + trace::{CallTraceArena, TraceKind}, TestFilter, CALLER, }; -use rayon::iter::ParallelIterator; -use revm::db::DatabaseRef; - use ethers::{ - abi::{Abi, Event, Function, RawLog, Token}, - types::{Address, Bytes, H256, U256}, + abi::{Abi, Function, RawLog}, + types::{Address, Bytes, U256}, }; - use eyre::Result; -use std::{collections::BTreeMap, fmt, time::Instant}; - -use proptest::test_runner::{TestError, TestRunner}; -use rayon::iter::IntoParallelRefIterator; +use proptest::test_runner::TestRunner; +use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; +use revm::db::DatabaseRef; use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct CounterExample { - pub calldata: Bytes, - - // Token does not implement Serde (lol), so we just serialize the calldata - #[serde(skip)] - pub args: Vec, -} - -impl fmt::Display for CounterExample { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let args = foundry_utils::format_tokens(&self.args).collect::>().join(", "); - write!(f, "calldata=0x{}, args=[{}]", hex::encode(&self.calldata), args) - } -} +use std::{collections::BTreeMap, fmt, time::Instant}; /// The result of an executed solidity test #[derive(Clone, Debug, Serialize, Deserialize)] @@ -48,13 +29,6 @@ pub struct TestResult { /// still be successful (i.e self.success == true) when it's expected to fail. pub reason: Option, - /// The gas used during execution. - /// - /// If this is the result of a fuzz test (`TestKind::Fuzz`), then this is the median of all - /// successful cases - // TODO: The gas usage is both in TestKind and here. We should dedupe. - pub gas_used: u64, - /// Minimal reproduction test case for failing fuzz tests pub counterexample: Option, @@ -67,14 +41,9 @@ pub struct TestResult { pub kind: TestKind, /// Traces - // TODO - //pub traces: Option>, - traces: Option>, + pub traces: Vec<(TraceKind, CallTraceArena)>, - /// Identified contracts - pub identified_contracts: Option>, - - /// Debug Steps + /// Debug steps // TODO #[serde(skip)] //pub debug_calls: Option>, @@ -147,32 +116,39 @@ impl TestKind { } } -/// Type complexity wrapper around execution info -type MaybeExecutionInfo<'a> = - Option<(&'a BTreeMap<[u8; 4], Function>, &'a BTreeMap, &'a Abi)>; +#[derive(Clone, Debug, Default)] +pub struct TestSetup { + /// The address at which the test contract was deployed + pub address: Address, + /// The logs emitted during setup + pub logs: Vec, + /// Call traces of the setup + pub traces: Vec<(TraceKind, CallTraceArena)>, + /// Addresses labeled during setup + pub labeled_addresses: BTreeMap, + /// Whether the setup failed + pub setup_failed: bool, + /// The reason the setup failed + pub reason: Option, +} -// TODO: Get rid of known contracts, execution info and so on once we rewrite tracing, since we are -// moving all the decoding/display logic to the CLI. Traces and logs returned from the runner (and -// consequently the multi runner) are in a raw (but digestible) format. pub struct ContractRunner<'a, DB: DatabaseRef> { /// The executor used by the runner. pub executor: Executor, - // Contract deployment options - /// The deployed contract's ABI - pub contract: &'a Abi, + /// Library contracts to be deployed before the test contract + pub predeploy_libs: &'a [Bytes], /// The deployed contract's code - // This is cheap to clone due to [`bytes::Bytes`], so OK to own pub code: Bytes, + /// The test contract's ABI + pub contract: &'a Abi, + /// All known errors, used to decode reverts + pub errors: Option<&'a Abi>, + /// The initial balance of the test contract pub initial_balance: U256, /// The address which will be used as the `from` field in all EVM calls pub sender: Address, - - /// Contract execution info, (functions, events, errors) - pub execution_info: MaybeExecutionInfo<'a>, - /// library contracts to be deployed before this contract - pub predeploy_libs: &'a [Bytes], } impl<'a, DB: DatabaseRef> ContractRunner<'a, DB> { @@ -183,7 +159,7 @@ impl<'a, DB: DatabaseRef> ContractRunner<'a, DB> { code: Bytes, initial_balance: U256, sender: Option
, - execution_info: MaybeExecutionInfo<'a>, + errors: Option<&'a Abi>, predeploy_libs: &'a [Bytes], ) -> Self { Self { @@ -192,7 +168,7 @@ impl<'a, DB: DatabaseRef> ContractRunner<'a, DB> { code, initial_balance, sender: sender.unwrap_or_default(), - execution_info, + errors, predeploy_libs, } } @@ -201,7 +177,7 @@ impl<'a, DB: DatabaseRef> ContractRunner<'a, DB> { impl<'a, DB: DatabaseRef + Send + Sync> ContractRunner<'a, DB> { /// Deploys the test contract inside the runner from the sending account, and optionally runs /// the `setUp` function on the test contract. - pub fn deploy(&mut self, setup: bool) -> Result<(Address, Vec, bool, Option)> { + pub fn setup(&mut self, setup: bool) -> Result { // We max out their balance so that they can deploy and make calls. self.executor.set_balance(self.sender, U256::MAX); self.executor.set_balance(*CALLER, U256::MAX); @@ -210,34 +186,54 @@ impl<'a, DB: DatabaseRef + Send + Sync> ContractRunner<'a, DB> { self.executor.set_nonce(self.sender, 1); // Deploy libraries - self.predeploy_libs.iter().for_each(|code| { - self.executor - .deploy(*CALLER, code.0.clone(), 0u32.into()) - .expect("couldn't deploy library"); - }); + let mut traces: Vec<(TraceKind, CallTraceArena)> = self + .predeploy_libs + .iter() + .filter_map(|code| { + let DeployResult { traces, .. } = self + .executor + .deploy(self.sender, code.0.clone(), 0u32.into()) + .expect("couldn't deploy library"); + + traces + }) + .map(|traces| (TraceKind::Deployment, traces)) + .collect(); // Deploy an instance of the contract - let (addr, _, _, mut logs) = self + let DeployResult { address, mut logs, traces: contract_traces, .. } = self .executor .deploy(self.sender, self.code.0.clone(), 0u32.into()) .expect("couldn't deploy"); - self.executor.set_balance(addr, self.initial_balance); + traces.extend(contract_traces.map(|traces| (TraceKind::Deployment, traces)).into_iter()); + self.executor.set_balance(address, self.initial_balance); // Optionally call the `setUp` function - if setup { + Ok(if setup { tracing::trace!("setting up"); - let (setup_failed, setup_logs, reason) = match self.executor.setup(addr) { - Ok((_, logs)) => (false, logs, None), - Err(EvmError::Execution { logs, reason, .. }) => { - (true, logs, Some(format!("Setup failed: {}", reason))) + let (setup_failed, setup_logs, setup_traces, labeled_addresses, reason) = match self + .executor + .setup(address) + { + Ok(CallResult { traces, labels, logs, .. }) => (false, logs, traces, labels, None), + Err(EvmError::Execution { traces, labels, logs, reason, .. }) => { + (true, logs, traces, labels, Some(format!("Setup failed: {}", reason))) } - Err(e) => (true, Vec::new(), Some(format!("Setup failed: {}", &e.to_string()))), + Err(e) => ( + true, + Vec::new(), + None, + BTreeMap::new(), + Some(format!("Setup failed: {}", &e.to_string())), + ), }; + traces.extend(setup_traces.map(|traces| (TraceKind::Setup, traces)).into_iter()); logs.extend_from_slice(&setup_logs); - Ok((addr, logs, setup_failed, reason)) + + TestSetup { address, logs, traces, labeled_addresses, setup_failed, reason } } else { - Ok((addr, logs, false, None)) - } + TestSetup { address, logs, traces, ..Default::default() } + }) } /// Runs all tests for a contract whose names match the provided regular expression @@ -245,216 +241,181 @@ impl<'a, DB: DatabaseRef + Send + Sync> ContractRunner<'a, DB> { &mut self, filter: &impl TestFilter, fuzzer: Option, - known_contracts: Option<&BTreeMap)>>, ) -> Result> { tracing::info!("starting tests"); let start = Instant::now(); let needs_setup = self.contract.functions().any(|func| func.name == "setUp"); - let (unit_tests, fuzz_tests): (Vec<_>, Vec<_>) = self - .contract - .functions() - .into_iter() - .filter(|func| func.name.starts_with("test")) - .filter(|func| filter.matches_test(&func.name)) - .partition(|func| func.inputs.is_empty()); - let (addr, init_logs, setup_failed, reason) = self.deploy(needs_setup)?; - if setup_failed { + let setup = self.setup(needs_setup)?; + if setup.setup_failed { // The setup failed, so we return a single test result for `setUp` return Ok([( "setUp()".to_string(), TestResult { success: false, - reason, - gas_used: 0, + reason: setup.reason, counterexample: None, - logs: init_logs, + logs: setup.logs, kind: TestKind::Standard(0), - traces: None, - identified_contracts: None, + traces: setup.traces, + // TODO debug_calls: None, - labeled_addresses: Default::default(), + labeled_addresses: setup.labeled_addresses, }, )] .into()) } - // Run all unit tests - let mut test_results = unit_tests + // Collect valid test functions + let tests: Vec<_> = self + .contract + .functions() + .into_iter() + .filter(|func| func.name.starts_with("test") && filter.matches_test(&func.name)) + .map(|func| (func, func.name.starts_with("testFail"))) + .collect(); + + let test_results = tests .par_iter() - .map(|func| { - let result = self.run_test(func, known_contracts, addr, init_logs.clone())?; - Ok((func.signature(), result)) + .filter_map(|(func, should_fail)| { + let result = if func.inputs.is_empty() { + Some(self.run_test(func, *should_fail, setup.clone())) + } else { + fuzzer.as_ref().map(|fuzzer| { + self.run_fuzz_test(func, *should_fail, fuzzer.clone(), setup.clone()) + }) + }; + + result.map(|result| Ok((func.signature(), result?))) }) .collect::>>()?; - if let Some(fuzzer) = fuzzer { - let fuzz_results = fuzz_tests - .par_iter() - .filter(|func| !func.inputs.is_empty()) - .map(|func| { - let result = self.run_fuzz_test( - func, - fuzzer.clone(), - known_contracts, - addr, - init_logs.clone(), - )?; - Ok((func.signature(), result)) - }) - .collect::>>()?; - test_results.extend(fuzz_results); - } - if !test_results.is_empty() { let successful = test_results.iter().filter(|(_, tst)| tst.success).count(); - let duration = Instant::now().duration_since(start); - tracing::info!(?duration, "done. {}/{} successful", successful, test_results.len()); + tracing::info!( + duration = ?Instant::now().duration_since(start), + "done. {}/{} successful", + successful, + test_results.len() + ); } Ok(test_results) } - #[tracing::instrument(name = "test", skip_all, fields(name = %func.signature()))] + #[tracing::instrument(name = "test", skip_all, fields(name = %func.signature(), %should_fail))] pub fn run_test( &self, func: &Function, - _known_contracts: Option<&BTreeMap)>>, - address: Address, - mut logs: Vec, + should_fail: bool, + setup: TestSetup, ) -> Result { - let start = Instant::now(); - // The expected result depends on the function name. - // TODO: Dedupe (`TestDescriptor`?) - let should_fail = func.name.starts_with("testFail"); - tracing::debug!(func = ?func.signature(), should_fail, "unit-testing"); + let TestSetup { address, mut logs, mut traces, mut labeled_addresses, .. } = setup; - let errors_abi = self.execution_info.as_ref().map(|(_, _, errors)| errors); - let errors_abi = if let Some(ref abi) = errors_abi { abi } else { self.contract }; - - let identified_contracts: Option> = None; - - let (status, reason, gas_used, logs, state_changeset) = match self + // Run unit test + let start = Instant::now(); + let (status, reason, gas_used, execution_traces, state_changeset) = match self .executor - .call::<(), _, _>(self.sender, address, func.clone(), (), 0.into(), Some(errors_abi)) + .call::<(), _, _>(self.sender, address, func.clone(), (), 0.into(), self.errors) { Ok(CallResult { - status, gas: gas_used, logs: execution_logs, state_changeset, .. + status, + gas: gas_used, + logs: execution_logs, + traces: execution_trace, + labels: new_labels, + state_changeset, + .. }) => { + labeled_addresses.extend(new_labels); logs.extend(execution_logs); - (status, None, gas_used, logs, state_changeset) + (status, None, gas_used, execution_trace, state_changeset) + } + Err(EvmError::Execution { + status, + reason, + gas_used, + logs: execution_logs, + traces: execution_trace, + labels: new_labels, + state_changeset, + .. + }) => { + labeled_addresses.extend(new_labels); + logs.extend(execution_logs); + (status, Some(reason), gas_used, execution_trace, state_changeset) + } + Err(err) => { + tracing::error!(?err); + return Err(err.into()) } - Err(err) => match err { - EvmError::Execution { - status, - reason, - gas_used, - logs: execution_logs, - state_changeset, - } => { - logs.extend(execution_logs); - (status, Some(reason), gas_used, logs, state_changeset) - } - err => { - tracing::error!(?err); - return Err(err.into()) - } - }, }; + traces.extend(execution_traces.map(|traces| (TraceKind::Execution, traces)).into_iter()); - // DSTest will not revert inside its `assertEq`-like functions - // which allows to test multiple assertions in 1 test function while also - // preserving logs - instead it sets `failed` to `true` which we must check. let success = self.executor.is_success( - address, + setup.address, status, state_changeset.expect("we should have a state changeset"), should_fail, ); - let duration = Instant::now().duration_since(start); - tracing::debug!(?duration, %success, %gas_used); + + // Record test execution time + tracing::debug!( + duration = ?Instant::now().duration_since(start), + %success, + %gas_used + ); Ok(TestResult { success, reason, - gas_used, counterexample: None, logs, kind: TestKind::Standard(gas_used), - traces: None, - identified_contracts, + traces, debug_calls: None, - labeled_addresses: Default::default(), + labeled_addresses, }) } - #[tracing::instrument(name = "fuzz-test", skip_all, fields(name = %func.signature()))] + #[tracing::instrument(name = "fuzz-test", skip_all, fields(name = %func.signature(), %should_fail))] pub fn run_fuzz_test( &self, func: &Function, + should_fail: bool, runner: TestRunner, - _known_contracts: Option<&BTreeMap)>>, - address: Address, - mut logs: Vec, + setup: TestSetup, ) -> Result { - // We do not trace in fuzz tests as it is a big performance hit - let start = Instant::now(); - // TODO: Dedupe (`TestDescriptor`?) - let should_fail = func.name.starts_with("testFail"); - - let identified_contracts: Option> = None; - - // Wrap the executor in a fuzzed version - // TODO: When tracing is ported, we should disable it here. - let executor = FuzzedExecutor::new(&self.executor, runner, self.sender); - let FuzzTestResult { cases, test_error } = - executor.fuzz(func, address, should_fail, Some(self.contract)); - - // Rerun the failed fuzz case to get more information like traces and logs - if let Some(FuzzError { test_error: TestError::Fail(_, ref calldata), .. }) = test_error { - // TODO: When tracing is ported, we should re-enable it here to get traces. - let RawCallResult { logs: execution_logs, .. } = - self.executor.call_raw(self.sender, address, calldata.0.clone(), 0.into())?; - logs.extend(execution_logs); - } + let TestSetup { address, mut logs, mut traces, mut labeled_addresses, .. } = setup; - let (success, counterexample, reason) = match test_error { - Some(err) => { - let (counterexample, reason) = match err.test_error { - TestError::Abort(r) if r == "Too many global rejects".into() => { - (None, Some(r.message().to_string())) - } - TestError::Fail(_, calldata) => { - // Skip the function selector when decoding - let args = func.decode_input(&calldata.as_ref()[4..])?; - - (Some(CounterExample { calldata, args }), None) - } - e => panic!("Unexpected test error: {:?}", e), - }; + // Run fuzz test + let start = Instant::now(); + let mut result = FuzzedExecutor::new(&self.executor, runner, self.sender).fuzz( + func, + address, + should_fail, + self.errors, + ); - if !err.revert_reason.is_empty() { - (false, counterexample, Some(err.revert_reason)) - } else { - (false, counterexample, reason) - } - } - _ => (true, None, None), - }; + // Record logs, labels and traces + logs.append(&mut result.logs); + labeled_addresses.append(&mut result.labeled_addresses); + traces.extend(result.traces.map(|traces| (TraceKind::Execution, traces)).into_iter()); - let duration = Instant::now().duration_since(start); - tracing::debug!(?duration, %success); + // Record test execution time + tracing::debug!( + duration = ?Instant::now().duration_since(start), + success = %result.success + ); Ok(TestResult { - success, - reason, - gas_used: cases.median_gas(), - counterexample, + success: result.success, + reason: result.reason, + counterexample: result.counterexample, logs, - kind: TestKind::Fuzz(cases), - traces: Default::default(), - identified_contracts, + kind: TestKind::Fuzz(result.cases), + traces, debug_calls: None, - labeled_addresses: Default::default(), + labeled_addresses, }) } } @@ -496,7 +457,7 @@ mod tests { cfg.failure_persistence = None; let fuzzer = TestRunner::new(cfg); let results = - runner.run_tests(&Filter::new("testGreeting", ".*", ".*"), Some(fuzzer), None).unwrap(); + runner.run_tests(&Filter::new("testGreeting", ".*", ".*"), Some(fuzzer)).unwrap(); assert!(results["testGreeting()"].success); assert!(results["testGreeting(string)"].success); assert!(results["testGreeting(string,string)"].success); @@ -513,7 +474,7 @@ mod tests { cfg.failure_persistence = None; let fuzzer = TestRunner::new(cfg); let results = - runner.run_tests(&Filter::new("testFuzz.*", ".*", ".*"), Some(fuzzer), None).unwrap(); + runner.run_tests(&Filter::new("testFuzz.*", ".*", ".*"), Some(fuzzer)).unwrap(); for (_, res) in results { assert!(!res.success); assert!(res.counterexample.is_some()); @@ -530,9 +491,8 @@ mod tests { let mut cfg = FuzzConfig::default(); cfg.failure_persistence = None; let fuzzer = TestRunner::new(cfg); - let res = runner - .run_tests(&Filter::new("testStringFuzz.*", ".*", ".*"), Some(fuzzer), None) - .unwrap(); + let res = + runner.run_tests(&Filter::new("testStringFuzz.*", ".*", ".*"), Some(fuzzer)).unwrap(); assert_eq!(res.len(), 1); assert!(res["testStringFuzz(string)"].success); assert!(res["testStringFuzz(string)"].counterexample.is_none()); @@ -548,9 +508,8 @@ mod tests { let mut cfg = FuzzConfig::default(); cfg.failure_persistence = None; let fuzzer = TestRunner::new(cfg); - let res = runner - .run_tests(&Filter::new("testShrinking.*", ".*", ".*"), Some(fuzzer), None) - .unwrap(); + let res = + runner.run_tests(&Filter::new("testShrinking.*", ".*", ".*"), Some(fuzzer)).unwrap(); assert_eq!(res.len(), 1); let res = res["testShrinking(uint256,uint256)"].clone(); @@ -569,9 +528,8 @@ mod tests { // we reduce the shrinking iters and observe a larger result cfg.max_shrink_iters = 5; let fuzzer = TestRunner::new(cfg); - let res = runner - .run_tests(&Filter::new("testShrinking.*", ".*", ".*"), Some(fuzzer), None) - .unwrap(); + let res = + runner.run_tests(&Filter::new("testShrinking.*", ".*", ".*"), Some(fuzzer)).unwrap(); assert_eq!(res.len(), 1); let res = res["testShrinking(uint256,uint256)"].clone(); diff --git a/forge/src/trace/decoder.rs b/forge/src/trace/decoder.rs new file mode 100644 index 0000000000000..c8003f8de2eee --- /dev/null +++ b/forge/src/trace/decoder.rs @@ -0,0 +1,239 @@ +use super::{ + CallTraceArena, RawOrDecodedCall, RawOrDecodedLog, RawOrDecodedReturnData, TraceIdentifier, +}; +use crate::abi::{CHEATCODE_ADDRESS, CONSOLE_ABI, HEVM_ABI}; +use ethers::{ + abi::{Abi, Address, Event, Function, Token}, + types::H256, +}; +use foundry_utils::format_token; +use std::collections::BTreeMap; + +/// The call trace decoder. +/// +/// The decoder collects address labels and ABIs from any number of [TraceIdentifier]s, which it +/// then uses to decode the call trace. +/// +/// Note that a call trace decoder is required for each new set of traces, since addresses in +/// different sets might overlap. +#[derive(Default, Debug)] +pub struct CallTraceDecoder { + /// Addresses identified to be a specific contract. + /// + /// The values are in the form `":"`. + pub contracts: BTreeMap, + /// Address labels + pub labels: BTreeMap, + /// A mapping of addresses to their known functions + pub functions: BTreeMap<[u8; 4], Vec>, + /// All known events + pub events: BTreeMap, + /// All known errors + pub errors: Abi, +} + +impl CallTraceDecoder { + /// Creates a new call trace decoder. + /// + /// The call trace decoder always knows how to decode calls to the cheatcode address, as well + /// as DSTest-style logs. + pub fn new() -> Self { + Self { + contracts: BTreeMap::new(), + labels: [(*CHEATCODE_ADDRESS, "VM".to_string())].into(), + functions: HEVM_ABI + .functions() + .map(|func| (func.short_signature(), vec![func.clone()])) + .collect::>>(), + events: CONSOLE_ABI + .events() + .map(|event| (event.signature(), event.clone())) + .collect::>(), + errors: Abi::default(), + } + } + + /// Creates a new call trace decoder with predetermined address labels. + pub fn new_with_labels(labels: BTreeMap) -> Self { + let mut info = Self::new(); + for (address, label) in labels.into_iter() { + info.labels.insert(address, label); + } + info + } + + /// Identify unknown addresses in the specified call trace using the specified identifier. + /// + /// Unknown contracts are contracts that either lack a label or an ABI. + pub fn identify(&mut self, trace: &CallTraceArena, identifier: &impl TraceIdentifier) { + trace.addresses_iter().for_each(|(address, code)| { + // We only try to identify addresses with missing data + if self.labels.contains_key(address) && self.contracts.contains_key(address) { + return + } + + let (contract, label, abi) = identifier.identify_address(address, code); + if let Some(contract) = contract { + self.contracts.entry(*address).or_insert(contract); + } + + if let Some(label) = label { + self.labels.entry(*address).or_insert(label); + } + + if let Some(abi) = abi { + // Store known functions for the address + abi.functions() + .map(|func| (func.short_signature(), func.clone())) + .for_each(|(sig, func)| self.functions.entry(sig).or_default().push(func)); + + // Flatten events from all ABIs + abi.events().map(|event| (event.signature(), event.clone())).for_each( + |(sig, event)| { + self.events.insert(sig, event); + }, + ); + + // Flatten errors from all ABIs + abi.errors().for_each(|error| { + let entry = self + .errors + .errors + .entry(error.name.clone()) + .or_insert_with(Default::default); + entry.push(error.clone()); + }); + } + }); + } + + pub fn decode(&self, traces: &mut CallTraceArena) { + for node in traces.arena.iter_mut() { + // Set contract name + if let Some(contract) = self.contracts.get(&node.trace.address) { + node.trace.contract = Some(contract.clone()); + } + + // Set label + if let Some(label) = self.labels.get(&node.trace.address) { + node.trace.label = Some(label.clone()); + } + + // Decode call + if let RawOrDecodedCall::Raw(bytes) = &node.trace.data { + if bytes.len() >= 4 { + if let Some(funcs) = self.functions.get(&bytes[0..4]) { + // This is safe because (1) we would not have an entry for the given + // selector if no functions with that selector were added and (2) the same + // selector implies the function has the same name and inputs. + let func = &funcs[0]; + + // Decode inputs + let inputs = if !bytes[4..].is_empty() { + if node.trace.address == *CHEATCODE_ADDRESS { + // Try to decode cheatcode inputs in a more custom way + self.decode_cheatcode_inputs(func, bytes).unwrap_or_else(|| { + func.decode_input(&bytes[4..]) + .expect("bad function input decode") + .iter() + .map(|token| self.apply_label(token)) + .collect() + }) + } else { + func.decode_input(&bytes[4..]) + .expect("bad function input decode") + .iter() + .map(|token| self.apply_label(token)) + .collect() + } + } else { + Vec::new() + }; + node.trace.data = RawOrDecodedCall::Decoded(func.name.clone(), inputs); + + if let RawOrDecodedReturnData::Raw(bytes) = &node.trace.output { + if !bytes.is_empty() { + if node.trace.success { + if let Some(tokens) = funcs + .iter() + .find_map(|func| func.decode_output(&bytes[..]).ok()) + { + node.trace.output = RawOrDecodedReturnData::Decoded( + tokens + .iter() + .map(|token| self.apply_label(token)) + .collect::>() + .join(", "), + ); + } + } else if let Ok(decoded_error) = + foundry_utils::decode_revert(&bytes[..], Some(&self.errors)) + { + node.trace.output = RawOrDecodedReturnData::Decoded(format!( + r#""{}""#, + decoded_error + )); + } + } + } + } + } else { + node.trace.data = RawOrDecodedCall::Decoded("fallback".to_string(), Vec::new()); + + if let RawOrDecodedReturnData::Raw(bytes) = &node.trace.output { + if !node.trace.success { + if let Ok(decoded_error) = + foundry_utils::decode_revert(&bytes[..], Some(&self.errors)) + { + node.trace.output = RawOrDecodedReturnData::Decoded(format!( + r#""{}""#, + decoded_error + )); + } + } + } + } + } + + // Decode events + node.logs.iter_mut().for_each(|log| { + if let RawOrDecodedLog::Raw(raw_log) = log { + if let Some(event) = self.events.get(&raw_log.topics[0]) { + if let Ok(decoded) = event.parse_log(raw_log.clone()) { + *log = RawOrDecodedLog::Decoded( + event.name.clone(), + decoded + .params + .into_iter() + .map(|param| (param.name, self.apply_label(¶m.value))) + .collect(), + ) + } + } + } + }); + } + } + + fn apply_label(&self, token: &Token) -> String { + match token { + Token::Address(addr) => { + if let Some(label) = self.labels.get(addr) { + format!("{}: [{:?}]", label, addr) + } else { + format_token(token) + } + } + _ => format_token(token), + } + } + + fn decode_cheatcode_inputs(&self, func: &Function, data: &[u8]) -> Option> { + match func.name.as_str() { + "expectRevert" => foundry_utils::decode_revert(data, Some(&self.errors)) + .ok() + .map(|decoded| vec![decoded]), + _ => None, + } + } +} diff --git a/forge/src/trace/identifier.rs b/forge/src/trace/identifier.rs new file mode 100644 index 0000000000000..0d0481de72cf6 --- /dev/null +++ b/forge/src/trace/identifier.rs @@ -0,0 +1,75 @@ +use ethers::abi::{Abi, Address}; +use std::collections::BTreeMap; + +/// Trace identifiers figure out what ABIs and labels belong to all the addresses of the trace. +pub trait TraceIdentifier { + /// Attempts to identify an address in one or more call traces. + /// + /// The tuple is of the format `(contract, label, abi)`, where `contract` is intended to be of + /// the format `":"`, e.g. `"Foo.json:Foo"`. + fn identify_address( + &self, + address: &Address, + code: Option<&Vec>, + ) -> (Option, Option, Option<&Abi>); +} + +/// The local trace identifier keeps track of addresses that are instances of local contracts. +pub struct LocalTraceIdentifier { + local_contracts: BTreeMap, (String, Abi)>, +} + +impl LocalTraceIdentifier { + pub fn new(known_contracts: &BTreeMap)>) -> Self { + Self { + local_contracts: known_contracts + .iter() + .map(|(name, (abi, runtime_code))| { + (runtime_code.clone(), (name.clone(), abi.clone())) + }) + .collect(), + } + } +} + +impl TraceIdentifier for LocalTraceIdentifier { + fn identify_address( + &self, + _: &Address, + code: Option<&Vec>, + ) -> (Option, Option, Option<&Abi>) { + if let Some(code) = code { + if let Some((_, (name, abi))) = self + .local_contracts + .iter() + .find(|(known_code, _)| diff_score(known_code, code) < 0.1) + { + (Some(name.clone()), Some(name.clone()), Some(abi)) + } else { + (None, None, None) + } + } else { + (None, None, None) + } + } +} + +/// Very simple fuzzy matching of contract bytecode. +/// +/// Will fail for small contracts that are essentially all immutable variables. +fn diff_score(a: &[u8], b: &[u8]) -> f64 { + let cutoff_len = usize::min(a.len(), b.len()); + if cutoff_len == 0 { + return 1.0 + } + + let a = &a[..cutoff_len]; + let b = &b[..cutoff_len]; + let mut diff_chars = 0; + for i in 0..cutoff_len { + if a[i] != b[i] { + diff_chars += 1; + } + } + diff_chars as f64 / cutoff_len as f64 +} diff --git a/forge/src/trace/mod.rs b/forge/src/trace/mod.rs new file mode 100644 index 0000000000000..ad411b2817a1e --- /dev/null +++ b/forge/src/trace/mod.rs @@ -0,0 +1,373 @@ +/// Call trace address identifiers. +/// +/// Identifiers figure out what ABIs and labels belong to all the addresses of the trace. +pub mod identifier; +pub use identifier::TraceIdentifier; + +mod decoder; +pub use decoder::CallTraceDecoder; + +use crate::abi::CHEATCODE_ADDRESS; +use ansi_term::Colour; +use ethers::{ + abi::{Address, RawLog}, + types::U256, +}; +use serde::{Deserialize, Serialize}; +use std::fmt::{self, Write}; + +/// An arena of [CallTraceNode]s +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CallTraceArena { + /// The arena of nodes + pub arena: Vec, +} + +impl Default for CallTraceArena { + fn default() -> Self { + CallTraceArena { arena: vec![Default::default()] } + } +} + +impl CallTraceArena { + /// Pushes a new trace into the arena, returning the trace ID + pub fn push_trace(&mut self, entry: usize, new_trace: CallTrace) -> usize { + match new_trace.depth { + // The entry node, just update it + 0 => { + let node = &mut self.arena[0]; + node.trace.update(new_trace); + 0 + } + // We found the parent node, add the new trace as a child + _ if self.arena[entry].trace.depth == new_trace.depth - 1 => { + let id = self.arena.len(); + + let trace_location = self.arena[entry].children.len(); + self.arena[entry].ordering.push(LogCallOrder::Call(trace_location)); + let node = + CallTraceNode { parent: Some(entry), trace: new_trace, ..Default::default() }; + self.arena.push(node); + self.arena[entry].children.push(id); + + id + } + // We haven't found the parent node, go deeper + _ => self.push_trace( + *self.arena[entry].children.last().expect("Disconnected trace"), + new_trace, + ), + } + } + + pub fn addresses_iter(&self) -> impl Iterator>)> { + self.arena.iter().map(|node| { + let code = if node.trace.created { + if let RawOrDecodedReturnData::Raw(bytes) = &node.trace.output { + Some(bytes) + } else { + None + } + } else { + None + }; + + (&node.trace.address, code) + }) + } +} + +const PIPE: &str = " │ "; +const EDGE: &str = " └─ "; +const BRANCH: &str = " ├─ "; +const CALL: &str = "→ "; +const RETURN: &str = "← "; + +impl fmt::Display for CallTraceArena { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn inner( + arena: &CallTraceArena, + writer: &mut (impl Write + ?Sized), + idx: usize, + left: &str, + child: &str, + ) -> fmt::Result { + let node = &arena.arena[idx]; + + // Display trace header + writeln!(writer, "{}{}", left, node.trace)?; + + // Display logs and subcalls + let left_prefix = format!("{}{}", child, BRANCH); + let right_prefix = format!("{}{}", child, PIPE); + for child in &node.ordering { + match child { + LogCallOrder::Log(index) => { + let mut log = String::new(); + write!(log, "{}", node.logs[*index])?; + + // Prepend our tree structure symbols to each line of the displayed log + log.lines().enumerate().try_for_each(|(i, line)| { + writeln!( + writer, + "{}{}", + if i == 0 { &left_prefix } else { &right_prefix }, + line + ) + })?; + } + LogCallOrder::Call(index) => { + inner(arena, writer, node.children[*index], &left_prefix, &right_prefix)?; + } + } + } + + // Display trace return data + let color = trace_color(&node.trace); + write!(writer, "{}{}", child, EDGE)?; + write!(writer, "{}", color.paint(RETURN))?; + if node.trace.created { + if let RawOrDecodedReturnData::Raw(bytes) = &node.trace.output { + writeln!(writer, "{} bytes of code", bytes.len())?; + } else { + unreachable!("We should never have decoded calldata for contract creations"); + } + } else { + writeln!(writer, "{}", node.trace.output)?; + } + + Ok(()) + } + + inner(self, f, 0, " ", " ") + } +} + +/// A raw or decoded log. +#[derive(Debug, Clone)] +pub enum RawOrDecodedLog { + /// A raw log + Raw(RawLog), + /// A decoded log. + /// + /// The first member of the tuple is the event name, and the second is a vector of decoded + /// parameters. + Decoded(String, Vec<(String, String)>), +} + +impl fmt::Display for RawOrDecodedLog { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + RawOrDecodedLog::Raw(log) => { + for (i, topic) in log.topics.iter().enumerate() { + writeln!( + f, + "{:>13}: {}", + if i == 0 { "emit topic 0".to_string() } else { format!("topic {}", i) }, + Colour::Cyan.paint(format!("0x{}", hex::encode(&topic))) + )?; + } + + write!( + f, + " data: {}", + Colour::Cyan.paint(format!("0x{}", hex::encode(&log.data))) + ) + } + RawOrDecodedLog::Decoded(name, params) => { + let params = params + .iter() + .map(|(name, value)| format!("{}: {}", name, value)) + .collect::>() + .join(", "); + + write!(f, "emit {}({})", Colour::Cyan.paint(name.clone()), params) + } + } + } +} + +/// Ordering enum for calls and logs +/// +/// i.e. if Call 0 occurs before Log 0, it will be pushed into the `CallTraceNode`'s ordering before +/// the log. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum LogCallOrder { + Log(usize), + Call(usize), +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +/// A node in the arena +pub struct CallTraceNode { + /// Parent node index in the arena + pub parent: Option, + /// Children node indexes in the arena + pub children: Vec, + /// This node's index in the arena + pub idx: usize, + /// The call trace + pub trace: CallTrace, + /// Logs + #[serde(skip)] + pub logs: Vec, + /// Ordering of child calls and logs + pub ordering: Vec, +} + +// TODO: Maybe unify with output +/// Raw or decoded calldata. +#[derive(Debug, Clone, Deserialize, Serialize)] +pub enum RawOrDecodedCall { + /// Raw calldata + Raw(Vec), + /// Decoded calldata. + /// + /// The first element in the tuple is the function name, and the second element is a vector of + /// decoded parameters. + Decoded(String, Vec), +} + +impl Default for RawOrDecodedCall { + fn default() -> Self { + RawOrDecodedCall::Raw(Vec::new()) + } +} + +/// Raw or decoded return data. +#[derive(Debug, Clone, Deserialize, Serialize)] +pub enum RawOrDecodedReturnData { + /// Raw return data + Raw(Vec), + /// Decoded return data + Decoded(String), +} + +impl Default for RawOrDecodedReturnData { + fn default() -> Self { + RawOrDecodedReturnData::Raw(Vec::new()) + } +} + +impl fmt::Display for RawOrDecodedReturnData { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self { + RawOrDecodedReturnData::Raw(bytes) => { + if bytes.is_empty() { + write!(f, "()") + } else { + write!(f, "0x{}", hex::encode(&bytes)) + } + } + RawOrDecodedReturnData::Decoded(decoded) => write!(f, "{}", decoded.clone()), + } + } +} + +/// A trace of a call. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct CallTrace { + /// The depth of the call + pub depth: usize, + /// Whether the call was successful + pub success: bool, + /// The name of the contract, if any. + /// + /// The format is `":"` for easy lookup in local contracts. + /// + /// This member is not used by the core call tracing functionality (decoding/displaying). The + /// intended use case is for other components that may want to process traces by specific + /// contracts (e.g. gas reports). + pub contract: Option, + /// The label for the destination address, if any + pub label: Option, + /// The destination address of the call + pub address: Address, + /// Whether the call was a contract creation or not + pub created: bool, + /// The value tranferred in the call + pub value: U256, + /// The calldata for the call, or the init code for contract creations + pub data: RawOrDecodedCall, + /// The return data of the call if this was not a contract creation, otherwise it is the + /// runtime bytecode of the created contract + pub output: RawOrDecodedReturnData, + /// The gas cost of the call + pub gas_cost: u64, +} + +impl CallTrace { + /// Updates a trace given another trace + fn update(&mut self, new_trace: Self) { + self.success = new_trace.success; + self.address = new_trace.address; + self.created = new_trace.created; + self.value = new_trace.value; + self.data = new_trace.data; + self.output = new_trace.output; + self.address = new_trace.address; + self.gas_cost = new_trace.gas_cost; + } +} + +impl fmt::Display for CallTrace { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.created { + write!( + f, + "[{}] {}{} {}@{:?}", + self.gas_cost, + Colour::Yellow.paint(CALL), + Colour::Yellow.paint("new"), + self.label.as_ref().unwrap_or(&"".to_string()), + self.address + )?; + } else { + let (func, inputs) = match &self.data { + RawOrDecodedCall::Raw(bytes) => { + // We assume that the fallback function (`data.len() < 4`) counts as decoded + // calldata + assert!(bytes.len() >= 4); + (hex::encode(&bytes[0..4]), hex::encode(&bytes[4..])) + } + RawOrDecodedCall::Decoded(func, inputs) => (func.clone(), inputs.join(", ")), + }; + + let color = trace_color(self); + write!( + f, + "[{}] {}::{}{}({})", + self.gas_cost, + color.paint(self.label.as_ref().unwrap_or(&self.address.to_string())), + color.paint(func), + if !self.value.is_zero() { + format!("{{value: {}}}", self.value) + } else { + "".to_string() + }, + inputs + )?; + } + + Ok(()) + } +} + +/// Specifies the kind of trace. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum TraceKind { + Deployment, + Setup, + Execution, +} + +/// Chooses the color of the trace depending on the destination address and status of the call. +fn trace_color(trace: &CallTrace) -> Colour { + if trace.address == *CHEATCODE_ADDRESS { + Colour::Blue + } else if trace.success { + Colour::Green + } else { + Colour::Red + } +}