Skip to content

Commit ef0c672

Browse files
grandizzyrplusq
authored andcommitted
feat(cast): decode-error with sig, local cache and openchain api (foundry-rs#9428)
* feat(cast): Add custom error decoding support * Review changes * Changes after review: decode with Openchain too, add test * Review changes: nit, handle incomplete selectors
1 parent c5ba719 commit ef0c672

File tree

7 files changed

+124
-12
lines changed

7 files changed

+124
-12
lines changed

crates/cast/bin/args.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,16 @@ pub enum CastSubcommand {
539539
data: String,
540540
},
541541

542+
/// Decode custom error data.
543+
#[command(visible_aliases = &["error-decode", "--error-decode", "erd"])]
544+
DecodeError {
545+
/// The error signature. If none provided then tries to decode from local cache or `https://api.openchain.xyz`.
546+
#[arg(long, visible_alias = "error-sig")]
547+
sig: Option<String>,
548+
/// The error data to decode.
549+
data: String,
550+
},
551+
542552
/// Decode ABI-encoded input or output data.
543553
///
544554
/// Defaults to decoding output data. To decode input data pass --input.

crates/cast/bin/main.rs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#[macro_use]
22
extern crate tracing;
33

4-
use alloy_dyn_abi::{DynSolValue, EventExt};
4+
use alloy_dyn_abi::{DynSolValue, ErrorExt, EventExt};
55
use alloy_primitives::{eip191_hash_message, hex, keccak256, Address, B256};
66
use alloy_provider::Provider;
77
use alloy_rpc_types::{BlockId, BlockNumberOrTag::Latest};
@@ -11,7 +11,7 @@ use clap_complete::generate;
1111
use eyre::Result;
1212
use foundry_cli::{handler, utils};
1313
use foundry_common::{
14-
abi::get_event,
14+
abi::{get_error, get_event},
1515
ens::{namehash, ProviderEnsExt},
1616
fmt::{format_tokens, format_tokens_raw, format_uint_exp},
1717
fs,
@@ -30,6 +30,7 @@ pub mod cmd;
3030
pub mod tx;
3131

3232
use args::{Cast as CastArgs, CastSubcommand, ToBaseArgs};
33+
use cast::traces::identifier::SignaturesIdentifier;
3334

3435
#[macro_use]
3536
extern crate foundry_common;
@@ -216,6 +217,28 @@ async fn main_args(args: CastArgs) -> Result<()> {
216217
let decoded_event = event.decode_log_parts(None, &hex::decode(data)?, false)?;
217218
print_tokens(&decoded_event.body);
218219
}
220+
CastSubcommand::DecodeError { sig, data } => {
221+
let error = if let Some(err_sig) = sig {
222+
get_error(err_sig.as_str())?
223+
} else {
224+
let data = data.strip_prefix("0x").unwrap_or(data.as_str());
225+
let selector = data.get(..8).unwrap_or_default();
226+
let identified_error =
227+
SignaturesIdentifier::new(Config::foundry_cache_dir(), false)?
228+
.write()
229+
.await
230+
.identify_error(&hex::decode(selector)?)
231+
.await;
232+
if let Some(error) = identified_error {
233+
let _ = sh_println!("{}", error.signature());
234+
error
235+
} else {
236+
eyre::bail!("No matching error signature found for selector `{selector}`")
237+
}
238+
};
239+
let decoded_error = error.decode_error(&hex::decode(data)?)?;
240+
print_tokens(&decoded_error.body);
241+
}
219242
CastSubcommand::Interface(cmd) => cmd.run().await?,
220243
CastSubcommand::CreationCode(cmd) => cmd.run().await?,
221244
CastSubcommand::ConstructorArgs(cmd) => cmd.run().await?,

crates/cast/tests/cli/main.rs

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use alloy_primitives::{b256, B256};
66
use alloy_rpc_types::{BlockNumberOrTag, Index};
77
use anvil::{EthereumHardfork, NodeConfig};
88
use foundry_test_utils::{
9-
casttest, file, forgetest_async,
9+
casttest, file, forgetest, forgetest_async,
1010
rpc::{
1111
next_etherscan_api_key, next_http_rpc_endpoint, next_mainnet_etherscan_api_key,
1212
next_rpc_endpoint, next_ws_rpc_endpoint,
@@ -1547,6 +1547,60 @@ casttest!(event_decode, |_prj, cmd| {
15471547
"#]]);
15481548
});
15491549

1550+
// tests cast can decode traces with provided signature
1551+
casttest!(error_decode_with_sig, |_prj, cmd| {
1552+
cmd.args(["decode-error", "--sig", "AnotherValueTooHigh(uint256,address)", "0x7191bc6200000000000000000000000000000000000000000000000000000000000000650000000000000000000000000000000000000000000000000000000000D0004F"]).assert_success().stdout_eq(str![[r#"
1553+
101
1554+
0x0000000000000000000000000000000000D0004F
1555+
1556+
"#]]);
1557+
1558+
cmd.args(["--json"]).assert_success().stdout_eq(str![[r#"
1559+
[
1560+
"101",
1561+
"0x0000000000000000000000000000000000D0004F"
1562+
]
1563+
1564+
"#]]);
1565+
});
1566+
1567+
// tests cast can decode traces with Openchain API
1568+
casttest!(error_decode_with_openchain, |_prj, cmd| {
1569+
cmd.args(["decode-error", "0x7a0e198500000000000000000000000000000000000000000000000000000000000000650000000000000000000000000000000000000000000000000000000000000064"]).assert_success().stdout_eq(str![[r#"
1570+
ValueTooHigh(uint256,uint256)
1571+
101
1572+
100
1573+
1574+
"#]]);
1575+
});
1576+
1577+
// tests cast can decode traces when using local sig identifiers cache
1578+
forgetest!(error_decode_with_cache, |prj, cmd| {
1579+
foundry_test_utils::util::initialize(prj.root());
1580+
prj.add_source(
1581+
"LocalProjectContract",
1582+
r#"
1583+
contract ContractWithCustomError {
1584+
error AnotherValueTooHigh(uint256, address);
1585+
}
1586+
"#,
1587+
)
1588+
.unwrap();
1589+
// Store selectors in local cache.
1590+
cmd.forge_fuse().args(["selectors", "cache"]).assert_success();
1591+
1592+
// Assert cast can decode custom error with local cache.
1593+
cmd.cast_fuse()
1594+
.args(["decode-error", "0x7191bc6200000000000000000000000000000000000000000000000000000000000000650000000000000000000000000000000000000000000000000000000000D0004F"])
1595+
.assert_success()
1596+
.stdout_eq(str![[r#"
1597+
AnotherValueTooHigh(uint256,address)
1598+
101
1599+
0x0000000000000000000000000000000000D0004F
1600+
1601+
"#]]);
1602+
});
1603+
15501604
casttest!(format_units, |_prj, cmd| {
15511605
cmd.args(["format-units", "1000000", "6"]).assert_success().stdout_eq(str![[r#"
15521606
1

crates/cli/src/utils/cmd.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,9 @@ pub fn cache_local_signatures(output: &ProjectCompileOutput, cache_path: PathBuf
501501
.events
502502
.insert(event.selector().to_string(), event.full_signature());
503503
}
504+
for error in abi.errors() {
505+
cached_signatures.errors.insert(error.selector().to_string(), error.signature());
506+
}
504507
// External libraries doesn't have functions included in abi, but `methodIdentifiers`.
505508
if let Some(method_identifiers) = &artifact.method_identifiers {
506509
method_identifiers.iter().for_each(|(signature, selector)| {

crates/common/src/abi.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! ABI related helper functions.
22
33
use alloy_dyn_abi::{DynSolType, DynSolValue, FunctionExt, JsonAbiExt};
4-
use alloy_json_abi::{Event, Function, Param};
4+
use alloy_json_abi::{Error, Event, Function, Param};
55
use alloy_primitives::{hex, Address, LogData};
66
use eyre::{Context, ContextCompat, Result};
77
use foundry_block_explorers::{contract::ContractMetadata, errors::EtherscanError, Client};
@@ -85,6 +85,11 @@ pub fn get_event(sig: &str) -> Result<Event> {
8585
Event::parse(sig).wrap_err("could not parse event signature")
8686
}
8787

88+
/// Given an error signature string, it tries to parse it as a `Error`
89+
pub fn get_error(sig: &str) -> Result<Error> {
90+
Error::parse(sig).wrap_err("could not parse event signature")
91+
}
92+
8893
/// Given an event without indexed parameters and a rawlog, it tries to return the event with the
8994
/// proper indexed parameters. Otherwise, it returns the original event.
9095
pub fn get_indexed_event(mut event: Event, raw_log: &LogData) -> Event {

crates/common/src/selectors.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ impl OpenChainClient {
140140
.ok_or_else(|| eyre::eyre!("No signature found"))
141141
}
142142

143-
/// Decodes the given function or event selectors using OpenChain
143+
/// Decodes the given function, error or event selectors using OpenChain.
144144
pub async fn decode_selectors(
145145
&self,
146146
selector_type: SelectorType,
@@ -164,8 +164,8 @@ impl OpenChainClient {
164164
self.ensure_not_spurious()?;
165165

166166
let expected_len = match selector_type {
167-
SelectorType::Function => 10, // 0x + hex(4bytes)
168-
SelectorType::Event => 66, // 0x + hex(32bytes)
167+
SelectorType::Function | SelectorType::Error => 10, // 0x + hex(4bytes)
168+
SelectorType::Event => 66, // 0x + hex(32bytes)
169169
};
170170
if let Some(s) = selectors.iter().find(|s| s.len() != expected_len) {
171171
eyre::bail!(
@@ -193,7 +193,7 @@ impl OpenChainClient {
193193
let url = format!(
194194
"{SELECTOR_LOOKUP_URL}?{ltype}={selectors_str}",
195195
ltype = match selector_type {
196-
SelectorType::Function => "function",
196+
SelectorType::Function | SelectorType::Error => "function",
197197
SelectorType::Event => "event",
198198
},
199199
selectors_str = selectors.join(",")
@@ -212,7 +212,7 @@ impl OpenChainClient {
212212
}
213213

214214
let decoded = match selector_type {
215-
SelectorType::Function => api_response.result.function,
215+
SelectorType::Function | SelectorType::Error => api_response.result.function,
216216
SelectorType::Event => api_response.result.event,
217217
};
218218

@@ -391,6 +391,8 @@ pub enum SelectorType {
391391
Function,
392392
/// An event selector.
393393
Event,
394+
/// An custom error selector.
395+
Error,
394396
}
395397

396398
/// Decodes the given function or event selector using OpenChain.

crates/evm/traces/src/identifier/signatures.rs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
use alloy_json_abi::{Event, Function};
1+
use alloy_json_abi::{Error, Event, Function};
22
use alloy_primitives::{hex, map::HashSet};
33
use foundry_common::{
4-
abi::{get_event, get_func},
4+
abi::{get_error, get_event, get_func},
55
fs,
66
selectors::{OpenChainClient, SelectorType},
77
};
@@ -13,6 +13,7 @@ pub type SingleSignaturesIdentifier = Arc<RwLock<SignaturesIdentifier>>;
1313

1414
#[derive(Debug, Default, Serialize, Deserialize)]
1515
pub struct CachedSignatures {
16+
pub errors: BTreeMap<String, String>,
1617
pub events: BTreeMap<String, String>,
1718
pub functions: BTreeMap<String, String>,
1819
}
@@ -39,7 +40,7 @@ impl CachedSignatures {
3940
/// `https://openchain.xyz` or a local cache.
4041
#[derive(Debug)]
4142
pub struct SignaturesIdentifier {
42-
/// Cached selectors for functions and events.
43+
/// Cached selectors for functions, events and custom errors.
4344
cached: CachedSignatures,
4445
/// Location where to save `CachedSignatures`.
4546
cached_path: Option<PathBuf>,
@@ -101,6 +102,7 @@ impl SignaturesIdentifier {
101102
let cache = match selector_type {
102103
SelectorType::Function => &mut self.cached.functions,
103104
SelectorType::Event => &mut self.cached.events,
105+
SelectorType::Error => &mut self.cached.errors,
104106
};
105107

106108
let hex_identifiers: Vec<String> =
@@ -157,6 +159,19 @@ impl SignaturesIdentifier {
157159
pub async fn identify_event(&mut self, identifier: &[u8]) -> Option<Event> {
158160
self.identify_events(&[identifier]).await.pop().unwrap()
159161
}
162+
163+
/// Identifies `Error`s from its cache or `https://api.openchain.xyz`.
164+
pub async fn identify_errors(
165+
&mut self,
166+
identifiers: impl IntoIterator<Item = impl AsRef<[u8]>>,
167+
) -> Vec<Option<Error>> {
168+
self.identify(SelectorType::Error, identifiers, get_error).await
169+
}
170+
171+
/// Identifies `Error` from its cache or `https://api.openchain.xyz`.
172+
pub async fn identify_error(&mut self, identifier: &[u8]) -> Option<Error> {
173+
self.identify_errors(&[identifier]).await.pop().unwrap()
174+
}
160175
}
161176

162177
impl Drop for SignaturesIdentifier {

0 commit comments

Comments
 (0)