From 547bf51fdd54af3ab6eea5fd853b3c5a81aac90b Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Tue, 17 Oct 2023 18:57:07 +0200 Subject: [PATCH] wip refactor traces --- Cargo.lock | 1 + crates/anvil/src/eth/backend/mem/mod.rs | 52 +---- crates/evm/Cargo.toml | 1 + crates/evm/src/debug.rs | 10 +- crates/evm/src/decode.rs | 208 ++++++------------ crates/evm/src/executor/fork/backend.rs | 2 +- crates/evm/src/executor/mod.rs | 13 +- crates/evm/src/executor/opts.rs | 3 +- crates/evm/src/fuzz/invariant/error.rs | 12 +- crates/evm/src/fuzz/mod.rs | 4 +- .../src/trace/{decoder.rs => decoder/mod.rs} | 103 +++------ crates/evm/src/trace/decoder/precompiles.rs | 86 ++++++++ crates/evm/src/trace/mod.rs | 48 ++-- crates/evm/src/trace/node.rs | 29 ++- crates/evm/src/trace/utils.rs | 4 +- crates/forge/bin/cmd/script/executor.rs | 2 +- crates/forge/bin/cmd/script/mod.rs | 6 +- crates/forge/src/gas_report.rs | 17 +- crates/macros/impl/src/cheatcodes.rs | 6 +- 19 files changed, 275 insertions(+), 332 deletions(-) rename crates/evm/src/trace/{decoder.rs => decoder/mod.rs} (74%) create mode 100644 crates/evm/src/trace/decoder/precompiles.rs diff --git a/Cargo.lock b/Cargo.lock index 0babd272c9524..da4d416990c85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2737,6 +2737,7 @@ name = "foundry-evm" version = "0.2.0" dependencies = [ "alloy-primitives", + "alloy-sol-types", "const-hex", "ethers", "eyre", diff --git a/crates/anvil/src/eth/backend/mem/mod.rs b/crates/anvil/src/eth/backend/mem/mod.rs index cea99ed1ecc55..6e2f5fd33d8cb 100644 --- a/crates/anvil/src/eth/backend/mem/mod.rs +++ b/crates/anvil/src/eth/backend/mem/mod.rs @@ -53,11 +53,11 @@ use ethers::{ DefaultFrame, Filter, FilteredParams, GethDebugTracingOptions, GethTrace, Log, OtherFields, Trace, Transaction, TransactionReceipt, H160, }, - utils::{hex, keccak256, rlp}, + utils::{keccak256, rlp}, }; use flate2::{read::GzDecoder, write::GzEncoder, Compression}; use foundry_evm::{ - decode::{decode_custom_error_args, decode_revert}, + decode::decode_revert, executor::{ backend::{DatabaseError, DatabaseResult}, inspector::AccessListTracer, @@ -66,7 +66,7 @@ use foundry_evm::{ revm::{ self, db::CacheDB, - interpreter::{return_ok, InstructionResult}, + interpreter::InstructionResult, primitives::{ Account, BlockEnv, CreateScheme, EVMError, Env, ExecutionResult, InvalidHeader, Output, SpecId, TransactTo, TxEnv, KECCAK_EMPTY, @@ -285,7 +285,7 @@ impl Backend { fork_genesis_infos.clear(); for res in genesis_accounts { - let (address, mut info) = res??; + let (address, mut info) = res.map_err(DatabaseError::display)??; info.balance = self.genesis.balance; db.insert_account(address, info.clone()); @@ -900,45 +900,11 @@ impl Backend { node_info!(" Contract created: {:?}", contract); } node_info!(" Gas used: {}", receipt.gas_used()); - match info.exit { - return_ok!() => (), - InstructionResult::OutOfFund => { - node_info!(" Error: reverted due to running out of funds"); - } - InstructionResult::CallTooDeep => { - node_info!(" Error: reverted with call too deep"); - } - InstructionResult::Revert => { - if let Some(ref r) = info.out { - if let Ok(reason) = decode_revert(r.as_ref(), None, None) { - node_info!(" Error: reverted with '{}'", reason); - } else { - match decode_custom_error_args(r, 5) { - // assuming max 5 args - Some(token) => { - node_info!( - " Error: reverted with custom error: {:?}", - token - ); - } - None => { - node_info!( - " Error: reverted with custom error: {}", - hex::encode(r) - ); - } - } - } - } else { - node_info!(" Error: reverted without a reason"); - } - } - InstructionResult::OutOfGas => { - node_info!(" Error: ran out of gas"); - } - reason => { - node_info!(" Error: failed due to {:?}", reason); - } + if !info.exit.is_ok() { + node_info!( + " Error: reverted with '{}'", + decode_revert(info.out.as_deref().unwrap_or_default(), None, None) + ); } node_info!(""); } diff --git a/crates/evm/Cargo.toml b/crates/evm/Cargo.toml index 8710af9b11d16..264ee8860928d 100644 --- a/crates/evm/Cargo.toml +++ b/crates/evm/Cargo.toml @@ -19,6 +19,7 @@ foundry-macros.workspace = true # EVM alloy-primitives = { workspace = true, features = ["serde", "getrandom", "arbitrary", "rlp"] } +alloy-sol-types.workspace = true ethers = { workspace = true, features = ["ethers-solc"] } hashbrown = { version = "0.14", features = ["serde"] } revm = { workspace = true, default-features = false, features = [ diff --git a/crates/evm/src/debug.rs b/crates/evm/src/debug.rs index be6252109e72c..7aded67da8ebf 100644 --- a/crates/evm/src/debug.rs +++ b/crates/evm/src/debug.rs @@ -1,4 +1,4 @@ -use crate::{abi::HEVM_ABI, CallKind}; +use crate::CallKind; use alloy_primitives::{Address, U256}; use revm::interpreter::{Memory, OpCode}; use serde::{Deserialize, Serialize}; @@ -178,11 +178,11 @@ impl Display for Instruction { Instruction::Cheatcode(cheat) => write!( f, "VM_{}", - &*HEVM_ABI - .functions() - .find(|func| func.short_signature() == *cheat) + foundry_cheatcodes::Vm::CHEATCODES + .iter() + .find(|c| c.selector_bytes == *cheat) .expect("unknown cheatcode found in debugger") - .name + .id .to_uppercase() ), } diff --git a/crates/evm/src/decode.rs b/crates/evm/src/decode.rs index c20746090b953..5fb925fc43c23 100644 --- a/crates/evm/src/decode.rs +++ b/crates/evm/src/decode.rs @@ -1,15 +1,14 @@ //! Various utilities to decode test results use crate::abi::ConsoleEvents::{self, *}; use alloy_primitives::B256; +use alloy_sol_types::{ContractError, SolCall, SolInterface}; use ethers::{ - abi::{decode, AbiDecode, Contract as Abi, ParamType, RawLog, Token}, + abi::{decode, Contract as Abi, ParamType, RawLog, Token}, contract::EthLogDecode, - prelude::U256, types::Log, }; -use foundry_cheatcodes::impls::MAGIC_SKIP_BYTES; -use foundry_common::{abi::format_token, SELECTOR_LEN}; -use foundry_utils::error::ERROR_PREFIX; +use foundry_cheatcodes::{impls::MAGIC_SKIP_BYTES, Vm}; +use foundry_common::SELECTOR_LEN; use itertools::Itertools; use once_cell::sync::Lazy; use revm::interpreter::{return_ok, InstructionResult}; @@ -71,149 +70,83 @@ pub fn decode_revert( err: &[u8], maybe_abi: Option<&Abi>, status: Option, -) -> eyre::Result { +) -> String { if err.len() < SELECTOR_LEN { if let Some(status) = status { if !matches!(status, return_ok!()) { - return Ok(format!("EvmError: {status:?}")) + return format!("EvmError: {status:?}") } } - eyre::bail!("Not enough error data to decode") - } - match err[..SELECTOR_LEN] { - // keccak(Panic(uint256)) - [78, 72, 123, 113] => { - // ref: https://soliditydeveloper.com/solidity-0.8 - match err[err.len() - 1] { - 1 => { - // assert - Ok("Assertion violated".to_string()) - } - 17 => { - // safemath over/underflow - Ok("Arithmetic over/underflow".to_string()) - } - 18 => { - // divide by 0 - Ok("Division or modulo by 0".to_string()) - } - 33 => { - // conversion into non-existent enum type - Ok("Conversion into non-existent enum type".to_string()) - } - 34 => { - // incorrectly encoded storage byte array - Ok("Incorrectly encoded storage byte array".to_string()) - } - 49 => { - // pop() on empty array - Ok("`pop()` on empty array".to_string()) - } - 50 => { - // index out of bounds - Ok("Index out of bounds".to_string()) - } - 65 => { - // allocating too much memory or creating too large array - Ok("Memory allocation overflow".to_string()) - } - 81 => { - // calling a zero initialized variable of internal function type - Ok("Calling a zero initialized variable of internal function type".to_string()) - } - _ => { - eyre::bail!("Unsupported solidity builtin panic") - } - } - } - // keccak(Error(string)) - [8, 195, 121, 160] => { - String::decode(&err[SELECTOR_LEN..]).map_err(|_| eyre::eyre!("Bad string decode")) + return if let Ok(s) = std::str::from_utf8(err) { + s.to_string() + } else { + hex::encode_prefixed(err) } - // keccak(expectRevert(bytes)) - [242, 141, 206, 179] => { - let err_data = &err[SELECTOR_LEN..]; - if err_data.len() > 64 { - let len = U256::from(&err_data[32..64]).as_usize(); - if err_data.len() > 64 + len { - let actual_err = &err_data[64..64 + len]; - if let Ok(decoded) = decode_revert(actual_err, maybe_abi, None) { - // check if it's a builtin - return Ok(decoded) - } else if let Ok(as_str) = String::from_utf8(actual_err.to_vec()) { - // check if it's a true string - return Ok(as_str) - } - } - } - eyre::bail!("Non-native error and not string") + } + + // `skip` special case + if err == MAGIC_SKIP_BYTES { + return "SKIPPED".to_string() + } + + // `Panic(uint256)`, `Error(string)`, `CheatCodeError(string)` + if let Ok(e) = ContractError::::abi_decode(err, true) { + return match e { + ContractError::CustomError(vm) => match vm { + Vm::VmErrors::CheatCodeError(cheatcode) => cheatcode.message, + }, + ContractError::Panic(panic) => panic.to_string(), + ContractError::Revert(revert) => revert.to_string(), } - // keccak(expectRevert(bytes4)) - [195, 30, 176, 224] => { - let err_data = &err[SELECTOR_LEN..]; - if err_data.len() == 32 { - let actual_err = &err_data[..SELECTOR_LEN]; - if let Ok(decoded) = decode_revert(actual_err, maybe_abi, None) { - // it's a known selector - return Ok(decoded) + } + + // `expectRevert(bytes)` + if let Ok(e) = Vm::expectRevert_2Call::abi_decode(err, true) { + return decode_revert(&e.revertData[..], maybe_abi, status) + } + + // `expectRevert(bytes4)` + if let Ok(e) = Vm::expectRevert_1Call::abi_decode(err, true) { + return decode_revert(&e.revertData[..], maybe_abi, status) + } + + // try to decode a custom error if provided an abi + if let Some(abi) = maybe_abi { + for abi_error in abi.errors() { + if abi_error.signature()[..SELECTOR_LEN] == err[..SELECTOR_LEN] { + // if we don't decode, don't return an error, try to decode as a string later + if let Ok(decoded) = abi_error.decode(&err[SELECTOR_LEN..]) { + let inputs = decoded + .iter() + .map(foundry_common::abi::format_token) + .collect::>() + .join(", "); + return format!("{}({inputs})", abi_error.name) } } - eyre::bail!("Unknown error selector") } - _ => { - // See if the revert is caused by a skip() call. - if err == MAGIC_SKIP_BYTES { - return Ok("SKIPPED".to_string()) - } - // try to decode a custom error if provided an abi - if let Some(abi) = maybe_abi { - for abi_error in abi.errors() { - if abi_error.signature()[..SELECTOR_LEN] == err[..SELECTOR_LEN] { - // if we don't decode, don't return an error, try to decode as a - // string later - if let Ok(decoded) = abi_error.decode(&err[SELECTOR_LEN..]) { - let inputs = decoded - .iter() - .map(foundry_common::abi::format_token) - .collect::>() - .join(", "); - return Ok(format!("{}({inputs})", abi_error.name)) - } - } - } - } - // optimistically try to decode as string, unknown selector or `CheatcodeError` - String::decode(err) - .ok() - .or_else(|| { - // try decoding as cheatcode error - if err.starts_with(ERROR_PREFIX.as_slice()) { - String::decode(&err[ERROR_PREFIX.len()..]).ok() - } else { - None - } - }) - .or_else(|| { - // try decoding as unknown err - String::decode(&err[SELECTOR_LEN..]) - .map(|err_str| format!("{}:{err_str}", hex::encode(&err[..SELECTOR_LEN]))) - .ok() - }) - .or_else(|| { - // try to decode possible variations of custom error types - decode_custom_error(err).map(|token| { - let s = format!("Custom Error {}:", hex::encode(&err[..SELECTOR_LEN])); + } - let err_str = format_token(&token); - if err_str.starts_with('(') { - format!("{s}{err_str}") - } else { - format!("{s}({err_str})") - } - }) - }) - .ok_or_else(|| eyre::eyre!("Non-native error and not string")) - } + // `string` + if let Ok(s) = std::str::from_utf8(err) { + return s.to_string() + } + + // Generic custom error + let (selector, err) = err.split_at(SELECTOR_LEN); + format!( + "Custom error {}:{}", + hex::encode(selector), + std::str::from_utf8(err).map_or_else(|_| trimmed_hex(err), String::from) + ) +} + +fn trimmed_hex(s: &[u8]) -> String { + let s = hex::encode(s); + if s.len() <= 32 { + s + } else { + format!("{}…{} ({} bytes)", &s[..16], &s[s.len() - 16..], s.len()) } } @@ -282,6 +215,7 @@ mod tests { use ethers::{ abi::{AbiEncode, Address}, contract::EthError, + types::U256, }; #[test] diff --git a/crates/evm/src/executor/fork/backend.rs b/crates/evm/src/executor/fork/backend.rs index bc6fe033511d4..0e385f738e2b8 100644 --- a/crates/evm/src/executor/fork/backend.rs +++ b/crates/evm/src/executor/fork/backend.rs @@ -772,7 +772,7 @@ mod tests { enable_caching: true, url: ENDPOINT.to_string(), env: env.clone(), - evm_opts, + evm_opts: todo!(), }; let backend = Backend::spawn(Some(fork)).await; diff --git a/crates/evm/src/executor/mod.rs b/crates/evm/src/executor/mod.rs index de3459e4d235f..4e12773a313fb 100644 --- a/crates/evm/src/executor/mod.rs +++ b/crates/evm/src/executor/mod.rs @@ -14,8 +14,7 @@ use crate::{ CALLER, }; pub use abi::{ - patch_hardhat_console_selector, HardhatConsoleCalls, CHEATCODE_ADDRESS, CONSOLE_ABI, - HARDHAT_CONSOLE_ABI, HARDHAT_CONSOLE_ADDRESS, + patch_hardhat_console_selector, HardhatConsoleCalls, CONSOLE_ABI, HARDHAT_CONSOLE_ABI, }; /// Reexport commonly used revm types pub use alloy_primitives::{Address, Bytes, U256}; @@ -26,7 +25,9 @@ use ethers::{ signers::LocalWallet, types::Log, }; -use foundry_cheatcodes::{impls::BroadcastableTransactions, Cheatcodes, DEFAULT_CREATE2_DEPLOYER}; +use foundry_cheatcodes::{ + impls::BroadcastableTransactions, Cheatcodes, CHEATCODE_ADDRESS, DEFAULT_CREATE2_DEPLOYER, +}; use foundry_common::{abi::IntoFunction, evm::Breakpoints}; use revm::primitives::hex_literal::hex; pub use revm::{ @@ -419,8 +420,7 @@ impl Executor { } } _ => { - let reason = decode::decode_revert(result.as_ref(), abi, Some(exit_reason)) - .unwrap_or_else(|_| format!("{exit_reason:?}")); + let reason = decode::decode_revert(result.as_ref(), abi, Some(exit_reason)); return Err(EvmError::Execution(Box::new(ExecutionErr { reverted: true, reason, @@ -858,8 +858,7 @@ fn convert_call_result( }) } _ => { - let reason = decode::decode_revert(result.as_ref(), abi, Some(status)) - .unwrap_or_else(|_| format!("{status:?}")); + let reason = decode::decode_revert(result.as_ref(), abi, Some(status)); if reason == "SKIPPED" { return Err(EvmError::SkipError) } diff --git a/crates/evm/src/executor/opts.rs b/crates/evm/src/executor/opts.rs index a09e7c44d2087..2ff2659254576 100644 --- a/crates/evm/src/executor/opts.rs +++ b/crates/evm/src/executor/opts.rs @@ -138,7 +138,8 @@ impl EvmOpts { pub fn get_fork(&self, config: &Config, env: revm::primitives::Env) -> Option { let url = self.fork_url.clone()?; let enable_caching = config.enable_caching(&url, env.cfg.chain_id); - Some(CreateFork { url, enable_caching, env, evm_opts: self.clone() }) + // TODO(EvmOpts) + Some(CreateFork { url, enable_caching, env, evm_opts: todo!() }) } /// Returns the gas limit to use diff --git a/crates/evm/src/fuzz/invariant/error.rs b/crates/evm/src/fuzz/invariant/error.rs index 99e3efb59c9ea..60492c5fabd99 100644 --- a/crates/evm/src/fuzz/invariant/error.rs +++ b/crates/evm/src/fuzz/invariant/error.rs @@ -56,19 +56,11 @@ impl InvariantFuzzError { logs: call_result.logs, traces: call_result.traces, test_error: proptest::test_runner::TestError::Fail( - format!( - "{}, reason: '{}'", - origin, - match &revert_reason { - Ok(s) => s.clone(), - Err(e) => e.to_string(), - } - ) - .into(), + format!("{origin}, reason: {revert_reason}").into(), calldata.to_vec(), ), return_reason: "".into(), - revert_reason: revert_reason.unwrap_or_default(), + revert_reason, addr: invariant_contract.address, func, inner_sequence: inner_sequence.to_vec(), diff --git a/crates/evm/src/fuzz/mod.rs b/crates/evm/src/fuzz/mod.rs index edf079343121a..7a4e24f380725 100644 --- a/crates/evm/src/fuzz/mod.rs +++ b/crates/evm/src/fuzz/mod.rs @@ -136,9 +136,7 @@ impl<'a> FuzzedExecutor<'a> { // case. let call_res = _counterexample.1.result.clone(); *counterexample.borrow_mut() = _counterexample; - Err(TestCaseError::fail( - decode::decode_revert(&call_res, errors, Some(status)).unwrap_or_default(), - )) + Err(TestCaseError::fail(decode::decode_revert(&call_res, errors, Some(status)))) } } }); diff --git a/crates/evm/src/trace/decoder.rs b/crates/evm/src/trace/decoder/mod.rs similarity index 74% rename from crates/evm/src/trace/decoder.rs rename to crates/evm/src/trace/decoder/mod.rs index d823c392f90fd..be8bc98a41602 100644 --- a/crates/evm/src/trace/decoder.rs +++ b/crates/evm/src/trace/decoder/mod.rs @@ -3,22 +3,24 @@ use super::{ CallTraceArena, RawOrDecodedCall, RawOrDecodedLog, RawOrDecodedReturnData, }; use crate::{ - abi::{CHEATCODE_ADDRESS, CONSOLE_ABI, HARDHAT_CONSOLE_ABI, HARDHAT_CONSOLE_ADDRESS, HEVM_ABI}, + abi::{CONSOLE_ABI, HARDHAT_CONSOLE_ABI}, decode, trace::{node::CallTraceNode, utils}, CALLER, TEST_CONTRACT_ADDRESS, }; use ethers::{ - abi::{Abi, Address, Event, Function, Param, ParamType, Token}, - types::{H160, H256}, + abi::{Abi, Address, Event, Function, Token}, + types::H256, }; -use foundry_cheatcodes::DEFAULT_CREATE2_DEPLOYER; +use foundry_cheatcodes::{CHEATCODE_ADDRESS, DEFAULT_CREATE2_DEPLOYER, HARDHAT_CONSOLE_ADDRESS}; use foundry_common::{abi::get_indexed_event, SELECTOR_LEN}; use foundry_utils::types::ToEthers; use hashbrown::HashSet; use once_cell::sync::OnceCell; use std::collections::{BTreeMap, HashMap}; +mod precompiles; + /// Build a new [CallTraceDecoder]. #[derive(Default)] #[must_use = "builders do nothing unless you call `build` on them"] @@ -83,8 +85,6 @@ impl CallTraceDecoderBuilder { /// different sets might overlap. #[derive(Clone, Default, Debug)] pub struct CallTraceDecoder { - /// Information for decoding precompile calls. - pub precompiles: HashMap, /// Addresses identified to be a specific contract. /// /// The values are in the form `":"`. @@ -105,27 +105,6 @@ pub struct CallTraceDecoder { pub verbosity: u8, } -/// Returns an expression of the type `[(Address, Function); N]` -macro_rules! precompiles { - ($($number:literal : $name:ident($( $name_in:ident : $in:expr ),* $(,)?) -> ($( $name_out:ident : $out:expr ),* $(,)?)),+ $(,)?) => {{ - use std::string::String as RustString; - use ParamType::*; - [$( - ( - H160([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, $number]), - #[allow(deprecated)] - Function { - name: RustString::from(stringify!($name)), - inputs: vec![$(Param { name: RustString::from(stringify!($name_in)), kind: $in, internal_type: None, }),*], - outputs: vec![$(Param { name: RustString::from(stringify!($name_out)), kind: $out, internal_type: None, }),*], - constant: None, - state_mutability: ethers::abi::StateMutability::Pure, - }, - ), - )+] - }}; -} - impl CallTraceDecoder { /// Creates a new call trace decoder. /// @@ -140,20 +119,6 @@ impl CallTraceDecoder { fn init() -> Self { Self { - // TODO: These are the Ethereum precompiles. We should add a way to support precompiles - // for other networks, too. - precompiles: precompiles!( - 0x01: ecrecover(hash: FixedBytes(32), v: Uint(256), r: Uint(256), s: Uint(256)) -> (publicAddress: Address), - 0x02: sha256(data: Bytes) -> (hash: FixedBytes(32)), - 0x03: ripemd(data: Bytes) -> (hash: FixedBytes(32)), - 0x04: identity(data: Bytes) -> (data: Bytes), - 0x05: modexp(Bsize: Uint(256), Esize: Uint(256), Msize: Uint(256), BEM: Bytes) -> (value: Bytes), - 0x06: ecadd(x1: Uint(256), y1: Uint(256), x2: Uint(256), y2: Uint(256)) -> (x: Uint(256), y: Uint(256)), - 0x07: ecmul(x1: Uint(256), y1: Uint(256), s: Uint(256)) -> (x: Uint(256), y: Uint(256)), - 0x08: ecpairing(x1: Uint(256), y1: Uint(256), x2: Uint(256), y2: Uint(256), x3: Uint(256), y3: Uint(256)) -> (success: Uint(256)), - 0x09: blake2f(rounds: Uint(4), h: FixedBytes(64), m: FixedBytes(128), t: FixedBytes(16), f: FixedBytes(1)) -> (h: FixedBytes(64)), - ).into(), - contracts: Default::default(), labels: [ @@ -167,7 +132,8 @@ impl CallTraceDecoder { functions: HARDHAT_CONSOLE_ABI .functions() - .chain(HEVM_ABI.functions()) + // TODO + // .chain(HEVM_ABI.functions()) .map(|func| (func.short_signature(), vec![func.clone()])) .collect(), @@ -241,25 +207,29 @@ impl CallTraceDecoder { pub async fn decode(&self, traces: &mut CallTraceArena) { for node in &mut traces.arena { // Set contract name - if let Some(contract) = self.contracts.get(&node.trace.address).cloned() { - node.trace.contract = Some(contract); + 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).cloned() { - node.trace.label = Some(label); + if let Some(label) = self.labels.get(&node.trace.address) { + node.trace.label = Some(label.clone()); } // Decode call - if let Some(precompile_fn) = self.precompiles.get(&node.trace.address) { - node.decode_precompile(precompile_fn, &self.labels); - } else if let RawOrDecodedCall::Raw(ref bytes) = node.trace.data { + if precompiles::decode(&mut node.trace) { + return + } + + if let RawOrDecodedCall::Raw(bytes) = &node.trace.data { if bytes.len() >= 4 { if let Some(funcs) = self.functions.get(&bytes[..SELECTOR_LEN]) { node.decode_function(funcs, &self.labels, &self.errors, self.verbosity); } else if node.trace.address == DEFAULT_CREATE2_DEPLOYER.to_ethers() { - node.trace.data = - RawOrDecodedCall::Decoded("create2".to_string(), String::new(), vec![]); + node.trace.data = RawOrDecodedCall::Decoded { + signature: "create2".to_string(), + args: vec![], + }; } else if let Some(identifier) = &self.signature_identifier { if let Some(function) = identifier.write().await.identify_function(&bytes[..SELECTOR_LEN]).await @@ -273,28 +243,21 @@ impl CallTraceDecoder { } } } else { - let has_receive = self - .receive_contracts - .get(&node.trace.address) - .copied() - .unwrap_or_default(); - let func_name = - if bytes.is_empty() && has_receive { "receive" } else { "fallback" }; - - node.trace.data = - RawOrDecodedCall::Decoded(func_name.to_string(), String::new(), Vec::new()); + let has_receive = + self.receive_contracts.get(&node.trace.address).copied().unwrap_or(false); + let signature = + if bytes.is_empty() && has_receive { "receive()" } else { "fallback()" } + .into(); + node.trace.data = RawOrDecodedCall::Decoded { signature, args: Vec::new() }; if let RawOrDecodedReturnData::Raw(bytes) = &node.trace.output { if !node.trace.success { - if let Ok(decoded_error) = decode::decode_revert( - &bytes[..], - Some(&self.errors), - Some(node.trace.status), - ) { - node.trace.output = RawOrDecodedReturnData::Decoded(format!( - r#""{decoded_error}""# + node.trace.output = + RawOrDecodedReturnData::Decoded(decode::decode_revert( + bytes, + Some(&self.errors), + Some(node.trace.status), )); - } } } } @@ -341,7 +304,7 @@ impl CallTraceDecoder { .map(|param| { // undo patched names let name = if empty_params.contains(¶m.name) { - "".to_string() + String::new() } else { param.name }; diff --git a/crates/evm/src/trace/decoder/precompiles.rs b/crates/evm/src/trace/decoder/precompiles.rs new file mode 100644 index 0000000000000..d7f0d5c090806 --- /dev/null +++ b/crates/evm/src/trace/decoder/precompiles.rs @@ -0,0 +1,86 @@ +use crate::trace::CallTrace; +use alloy_primitives::U256; +use alloy_sol_types::{abi, abi::token, sol}; + +sol! { + /// Ethereum precompiles interface. + /// + /// Parameter names and types are taken from [evm.codes](https://www.evm.codes/precompiled). + /// + /// Note that this interface should not be used directly for decoding, but rather through + /// [CallTraceDecoder]. + /// This is because `modexp`, `ecpairing`, and `blake2f` don't strictly follow the ABI codec. + interface EthereumPrecompiles { + /* 0x01 */ function ecrecover(bytes32 hash, uint8 v, uint256 r, uint256 s) returns (address publicAddress); + /* 0x02 */ function sha256(bytes data) returns (bytes32 hash); + /* 0x03 */ function ripemd(bytes data) returns (bytes20 hash); + /* 0x04 */ function identity(bytes data) returns (bytes data); + /* 0x05 */ function modexp(uint256 Bsize, uint256 Esize, uint256 Msize, bytes B, bytes E, bytes M) returns (bytes value); + /* 0x06 */ function ecadd(uint256 x1, uint256 y1, uint256 x2, uint256 y2) returns (uint256 x, uint256 y); + /* 0x07 */ function ecmul(uint256 x1, uint256 y1, uint256 s) returns (uint256 x, uint256 y); + /* 0x08 */ function ecpairing(uint256[] x, uint256[] y) returns (bool success); + /* 0x09 */ function blake2f(uint32 rounds, uint64[8] h, uint64[16] m, uint64[2] t, bool f) returns (uint64[8] h); + } +} + +macro_rules! try_ { + ($e:expr) => { + match $e { + Ok(x) => x, + Err(_) => return false, + } + }; +} + +#[must_use] +pub(super) fn decode(trace: &mut CallTrace) -> bool { + let [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, x] = &trace.address.0 else { + return false + }; + + match *x { + 0x01 => {} + 0x02 => {} + 0x03 => {} + 0x04 => {} + 0x05 => {} + 0x06 => {} + 0x07 => {} + 0x08 => {} + 0x09 => {} + _ => return false, + } + + trace.contract = Some("PRECOMPILES".into()); + + true +} + +fn decode_modexp(data: &[u8]) -> alloy_sol_types::Result> { + let mut decoder = abi::Decoder::new(data, false); + let b_size = decode_usize(&mut decoder)?; + let e_size = decode_usize(&mut decoder)?; + let m_size = decode_usize(&mut decoder)?; + // TODO + let b = b""; + let e = b""; + let m = b""; + // let b = decoder.take_slice_unchecked(b_size)?; + // let e = decoder.take_slice_unchecked(e_size)?; + // let m = decoder.take_slice_unchecked(m_size)?; + // decoder.finish()?; + Ok(vec![ + b_size.to_string(), + e_size.to_string(), + m_size.to_string(), + hex::encode_prefixed(b), + hex::encode_prefixed(e), + hex::encode_prefixed(m), + ]) +} + +fn decode_usize(decoder: &mut abi::Decoder<'_>) -> alloy_sol_types::Result { + let word = decoder.decode::()?; + usize::try_from(>::from(word.0)) + .map_err(|e| alloy_sol_types::Error::custom(e.to_string())) +} diff --git a/crates/evm/src/trace/mod.rs b/crates/evm/src/trace/mod.rs index 999ed94d7e37a..db294e20557d6 100644 --- a/crates/evm/src/trace/mod.rs +++ b/crates/evm/src/trace/mod.rs @@ -311,29 +311,29 @@ pub enum LogCallOrder { /// Raw or decoded calldata. #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] pub enum RawOrDecodedCall { - /// Raw calldata + /// Raw calldata bytes. Raw(Bytes), /// Decoded calldata. - /// - /// The first element in the tuple is the function name, second is the function signature and - /// the third element is a vector of decoded parameters. - Decoded(String, String, Vec), + Decoded { + /// The function signature. + signature: String, + /// The function arguments. + args: Vec, + }, } -impl RawOrDecodedCall { - pub fn to_raw(&self) -> Vec { - match self { - RawOrDecodedCall::Raw(raw) => raw.to_vec(), - RawOrDecodedCall::Decoded(_, _, _) => { - vec![] - } - } +impl Default for RawOrDecodedCall { + fn default() -> Self { + Self::Raw(Bytes::new()) } } -impl Default for RawOrDecodedCall { - fn default() -> Self { - RawOrDecodedCall::Raw(Default::default()) +impl RawOrDecodedCall { + pub fn to_raw(&self) -> &[u8] { + match self { + RawOrDecodedCall::Raw(raw) => raw, + RawOrDecodedCall::Decoded { .. } => &[], + } } } @@ -346,6 +346,12 @@ pub enum RawOrDecodedReturnData { Decoded(String), } +impl Default for RawOrDecodedReturnData { + fn default() -> Self { + Self::Raw(Bytes::new()) + } +} + impl RawOrDecodedReturnData { /// Returns the data as [`Bytes`] pub fn to_bytes(&self) -> Bytes { @@ -360,12 +366,6 @@ impl RawOrDecodedReturnData { } } -impl Default for RawOrDecodedReturnData { - fn default() -> Self { - RawOrDecodedReturnData::Raw(Default::default()) - } -} - impl fmt::Display for RawOrDecodedReturnData { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &self { @@ -523,7 +523,9 @@ impl fmt::Display for CallTrace { assert!(bytes.len() >= 4); (hex::encode(&bytes[0..4]), hex::encode(&bytes[4..])) } - RawOrDecodedCall::Decoded(func, _, inputs) => (func.clone(), inputs.join(", ")), + RawOrDecodedCall::Decoded { signature, args } => { + (signature.clone(), args.join(", ")) + } }; let action = match self.kind { diff --git a/crates/evm/src/trace/node.rs b/crates/evm/src/trace/node.rs index 6aea5d4dff650..4fcdce44bb313 100644 --- a/crates/evm/src/trace/node.rs +++ b/crates/evm/src/trace/node.rs @@ -80,7 +80,7 @@ impl CallTraceNode { to: self.trace.address, value: self.trace.value, gas: self.trace.gas_cost.into(), - input: self.trace.data.to_raw().into(), + input: self.trace.data.to_raw().to_vec().into(), call_type: self.kind().into(), }) } @@ -88,7 +88,7 @@ impl CallTraceNode { from: self.trace.caller, value: self.trace.value, gas: self.trace.gas_cost.into(), - init: self.trace.data.to_raw().into(), + init: self.trace.data.to_raw().to_vec().into(), }), } } @@ -109,7 +109,7 @@ impl CallTraceNode { let func = &funcs[0]; if let RawOrDecodedCall::Raw(ref bytes) = self.trace.data { - let inputs = if bytes.len() >= SELECTOR_LEN { + let args = if bytes.len() >= SELECTOR_LEN { if self.trace.address == CHEATCODE_ADDRESS.to_ethers() { // Try to decode cheatcode inputs in a more custom way utils::decode_cheatcode_inputs(func, bytes, errors, verbosity).unwrap_or_else( @@ -132,8 +132,7 @@ impl CallTraceNode { }; // add signature to decoded calls for better calls filtering - self.trace.data = - RawOrDecodedCall::Decoded(func.name.clone(), func.signature(), inputs); + self.trace.data = RawOrDecodedCall::Decoded { signature: func.signature(), args }; if let RawOrDecodedReturnData::Raw(bytes) = &self.trace.output { if !bytes.is_empty() && self.trace.success { @@ -162,11 +161,12 @@ impl CallTraceNode { ); } } - } else if let Ok(decoded_error) = - decode::decode_revert(bytes, Some(errors), Some(self.trace.status)) - { - self.trace.output = - RawOrDecodedReturnData::Decoded(format!(r#""{decoded_error}""#)); + } else { + self.trace.output = RawOrDecodedReturnData::Decoded(decode::decode_revert( + bytes, + Some(errors), + Some(self.trace.status), + )); } } } @@ -180,14 +180,13 @@ impl CallTraceNode { ) { if let RawOrDecodedCall::Raw(ref bytes) = self.trace.data { self.trace.label = Some("PRECOMPILE".to_string()); - self.trace.data = RawOrDecodedCall::Decoded( - precompile_fn.name.clone(), - precompile_fn.signature(), - precompile_fn.decode_input(bytes).map_or_else( + self.trace.data = RawOrDecodedCall::Decoded { + signature: precompile_fn.signature(), + args: precompile_fn.decode_input(bytes).map_or_else( |_| vec![hex::encode(bytes)], |tokens| tokens.iter().map(|token| utils::label(token, labels)).collect(), ), - ); + }; if let RawOrDecodedReturnData::Raw(ref bytes) = self.trace.output { self.trace.output = RawOrDecodedReturnData::Decoded( diff --git a/crates/evm/src/trace/utils.rs b/crates/evm/src/trace/utils.rs index 14d4f245d2f33..32a867bf308ca 100644 --- a/crates/evm/src/trace/utils.rs +++ b/crates/evm/src/trace/utils.rs @@ -33,9 +33,7 @@ pub(crate) fn decode_cheatcode_inputs( verbosity: u8, ) -> Option> { match func.name.as_str() { - "expectRevert" => { - decode::decode_revert(data, Some(errors), None).ok().map(|decoded| vec![decoded]) - } + "expectRevert" => Some(vec![decode::decode_revert(data, Some(errors), None)]), "rememberKey" | "addr" | "startBroadcast" | "broadcast" => { // these functions accept a private key as uint256, which should not be // converted to plain text diff --git a/crates/forge/bin/cmd/script/executor.rs b/crates/forge/bin/cmd/script/executor.rs index 648b179c8444b..a7f81d546dae6 100644 --- a/crates/forge/bin/cmd/script/executor.rs +++ b/crates/forge/bin/cmd/script/executor.rs @@ -170,7 +170,7 @@ impl ScriptArgs { return Some(AdditionalContract { opcode: node.kind(), address: node.trace.address.to_alloy(), - init_code: node.trace.data.to_raw(), + init_code: node.trace.data.to_raw().into(), }) } None diff --git a/crates/forge/bin/cmd/script/mod.rs b/crates/forge/bin/cmd/script/mod.rs index b17f958859bbc..87af3892932cb 100644 --- a/crates/forge/bin/cmd/script/mod.rs +++ b/crates/forge/bin/cmd/script/mod.rs @@ -363,11 +363,7 @@ impl ScriptArgs { } if !result.success { - let revert_msg = decode::decode_revert(&result.returned[..], None, None) - .map(|err| format!("{err}\n")) - .unwrap_or_else(|_| "Script failed.\n".to_string()); - - eyre::bail!("{}", Paint::red(revert_msg)); + eyre::bail!("{}", Paint::red(decode::decode_revert(&result.returned[..], None, None))); } Ok(()) diff --git a/crates/forge/src/gas_report.rs b/crates/forge/src/gas_report.rs index 602d65b4db2a1..60ea3e47e441b 100644 --- a/crates/forge/src/gas_report.rs +++ b/crates/forge/src/gas_report.rs @@ -18,6 +18,7 @@ pub struct GasReport { pub struct ContractInfo { pub gas: U256, pub size: U256, + /// `name -> signature -> GasInfo` pub functions: BTreeMap>, } @@ -78,16 +79,18 @@ impl GasReport { contract_report.size = bytes.len().into(); } // TODO: More robust test contract filtering - RawOrDecodedCall::Decoded(func, sig, _) - if !func.is_test() && !func.is_setup() => + RawOrDecodedCall::Decoded { signature, .. } + if !signature.is_test() && !signature.is_setup() => { - let function_report = contract_report + let name = signature.split_once('(').unwrap().0; + contract_report .functions - .entry(func.clone()) + .entry(name.to_string()) .or_default() - .entry(sig.clone()) - .or_default(); - function_report.calls.push(trace.gas_cost.into()); + .entry(signature.clone()) + .or_default() + .calls + .push(trace.gas_cost.into()); } _ => (), } diff --git a/crates/macros/impl/src/cheatcodes.rs b/crates/macros/impl/src/cheatcodes.rs index e862c26ad5b37..4bd978e52a699 100644 --- a/crates/macros/impl/src/cheatcodes.rs +++ b/crates/macros/impl/src/cheatcodes.rs @@ -2,6 +2,9 @@ use proc_macro2::{Ident, Span, TokenStream}; use quote::{quote, quote_spanned}; use syn::{Attribute, Data, DataStruct, DeriveInput, Error, Result}; +// Skip warnings for these items. +const ALLOWED_ITEMS: &[&str] = &["CheatCodeError", "VmErrors"]; + pub fn derive_cheatcode(input: &DeriveInput) -> Result { let name = &input.ident; let name_s = name.to_string(); @@ -11,7 +14,7 @@ pub fn derive_cheatcode(input: &DeriveInput) -> Result { _ => {} } - if name_s.ends_with("Return") { + if name_s.ends_with("Return") || ALLOWED_ITEMS.contains(&name_s.as_str()) { if let Data::Struct(data) = &input.data { check_named_fields(data, name); } @@ -103,6 +106,7 @@ fn derive_struct(name: &Ident, data: &DataStruct, attrs: &[Attribute]) -> Result mutability: Mutability::#mutability, signature: #signature, selector: #selector, + selector_bytes: ::SELECTOR, description: #description, group: Group::#group, status: Status::#status,