From 7ec0aa1be52f2d69c98fbae9be05630860e0d166 Mon Sep 17 00:00:00 2001 From: grandizzy Date: Thu, 28 Nov 2024 11:13:14 +0200 Subject: [PATCH 1/4] feat(cast): Add custom error decoding support --- crates/cast/bin/args.rs | 10 +++ crates/cast/bin/main.rs | 27 +++++++- crates/cast/tests/cli/main.rs | 37 ++++++++++- crates/cli/src/utils/cmd.rs | 3 + crates/common/src/abi.rs | 7 +- crates/common/src/selectors.rs | 5 ++ .../evm/traces/src/identifier/signatures.rs | 64 ++++++++++++------- 7 files changed, 126 insertions(+), 27 deletions(-) diff --git a/crates/cast/bin/args.rs b/crates/cast/bin/args.rs index 7078810e4ce9..bddb5867e73b 100644 --- a/crates/cast/bin/args.rs +++ b/crates/cast/bin/args.rs @@ -539,6 +539,16 @@ pub enum CastSubcommand { data: String, }, + /// Decode custom error data. + #[command(visible_aliases = &["error-decode", "--error-decode", "erd"])] + DecodeError { + /// The error signature. If none provided then tries to decode from local cache. + #[arg(long = "sig", visible_alias = "error-sig")] + sig: Option, + /// The error data to decode. + data: String, + }, + /// Decode ABI-encoded input or output data. /// /// Defaults to decoding output data. To decode input data pass --input. diff --git a/crates/cast/bin/main.rs b/crates/cast/bin/main.rs index 21b1df36d6cd..b79280fe77f1 100644 --- a/crates/cast/bin/main.rs +++ b/crates/cast/bin/main.rs @@ -1,7 +1,7 @@ #[macro_use] extern crate tracing; -use alloy_dyn_abi::{DynSolValue, EventExt}; +use alloy_dyn_abi::{DynSolValue, ErrorExt, EventExt}; use alloy_primitives::{eip191_hash_message, hex, keccak256, Address, B256}; use alloy_provider::Provider; use alloy_rpc_types::{BlockId, BlockNumberOrTag::Latest}; @@ -11,7 +11,7 @@ use clap_complete::generate; use eyre::Result; use foundry_cli::{handler, utils}; use foundry_common::{ - abi::get_event, + abi::{get_error, get_event}, ens::{namehash, ProviderEnsExt}, fmt::{format_tokens, format_tokens_raw, format_uint_exp}, fs, @@ -30,6 +30,7 @@ pub mod cmd; pub mod tx; use args::{Cast as CastArgs, CastSubcommand, ToBaseArgs}; +use cast::traces::identifier::SignaturesIdentifier; #[macro_use] extern crate foundry_common; @@ -216,6 +217,28 @@ async fn main_args(args: CastArgs) -> Result<()> { let decoded_event = event.decode_log_parts(None, &hex::decode(data)?, false)?; print_tokens(&decoded_event.body); } + CastSubcommand::DecodeError { sig, data } => { + let error = if let Some(err_sig) = sig { + get_error(err_sig.as_str())? + } else { + let data = data.strip_prefix("0x").unwrap_or(data.as_str()); + let selector = &data[..8]; + let err = SignaturesIdentifier::new(Config::foundry_cache_dir(), true)? + .write() + .await + .identify_error(&hex::decode(selector)?) + .await; + if err.is_none() { + eyre::bail!("No matching error signature found for selector `{selector}`") + } + + let error = err.unwrap(); + let _ = sh_println!("{}", error.signature()); + error + }; + let decoded_error = error.decode_error(&hex::decode(data)?)?; + print_tokens(&decoded_error.body); + } CastSubcommand::Interface(cmd) => cmd.run().await?, CastSubcommand::CreationCode(cmd) => cmd.run().await?, CastSubcommand::ConstructorArgs(cmd) => cmd.run().await?, diff --git a/crates/cast/tests/cli/main.rs b/crates/cast/tests/cli/main.rs index d2c70a779e65..42258710ccf3 100644 --- a/crates/cast/tests/cli/main.rs +++ b/crates/cast/tests/cli/main.rs @@ -6,7 +6,7 @@ use alloy_primitives::{b256, B256}; use alloy_rpc_types::{BlockNumberOrTag, Index}; use anvil::{EthereumHardfork, NodeConfig}; use foundry_test_utils::{ - casttest, file, forgetest_async, + casttest, file, forgetest, forgetest_async, rpc::{ next_etherscan_api_key, next_http_rpc_endpoint, next_mainnet_etherscan_api_key, next_rpc_endpoint, next_ws_rpc_endpoint, @@ -1482,6 +1482,41 @@ casttest!(event_decode, |_prj, cmd| { "#]]); }); +casttest!(error_decode_with_sig, |_prj, cmd| { + cmd.args(["decode-error", "--sig", "AnotherValueTooHigh(uint256,address)", "0x7191bc6200000000000000000000000000000000000000000000000000000000000000650000000000000000000000000000000000000000000000000000000000D0004F"]).assert_success().stdout_eq(str![[r#" +101 +0x0000000000000000000000000000000000D0004F + +"#]]); +}); + +// tests cast can decode traces when using local sig identifiers cache +forgetest!(error_decode_with_cache, |prj, cmd| { + foundry_test_utils::util::initialize(prj.root()); + prj.add_source( + "LocalProjectContract", + r#" +contract ContractWithCustomError { + error AnotherValueTooHigh(uint256, address); +} + "#, + ) + .unwrap(); + // Store selectors in local cache. + cmd.forge_fuse().args(["selectors", "cache"]).assert_success(); + + // Assert cast can decode custom error with local cache. + cmd.cast_fuse() + .args(["decode-error", "0x7191bc6200000000000000000000000000000000000000000000000000000000000000650000000000000000000000000000000000000000000000000000000000D0004F"]) + .assert_success() + .stdout_eq(str![[r#" +AnotherValueTooHigh(uint256,address) +101 +0x0000000000000000000000000000000000D0004F + +"#]]); +}); + casttest!(format_units, |_prj, cmd| { cmd.args(["format-units", "1000000", "6"]).assert_success().stdout_eq(str![[r#" 1 diff --git a/crates/cli/src/utils/cmd.rs b/crates/cli/src/utils/cmd.rs index 67aa65073ff8..4cf24221bbee 100644 --- a/crates/cli/src/utils/cmd.rs +++ b/crates/cli/src/utils/cmd.rs @@ -501,6 +501,9 @@ pub fn cache_local_signatures(output: &ProjectCompileOutput, cache_path: PathBuf .events .insert(event.selector().to_string(), event.full_signature()); } + for error in abi.errors() { + cached_signatures.errors.insert(error.selector().to_string(), error.signature()); + } // External libraries doesn't have functions included in abi, but `methodIdentifiers`. if let Some(method_identifiers) = &artifact.method_identifiers { method_identifiers.iter().for_each(|(signature, selector)| { diff --git a/crates/common/src/abi.rs b/crates/common/src/abi.rs index de9b36219ecd..fa9f241719fd 100644 --- a/crates/common/src/abi.rs +++ b/crates/common/src/abi.rs @@ -1,7 +1,7 @@ //! ABI related helper functions. use alloy_dyn_abi::{DynSolType, DynSolValue, FunctionExt, JsonAbiExt}; -use alloy_json_abi::{Event, Function, Param}; +use alloy_json_abi::{Error, Event, Function, Param}; use alloy_primitives::{hex, Address, LogData}; use eyre::{Context, ContextCompat, Result}; use foundry_block_explorers::{contract::ContractMetadata, errors::EtherscanError, Client}; @@ -85,6 +85,11 @@ pub fn get_event(sig: &str) -> Result { Event::parse(sig).wrap_err("could not parse event signature") } +/// Given an error signature string, it tries to parse it as a `Error` +pub fn get_error(sig: &str) -> Result { + Error::parse(sig).wrap_err("could not parse event signature") +} + /// Given an event without indexed parameters and a rawlog, it tries to return the event with the /// proper indexed parameters. Otherwise, it returns the original event. pub fn get_indexed_event(mut event: Event, raw_log: &LogData) -> Event { diff --git a/crates/common/src/selectors.rs b/crates/common/src/selectors.rs index cd4e2ffd0882..d5083d2da225 100644 --- a/crates/common/src/selectors.rs +++ b/crates/common/src/selectors.rs @@ -166,6 +166,7 @@ impl OpenChainClient { let expected_len = match selector_type { SelectorType::Function => 10, // 0x + hex(4bytes) SelectorType::Event => 66, // 0x + hex(32bytes) + _ => eyre::bail!("Could decode only functions and events"), }; if let Some(s) = selectors.iter().find(|s| s.len() != expected_len) { eyre::bail!( @@ -195,6 +196,7 @@ impl OpenChainClient { ltype = match selector_type { SelectorType::Function => "function", SelectorType::Event => "event", + _ => eyre::bail!("Could decode only functions and events"), }, selectors_str = selectors.join(",") ); @@ -214,6 +216,7 @@ impl OpenChainClient { let decoded = match selector_type { SelectorType::Function => api_response.result.function, SelectorType::Event => api_response.result.event, + _ => eyre::bail!("Could decode only functions and events"), }; Ok(selectors @@ -391,6 +394,8 @@ pub enum SelectorType { Function, /// An event selector. Event, + /// An custom error selector. + Error, } /// Decodes the given function or event selector using OpenChain. diff --git a/crates/evm/traces/src/identifier/signatures.rs b/crates/evm/traces/src/identifier/signatures.rs index 2a5ef354a753..c53930849e42 100644 --- a/crates/evm/traces/src/identifier/signatures.rs +++ b/crates/evm/traces/src/identifier/signatures.rs @@ -1,7 +1,7 @@ -use alloy_json_abi::{Event, Function}; +use alloy_json_abi::{Error, Event, Function}; use alloy_primitives::{hex, map::HashSet}; use foundry_common::{ - abi::{get_event, get_func}, + abi::{get_error, get_event, get_func}, fs, selectors::{OpenChainClient, SelectorType}, }; @@ -13,6 +13,7 @@ pub type SingleSignaturesIdentifier = Arc>; #[derive(Debug, Default, Serialize, Deserialize)] pub struct CachedSignatures { + pub errors: BTreeMap, pub events: BTreeMap, pub functions: BTreeMap, } @@ -39,7 +40,7 @@ impl CachedSignatures { /// `https://openchain.xyz` or a local cache. #[derive(Debug)] pub struct SignaturesIdentifier { - /// Cached selectors for functions and events. + /// Cached selectors for functions, events and custom errors. cached: CachedSignatures, /// Location where to save `CachedSignatures`. cached_path: Option, @@ -98,32 +99,36 @@ impl SignaturesIdentifier { identifiers: impl IntoIterator>, get_type: impl Fn(&str) -> eyre::Result, ) -> Vec> { - let cache = match selector_type { - SelectorType::Function => &mut self.cached.functions, - SelectorType::Event => &mut self.cached.events, + let (cache, with_openchain) = match selector_type { + SelectorType::Function => (&mut self.cached.functions, true), + SelectorType::Event => (&mut self.cached.events, true), + // Openchain API does not support custom errors. + SelectorType::Error => (&mut self.cached.errors, false), }; let hex_identifiers: Vec = identifiers.into_iter().map(hex::encode_prefixed).collect(); - if let Some(client) = &self.client { - let query: Vec<_> = hex_identifiers - .iter() - .filter(|v| !cache.contains_key(v.as_str())) - .filter(|v| !self.unavailable.contains(v.as_str())) - .collect(); - - if let Ok(res) = client.decode_selectors(selector_type, query.clone()).await { - for (hex_id, selector_result) in query.into_iter().zip(res.into_iter()) { - let mut found = false; - if let Some(decoded_results) = selector_result { - if let Some(decoded_result) = decoded_results.into_iter().next() { - cache.insert(hex_id.clone(), decoded_result); - found = true; + if with_openchain { + if let Some(client) = &self.client { + let query: Vec<_> = hex_identifiers + .iter() + .filter(|v| !cache.contains_key(v.as_str())) + .filter(|v| !self.unavailable.contains(v.as_str())) + .collect(); + + if let Ok(res) = client.decode_selectors(selector_type, query.clone()).await { + for (hex_id, selector_result) in query.into_iter().zip(res.into_iter()) { + let mut found = false; + if let Some(decoded_results) = selector_result { + if let Some(decoded_result) = decoded_results.into_iter().next() { + cache.insert(hex_id.clone(), decoded_result); + found = true; + } + } + if !found { + self.unavailable.insert(hex_id.clone()); } - } - if !found { - self.unavailable.insert(hex_id.clone()); } } } @@ -157,6 +162,19 @@ impl SignaturesIdentifier { pub async fn identify_event(&mut self, identifier: &[u8]) -> Option { self.identify_events(&[identifier]).await.pop().unwrap() } + + /// Identifies `Error`s from its cache. + pub async fn identify_errors( + &mut self, + identifiers: impl IntoIterator>, + ) -> Vec> { + self.identify(SelectorType::Error, identifiers, get_error).await + } + + /// Identifies `Error` from its cache. + pub async fn identify_error(&mut self, identifier: &[u8]) -> Option { + self.identify_errors(&[identifier]).await.pop().unwrap() + } } impl Drop for SignaturesIdentifier { From d349c9a3cbf22e65db5670ed3d047712926642ea Mon Sep 17 00:00:00 2001 From: grandizzy Date: Thu, 28 Nov 2024 17:19:17 +0200 Subject: [PATCH 2/4] Review changes --- crates/cast/bin/args.rs | 2 +- crates/cast/tests/cli/main.rs | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/cast/bin/args.rs b/crates/cast/bin/args.rs index bddb5867e73b..e2d8fc23c3e2 100644 --- a/crates/cast/bin/args.rs +++ b/crates/cast/bin/args.rs @@ -543,7 +543,7 @@ pub enum CastSubcommand { #[command(visible_aliases = &["error-decode", "--error-decode", "erd"])] DecodeError { /// The error signature. If none provided then tries to decode from local cache. - #[arg(long = "sig", visible_alias = "error-sig")] + #[arg(long, visible_alias = "error-sig")] sig: Option, /// The error data to decode. data: String, diff --git a/crates/cast/tests/cli/main.rs b/crates/cast/tests/cli/main.rs index 42258710ccf3..4a5513191144 100644 --- a/crates/cast/tests/cli/main.rs +++ b/crates/cast/tests/cli/main.rs @@ -1487,6 +1487,14 @@ casttest!(error_decode_with_sig, |_prj, cmd| { 101 0x0000000000000000000000000000000000D0004F +"#]]); + + cmd.args(["--json"]).assert_success().stdout_eq(str![[r#" +[ + "101", + "0x0000000000000000000000000000000000D0004F" +] + "#]]); }); From 35f5dc101c3450773423d54c43be0c24a6817d4e Mon Sep 17 00:00:00 2001 From: grandizzy Date: Thu, 28 Nov 2024 20:50:04 +0200 Subject: [PATCH 3/4] Changes after review: decode with Openchain too, add test --- crates/cast/bin/args.rs | 2 +- crates/cast/bin/main.rs | 2 +- crates/cast/tests/cli/main.rs | 11 +++++ crates/common/src/selectors.rs | 13 ++--- .../evm/traces/src/identifier/signatures.rs | 49 +++++++++---------- 5 files changed, 41 insertions(+), 36 deletions(-) diff --git a/crates/cast/bin/args.rs b/crates/cast/bin/args.rs index e2d8fc23c3e2..e4cf32639d4b 100644 --- a/crates/cast/bin/args.rs +++ b/crates/cast/bin/args.rs @@ -542,7 +542,7 @@ pub enum CastSubcommand { /// Decode custom error data. #[command(visible_aliases = &["error-decode", "--error-decode", "erd"])] DecodeError { - /// The error signature. If none provided then tries to decode from local cache. + /// The error signature. If none provided then tries to decode from local cache or `https://api.openchain.xyz`. #[arg(long, visible_alias = "error-sig")] sig: Option, /// The error data to decode. diff --git a/crates/cast/bin/main.rs b/crates/cast/bin/main.rs index b79280fe77f1..721abeb81fa9 100644 --- a/crates/cast/bin/main.rs +++ b/crates/cast/bin/main.rs @@ -223,7 +223,7 @@ async fn main_args(args: CastArgs) -> Result<()> { } else { let data = data.strip_prefix("0x").unwrap_or(data.as_str()); let selector = &data[..8]; - let err = SignaturesIdentifier::new(Config::foundry_cache_dir(), true)? + let err = SignaturesIdentifier::new(Config::foundry_cache_dir(), false)? .write() .await .identify_error(&hex::decode(selector)?) diff --git a/crates/cast/tests/cli/main.rs b/crates/cast/tests/cli/main.rs index 4a5513191144..0cd43766e3e1 100644 --- a/crates/cast/tests/cli/main.rs +++ b/crates/cast/tests/cli/main.rs @@ -1482,6 +1482,7 @@ casttest!(event_decode, |_prj, cmd| { "#]]); }); +// tests cast can decode traces with provided signature casttest!(error_decode_with_sig, |_prj, cmd| { cmd.args(["decode-error", "--sig", "AnotherValueTooHigh(uint256,address)", "0x7191bc6200000000000000000000000000000000000000000000000000000000000000650000000000000000000000000000000000000000000000000000000000D0004F"]).assert_success().stdout_eq(str![[r#" 101 @@ -1498,6 +1499,16 @@ casttest!(error_decode_with_sig, |_prj, cmd| { "#]]); }); +// tests cast can decode traces with Openchain API +casttest!(error_decode_with_openchain, |_prj, cmd| { + cmd.args(["decode-error", "0x7a0e198500000000000000000000000000000000000000000000000000000000000000650000000000000000000000000000000000000000000000000000000000000064"]).assert_success().stdout_eq(str![[r#" +ValueTooHigh(uint256,uint256) +101 +100 + +"#]]); +}); + // tests cast can decode traces when using local sig identifiers cache forgetest!(error_decode_with_cache, |prj, cmd| { foundry_test_utils::util::initialize(prj.root()); diff --git a/crates/common/src/selectors.rs b/crates/common/src/selectors.rs index d5083d2da225..cb59e1f32e37 100644 --- a/crates/common/src/selectors.rs +++ b/crates/common/src/selectors.rs @@ -140,7 +140,7 @@ impl OpenChainClient { .ok_or_else(|| eyre::eyre!("No signature found")) } - /// Decodes the given function or event selectors using OpenChain + /// Decodes the given function, error or event selectors using OpenChain. pub async fn decode_selectors( &self, selector_type: SelectorType, @@ -164,9 +164,8 @@ impl OpenChainClient { self.ensure_not_spurious()?; let expected_len = match selector_type { - SelectorType::Function => 10, // 0x + hex(4bytes) - SelectorType::Event => 66, // 0x + hex(32bytes) - _ => eyre::bail!("Could decode only functions and events"), + SelectorType::Function | SelectorType::Error => 10, // 0x + hex(4bytes) + SelectorType::Event => 66, // 0x + hex(32bytes) }; if let Some(s) = selectors.iter().find(|s| s.len() != expected_len) { eyre::bail!( @@ -194,9 +193,8 @@ impl OpenChainClient { let url = format!( "{SELECTOR_LOOKUP_URL}?{ltype}={selectors_str}", ltype = match selector_type { - SelectorType::Function => "function", + SelectorType::Function | SelectorType::Error => "function", SelectorType::Event => "event", - _ => eyre::bail!("Could decode only functions and events"), }, selectors_str = selectors.join(",") ); @@ -214,9 +212,8 @@ impl OpenChainClient { } let decoded = match selector_type { - SelectorType::Function => api_response.result.function, + SelectorType::Function | SelectorType::Error => api_response.result.function, SelectorType::Event => api_response.result.event, - _ => eyre::bail!("Could decode only functions and events"), }; Ok(selectors diff --git a/crates/evm/traces/src/identifier/signatures.rs b/crates/evm/traces/src/identifier/signatures.rs index c53930849e42..801f9da373d5 100644 --- a/crates/evm/traces/src/identifier/signatures.rs +++ b/crates/evm/traces/src/identifier/signatures.rs @@ -99,37 +99,34 @@ impl SignaturesIdentifier { identifiers: impl IntoIterator>, get_type: impl Fn(&str) -> eyre::Result, ) -> Vec> { - let (cache, with_openchain) = match selector_type { - SelectorType::Function => (&mut self.cached.functions, true), - SelectorType::Event => (&mut self.cached.events, true), - // Openchain API does not support custom errors. - SelectorType::Error => (&mut self.cached.errors, false), + let cache = match selector_type { + SelectorType::Function => &mut self.cached.functions, + SelectorType::Event => &mut self.cached.events, + SelectorType::Error => &mut self.cached.errors, }; let hex_identifiers: Vec = identifiers.into_iter().map(hex::encode_prefixed).collect(); - if with_openchain { - if let Some(client) = &self.client { - let query: Vec<_> = hex_identifiers - .iter() - .filter(|v| !cache.contains_key(v.as_str())) - .filter(|v| !self.unavailable.contains(v.as_str())) - .collect(); - - if let Ok(res) = client.decode_selectors(selector_type, query.clone()).await { - for (hex_id, selector_result) in query.into_iter().zip(res.into_iter()) { - let mut found = false; - if let Some(decoded_results) = selector_result { - if let Some(decoded_result) = decoded_results.into_iter().next() { - cache.insert(hex_id.clone(), decoded_result); - found = true; - } - } - if !found { - self.unavailable.insert(hex_id.clone()); + if let Some(client) = &self.client { + let query: Vec<_> = hex_identifiers + .iter() + .filter(|v| !cache.contains_key(v.as_str())) + .filter(|v| !self.unavailable.contains(v.as_str())) + .collect(); + + if let Ok(res) = client.decode_selectors(selector_type, query.clone()).await { + for (hex_id, selector_result) in query.into_iter().zip(res.into_iter()) { + let mut found = false; + if let Some(decoded_results) = selector_result { + if let Some(decoded_result) = decoded_results.into_iter().next() { + cache.insert(hex_id.clone(), decoded_result); + found = true; } } + if !found { + self.unavailable.insert(hex_id.clone()); + } } } } @@ -163,7 +160,7 @@ impl SignaturesIdentifier { self.identify_events(&[identifier]).await.pop().unwrap() } - /// Identifies `Error`s from its cache. + /// Identifies `Error`s from its cache or `https://api.openchain.xyz`. pub async fn identify_errors( &mut self, identifiers: impl IntoIterator>, @@ -171,7 +168,7 @@ impl SignaturesIdentifier { self.identify(SelectorType::Error, identifiers, get_error).await } - /// Identifies `Error` from its cache. + /// Identifies `Error` from its cache or `https://api.openchain.xyz`. pub async fn identify_error(&mut self, identifier: &[u8]) -> Option { self.identify_errors(&[identifier]).await.pop().unwrap() } From 1834215d256f0afb1d574ae538c03b67d6ed5eb1 Mon Sep 17 00:00:00 2001 From: grandizzy Date: Fri, 29 Nov 2024 08:13:18 +0200 Subject: [PATCH 4/4] Review changes: nit, handle incomplete selectors --- crates/cast/bin/main.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/cast/bin/main.rs b/crates/cast/bin/main.rs index 721abeb81fa9..cacddb8344f5 100644 --- a/crates/cast/bin/main.rs +++ b/crates/cast/bin/main.rs @@ -222,19 +222,19 @@ async fn main_args(args: CastArgs) -> Result<()> { get_error(err_sig.as_str())? } else { let data = data.strip_prefix("0x").unwrap_or(data.as_str()); - let selector = &data[..8]; - let err = SignaturesIdentifier::new(Config::foundry_cache_dir(), false)? - .write() - .await - .identify_error(&hex::decode(selector)?) - .await; - if err.is_none() { + let selector = data.get(..8).unwrap_or_default(); + let identified_error = + SignaturesIdentifier::new(Config::foundry_cache_dir(), false)? + .write() + .await + .identify_error(&hex::decode(selector)?) + .await; + if let Some(error) = identified_error { + let _ = sh_println!("{}", error.signature()); + error + } else { eyre::bail!("No matching error signature found for selector `{selector}`") } - - let error = err.unwrap(); - let _ = sh_println!("{}", error.signature()); - error }; let decoded_error = error.decode_error(&hex::decode(data)?)?; print_tokens(&decoded_error.body);