Skip to content

Commit

Permalink
feat: forge decode-error
Browse files Browse the repository at this point in the history
  • Loading branch information
nhtyy committed Apr 24, 2024
1 parent ad04f23 commit 2b3e76c
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 1 deletion.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/forge/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ alloy-transport.workspace = true
alloy-signer.workspace = true
alloy-consensus.workspace = true
alloy-chains.workspace = true
alloy-sol-types.workspace = true

async-trait = "0.1"
clap = { version = "4", features = ["derive", "env", "unicode", "wrap_help"] }
Expand Down
170 changes: 170 additions & 0 deletions crates/forge/bin/cmd/decode_error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
use clap::Parser;
use eyre::{eyre, Result};
use foundry_cli::opts::{CompilerArgs, CoreBuildArgs};
use foundry_common::compile::ProjectCompiler;
use foundry_compilers::artifacts::output_selection::ContractOutputSelection;
use std::fmt;

use alloy_dyn_abi::ErrorExt;
use alloy_json_abi::Error;
use alloy_sol_types::{Panic, Revert, SolError};

macro_rules! spaced_print {
($($arg:tt)*) => {
println!($($arg)*);
println!();
};
}

#[derive(Debug, Clone)]
enum RevertType {
Revert,
Panic,
/// The 4 byte signature of the error
Custom([u8; 4]),
}

impl fmt::Display for RevertType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RevertType::Revert => write!(f, "Revert"),
RevertType::Panic => write!(f, "Panic"),
RevertType::Custom(selector) => write!(f, "Custom(0x{})", hex::encode(selector)),
}
}
}

impl From<[u8; 4]> for RevertType {
fn from(selector: [u8; 4]) -> Self {
match selector {
Revert::SELECTOR => RevertType::Revert,
Panic::SELECTOR => RevertType::Panic,
_ => RevertType::Custom(selector),
}
}
}

/// CLI arguments for `forge inspect`.
#[derive(Clone, Debug, Parser)]
pub struct DecodeError {
/// The hex encoded revert data
revert_data: String,

/// All build arguments are supported
#[command(flatten)]
build: CoreBuildArgs,
}

impl DecodeError {
pub fn run(self) -> Result<()> {
let DecodeError { revert_data, build } = self;

if revert_data.len() < 8 {
return Err(eyre!("Revert data is too short"));
}

// convert to bytes and get the selector
let data_bytes = hex::decode(revert_data.trim_start_matches("0x"))?;
let selector: [u8; 4] = data_bytes[..4].try_into()?;

trace!(target: "forge", "running forge decode-error on error type {}", RevertType::from(selector));

// Make sure were gonna get the abi out
let mut cos = build.compiler.extra_output;
if !cos.iter().any(|selected| *selected == ContractOutputSelection::Abi) {
cos.push(ContractOutputSelection::Abi);
}

// Build modified Args
let modified_build_args = CoreBuildArgs {
compiler: CompilerArgs { extra_output: cos, ..build.compiler },
..build
};

// Build the project
if let Ok(project) = modified_build_args.project() {
let compiler = ProjectCompiler::new().quiet(true);
let output = compiler.compile(&project)?;

// search the project for the error
//
// we want to search even it matches the builtin errors because there could be a collision
let found_errs = output
.artifacts()
.filter_map(|(name, artifact)| {
Some((
name,
artifact.abi.as_ref()?.errors.iter().find_map(|(_, err)| {
// check if we have an error with a matching selector
// there can only be one per artifact
err.iter().find(|err| err.selector() == selector)
})?,
))
})
.collect::<Vec<_>>();

if !found_errs.is_empty() {
pretty_print_custom_errros(found_errs, &data_bytes);
}
} else {
tracing::trace!("No project found")
}

// try to decode the builtin errors if it matches
pretty_print_builtin_errors(selector.into(), &data_bytes);

Ok(())
}
}

fn pretty_print_custom_errros(found_errs: Vec<(String, &Error)>, data: &[u8]) {
let mut failures = Vec::with_capacity(found_errs.len());
let mut did_succeed = false;
for (artifact, dyn_err) in found_errs {
match dyn_err.decode_error(data) {
Ok(decoded) => {
did_succeed = true;

print_line();
println!("Artifact: {}", artifact);
println!("Error Name: {}", dyn_err.name);
for (param, value) in dyn_err.inputs.iter().zip(decoded.body.iter()) {
println!(" {}: {:?}", param.name, value);
}
println!("");
}
Err(e) => {
tracing::error!("Error decoding dyn err: {}", e);
failures.push(format!("decoding data for {} failed", dyn_err.signature()));
}
};
}

if !did_succeed {
for failure in failures {
tracing::error!("{}", failure);
}
}
}

fn pretty_print_builtin_errors(revert_type: RevertType, data: &[u8]) {
match revert_type {
RevertType::Revert => {
if let Ok(revert) = Revert::abi_decode(data, true) {
print_line();
spaced_print!("{:#?}\n", revert);
}
}
RevertType::Panic => {
if let Ok(panic) = Panic::abi_decode(data, true) {
print_line();
spaced_print!("{:#?}", panic);
}
}
_ => {}
}
}

fn print_line() {
spaced_print!("--------------------------------------------------------");
}
1 change: 1 addition & 0 deletions crates/forge/bin/cmd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,4 @@ pub mod test;
pub mod tree;
pub mod update;
pub mod watch;
pub mod decode_error;
1 change: 1 addition & 0 deletions crates/forge/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ fn main() -> Result<()> {
GenerateSubcommands::Test(cmd) => cmd.run(),
},
ForgeSubcommand::VerifyBytecode(cmd) => utils::block_on(cmd.run()),
ForgeSubcommand::DecodeError(cmd) => cmd.run(),
}
}

Expand Down
5 changes: 4 additions & 1 deletion crates/forge/bin/opts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::cmd::{
bind::BindArgs, build::BuildArgs, cache::CacheArgs, clone::CloneArgs, config, coverage,
create::CreateArgs, debug::DebugArgs, doc::DocArgs, flatten, fmt::FmtArgs, geiger, generate,
init::InitArgs, inspect, install::InstallArgs, remappings::RemappingArgs, remove::RemoveArgs,
selectors::SelectorsSubcommands, snapshot, test, tree, update,
selectors::SelectorsSubcommands, snapshot, test, tree, update, decode_error,
};
use clap::{Parser, Subcommand, ValueHint};
use forge_script::ScriptArgs;
Expand Down Expand Up @@ -161,6 +161,9 @@ pub enum ForgeSubcommand {
/// Verify the deployed bytecode against its source.
#[clap(visible_alias = "vb")]
VerifyBytecode(VerifyBytecodeArgs),

/// Attempt to decode raw bytes from a revert message
DecodeError(decode_error::DecodeError),
}

#[cfg(test)]
Expand Down

0 comments on commit 2b3e76c

Please sign in to comment.