Skip to content

Commit 031f55b

Browse files
committed
feat: forge decode-error
1 parent ad04f23 commit 031f55b

File tree

6 files changed

+178
-1
lines changed

6 files changed

+178
-1
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/forge/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ alloy-transport.workspace = true
5858
alloy-signer.workspace = true
5959
alloy-consensus.workspace = true
6060
alloy-chains.workspace = true
61+
alloy-sol-types.workspace = true
6162

6263
async-trait = "0.1"
6364
clap = { version = "4", features = ["derive", "env", "unicode", "wrap_help"] }

crates/forge/bin/cmd/decode_error.rs

+170
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
use clap::Parser;
2+
use eyre::{eyre, Result};
3+
use foundry_cli::opts::{CompilerArgs, CoreBuildArgs};
4+
use foundry_common::compile::ProjectCompiler;
5+
use foundry_compilers::artifacts::output_selection::ContractOutputSelection;
6+
use std::fmt;
7+
8+
use alloy_dyn_abi::ErrorExt;
9+
use alloy_json_abi::Error;
10+
use alloy_sol_types::{Panic, Revert, SolError};
11+
12+
macro_rules! spaced_print {
13+
($($arg:tt)*) => {
14+
println!($($arg)*);
15+
println!();
16+
};
17+
}
18+
19+
#[derive(Debug, Clone)]
20+
enum RevertType {
21+
Revert,
22+
Panic,
23+
/// The 4 byte signature of the error
24+
Custom([u8; 4]),
25+
}
26+
27+
impl fmt::Display for RevertType {
28+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29+
match self {
30+
RevertType::Revert => write!(f, "Revert"),
31+
RevertType::Panic => write!(f, "Panic"),
32+
RevertType::Custom(selector) => write!(f, "Custom(0x{})", hex::encode(selector)),
33+
}
34+
}
35+
}
36+
37+
impl From<[u8; 4]> for RevertType {
38+
fn from(selector: [u8; 4]) -> Self {
39+
match selector {
40+
Revert::SELECTOR => RevertType::Revert,
41+
Panic::SELECTOR => RevertType::Panic,
42+
_ => RevertType::Custom(selector),
43+
}
44+
}
45+
}
46+
47+
/// CLI arguments for `forge inspect`.
48+
#[derive(Clone, Debug, Parser)]
49+
pub struct DecodeError {
50+
/// The hex encoded revert data
51+
revert_data: String,
52+
53+
/// All build arguments are supported
54+
#[command(flatten)]
55+
build: CoreBuildArgs,
56+
}
57+
58+
impl DecodeError {
59+
pub fn run(self) -> Result<()> {
60+
let DecodeError { revert_data, build } = self;
61+
62+
if revert_data.len() < 8 {
63+
return Err(eyre!("Revert data is too short"));
64+
}
65+
66+
// convert to bytes and get the selector
67+
let data_bytes = hex::decode(revert_data.trim_start_matches("0x"))?;
68+
let selector: [u8; 4] = data_bytes[..4].try_into()?;
69+
70+
trace!(target: "forge", "running forge decode-error on error type {}", RevertType::from(selector));
71+
72+
// Make sure were gonna get the abi out
73+
let mut cos = build.compiler.extra_output;
74+
if !cos.iter().any(|selected| *selected == ContractOutputSelection::Abi) {
75+
cos.push(ContractOutputSelection::Abi);
76+
}
77+
78+
// Build modified Args
79+
let modified_build_args = CoreBuildArgs {
80+
compiler: CompilerArgs { extra_output: cos, ..build.compiler },
81+
..build
82+
};
83+
84+
// Build the project
85+
if let Ok(project) = modified_build_args.project() {
86+
let compiler = ProjectCompiler::new().quiet(true);
87+
let output = compiler.compile(&project)?;
88+
89+
// search the project for the error
90+
//
91+
// we want to search even it matches the builtin errors because there could be a collision
92+
let found_errs = output
93+
.artifacts()
94+
.filter_map(|(name, artifact)| {
95+
Some((
96+
name,
97+
artifact.abi.as_ref()?.errors.iter().find_map(|(_, err)| {
98+
// check if we have an error with a matching selector
99+
// there can only be one per artifact
100+
err.iter().find(|err| err.selector() == selector)
101+
})?,
102+
))
103+
})
104+
.collect::<Vec<_>>();
105+
106+
if !found_errs.is_empty() {
107+
pretty_print_custom_errros(found_errs, &data_bytes);
108+
}
109+
} else {
110+
tracing::trace!("No project found")
111+
}
112+
113+
// try to decode the builtin errors if it matches
114+
pretty_print_builtin_errors(selector.into(), &data_bytes);
115+
116+
Ok(())
117+
}
118+
}
119+
120+
fn pretty_print_custom_errros(found_errs: Vec<(String, &Error)>, data: &[u8]) {
121+
let mut failures = Vec::with_capacity(found_errs.len());
122+
let mut did_succeed = false;
123+
for (artifact, dyn_err) in found_errs {
124+
match dyn_err.decode_error(data) {
125+
Ok(decoded) => {
126+
did_succeed = true;
127+
128+
print_line();
129+
println!("Artifact: {}", artifact);
130+
println!("Error Name: {}", dyn_err.name);
131+
for (param, value) in dyn_err.inputs.iter().zip(decoded.body.iter()) {
132+
println!(" {}: {:?}", param.name, value);
133+
}
134+
println!("");
135+
}
136+
Err(e) => {
137+
tracing::error!("Error decoding dyn err: {}", e);
138+
failures.push(format!("decoding data for {} failed", dyn_err.signature()));
139+
}
140+
};
141+
}
142+
143+
if !did_succeed {
144+
for failure in failures {
145+
tracing::error!("{}", failure);
146+
}
147+
}
148+
}
149+
150+
fn pretty_print_builtin_errors(revert_type: RevertType, data: &[u8]) {
151+
match revert_type {
152+
RevertType::Revert => {
153+
if let Ok(revert) = Revert::abi_decode(data, true) {
154+
print_line();
155+
spaced_print!("{:#?}\n", revert);
156+
}
157+
}
158+
RevertType::Panic => {
159+
if let Ok(panic) = Panic::abi_decode(data, true) {
160+
print_line();
161+
spaced_print!("{:#?}", panic);
162+
}
163+
}
164+
_ => {}
165+
}
166+
}
167+
168+
fn print_line() {
169+
spaced_print!("--------------------------------------------------------");
170+
}

crates/forge/bin/cmd/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,4 @@ pub mod test;
6363
pub mod tree;
6464
pub mod update;
6565
pub mod watch;
66+
pub mod decode_error;

crates/forge/bin/main.rs

+1
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ fn main() -> Result<()> {
110110
GenerateSubcommands::Test(cmd) => cmd.run(),
111111
},
112112
ForgeSubcommand::VerifyBytecode(cmd) => utils::block_on(cmd.run()),
113+
ForgeSubcommand::DecodeError(cmd) => cmd.run(),
113114
}
114115
}
115116

crates/forge/bin/opts.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use crate::cmd::{
22
bind::BindArgs, build::BuildArgs, cache::CacheArgs, clone::CloneArgs, config, coverage,
33
create::CreateArgs, debug::DebugArgs, doc::DocArgs, flatten, fmt::FmtArgs, geiger, generate,
44
init::InitArgs, inspect, install::InstallArgs, remappings::RemappingArgs, remove::RemoveArgs,
5-
selectors::SelectorsSubcommands, snapshot, test, tree, update,
5+
selectors::SelectorsSubcommands, snapshot, test, tree, update, decode_error,
66
};
77
use clap::{Parser, Subcommand, ValueHint};
88
use forge_script::ScriptArgs;
@@ -161,6 +161,9 @@ pub enum ForgeSubcommand {
161161
/// Verify the deployed bytecode against its source.
162162
#[clap(visible_alias = "vb")]
163163
VerifyBytecode(VerifyBytecodeArgs),
164+
165+
/// Attempt to decode raw bytes from a revert message
166+
DecodeError(decode_error::DecodeError),
164167
}
165168

166169
#[cfg(test)]

0 commit comments

Comments
 (0)