Skip to content

Commit be5e714

Browse files
authored
fix(cast): try decoding custom errors when execution reverted in cast send (#9794)
* fix(cast): try decoding custom errors when gas estimation in cast send * Changes after review: use serde_json::from_str, use itertools format * Nits * More nits
1 parent f6133f9 commit be5e714

File tree

2 files changed

+126
-3
lines changed

2 files changed

+126
-3
lines changed

crates/cast/bin/tx.rs

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use alloy_consensus::{SidecarBuilder, SimpleCoder};
2+
use alloy_dyn_abi::ErrorExt;
23
use alloy_json_abi::Function;
34
use alloy_network::{
45
AnyNetwork, TransactionBuilder, TransactionBuilder4844, TransactionBuilder7702,
@@ -8,21 +9,26 @@ use alloy_provider::Provider;
89
use alloy_rpc_types::{AccessList, Authorization, TransactionInput, TransactionRequest};
910
use alloy_serde::WithOtherFields;
1011
use alloy_signer::Signer;
12+
use alloy_transport::TransportError;
13+
use cast::traces::identifier::SignaturesIdentifier;
1114
use eyre::Result;
1215
use foundry_cli::{
1316
opts::{CliAuthorizationList, TransactionOpts},
1417
utils::{self, parse_function_args},
1518
};
16-
use foundry_common::ens::NameOrAddress;
19+
use foundry_common::{ens::NameOrAddress, fmt::format_tokens};
1720
use foundry_config::{Chain, Config};
1821
use foundry_wallets::{WalletOpts, WalletSigner};
22+
use itertools::Itertools;
23+
use serde_json::value::RawValue;
24+
use std::fmt::Write;
1925

2026
/// Different sender kinds used by [`CastTxBuilder`].
2127
pub enum SenderKind<'a> {
2228
/// An address without signer. Used for read-only calls and transactions sent through unlocked
2329
/// accounts.
2430
Address(Address),
25-
/// A refersnce to a signer.
31+
/// A reference to a signer.
2632
Signer(&'a WalletSigner),
2733
/// An owned signer.
2834
OwnedSigner(WalletSigner),
@@ -350,12 +356,36 @@ impl<P: Provider<AnyNetwork>> CastTxBuilder<P, InputState> {
350356
}
351357

352358
if self.tx.gas.is_none() {
353-
self.tx.gas = Some(self.provider.estimate_gas(&self.tx).await?);
359+
self.estimate_gas().await?;
354360
}
355361

356362
Ok((self.tx, self.state.func))
357363
}
358364

365+
/// Estimate tx gas from provider call. Tries to decode custom error if execution reverted.
366+
async fn estimate_gas(&mut self) -> Result<()> {
367+
match self.provider.estimate_gas(&self.tx).await {
368+
Ok(estimated) => {
369+
self.tx.gas = Some(estimated);
370+
Ok(())
371+
}
372+
Err(err) => {
373+
if let TransportError::ErrorResp(payload) = &err {
374+
// If execution reverted with code 3 during provider gas estimation then try
375+
// to decode custom errors and append it to the error message.
376+
if payload.code == 3 {
377+
if let Some(data) = &payload.data {
378+
if let Ok(Some(decoded_error)) = decode_execution_revert(data).await {
379+
eyre::bail!("Failed to estimate gas: {}: {}", err, decoded_error)
380+
}
381+
}
382+
}
383+
}
384+
eyre::bail!("Failed to estimate gas: {}", err)
385+
}
386+
}
387+
}
388+
359389
/// Parses the passed --auth value and sets the authorization list on the transaction.
360390
async fn resolve_auth(&mut self, sender: SenderKind<'_>, tx_nonce: u64) -> Result<()> {
361391
let Some(auth) = self.auth.take() else { return Ok(()) };
@@ -401,3 +431,25 @@ where
401431
Ok(self)
402432
}
403433
}
434+
435+
/// Helper function that tries to decode custom error name and inputs from error payload data.
436+
async fn decode_execution_revert(data: &RawValue) -> Result<Option<String>> {
437+
if let Some(err_data) = serde_json::from_str::<String>(data.get())?.strip_prefix("0x") {
438+
let selector = err_data.get(..8).unwrap();
439+
if let Some(known_error) = SignaturesIdentifier::new(Config::foundry_cache_dir(), false)?
440+
.write()
441+
.await
442+
.identify_error(&hex::decode(selector)?)
443+
.await
444+
{
445+
let mut decoded_error = known_error.name.clone();
446+
if !known_error.inputs.is_empty() {
447+
if let Ok(error) = known_error.decode_error(&hex::decode(err_data)?) {
448+
write!(decoded_error, "({})", format_tokens(&error.body).format(", "))?;
449+
}
450+
}
451+
return Ok(Some(decoded_error))
452+
}
453+
}
454+
Ok(None)
455+
}

crates/cast/tests/cli/main.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1971,3 +1971,74 @@ contract WETH9 {
19711971
uint8 public decimals = 18;
19721972
..."#]]);
19731973
});
1974+
1975+
// tests cast send gas estimate execution failure message contains decoded custom error
1976+
// <https://github.com/foundry-rs/foundry/issues/9789>
1977+
forgetest_async!(cast_send_estimate_gas_error, |prj, cmd| {
1978+
let (_, handle) = anvil::spawn(NodeConfig::test()).await;
1979+
1980+
foundry_test_utils::util::initialize(prj.root());
1981+
prj.add_source(
1982+
"SimpleStorage",
1983+
r#"
1984+
contract SimpleStorage {
1985+
uint256 private storedValue;
1986+
error AddressInsufficientBalance(address account, uint256 newValue);
1987+
function setValue(uint256 _newValue) public {
1988+
if (_newValue > 100) {
1989+
revert AddressInsufficientBalance(msg.sender, _newValue);
1990+
}
1991+
storedValue = _newValue;
1992+
}
1993+
}
1994+
"#,
1995+
)
1996+
.unwrap();
1997+
prj.add_script(
1998+
"SimpleStorageScript",
1999+
r#"
2000+
import "forge-std/Script.sol";
2001+
import {SimpleStorage} from "../src/SimpleStorage.sol";
2002+
contract SimpleStorageScript is Script {
2003+
function run() public {
2004+
vm.startBroadcast();
2005+
new SimpleStorage();
2006+
vm.stopBroadcast();
2007+
}
2008+
}
2009+
"#,
2010+
)
2011+
.unwrap();
2012+
2013+
cmd.args([
2014+
"script",
2015+
"--private-key",
2016+
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
2017+
"--rpc-url",
2018+
&handle.http_endpoint(),
2019+
"--broadcast",
2020+
"SimpleStorageScript",
2021+
])
2022+
.assert_success();
2023+
2024+
// Cache project selectors.
2025+
cmd.forge_fuse().set_current_dir(prj.root());
2026+
cmd.forge_fuse().args(["selectors", "cache"]).assert_success();
2027+
2028+
// Assert cast send can decode custom error on estimate gas execution failure.
2029+
cmd.cast_fuse()
2030+
.args([
2031+
"send",
2032+
"0x5FbDB2315678afecb367f032d93F642f64180aa3",
2033+
"setValue(uint256)",
2034+
"1000",
2035+
"--private-key",
2036+
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
2037+
"--rpc-url",
2038+
&handle.http_endpoint(),
2039+
])
2040+
.assert_failure().stderr_eq(str![[r#"
2041+
Error: Failed to estimate gas: server returned an error response: error code 3: execution reverted: custom error 0x6786ad34: 000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb9226600000000000000000000000000000000000000000000000000000000000003e8, data: "0x6786ad34000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb9226600000000000000000000000000000000000000000000000000000000000003e8": AddressInsufficientBalance(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266, 1000)
2042+
2043+
"#]]);
2044+
});

0 commit comments

Comments
 (0)