Skip to content

Commit

Permalink
feat(cast): decode-error with sig, local cache and openchain api (#…
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
grandizzy authored Nov 29, 2024
1 parent 2e9f536 commit 0d76df5
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 12 deletions.
10 changes: 10 additions & 0 deletions crates/cast/bin/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 or `https://api.openchain.xyz`.
#[arg(long, visible_alias = "error-sig")]
sig: Option<String>,
/// 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.
Expand Down
27 changes: 25 additions & 2 deletions crates/cast/bin/main.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -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.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 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?,
Expand Down
56 changes: 55 additions & 1 deletion crates/cast/tests/cli/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -1482,6 +1482,60 @@ 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
0x0000000000000000000000000000000000D0004F
"#]]);

cmd.args(["--json"]).assert_success().stdout_eq(str![[r#"
[
"101",
"0x0000000000000000000000000000000000D0004F"
]
"#]]);
});

// 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());
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
Expand Down
3 changes: 3 additions & 0 deletions crates/cli/src/utils/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)| {
Expand Down
7 changes: 6 additions & 1 deletion crates/common/src/abi.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -85,6 +85,11 @@ pub fn get_event(sig: &str) -> Result<Event> {
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> {
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 {
Expand Down
12 changes: 7 additions & 5 deletions crates/common/src/selectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -164,8 +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)
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!(
Expand Down Expand Up @@ -193,7 +193,7 @@ 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",
},
selectors_str = selectors.join(",")
Expand All @@ -212,7 +212,7 @@ 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,
};

Expand Down Expand Up @@ -391,6 +391,8 @@ pub enum SelectorType {
Function,
/// An event selector.
Event,
/// An custom error selector.
Error,
}

/// Decodes the given function or event selector using OpenChain.
Expand Down
21 changes: 18 additions & 3 deletions crates/evm/traces/src/identifier/signatures.rs
Original file line number Diff line number Diff line change
@@ -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},
};
Expand All @@ -13,6 +13,7 @@ pub type SingleSignaturesIdentifier = Arc<RwLock<SignaturesIdentifier>>;

#[derive(Debug, Default, Serialize, Deserialize)]
pub struct CachedSignatures {
pub errors: BTreeMap<String, String>,
pub events: BTreeMap<String, String>,
pub functions: BTreeMap<String, String>,
}
Expand All @@ -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<PathBuf>,
Expand Down Expand Up @@ -101,6 +102,7 @@ impl SignaturesIdentifier {
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<String> =
Expand Down Expand Up @@ -157,6 +159,19 @@ impl SignaturesIdentifier {
pub async fn identify_event(&mut self, identifier: &[u8]) -> Option<Event> {
self.identify_events(&[identifier]).await.pop().unwrap()
}

/// Identifies `Error`s from its cache or `https://api.openchain.xyz`.
pub async fn identify_errors(
&mut self,
identifiers: impl IntoIterator<Item = impl AsRef<[u8]>>,
) -> Vec<Option<Error>> {
self.identify(SelectorType::Error, identifiers, get_error).await
}

/// Identifies `Error` from its cache or `https://api.openchain.xyz`.
pub async fn identify_error(&mut self, identifier: &[u8]) -> Option<Error> {
self.identify_errors(&[identifier]).await.pop().unwrap()
}
}

impl Drop for SignaturesIdentifier {
Expand Down

0 comments on commit 0d76df5

Please sign in to comment.