Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cast): decode-error with sig, local cache and openchain api #9428

Merged
merged 6 commits into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>,
Copy link
Member

@DaniPopes DaniPopes Nov 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we make this optional for decode event too? followup

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, will follow up with a PR, have to figure out how to parse with topics

/// 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