Skip to content

Commit

Permalink
fix: precompile trace decoding (#6263)
Browse files Browse the repository at this point in the history
* fix: precompile trace decoding

* refactor: move decoding to decoder module

* renames

* renames2

* stuff

* chore: clippy

* move decoding out of utils

* move cheatcode decoding

* fix: empty decode
  • Loading branch information
DaniPopes authored Nov 9, 2023
1 parent 57180fc commit 9194d86
Show file tree
Hide file tree
Showing 9 changed files with 282 additions and 369 deletions.
2 changes: 1 addition & 1 deletion crates/anvil/src/eth/backend/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use foundry_evm::{
interpreter::InstructionResult,
primitives::{BlockEnv, CfgEnv, EVMError, Env, ExecutionResult, Output, SpecId},
},
traces::{node::CallTraceNode, CallTraceArena},
traces::{CallTraceArena, CallTraceNode},
utils::{eval_to_instruction_result, halt_to_instruction_result},
};
use foundry_utils::types::{ToAlloy, ToEthers};
Expand Down
334 changes: 240 additions & 94 deletions crates/evm/traces/src/decoder/mod.rs

Large diffs are not rendered by default.

5 changes: 2 additions & 3 deletions crates/evm/traces/src/decoder/precompiles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,13 @@ pub(super) fn decode(trace: &mut CallTrace, _chain_id: u64) -> bool {
0x08 => (ecpairingCall::SIGNATURE, tri!(decode_ecpairing(data))),
0x09 => (blake2fCall::SIGNATURE, tri!(decode_blake2f(data))),
0x0a => (pointEvaluationCall::SIGNATURE, tri!(decode_kzg(data))),
_ => unreachable!(),
0x00 | 0x0b.. => unreachable!(),
};

// TODO: Other chain precompiles

trace.data = TraceCallData::Decoded { signature: signature.to_string(), args };

trace.contract = Some("PRECOMPILES".into());
trace.label = Some("PRECOMPILES".into());

true
}
Expand Down
2 changes: 1 addition & 1 deletion crates/evm/traces/src/identifier/signatures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ impl SignaturesIdentifier {
&mut self,
selector_type: SelectorType,
identifier: &[u8],
get_type: fn(&str) -> eyre::Result<T>,
get_type: impl Fn(&str) -> eyre::Result<T>,
) -> Option<T> {
// Exit early if we have unsuccessfully queried it before.
if self.unavailable.contains(identifier) {
Expand Down
8 changes: 3 additions & 5 deletions crates/evm/traces/src/inspector.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use crate::{
CallTrace, CallTraceArena, CallTraceStep, LogCallOrder, RawOrDecodedLog, TraceCallData,
TraceRetData,
CallTrace, CallTraceArena, CallTraceStep, LogCallOrder, TraceCallData, TraceLog, TraceRetData,
};
use alloy_primitives::{Address, Bytes, Log as RawLog, B256, U256};
use foundry_evm_core::{
Expand Down Expand Up @@ -162,9 +161,8 @@ impl<DB: Database> Inspector<DB> for Tracer {
let node = &mut self.traces.arena[*self.trace_stack.last().expect("no ongoing trace")];
node.ordering.push(LogCallOrder::Log(node.logs.len()));
let data = data.clone();
node.logs.push(RawOrDecodedLog::Raw(
RawLog::new(topics.to_vec(), data).expect("Received invalid log"),
));
node.logs
.push(TraceLog::Raw(RawLog::new(topics.to_vec(), data).expect("Received invalid log")));
}

#[inline]
Expand Down
65 changes: 27 additions & 38 deletions crates/evm/traces/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,11 @@ use foundry_evm_core::{constants::CHEATCODE_ADDRESS, debug::Instruction, utils::
use foundry_utils::types::ToEthers;
use hashbrown::HashMap;
use itertools::Itertools;
use node::CallTraceNode;
use revm::interpreter::{opcode, CallContext, InstructionResult, Memory, Stack};
use serde::{Deserialize, Serialize};
use std::{
collections::{BTreeMap, HashSet},
fmt::{self, Write},
fmt,
};
use yansi::{Color, Paint};

Expand All @@ -35,7 +34,9 @@ pub use decoder::{CallTraceDecoder, CallTraceDecoderBuilder};
mod inspector;
pub use inspector::Tracer;

pub mod node;
mod node;
pub use node::CallTraceNode;

pub mod utils;

pub type Traces = Vec<(TraceKind, CallTraceArena)>;
Expand Down Expand Up @@ -198,79 +199,66 @@ impl fmt::Display for CallTraceArena {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fn inner(
arena: &CallTraceArena,
writer: &mut (impl Write + ?Sized),
f: &mut fmt::Formatter<'_>,
idx: usize,
left: &str,
child: &str,
verbose: bool,
) -> fmt::Result {
let node = &arena.arena[idx];

// Display trace header
if !verbose {
writeln!(writer, "{left}{}", node.trace)?;
} else {
writeln!(writer, "{left}{:#}", node.trace)?;
}
f.write_str(left)?;
node.trace.fmt(f)?;

// Display logs and subcalls
let left_prefix = format!("{child}{BRANCH}");
let right_prefix = format!("{child}{PIPE}");
for child in &node.ordering {
match child {
LogCallOrder::Log(index) => {
let mut log = String::new();
write!(log, "{}", node.logs[*index])?;

let log = node.logs[*index].to_string();
// Prepend our tree structure symbols to each line of the displayed log
log.lines().enumerate().try_for_each(|(i, line)| {
writeln!(
writer,
f,
"{}{}",
if i == 0 { &left_prefix } else { &right_prefix },
line
)
})?;
}
LogCallOrder::Call(index) => {
inner(
arena,
writer,
node.children[*index],
&left_prefix,
&right_prefix,
verbose,
)?;
inner(arena, f, node.children[*index], &left_prefix, &right_prefix)?;
}
}
}

// Display trace return data
let color = trace_color(&node.trace);
write!(writer, "{child}{EDGE}{}", color.paint(RETURN))?;
write!(f, "{child}{EDGE}{}", color.paint(RETURN))?;
if node.trace.created() {
match &node.trace.output {
TraceRetData::Raw(bytes) => {
writeln!(writer, "{} bytes of code", bytes.len())?;
writeln!(f, "{} bytes of code", bytes.len())?;
}
TraceRetData::Decoded(val) => {
writeln!(writer, "{val}")?;
writeln!(f, "{val}")?;
}
}
} else {
writeln!(writer, "{}", node.trace.output)?;
writeln!(f, "{}", node.trace.output)?;
}

Ok(())
}

inner(self, f, 0, " ", " ", f.alternate())
inner(self, f, 0, " ", " ")
}
}

/// A raw or decoded log.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RawOrDecodedLog {
pub enum TraceLog {
/// A raw log
Raw(RawLog),
/// A decoded log.
Expand All @@ -280,10 +268,10 @@ pub enum RawOrDecodedLog {
Decoded(String, Vec<(String, String)>),
}

impl fmt::Display for RawOrDecodedLog {
impl fmt::Display for TraceLog {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RawOrDecodedLog::Raw(log) => {
TraceLog::Raw(log) => {
for (i, topic) in log.topics().iter().enumerate() {
writeln!(
f,
Expand All @@ -295,7 +283,7 @@ impl fmt::Display for RawOrDecodedLog {

write!(f, " data: {}", Paint::cyan(hex::encode_prefixed(&log.data)))
}
RawOrDecodedLog::Decoded(name, params) => {
TraceLog::Decoded(name, params) => {
let params = params
.iter()
.map(|(name, value)| format!("{name}: {value}"))
Expand Down Expand Up @@ -484,8 +472,6 @@ pub struct CallTrace {
pub steps: Vec<CallTraceStep>,
}

// === impl CallTrace ===

impl CallTrace {
/// Whether this is a contract creation or not
pub fn created(&self) -> bool {
Expand Down Expand Up @@ -516,8 +502,8 @@ impl Default for CallTrace {

impl fmt::Display for CallTrace {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let address = self.address.to_checksum(None);
write!(f, "[{}] ", self.gas_cost)?;
let address = self.address.to_checksum(None);
if self.created() {
write!(
f,
Expand All @@ -530,10 +516,13 @@ impl fmt::Display for CallTrace {
} else {
let (func_name, inputs) = match &self.data {
TraceCallData::Raw(bytes) => {
// We assume that the fallback function (`data.len() < 4`) counts as decoded
// calldata
let (selector, data) = bytes.split_at(4);
(hex::encode(selector), hex::encode(data))
debug!(target: "evm::traces", trace=?self, "unhandled raw calldata");
if bytes.len() < 4 {
("fallback".into(), hex::encode(bytes))
} else {
let (selector, data) = bytes.split_at(4);
(hex::encode(selector), hex::encode(data))
}
}
TraceCallData::Decoded { signature, args } => {
let name = signature.split('(').next().unwrap();
Expand Down
126 changes: 3 additions & 123 deletions crates/evm/traces/src/node.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,9 @@
use crate::{
utils, utils::decode_cheatcode_outputs, CallTrace, LogCallOrder, RawOrDecodedLog,
TraceCallData, TraceRetData,
};
use alloy_dyn_abi::{FunctionExt, JsonAbiExt};
use alloy_json_abi::{Function, JsonAbi as Abi};
use alloy_primitives::Address;
use crate::{CallTrace, LogCallOrder, TraceLog};
use ethers::types::{Action, Call, CallResult, Create, CreateResult, Res, Suicide};
use foundry_common::SELECTOR_LEN;
use foundry_evm_core::{constants::CHEATCODE_ADDRESS, decode, utils::CallKind};
use foundry_evm_core::utils::CallKind;
use foundry_utils::types::ToEthers;
use revm::interpreter::InstructionResult;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// A node in the arena
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
Expand All @@ -26,7 +18,7 @@ pub struct CallTraceNode {
pub trace: CallTrace,
/// Logs
#[serde(skip)]
pub logs: Vec<RawOrDecodedLog>,
pub logs: Vec<TraceLog>,
/// Ordering of child calls and logs
pub ordering: Vec<LogCallOrder>,
}
Expand Down Expand Up @@ -88,116 +80,4 @@ impl CallTraceNode {
}),
}
}

/// Decode a regular function
pub fn decode_function(
&mut self,
funcs: &[Function],
labels: &HashMap<Address, String>,
errors: &Abi,
verbosity: u8,
) {
debug_assert!(!funcs.is_empty(), "requires at least 1 func");
// This is safe because (1) we would not have an entry for the given
// selector if no functions with that selector were added and (2) the
// same selector implies the function has
// the same name and inputs.
let func = &funcs[0];

if let TraceCallData::Raw(ref bytes) = self.trace.data {
let args = if bytes.len() >= SELECTOR_LEN {
if self.trace.address == CHEATCODE_ADDRESS {
// Try to decode cheatcode inputs in a more custom way
utils::decode_cheatcode_inputs(func, bytes, errors, verbosity).unwrap_or_else(
|| {
func.abi_decode_input(&bytes[SELECTOR_LEN..], false)
.expect("bad function input decode")
.iter()
.map(|token| utils::label(token, labels))
.collect()
},
)
} else {
match func.abi_decode_input(&bytes[SELECTOR_LEN..], false) {
Ok(v) => v.iter().map(|token| utils::label(token, labels)).collect(),
Err(_) => Vec::new(),
}
}
} else {
Vec::new()
};

// add signature to decoded calls for better calls filtering
self.trace.data = TraceCallData::Decoded { signature: func.signature(), args };

if let TraceRetData::Raw(bytes) = &self.trace.output {
if self.trace.success {
if self.trace.address == CHEATCODE_ADDRESS {
if let Some(decoded) = funcs
.iter()
.find_map(|func| decode_cheatcode_outputs(func, bytes, verbosity))
{
self.trace.output = TraceRetData::Decoded(decoded);
return
}
}

if let Some(tokens) =
funcs.iter().find_map(|func| func.abi_decode_output(bytes, false).ok())
{
// Functions coming from an external database do not have any outputs
// specified, and will lead to returning an empty list of tokens.
if !tokens.is_empty() {
self.trace.output = TraceRetData::Decoded(
tokens
.iter()
.map(|token| utils::label(token, labels))
.collect::<Vec<_>>()
.join(", "),
);
}
}
} else {
self.trace.output = TraceRetData::Decoded(decode::decode_revert(
bytes,
Some(errors),
Some(self.trace.status),
));
}
}
}
}

/// Decode the node's tracing data for the given precompile function
pub fn decode_precompile(
&mut self,
precompile_fn: &Function,
labels: &HashMap<Address, String>,
) {
if let TraceCallData::Raw(ref bytes) = self.trace.data {
self.trace.label = Some("PRECOMPILE".to_string());
self.trace.data = TraceCallData::Decoded {
signature: precompile_fn.signature(),
args: precompile_fn.abi_decode_input(bytes, false).map_or_else(
|_| vec![hex::encode(bytes)],
|tokens| tokens.iter().map(|token| utils::label(token, labels)).collect(),
),
};

if let TraceRetData::Raw(ref bytes) = self.trace.output {
self.trace.output = TraceRetData::Decoded(
precompile_fn.abi_decode_output(bytes, false).map_or_else(
|_| hex::encode(bytes),
|tokens| {
tokens
.iter()
.map(|token| utils::label(token, labels))
.collect::<Vec<_>>()
.join(", ")
},
),
);
}
}
}
}
Loading

0 comments on commit 9194d86

Please sign in to comment.