From 8602e1b58e6875e79b3794bc5a29ff5c330d3261 Mon Sep 17 00:00:00 2001 From: brockelmore <31553173+brockelmore@users.noreply.github.com> Date: Thu, 23 Dec 2021 13:21:02 -0700 Subject: [PATCH] feat(forge): Add call tracing support (#192) * first pass * fixes * fmt * better fmting * updates colored prints, better dev ux, verbosity > 2 trace printing * fmt * updates * fmt * fix after master merge * fix tests post master merge * warning fixes * fmt * lots of fixes * fmt * fix * cyan color * fixes * prettier raw logs + parse setup contracts * update diff_score threshold * better printing * remove integration tests * improvements * improvements + fmt + clippy * fixes * more cleanup * cleanup and verbosity > 3 setup print * refactor printing * documentation + cleanup * fix negative number printing * fix tests to match master and fix tracing_enabled * fix unnecessary trace_index set * refactor runner tracing + tracing_enabled * nits + value printing * last nits --- Cargo.lock | 1 + cli/src/cmd/test.rs | 40 +- evm-adapters/Cargo.toml | 1 + evm-adapters/src/call_tracing.rs | 577 ++++++++++++++++++ evm-adapters/src/evmodin.rs | 16 + evm-adapters/src/lib.rs | 16 + .../sputnik/cheatcodes/cheatcode_handler.rs | 294 ++++++++- .../cheatcodes/memory_stackstate_owned.rs | 29 +- evm-adapters/src/sputnik/cheatcodes/mod.rs | 2 +- evm-adapters/src/sputnik/evm.rs | 34 +- .../src/sputnik/forked_backend/rpc.rs | 2 +- evm-adapters/src/sputnik/mod.rs | 29 + evm-adapters/testdata/Trace.sol | 75 +++ forge/src/multi_runner.rs | 69 ++- forge/src/runner.rs | 75 ++- 15 files changed, 1200 insertions(+), 60 deletions(-) create mode 100644 evm-adapters/src/call_tracing.rs create mode 100644 evm-adapters/testdata/Trace.sol diff --git a/Cargo.lock b/Cargo.lock index 5abe2d070d5e..593118d67d13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1336,6 +1336,7 @@ dependencies = [ name = "evm-adapters" version = "0.1.0" dependencies = [ + "ansi_term", "bytes", "ethers", "evm", diff --git a/cli/src/cmd/test.rs b/cli/src/cmd/test.rs index 5789e979bb45..d61dd2c5bd27 100644 --- a/cli/src/cmd/test.rs +++ b/cli/src/cmd/test.rs @@ -158,8 +158,14 @@ impl Cmd for TestArgs { let backend = Arc::new(backend); let precompiles = PRECOMPILES_MAP.clone(); - let evm = - Executor::new_with_cheatcodes(backend, env.gas_limit, &cfg, &precompiles, ffi); + let evm = Executor::new_with_cheatcodes( + backend, + env.gas_limit, + &cfg, + &precompiles, + ffi, + verbosity > 2, + ); test(builder, project, evm, pattern, json, verbosity, allow_failure) } @@ -318,6 +324,36 @@ fn test>( } println!(); + + if verbosity > 2 { + if let (Some(traces), Some(identified_contracts)) = + (&result.traces, &result.identified_contracts) + { + let mut ident = identified_contracts.clone(); + if verbosity > 3 { + // print setup calls as well + traces.iter().for_each(|trace| { + trace.pretty_print( + 0, + &runner.known_contracts, + &mut ident, + &runner.evm, + "", + ); + }); + } else if !traces.is_empty() { + traces.last().expect("no last but not empty").pretty_print( + 0, + &runner.known_contracts, + &mut ident, + &runner.evm, + "", + ); + } + + println!(); + } + } } } } diff --git a/evm-adapters/Cargo.toml b/evm-adapters/Cargo.toml index 85403c778c13..66b64de90820 100644 --- a/evm-adapters/Cargo.toml +++ b/evm-adapters/Cargo.toml @@ -27,6 +27,7 @@ futures = "0.3.17" revm_precompiles = "0.1.0" serde_json = "1.0.72" serde = "1.0.130" +ansi_term = "0.12.1" [dev-dependencies] evmodin = { git = "https://github.com/vorot93/evmodin", features = ["util"] } diff --git a/evm-adapters/src/call_tracing.rs b/evm-adapters/src/call_tracing.rs new file mode 100644 index 000000000000..eb91a6878fae --- /dev/null +++ b/evm-adapters/src/call_tracing.rs @@ -0,0 +1,577 @@ +use ethers::{ + abi::{Abi, FunctionExt, RawLog}, + types::{H160, U256}, +}; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +use ansi_term::Colour; + +#[cfg(feature = "sputnik")] +use crate::sputnik::cheatcodes::{cheatcode_handler::CHEATCODE_ADDRESS, HEVM_ABI}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +/// An arena of `CallTraceNode`s +pub struct CallTraceArena { + /// The arena of nodes + pub arena: Vec, + /// The entry index, denoting the first node's index in the arena + pub entry: usize, +} + +impl Default for CallTraceArena { + fn default() -> Self { + CallTraceArena { arena: vec![Default::default()], entry: 0 } + } +} + +/// Function output type +pub enum Output { + /// Decoded vec of tokens + Token(Vec), + /// Not decoded raw bytes + Raw(Vec), +} + +impl Output { + /// Prints the output of a function call + pub fn print(self, color: Colour, left: &str) { + match self { + Output::Token(token) => { + let strings = + token.into_iter().map(format_token).collect::>().join(", "); + println!( + "{} └─ {} {}", + left.replace("├─", "│").replace("└─", " "), + color.paint("←"), + if strings.is_empty() { "()" } else { &*strings } + ); + } + Output::Raw(bytes) => { + println!( + "{} └─ {} {}", + left.replace("├─", "│").replace("└─", " "), + color.paint("←"), + if bytes.is_empty() { + "()".to_string() + } else { + "0x".to_string() + &hex::encode(&bytes) + } + ); + } + } + } +} + +impl CallTraceArena { + /// Pushes a new trace into the arena, returning the trace that was passed in with updated + /// values + pub fn push_trace(&mut self, entry: usize, mut new_trace: &mut CallTrace) { + match new_trace.depth { + // The entry node, just update it + 0 => { + self.update(new_trace.clone()); + } + // we found the parent node, add the new trace as a child + _ if self.arena[entry].trace.depth == new_trace.depth - 1 => { + new_trace.idx = self.arena.len(); + new_trace.location = self.arena[entry].children.len(); + self.arena[entry].ordering.push(LogCallOrder::Call(new_trace.location)); + let node = CallTraceNode { + parent: Some(entry), + idx: self.arena.len(), + trace: new_trace.clone(), + ..Default::default() + }; + self.arena.push(node); + self.arena[entry].children.push(new_trace.idx); + } + // we haven't found the parent node, go deeper + _ => self.push_trace( + *self.arena[entry].children.last().expect("Disconnected trace"), + new_trace, + ), + } + } + + /// Updates the values in the calltrace held by the arena based on the passed in trace + pub fn update(&mut self, trace: CallTrace) { + let node = &mut self.arena[trace.idx]; + node.trace.update(trace); + } + + /// Updates `identified_contracts` for future use so that after an `evm.reset_state()`, we + /// already know which contract corresponds to which address. + /// + /// `idx` is the call arena index to start at. Generally this will be 0, but if you want to + /// update a subset of the tree, you can pass in a different index + /// + /// `contracts` are the known contracts of (name => (abi, runtime_code)). It is used to identify + /// a deployed contract. + /// + /// `identified_contracts` are the identified contract addresses built up from comparing + /// deployed contracts against `contracts` + /// + /// `evm` is the evm that we used so that we can grab deployed code if needed. A lot of times, + /// the evm state is reset so we wont have any code but it can be useful if we want to + /// pretty print right after a test. + pub fn update_identified<'a, S: Clone, E: crate::Evm>( + &self, + idx: usize, + contracts: &BTreeMap)>, + identified_contracts: &mut BTreeMap, + evm: &'a E, + ) { + let trace = &self.arena[idx].trace; + + #[cfg(feature = "sputnik")] + identified_contracts.insert(*CHEATCODE_ADDRESS, ("VM".to_string(), HEVM_ABI.clone())); + + let res = identified_contracts.get(&trace.addr); + if res.is_none() { + let code = if trace.created { trace.output.clone() } else { evm.code(trace.addr) }; + if let Some((name, (abi, _code))) = contracts + .iter() + .find(|(_key, (_abi, known_code))| diff_score(known_code, &code) < 0.10) + { + identified_contracts.insert(trace.addr, (name.to_string(), abi.clone())); + } + } + + // update all children nodes + self.update_children(idx, contracts, identified_contracts, evm); + } + + /// Updates all children nodes by recursing into `update_identified` + pub fn update_children<'a, S: Clone, E: crate::Evm>( + &self, + idx: usize, + contracts: &BTreeMap)>, + identified_contracts: &mut BTreeMap, + evm: &'a E, + ) { + let children_idxs = &self.arena[idx].children; + children_idxs.iter().for_each(|child_idx| { + self.update_identified(*child_idx, contracts, identified_contracts, evm); + }); + } + + /// Pretty print a CallTraceArena + /// + /// `idx` is the call arena index to start at. Generally this will be 0, but if you want to + /// print a subset of the tree, you can pass in a different index + /// + /// `contracts` are the known contracts of (name => (abi, runtime_code)). It is used to identify + /// a deployed contract. + /// + /// `identified_contracts` are the identified contract addresses built up from comparing + /// deployed contracts against `contracts` + /// + /// `evm` is the evm that we used so that we can grab deployed code if needed. A lot of times, + /// the evm state is reset so we wont have any code but it can be useful if we want to + /// pretty print right after a test. + /// + /// For a user, `left` input should generally be `""`. Left is used recursively + /// to build the tree print out structure and is built up as we recurse down the tree. + pub fn pretty_print<'a, S: Clone, E: crate::Evm>( + &self, + idx: usize, + contracts: &BTreeMap)>, + identified_contracts: &mut BTreeMap, + evm: &'a E, + left: &str, + ) { + let trace = &self.arena[idx].trace; + + #[cfg(feature = "sputnik")] + identified_contracts.insert(*CHEATCODE_ADDRESS, ("VM".to_string(), HEVM_ABI.clone())); + + #[cfg(feature = "sputnik")] + // color the trace function call & output by success + let color = if trace.addr == *CHEATCODE_ADDRESS { + Colour::Blue + } else if trace.success { + Colour::Green + } else { + Colour::Red + }; + + #[cfg(not(feature = "sputnik"))] + let color = if trace.success { Colour::Green } else { Colour::Red }; + + // we have to clone the name and abi because identified_contracts is later borrowed + // immutably + let res = if let Some((name, abi)) = identified_contracts.get(&trace.addr) { + Some((name.clone(), abi.clone())) + } else { + None + }; + if res.is_none() { + // get the code to compare + let code = if trace.created { trace.output.clone() } else { evm.code(trace.addr) }; + if let Some((name, (abi, _code))) = contracts + .iter() + .find(|(_key, (_abi, known_code))| diff_score(known_code, &code) < 0.10) + { + // found matching contract, insert and print + identified_contracts.insert(trace.addr, (name.to_string(), abi.clone())); + if trace.created { + println!("{}{} {}@{}", left, Colour::Yellow.paint("→ new"), name, trace.addr); + self.print_children_and_logs( + idx, + Some(abi), + contracts, + identified_contracts, + evm, + left, + ); + println!( + "{} └─ {} {} bytes of code", + left.replace("├─", "│").replace("└─", " "), + color.paint("←"), + trace.output.len() + ); + } else { + // re-enter this function at the current node + self.pretty_print(idx, contracts, identified_contracts, evm, left); + } + } else if trace.created { + // we couldn't identify, print the children and logs without the abi + println!("{}{} @{}", left, Colour::Yellow.paint("→ new"), trace.addr); + self.print_children_and_logs(idx, None, contracts, identified_contracts, evm, left); + println!( + "{} └─ {} {} bytes of code", + left.replace("├─", "│").replace("└─", " "), + color.paint("←"), + trace.output.len() + ); + } else { + let output = trace.print_func_call(None, None, color, left); + self.print_children_and_logs(idx, None, contracts, identified_contracts, evm, left); + output.print(color, left); + } + } else if let Some((name, abi)) = res { + if trace.created { + println!("{}{} {}@{}", left, Colour::Yellow.paint("→ new"), name, trace.addr); + self.print_children_and_logs( + idx, + Some(&abi), + contracts, + identified_contracts, + evm, + left, + ); + println!( + "{} └─ {} {} bytes of code", + left.replace("├─", "│").replace("└─", " "), + color.paint("←"), + trace.output.len() + ); + } else { + let output = trace.print_func_call(Some(&abi), Some(&name), color, left); + self.print_children_and_logs( + idx, + Some(&abi), + contracts, + identified_contracts, + evm, + left, + ); + output.print(color, left); + } + } + } + + /// Prints child calls and logs in order + pub fn print_children_and_logs<'a, S: Clone, E: crate::Evm>( + &self, + node_idx: usize, + abi: Option<&Abi>, + contracts: &BTreeMap)>, + identified_contracts: &mut BTreeMap, + evm: &'a E, + left: &str, + ) { + // Ordering stores a vec of `LogCallOrder` which is populated based on if + // a log or a call was called first. This makes it such that we always print + // logs and calls in the correct order + self.arena[node_idx].ordering.iter().for_each(|ordering| match ordering { + LogCallOrder::Log(index) => { + self.arena[node_idx].print_log(*index, abi, left); + } + LogCallOrder::Call(index) => { + self.pretty_print( + self.arena[node_idx].children[*index], + contracts, + identified_contracts, + evm, + &(left.replace("├─", "│").replace("└─", " ") + " ├─ "), + ); + } + }); + } +} + +#[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, +} + +impl CallTraceNode { + /// Prints a log at a particular index, optionally decoding if abi is provided + pub fn print_log(&self, index: usize, abi: Option<&Abi>, left: &str) { + let log = &self.logs[index]; + let right = " ├─ "; + if let Some(abi) = abi { + for (event_name, overloaded_events) in abi.events.iter() { + for event in overloaded_events.iter() { + if event.signature() == log.topics[0] { + let params = event.parse_log(log.clone()).expect("Bad event").params; + let strings = params + .into_iter() + .map(|param| format!("{}: {}", param.name, format_token(param.value))) + .collect::>() + .join(", "); + println!( + "{}emit {}({})", + left.replace("├─", "│") + right, + Colour::Cyan.paint(event_name), + strings + ); + return + } + } + } + } + // we didnt decode the log, print it as an unknown log + for (i, topic) in log.topics.iter().enumerate() { + let right = if i == log.topics.len() - 1 && log.data.is_empty() { + " └─ " + } else { + " ├─" + }; + println!( + "{}{}topic {}: {}", + if i == 0 { + left.replace("├─", "│") + right + } else { + left.replace("├─", "│") + " │ " + }, + if i == 0 { " emit " } else { " " }, + i, + Colour::Cyan.paint(format!("0x{}", hex::encode(&topic))) + ) + } + println!( + "{} data: {}", + left.replace("├─", "│").replace("└─", " ") + " │ ", + Colour::Cyan.paint(format!("0x{}", hex::encode(&log.data))) + ) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +/// 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. +pub enum LogCallOrder { + Log(usize), + Call(usize), +} + +/// Call trace of a tx +#[derive(Clone, Default, Debug, Deserialize, Serialize)] +pub struct CallTrace { + pub depth: usize, + pub location: usize, + pub idx: usize, + /// Successful + pub success: bool, + /// Callee + pub addr: H160, + /// Creation + pub created: bool, + /// Ether value transfer + pub value: U256, + /// Call data, including function selector (if applicable) + pub data: Vec, + /// Gas cost + pub cost: u64, + /// Output + pub output: Vec, +} + +impl CallTrace { + /// Updates a trace given another trace + fn update(&mut self, new_trace: Self) { + self.success = new_trace.success; + self.addr = new_trace.addr; + self.cost = new_trace.cost; + self.output = new_trace.output; + self.data = new_trace.data; + self.addr = new_trace.addr; + } + + /// Prints function call, returning the decoded or raw output + pub fn print_func_call( + &self, + abi: Option<&Abi>, + name: Option<&String>, + color: Colour, + left: &str, + ) -> Output { + if let (Some(abi), Some(name)) = (abi, name) { + // Is data longer than 4, meaning we can attempt to decode it + if self.data.len() >= 4 { + for (func_name, overloaded_funcs) in abi.functions.iter() { + for func in overloaded_funcs.iter() { + if func.selector() == self.data[0..4] { + let mut strings = "".to_string(); + if !self.data[4..].is_empty() { + let params = func + .decode_input(&self.data[4..]) + .expect("Bad func data decode"); + strings = params + .into_iter() + .map(format_token) + .collect::>() + .join(", "); + } + + println!( + "{}[{}] {}::{}{}({})", + left, + self.cost, + color.paint(name), + color.paint(func_name), + if self.value > 0.into() { + format!("{{value: {}}}", self.value) + } else { + "".to_string() + }, + strings, + ); + + if !self.output.is_empty() { + return Output::Token( + func.decode_output(&self.output[..]) + .expect("Bad func output decode"), + ) + } else { + return Output::Raw(vec![]) + } + } + } + } + } else { + // fallback function + println!( + "{}[{}] {}::fallback{}()", + left, + self.cost, + color.paint(name), + if self.value > 0.into() { + format!("{{value: {}}}", self.value) + } else { + "".to_string() + } + ); + + return Output::Raw(self.output[..].to_vec()) + } + } + + // We couldn't decode the function call, so print it as an abstract call + println!( + "{}[{}] {}::{}{}({})", + left, + self.cost, + color.paint(format!("{}", self.addr)), + if self.data.len() >= 4 { + hex::encode(&self.data[0..4]) + } else { + hex::encode(&self.data[..]) + }, + if self.value > 0.into() { + format!("{{value: {}}}", self.value) + } else { + "".to_string() + }, + if self.data.len() >= 4 { + hex::encode(&self.data[4..]) + } else { + hex::encode(&vec![][..]) + }, + ); + + Output::Raw(self.output[..].to_vec()) + } +} + +// Gets pretty print strings for tokens +fn format_token(param: ethers::abi::Token) -> String { + use ethers::abi::Token; + match param { + Token::Address(addr) => format!("{:?}", addr), + Token::FixedBytes(bytes) => format!("0x{}", hex::encode(&bytes)), + Token::Bytes(bytes) => format!("0x{}", hex::encode(&bytes)), + Token::Int(mut num) => { + if num.bit(255) { + num = num - 1; + format!("-{}", num.overflowing_neg().0) + } else { + num.to_string() + } + } + Token::Uint(num) => num.to_string(), + Token::Bool(b) => format!("{}", b), + Token::String(s) => s, + Token::FixedArray(tokens) => { + let string = tokens.into_iter().map(format_token).collect::>().join(", "); + format!("[{}]", string) + } + Token::Array(tokens) => { + let string = tokens.into_iter().map(format_token).collect::>().join(", "); + format!("[{}]", string) + } + Token::Tuple(tokens) => { + let string = tokens.into_iter().map(format_token).collect::>().join(", "); + format!("({})", string) + } + } +} + +// very simple fuzzy matching to account for immutables. Will fail for small contracts that are +// basically all immutable vars +fn diff_score(bytecode1: &[u8], bytecode2: &[u8]) -> f64 { + let cutoff_len = usize::min(bytecode1.len(), bytecode2.len()); + let b1 = &bytecode1[..cutoff_len]; + let b2 = &bytecode2[..cutoff_len]; + if cutoff_len == 0 { + return 1.0 + } + + let mut diff_chars = 0; + for i in 0..cutoff_len { + if b1[i] != b2[i] { + diff_chars += 1; + } + } + + // println!("diff_score {}", diff_chars as f64 / cutoff_len as f64); + diff_chars as f64 / cutoff_len as f64 +} diff --git a/evm-adapters/src/evmodin.rs b/evm-adapters/src/evmodin.rs index 708500db2d8c..0c19c1751b0b 100644 --- a/evm-adapters/src/evmodin.rs +++ b/evm-adapters/src/evmodin.rs @@ -63,6 +63,14 @@ impl Evm for EvmOdin { self.host = state; } + fn set_tracing_enabled(&mut self, _enabled: bool) -> bool { + false + } + + fn tracing_enabled(&self) -> bool { + false + } + fn initialize_contracts>(&mut self, contracts: I) { contracts.into_iter().for_each(|(address, bytecode)| { self.host.set_code(address, bytecode.0); @@ -73,6 +81,14 @@ impl Evm for EvmOdin { &self.host } + fn code(&self, address: Address) -> Vec { + if let Some(bytes) = self.host.get_code(&address) { + bytes.to_vec() + } else { + vec![] + } + } + fn all_logs(&self) -> Vec { vec![] } diff --git a/evm-adapters/src/lib.rs b/evm-adapters/src/lib.rs index 78846aaa8136..ddd7e62ec736 100644 --- a/evm-adapters/src/lib.rs +++ b/evm-adapters/src/lib.rs @@ -8,10 +8,13 @@ pub mod sputnik; pub mod evmodin; mod blocking_provider; +use crate::call_tracing::CallTraceArena; pub use blocking_provider::BlockingProvider; pub mod fuzz; +pub mod call_tracing; + use ethers::{ abi::{Detokenize, Tokenize}, contract::{decode_function_data, encode_function_data}, @@ -65,12 +68,20 @@ pub trait Evm { /// Gets a reference to the current state of the EVM fn state(&self) -> &State; + fn code(&self, address: Address) -> Vec; + /// Sets the balance at the specified address fn set_balance(&mut self, address: Address, amount: U256); /// Resets the EVM's state to the provided value fn reset(&mut self, state: State); + /// Turns on/off tracing, returning the previously set value + fn set_tracing_enabled(&mut self, enabled: bool) -> bool; + + /// Returns whether tracing is enabled + fn tracing_enabled(&self) -> bool; + /// Gets all logs from the execution, regardless of reverts fn all_logs(&self) -> Vec; @@ -95,6 +106,11 @@ pub trait Evm { } } + fn traces(&self) -> Vec { + vec![] + } + + fn reset_traces(&mut self) {} /// Executes the specified EVM call against the state // TODO: Should we just make this take a `TransactionRequest` or other more // ergonomic type? diff --git a/evm-adapters/src/sputnik/cheatcodes/cheatcode_handler.rs b/evm-adapters/src/sputnik/cheatcodes/cheatcode_handler.rs index 6dbd23cd7f57..fac44a1982ea 100644 --- a/evm-adapters/src/sputnik/cheatcodes/cheatcode_handler.rs +++ b/evm-adapters/src/sputnik/cheatcodes/cheatcode_handler.rs @@ -4,6 +4,7 @@ use super::{ HEVMCalls, HevmConsoleEvents, }; use crate::{ + call_tracing::{CallTrace, CallTraceArena, LogCallOrder}, sputnik::{Executor, SputnikExecutor}, Evm, }; @@ -66,6 +67,7 @@ pub static DUMMY_OUTPUT: [u8; 320] = [0u8; 320]; pub struct CheatcodeHandler { handler: H, enable_ffi: bool, + enable_trace: bool, console_logs: Vec, } @@ -125,6 +127,16 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> SputnikExecutor bool { + let curr = self.state_mut().trace_enabled; + self.state_mut().trace_enabled = enabled; + curr + } + + fn tracing_enabled(&self) -> bool { + self.state().trace_enabled + } + fn gas_left(&self) -> U256 { // NB: We do this to avoid `function cannot return without recursing` U256::from(self.state().metadata().gasometer().gas()) @@ -175,8 +187,14 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> SputnikExecutor (s, v), - Capture::Trap(_) => unreachable!(), + Capture::Exit((s, v)) => { + self.state_mut().increment_call_index(); + (s, v) + } + Capture::Trap(_) => { + self.state_mut().increment_call_index(); + unreachable!() + } } } @@ -206,8 +224,14 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> SputnikExecutor s, - Capture::Trap(_) => unreachable!(), + Capture::Exit((s, _, _)) => { + self.state_mut().increment_call_index(); + s + } + Capture::Trap(_) => { + self.state_mut().increment_call_index(); + unreachable!() + } } } @@ -219,6 +243,19 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> SputnikExecutor Vec { + let logs = self.state().substate.logs().to_vec(); + logs.into_iter().map(|log| RawLog { topics: log.topics, data: log.data }).collect() + } + + fn traces(&self) -> Vec { + self.state().traces.clone() + } + + fn reset_traces(&mut self) { + self.state_mut().reset_traces(); + } + fn logs(&self) -> Vec { let logs = self.state().substate.logs().to_vec(); logs.into_iter().filter_map(convert_log).chain(self.console_logs.clone()).collect() @@ -243,6 +280,7 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> config: &'a Config, precompiles: &'b P, enable_ffi: bool, + enable_trace: bool, ) -> Self { // make this a cheatcode-enabled backend let backend = CheatcodeBackend { backend, cheats: Default::default() }; @@ -250,11 +288,16 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> // create the memory stack state (owned, so that we can modify the backend via // self.state_mut on the transact_call fn) let metadata = StackSubstateMetadata::new(gas_limit, config); - let state = MemoryStackStateOwned::new(metadata, backend); + let state = MemoryStackStateOwned::new(metadata, backend, enable_trace); // create the executor and wrap it with the cheatcode handler let executor = StackExecutor::new_with_precompiles(state, config, precompiles); - let executor = CheatcodeHandler { handler: executor, enable_ffi, console_logs: Vec::new() }; + let executor = CheatcodeHandler { + handler: executor, + enable_ffi, + enable_trace, + console_logs: Vec::new(), + }; let mut evm = Executor::from_executor(executor, gas_limit); @@ -298,7 +341,8 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> CheatcodeStackExecutor<'a, 'b, B, P> msg_sender: H160, ) -> Capture<(ExitReason, Vec), Infallible> { let mut res = vec![]; - + let pre_index = self.state().trace_index; + let trace = self.start_trace(*CHEATCODE_ADDRESS, input.clone(), 0.into(), false); // Get a mutable ref to the state so we can apply the cheats let state = self.state_mut(); let decoded = match HEVMCalls::decode(&input) { @@ -462,6 +506,8 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> CheatcodeStackExecutor<'a, 'b, B, P> } }; + self.fill_trace(&trace, true, Some(res.clone()), pre_index); + // TODO: Add more cheat codes. Capture::Exit((ExitReason::Succeed(ExitSucceed::Stopped), res)) } @@ -475,6 +521,54 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> CheatcodeStackExecutor<'a, 'b, B, P> } } + fn start_trace( + &mut self, + address: H160, + input: Vec, + transfer: U256, + creation: bool, + ) -> Option { + if self.enable_trace { + let mut trace: CallTrace = CallTrace { + // depth only starts tracking at first child substate and is 0. so add 1 when depth + // is some. + depth: if let Some(depth) = self.state().metadata().depth() { + depth + 1 + } else { + 0 + }, + addr: address, + created: creation, + data: input, + value: transfer, + ..Default::default() + }; + + self.state_mut().trace_mut().push_trace(0, &mut trace); + self.state_mut().trace_index = trace.idx; + Some(trace) + } else { + None + } + } + + fn fill_trace( + &mut self, + new_trace: &Option, + success: bool, + output: Option>, + pre_trace_index: usize, + ) { + self.state_mut().trace_index = pre_trace_index; + if let Some(new_trace) = new_trace { + let used_gas = self.handler.used_gas(); + let trace = &mut self.state_mut().trace_mut().arena[new_trace.idx].trace; + trace.output = output.unwrap_or_default(); + trace.cost = used_gas; + trace.success = success; + } + } + // NB: This function is copy-pasted from uptream's call_inner #[allow(clippy::too_many_arguments)] fn call_inner( @@ -488,11 +582,22 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> CheatcodeStackExecutor<'a, 'b, B, P> take_stipend: bool, context: Context, ) -> Capture<(ExitReason, Vec), Infallible> { + let pre_index = self.state().trace_index; + let trace = self.start_trace( + code_address, + input.clone(), + transfer.as_ref().map(|x| x.value).unwrap_or_default(), + false, + ); + macro_rules! try_or_fail { ( $e:expr ) => { match $e { Ok(v) => v, - Err(e) => return Capture::Exit((e.into(), Vec::new())), + Err(e) => { + self.fill_trace(&trace, false, None, pre_index); + return Capture::Exit((e.into(), Vec::new())) + } } }; } @@ -526,12 +631,12 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> CheatcodeStackExecutor<'a, 'b, B, P> } let code = self.code(code_address); - self.handler.enter_substate(gas_limit, is_static); self.state_mut().touch(context.address); if let Some(depth) = self.state().metadata().depth() { if depth > self.config().call_stack_limit { + self.fill_trace(&trace, false, None, pre_index); let _ = self.handler.exit_substate(StackExitKind::Reverted); return Capture::Exit((ExitError::CallTooDeep.into(), Vec::new())) } @@ -541,6 +646,7 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> CheatcodeStackExecutor<'a, 'b, B, P> match self.state_mut().transfer(transfer) { Ok(()) => (), Err(e) => { + self.fill_trace(&trace, false, None, pre_index); let _ = self.handler.exit_substate(StackExitKind::Reverted); return Capture::Exit((ExitReason::Error(e), Vec::new())) } @@ -559,11 +665,15 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> CheatcodeStackExecutor<'a, 'b, B, P> for Log { address, topics, data } in logs { match self.log(address, topics, data) { Ok(_) => continue, - Err(error) => return Capture::Exit((ExitReason::Error(error), output)), + Err(error) => { + self.fill_trace(&trace, false, Some(output.clone()), pre_index); + return Capture::Exit((ExitReason::Error(error), output)) + } } } let _ = self.state_mut().metadata_mut().gasometer_mut().record_cost(cost); + self.fill_trace(&trace, true, Some(output.clone()), pre_index); let _ = self.handler.exit_substate(StackExitKind::Succeeded); Capture::Exit((ExitReason::Succeed(exit_status), output)) } @@ -575,6 +685,7 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> CheatcodeStackExecutor<'a, 'b, B, P> } PrecompileFailure::Fatal { exit_status } => ExitReason::Fatal(exit_status), }; + self.fill_trace(&trace, false, None, pre_index); let _ = self.handler.exit_substate(StackExitKind::Failed); Capture::Exit((e, Vec::new())) } @@ -586,23 +697,28 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> CheatcodeStackExecutor<'a, 'b, B, P> let config = self.config().clone(); let mut runtime = Runtime::new(Rc::new(code), Rc::new(input), context, &config); let reason = self.execute(&mut runtime); + // // log::debug!(target: "evm", "Call execution using address {}: {:?}", code_address, // reason); match reason { ExitReason::Succeed(s) => { + self.fill_trace(&trace, true, Some(runtime.machine().return_value()), pre_index); let _ = self.handler.exit_substate(StackExitKind::Succeeded); Capture::Exit((ExitReason::Succeed(s), runtime.machine().return_value())) } ExitReason::Error(e) => { + self.fill_trace(&trace, false, Some(runtime.machine().return_value()), pre_index); let _ = self.handler.exit_substate(StackExitKind::Failed); Capture::Exit((ExitReason::Error(e), Vec::new())) } ExitReason::Revert(e) => { + self.fill_trace(&trace, false, Some(runtime.machine().return_value()), pre_index); let _ = self.handler.exit_substate(StackExitKind::Reverted); Capture::Exit((ExitReason::Revert(e), runtime.machine().return_value())) } ExitReason::Fatal(e) => { + self.fill_trace(&trace, false, Some(runtime.machine().return_value()), pre_index); self.state_mut().metadata_mut().gasometer_mut().fail(); let _ = self.handler.exit_substate(StackExitKind::Failed); Capture::Exit((ExitReason::Fatal(e), Vec::new())) @@ -620,11 +736,20 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> CheatcodeStackExecutor<'a, 'b, B, P> target_gas: Option, take_l64: bool, ) -> Capture<(ExitReason, Option, Vec), Infallible> { + let pre_index = self.state().trace_index; + + let address = self.create_address(scheme); + + let trace = self.start_trace(address, init_code.clone(), value, true); + macro_rules! try_or_fail { ( $e:expr ) => { match $e { Ok(v) => v, - Err(e) => return Capture::Exit((e.into(), None, Vec::new())), + Err(e) => { + self.fill_trace(&trace, false, None, pre_index); + return Capture::Exit((e.into(), None, Vec::new())) + } } }; } @@ -642,18 +767,18 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> CheatcodeStackExecutor<'a, 'b, B, P> gas - gas / 64 } - let address = self.create_address(scheme); - self.state_mut().metadata_mut().access_address(caller); self.state_mut().metadata_mut().access_address(address); if let Some(depth) = self.state().metadata().depth() { if depth > self.config().call_stack_limit { + self.fill_trace(&trace, false, None, pre_index); return Capture::Exit((ExitError::CallTooDeep.into(), None, Vec::new())) } } if self.balance(caller) < value { + self.fill_trace(&trace, false, None, pre_index); return Capture::Exit((ExitError::OutOfFund.into(), None, Vec::new())) } @@ -681,11 +806,13 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> CheatcodeStackExecutor<'a, 'b, B, P> { if self.code_size(address) != U256::zero() { + self.fill_trace(&trace, false, None, pre_index); let _ = self.handler.exit_substate(StackExitKind::Failed); return Capture::Exit((ExitError::CreateCollision.into(), None, Vec::new())) } if self.handler.nonce(address) > U256::zero() { + self.fill_trace(&trace, false, None, pre_index); let _ = self.handler.exit_substate(StackExitKind::Failed); return Capture::Exit((ExitError::CreateCollision.into(), None, Vec::new())) } @@ -698,6 +825,7 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> CheatcodeStackExecutor<'a, 'b, B, P> match self.state_mut().transfer(transfer) { Ok(()) => (), Err(e) => { + self.fill_trace(&trace, false, None, pre_index); let _ = self.handler.exit_substate(StackExitKind::Reverted); return Capture::Exit((ExitReason::Error(e), None, Vec::new())) } @@ -720,6 +848,7 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> CheatcodeStackExecutor<'a, 'b, B, P> // As of EIP-3541 code starting with 0xef cannot be deployed if let Err(e) = check_first_byte(self.config(), &out) { self.state_mut().metadata_mut().gasometer_mut().fail(); + self.fill_trace(&trace, false, None, pre_index); let _ = self.handler.exit_substate(StackExitKind::Failed); return Capture::Exit((e.into(), None, Vec::new())) } @@ -727,6 +856,7 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> CheatcodeStackExecutor<'a, 'b, B, P> if let Some(limit) = self.config().create_contract_limit { if out.len() > limit { self.state_mut().metadata_mut().gasometer_mut().fail(); + self.fill_trace(&trace, false, None, pre_index); let _ = self.handler.exit_substate(StackExitKind::Failed); return Capture::Exit(( ExitError::CreateContractLimit.into(), @@ -738,12 +868,15 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> CheatcodeStackExecutor<'a, 'b, B, P> match self.state_mut().metadata_mut().gasometer_mut().record_deposit(out.len()) { Ok(()) => { + self.fill_trace(&trace, true, Some(out.clone()), pre_index); let e = self.handler.exit_substate(StackExitKind::Succeeded); self.state_mut().set_code(address, out); + // this may overwrite the trace and thats okay try_or_fail!(e); Capture::Exit((ExitReason::Succeed(s), Some(address), Vec::new())) } Err(e) => { + self.fill_trace(&trace, false, None, pre_index); let _ = self.handler.exit_substate(StackExitKind::Failed); Capture::Exit((ExitReason::Error(e), None, Vec::new())) } @@ -751,15 +884,18 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> CheatcodeStackExecutor<'a, 'b, B, P> } ExitReason::Error(e) => { self.state_mut().metadata_mut().gasometer_mut().fail(); + self.fill_trace(&trace, false, None, pre_index); let _ = self.handler.exit_substate(StackExitKind::Failed); Capture::Exit((ExitReason::Error(e), None, Vec::new())) } ExitReason::Revert(e) => { + self.fill_trace(&trace, false, Some(runtime.machine().return_value()), pre_index); let _ = self.handler.exit_substate(StackExitKind::Reverted); Capture::Exit((ExitReason::Revert(e), None, runtime.machine().return_value())) } ExitReason::Fatal(e) => { self.state_mut().metadata_mut().gasometer_mut().fail(); + self.fill_trace(&trace, false, None, pre_index); let _ = self.handler.exit_substate(StackExitKind::Failed); Capture::Exit((ExitReason::Fatal(e), None, Vec::new())) } @@ -788,7 +924,6 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> Handler for CheatcodeStackExecutor<'a // to the state. // NB: This is very similar to how Optimism's custom intercept logic to "predeploys" work // (e.g. with the StateManager) - if code_address == *CHEATCODE_ADDRESS { self.apply_cheatcode(input, context.caller) } else if code_address == *CONSOLE_ADDRESS { @@ -974,6 +1109,13 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> Handler for CheatcodeStackExecutor<'a } fn log(&mut self, address: H160, topics: Vec, data: Vec) -> Result<(), ExitError> { + if self.state().trace_enabled { + let index = self.state().trace_index; + let node = &mut self.state_mut().traces.last_mut().expect("no traces").arena[index]; + node.ordering.push(LogCallOrder::Log(node.logs.len())); + node.logs.push(RawLog { topics: topics.clone(), data: data.clone() }); + } + if let Some(decoded) = convert_log(Log { address, topics: topics.clone(), data: data.clone() }) { @@ -995,7 +1137,7 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> Handler for CheatcodeStackExecutor<'a init_code: Vec, target_gas: Option, ) -> Capture<(ExitReason, Option, Vec), Self::CreateInterrupt> { - self.handler.create(caller, scheme, value, init_code, target_gas) + self.create_inner(caller, scheme, value, init_code, target_gas, true) } fn pre_validate( @@ -1010,7 +1152,12 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> Handler for CheatcodeStackExecutor<'a #[cfg(test)] mod tests { - use crate::{fuzz::FuzzedExecutor, sputnik::helpers::vm, test_helpers::COMPILED, Evm}; + use crate::{ + fuzz::FuzzedExecutor, + sputnik::helpers::{vm, vm_tracing}, + test_helpers::COMPILED, + Evm, + }; use super::*; @@ -1152,4 +1299,119 @@ mod tests { }; assert_eq!(reason, "ffi disabled: run again with --ffi if you want to allow tests to call external scripts"); } + + #[test] + fn tracing_call() { + use std::collections::BTreeMap; + let mut evm = vm_tracing(); + + let compiled = COMPILED.find("Trace").expect("could not find contract"); + let (addr, _, _, _) = evm + .deploy( + Address::zero(), + compiled.bin.unwrap().clone().into_bytes().expect("shouldn't be linked"), + 0.into(), + ) + .unwrap(); + + // after the evm call is done, we call `logs` and print it all to the user + let (_, _, _, _) = evm + .call::<(), _, _>( + Address::zero(), + addr, + "recurseCall(uint256,uint256)", + (U256::from(2u32), U256::from(0u32)), + 0u32.into(), + ) + .unwrap(); + + let mut mapping = BTreeMap::new(); + mapping.insert( + "Trace".to_string(), + ( + compiled.abi.expect("No abi").clone(), + compiled + .bin_runtime + .expect("No runtime") + .clone() + .into_bytes() + .expect("Linking?") + .to_vec(), + ), + ); + let compiled = COMPILED.find("RecursiveCall").expect("could not find contract"); + mapping.insert( + "RecursiveCall".to_string(), + ( + compiled.abi.expect("No abi").clone(), + compiled + .bin_runtime + .expect("No runtime") + .clone() + .into_bytes() + .expect("Linking?") + .to_vec(), + ), + ); + let mut identified = Default::default(); + evm.traces()[1].pretty_print(0, &mapping, &mut identified, &evm, ""); + } + + #[test] + fn tracing_create() { + use std::collections::BTreeMap; + + let mut evm = vm_tracing(); + + let compiled = COMPILED.find("Trace").expect("could not find contract"); + let (addr, _, _, _) = evm + .deploy( + Address::zero(), + compiled.bin.unwrap().clone().into_bytes().expect("shouldn't be linked"), + 0.into(), + ) + .unwrap(); + + // after the evm call is done, we call `logs` and print it all to the user + let (_, _, _, _) = evm + .call::<(), _, _>( + Address::zero(), + addr, + "recurseCreate(uint256,uint256)", + (U256::from(3u32), U256::from(0u32)), + 0u32.into(), + ) + .unwrap(); + + let mut mapping = BTreeMap::new(); + mapping.insert( + "Trace".to_string(), + ( + compiled.abi.expect("No abi").clone(), + compiled + .bin_runtime + .expect("No runtime") + .clone() + .into_bytes() + .expect("Linking?") + .to_vec(), + ), + ); + let compiled = COMPILED.find("RecursiveCall").expect("could not find contract"); + mapping.insert( + "RecursiveCall".to_string(), + ( + compiled.abi.expect("No abi").clone(), + compiled + .bin_runtime + .expect("No runtime") + .clone() + .into_bytes() + .expect("Linking?") + .to_vec(), + ), + ); + let mut identified = Default::default(); + evm.traces()[1].pretty_print(0, &mapping, &mut identified, &evm, ""); + } } diff --git a/evm-adapters/src/sputnik/cheatcodes/memory_stackstate_owned.rs b/evm-adapters/src/sputnik/cheatcodes/memory_stackstate_owned.rs index 0424d3776742..647c1bd047fd 100644 --- a/evm-adapters/src/sputnik/cheatcodes/memory_stackstate_owned.rs +++ b/evm-adapters/src/sputnik/cheatcodes/memory_stackstate_owned.rs @@ -4,6 +4,8 @@ use sputnik::{ ExitError, Transfer, }; +use crate::call_tracing::CallTraceArena; + use ethers::types::{H160, H256, U256}; /// This struct implementation is copied from [upstream](https://github.com/rust-blockchain/evm/blob/5ecf36ce393380a89c6f1b09ef79f686fe043624/src/executor/stack/state.rs#L412) and modified to own the Backend type. @@ -15,6 +17,10 @@ use ethers::types::{H160, H256, U256}; pub struct MemoryStackStateOwned<'config, B> { pub backend: B, pub substate: MemoryStackSubstate<'config>, + pub trace_enabled: bool, + pub call_index: usize, + pub trace_index: usize, + pub traces: Vec, pub expected_revert: Option>, pub next_msg_sender: Option, pub msg_sender: Option<(H160, H160, usize)>, @@ -25,13 +31,34 @@ impl<'config, B: Backend> MemoryStackStateOwned<'config, B> { pub fn deposit(&mut self, address: H160, value: U256) { self.substate.deposit(address, value, &self.backend); } + + pub fn increment_call_index(&mut self) { + self.traces.push(Default::default()); + self.call_index += 1; + } + pub fn trace_mut(&mut self) -> &mut CallTraceArena { + &mut self.traces[self.call_index] + } + + pub fn trace(&self) -> &CallTraceArena { + &self.traces[self.call_index] + } + + pub fn reset_traces(&mut self) { + self.traces = vec![Default::default()]; + self.call_index = 0; + } } impl<'config, B: Backend> MemoryStackStateOwned<'config, B> { - pub fn new(metadata: StackSubstateMetadata<'config>, backend: B) -> Self { + pub fn new(metadata: StackSubstateMetadata<'config>, backend: B, trace_enabled: bool) -> Self { Self { backend, substate: MemoryStackSubstate::new(metadata), + trace_enabled, + call_index: 0, + trace_index: 1, + traces: vec![Default::default()], expected_revert: None, next_msg_sender: None, msg_sender: None, diff --git a/evm-adapters/src/sputnik/cheatcodes/mod.rs b/evm-adapters/src/sputnik/cheatcodes/mod.rs index 08170292e249..c7a3717ef11d 100644 --- a/evm-adapters/src/sputnik/cheatcodes/mod.rs +++ b/evm-adapters/src/sputnik/cheatcodes/mod.rs @@ -60,7 +60,7 @@ ethers::contract::abigen!( expectRevert(bytes) ]"#, ); -pub use hevm_mod::HEVMCalls; +pub use hevm_mod::{HEVMCalls, HEVM_ABI}; ethers::contract::abigen!( HevmConsole, diff --git a/evm-adapters/src/sputnik/evm.rs b/evm-adapters/src/sputnik/evm.rs index 03ed669cb358..f874c1c607ee 100644 --- a/evm-adapters/src/sputnik/evm.rs +++ b/evm-adapters/src/sputnik/evm.rs @@ -1,5 +1,4 @@ -use crate::{Evm, FAUCET_ACCOUNT}; - +use crate::{call_tracing::CallTraceArena, Evm, FAUCET_ACCOUNT}; use ethers::types::{Address, Bytes, U256}; use sputnik::{ @@ -84,6 +83,14 @@ where *_state = state; } + fn set_tracing_enabled(&mut self, enabled: bool) -> bool { + self.executor.set_tracing_enabled(enabled) + } + + fn tracing_enabled(&self) -> bool { + self.executor.tracing_enabled() + } + /// given an iterator of contract address to contract bytecode, initializes /// the state with the contract deployed at the specified address fn initialize_contracts>(&mut self, contracts: T) { @@ -104,6 +111,18 @@ where self.executor.state() } + fn code(&self, address: Address) -> Vec { + self.executor.state().code(address) + } + + fn traces(&self) -> Vec { + self.executor.traces() + } + + fn reset_traces(&mut self) { + self.executor.reset_traces() + } + fn all_logs(&self) -> Vec { self.executor.all_logs() } @@ -196,10 +215,17 @@ pub mod helpers { const GAS_LIMIT: u64 = 30_000_000; /// Instantiates a Sputnik EVM with enabled cheatcodes + FFI and a simple non-forking in memory - /// backend + /// backend and tracing disabled pub fn vm<'a>() -> TestSputnikVM<'a, MemoryBackend<'a>> { let backend = new_backend(&*VICINITY, Default::default()); - Executor::new_with_cheatcodes(backend, GAS_LIMIT, &*CFG, &*PRECOMPILES_MAP, true) + Executor::new_with_cheatcodes(backend, GAS_LIMIT, &*CFG, &*PRECOMPILES_MAP, true, false) + } + + /// Instantiates a Sputnik EVM with enabled cheatcodes + FFI and a simple non-forking in memory + /// backend and tracing enabled + pub fn vm_tracing<'a>() -> TestSputnikVM<'a, MemoryBackend<'a>> { + let backend = new_backend(&*VICINITY, Default::default()); + Executor::new_with_cheatcodes(backend, GAS_LIMIT, &*CFG, &*PRECOMPILES_MAP, true, true) } /// Instantiates a FuzzedExecutor over provided Sputnik EVM diff --git a/evm-adapters/src/sputnik/forked_backend/rpc.rs b/evm-adapters/src/sputnik/forked_backend/rpc.rs index acbfb83f4dfc..a3d13882b7a7 100644 --- a/evm-adapters/src/sputnik/forked_backend/rpc.rs +++ b/evm-adapters/src/sputnik/forked_backend/rpc.rs @@ -215,7 +215,7 @@ mod tests { #[test] fn forked_backend() { - let cfg = Config::istanbul(); + let cfg = Config::london(); let compiled = COMPILED.find("Greeter").expect("could not find contract"); let provider = Provider::::try_from( diff --git a/evm-adapters/src/sputnik/mod.rs b/evm-adapters/src/sputnik/mod.rs index 35cd3cb20059..2fd0620ac86c 100644 --- a/evm-adapters/src/sputnik/mod.rs +++ b/evm-adapters/src/sputnik/mod.rs @@ -8,6 +8,7 @@ pub mod cheatcodes; pub mod state; use ethers::{ + abi::RawLog, providers::Middleware, types::{Address, H160, H256, U256}, }; @@ -18,6 +19,8 @@ use sputnik::{ Config, CreateScheme, ExitError, ExitReason, ExitSucceed, }; +use crate::call_tracing::CallTraceArena; + pub use sputnik as sputnik_evm; use sputnik_evm::executor::stack::PrecompileSet; @@ -60,6 +63,8 @@ pub trait SputnikExecutor { fn state(&self) -> &S; fn state_mut(&mut self) -> &mut S; fn expected_revert(&self) -> Option<&[u8]>; + fn set_tracing_enabled(&mut self, enabled: bool) -> bool; + fn tracing_enabled(&self) -> bool; fn all_logs(&self) -> Vec; fn gas_left(&self) -> U256; fn transact_call( @@ -83,6 +88,17 @@ pub trait SputnikExecutor { fn create_address(&self, caller: CreateScheme) -> Address; + /// Returns a vector of raw logs that occurred during the previous VM + /// execution + fn raw_logs(&self) -> Vec; + + /// Gets a trace + fn traces(&self) -> Vec { + vec![] + } + + fn reset_traces(&mut self) {} + /// Returns a vector of string parsed logs that occurred during the previous VM /// execution fn logs(&self) -> Vec; @@ -112,6 +128,14 @@ impl<'a, 'b, S: StackState<'a>, P: PrecompileSet> SputnikExecutor None } + fn set_tracing_enabled(&mut self, _enabled: bool) -> bool { + false + } + + fn tracing_enabled(&self) -> bool { + false + } + fn all_logs(&self) -> Vec { vec![] } @@ -152,6 +176,11 @@ impl<'a, 'b, S: StackState<'a>, P: PrecompileSet> SputnikExecutor fn logs(&self) -> Vec { vec![] } + + fn raw_logs(&self) -> Vec { + vec![] + } + fn clear_logs(&mut self) {} } diff --git a/evm-adapters/testdata/Trace.sol b/evm-adapters/testdata/Trace.sol new file mode 100644 index 000000000000..3d9b98d13f00 --- /dev/null +++ b/evm-adapters/testdata/Trace.sol @@ -0,0 +1,75 @@ +pragma solidity ^0.8.0; + + +interface RecursiveCallee { + function recurseCall(uint256 neededDepth, uint256 depth) external returns (uint256); + function recurseCreate(uint256 neededDepth, uint256 depth) external returns (uint256); + function someCall() external; + function negativeNum() external returns (int256); +} + +contract RecursiveCall { + event Depth(uint256 depth); + event ChildDepth(uint256 childDepth); + event CreatedChild(uint256 childDepth); + Trace factory; + + constructor(address _factory) { + factory = Trace(_factory); + } + + function recurseCall(uint256 neededDepth, uint256 depth) public returns (uint256) { + if (depth == neededDepth) { + RecursiveCallee(address(this)).negativeNum(); + return neededDepth; + } + uint256 childDepth = RecursiveCallee(address(this)).recurseCall(neededDepth, depth + 1); + emit ChildDepth(childDepth); + RecursiveCallee(address(this)).someCall(); + emit Depth(depth); + return depth; + } + + function recurseCreate(uint256 neededDepth, uint256 depth) public returns (uint256) { + if (depth == neededDepth) { + return neededDepth; + } + RecursiveCall child = factory.create(); + emit CreatedChild(depth + 1); + uint256 childDepth = child.recurseCreate(neededDepth, depth + 1); + emit Depth(depth); + return depth; + } + + function someCall() public {} + + function negativeNum() public returns (int256) { + return -1000000000; + } +} + +contract Trace { + RecursiveCall first; + + function create() public returns (RecursiveCall) { + if (address(first) == address(0)) { + first = new RecursiveCall(address(this)); + return first; + } + return new RecursiveCall(address(this)); + } + + function recurseCall(uint256 neededDepth, uint256 depth) public returns (uint256) { + if (address(first) == address(0)) { + first = new RecursiveCall(address(this)); + } + return first.recurseCall(neededDepth, depth); + } + + function recurseCreate(uint256 neededDepth, uint256 depth) public returns (uint256) { + if (address(first) == address(0)) { + first = new RecursiveCall(address(this)); + } + return first.recurseCreate(neededDepth, depth); + } +} \ No newline at end of file diff --git a/forge/src/multi_runner.rs b/forge/src/multi_runner.rs index 8434f35630a0..8a1b8ddc7760 100644 --- a/forge/src/multi_runner.rs +++ b/forge/src/multi_runner.rs @@ -1,17 +1,19 @@ use crate::{runner::TestResult, ContractRunner}; +use ethers::solc::Artifact; + use evm_adapters::Evm; use ethers::{ abi::Abi, prelude::ArtifactOutput, - solc::{Artifact, Project}, + solc::Project, types::{Address, U256}, }; use proptest::test_runner::TestRunner; use regex::Regex; -use eyre::{Context, Result}; +use eyre::Result; use std::{collections::BTreeMap, marker::PhantomData}; /// Builder used for instantiating the multi-contract runner @@ -56,34 +58,35 @@ impl MultiContractRunnerBuilder { // This is just the contracts compiled, but we need to merge this with the read cached // artifacts let contracts = output.into_artifacts(); - let contracts: BTreeMap)> = contracts - // only take contracts with valid abi and bytecode - .filter_map(|(fname, contract)| { - let (abi, bytecode) = contract.into_inner(); - abi.and_then(|abi| bytecode.map(|bytecode| (fname, abi, bytecode))) - }) - // Only take contracts with empty constructors. - .filter(|(_, abi, _)| { - abi.constructor.as_ref().map(|c| c.inputs.is_empty()).unwrap_or(true) - }) - // Only take contracts which contain a `test` function - .filter(|(_, abi, _)| abi.functions().any(|func| func.name.starts_with("test"))) - // deploy the contracts - .map(|(name, abi, bytecode)| { - let span = tracing::trace_span!("deploying", ?name); - let _enter = span.enter(); - - let (addr, _, _, logs) = evm - .deploy(sender, bytecode, 0.into()) - .wrap_err(format!("could not deploy {}", name))?; - - evm.set_balance(addr, initial_balance); - Ok((name, (abi, addr, logs))) - }) - .collect::>>()?; + let mut known_contracts: BTreeMap)> = Default::default(); + let mut deployed_contracts: BTreeMap)> = + Default::default(); + + for (fname, contract) in contracts { + let (maybe_abi, maybe_deploy_bytes, maybe_runtime_bytes) = contract.into_parts(); + if let (Some(abi), Some(bytecode)) = (maybe_abi, maybe_deploy_bytes) { + if abi.constructor.as_ref().map(|c| c.inputs.is_empty()).unwrap_or(true) && + abi.functions().any(|func| func.name.starts_with("test")) + { + let span = tracing::trace_span!("deploying", ?fname); + let _enter = span.enter(); + let (addr, _, _, logs) = evm.deploy(sender, bytecode.clone(), 0u32.into())?; + evm.set_balance(addr, initial_balance); + deployed_contracts.insert(fname.clone(), (abi.clone(), addr, logs)); + } + + let split = fname.split(':').collect::>(); + let contract_name = if split.len() > 1 { split[1] } else { split[0] }; + if let Some(runtime_code) = maybe_runtime_bytes { + known_contracts.insert(contract_name.to_string(), (abi, runtime_code.to_vec())); + } + } + } Ok(MultiContractRunner { - contracts, + contracts: deployed_contracts, + known_contracts, + identified_contracts: Default::default(), evm, state: PhantomData, sender: self.sender, @@ -115,9 +118,13 @@ impl MultiContractRunnerBuilder { pub struct MultiContractRunner { /// Mapping of contract name to compiled bytecode, deployed address and logs emitted during /// deployment - contracts: BTreeMap)>, + 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 - evm: E, + pub evm: E, /// 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 @@ -172,7 +179,7 @@ where ) -> Result> { let mut runner = ContractRunner::new(&mut self.evm, contract, address, self.sender, init_logs); - runner.run_tests(pattern, self.fuzzer.as_mut(), init_state) + runner.run_tests(pattern, self.fuzzer.as_mut(), init_state, Some(&self.known_contracts)) } } diff --git a/forge/src/runner.rs b/forge/src/runner.rs index 93db3d63b4e3..8565c5ad5912 100644 --- a/forge/src/runner.rs +++ b/forge/src/runner.rs @@ -2,6 +2,7 @@ use ethers::{ abi::{Abi, Function, Token}, types::{Address, Bytes}, }; +use evm_adapters::call_tracing::CallTraceArena; use evm_adapters::{ fuzz::{FuzzTestResult, FuzzedCases, FuzzedExecutor}, @@ -55,6 +56,12 @@ pub struct TestResult { /// What kind of test this was pub kind: TestKind, + + /// Traces + pub traces: Option>, + + /// Identified contracts + pub identified_contracts: Option>, } impl TestResult { @@ -164,6 +171,7 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { regex: &Regex, fuzzer: Option<&mut TestRunner>, init_state: &S, + known_contracts: Option<&BTreeMap)>>, ) -> Result> { tracing::info!("starting tests"); let start = Instant::now(); @@ -183,7 +191,7 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { .map(|func| { // Before each test run executes, ensure we're at our initial state. self.evm.reset(init_state.clone()); - let result = self.run_test(func, needs_setup)?; + let result = self.run_test(func, needs_setup, known_contracts)?; Ok((func.signature(), result)) }) .collect::>>()?; @@ -214,7 +222,12 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { } #[tracing::instrument(name = "test", skip_all, fields(name = %func.signature()))] - pub fn run_test(&mut self, func: &Function, setup: bool) -> Result { + pub fn run_test( + &mut self, + func: &Function, + setup: bool, + known_contracts: Option<&BTreeMap)>>, + ) -> Result { let start = Instant::now(); // the expected result depends on the function name // DAppTools' ds-test will not revert inside its `assertEq`-like functions @@ -225,6 +238,8 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { let mut logs = self.init_logs.to_vec(); + self.evm.reset_traces(); + // call the setup function in each test to reset the test's state. if setup { tracing::trace!("setting up"); @@ -260,6 +275,44 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { } }, }; + + let mut traces: Option> = None; + let mut identified_contracts: Option> = None; + + let evm_traces = self.evm.traces(); + if !evm_traces.is_empty() && self.evm.tracing_enabled() { + let mut ident = BTreeMap::new(); + // create an iter over the traces + let mut trace_iter = evm_traces.into_iter(); + let mut temp_traces = Vec::new(); + if setup { + // grab the setup trace if it exists + let setup = trace_iter.next().expect("no setup trace"); + setup.update_identified( + 0, + known_contracts.expect("traces enabled but no identified_contracts"), + &mut ident, + self.evm, + ); + temp_traces.push(setup); + } + // grab the test trace + let test_trace = trace_iter.next().expect("no test trace"); + test_trace.update_identified( + 0, + known_contracts.expect("traces enabled but no identified_contracts"), + &mut ident, + self.evm, + ); + temp_traces.push(test_trace); + + // pass back the identified contracts and traces + identified_contracts = Some(ident); + traces = Some(temp_traces); + } + + self.evm.reset_traces(); + let success = self.evm.check_success(self.address, &status, should_fail); let duration = Instant::now().duration_since(start); tracing::debug!(?duration, %success, %gas_used); @@ -271,6 +324,8 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { counterexample: None, logs, kind: TestKind::Standard(gas_used), + traces, + identified_contracts, }) } @@ -281,6 +336,8 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { setup: bool, runner: TestRunner, ) -> Result { + // do not trace in fuzztests, as it's a big performance hit + let prev = self.evm.set_tracing_enabled(false); let start = Instant::now(); let should_fail = func.name.starts_with("testFail"); tracing::debug!(func = ?func.signature(), should_fail, "fuzzing"); @@ -314,6 +371,8 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { let duration = Instant::now().duration_since(start); tracing::debug!(?duration, %success); + // reset tracing to previous value in case next test *isn't* a fuzz test + self.evm.set_tracing_enabled(prev); // from that call? Ok(TestResult { success, @@ -322,6 +381,8 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { counterexample, logs: vec![], kind: TestKind::Fuzz(cases), + traces: None, + identified_contracts: None, }) } } @@ -369,6 +430,7 @@ mod tests { &Regex::from_str("testGreeting").unwrap(), Some(&mut fuzzer), &init_state, + None, ) .unwrap(); assert!(results["testGreeting()"].success); @@ -393,7 +455,12 @@ mod tests { cfg.failure_persistence = None; let mut fuzzer = TestRunner::new(cfg); let results = runner - .run_tests(&Regex::from_str("testFuzz.*").unwrap(), Some(&mut fuzzer), &init_state) + .run_tests( + &Regex::from_str("testFuzz.*").unwrap(), + Some(&mut fuzzer), + &init_state, + None, + ) .unwrap(); for (_, res) in results { assert!(!res.success); @@ -492,7 +559,7 @@ mod tests { let mut runner = ContractRunner::new(&mut evm, compiled.abi.as_ref().unwrap(), addr, None, &[]); - let res = runner.run_tests(&".*".parse().unwrap(), None, &init_state).unwrap(); + let res = runner.run_tests(&".*".parse().unwrap(), None, &init_state, None).unwrap(); assert!(!res.is_empty()); assert!(res.iter().all(|(_, result)| result.success)); }