diff --git a/crates/relayer/src/chain/axon.rs b/crates/relayer/src/chain/axon.rs index ff5d9193f..e5f79ab6c 100644 --- a/crates/relayer/src/chain/axon.rs +++ b/crates/relayer/src/chain/axon.rs @@ -107,6 +107,7 @@ use super::{ use tokio::runtime::Runtime as TokioRuntime; pub mod contract; +mod eth_err; mod monitor; mod msg; mod rpc; @@ -1370,9 +1371,15 @@ impl AxonChain { macro_rules! convert { ($self:ident, $msg:ident, $eventy:ty, $method:ident) => {{ let msg: $eventy = $msg.try_into()?; - $self - .rt - .block_on(async { Ok($self.contract()?.$method(msg.clone()).send().await?.await?) }) + $self.rt.block_on(async { + Ok($self + .contract()? + .$method(msg.clone()) + .send() + .await + .map_err(decode_revert_error)? + .await?) + }) }}; } diff --git a/crates/relayer/src/chain/axon/eth_err.rs b/crates/relayer/src/chain/axon/eth_err.rs new file mode 100644 index 000000000..140d2c9a9 --- /dev/null +++ b/crates/relayer/src/chain/axon/eth_err.rs @@ -0,0 +1,104 @@ +use ethers::abi::Uint; +use ethers::contract::EthCall; + +/// For decoding and displaying `Panic(uint256)` errors. +/// +/// EthError derived decode_with_selector is buggy, so we derive `EthCall` instead. Decode with `AbiDecode::decode`. +#[derive(EthCall)] +pub struct Panic(Uint); + +impl std::fmt::Display for Panic { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let e = PanicError::from_code(self.0.low_u32()); + write!(f, "Panic code: {:#x}, {e}", self.0) + } +} + +enum PanicError { + Generic, + AssertFailed, + ArithmeticOverflow = 0x11, + DivisionByZero, + InvalidEnumConversion = 0x21, + InvalidEncoding, + EmptyArrayPop = 0x31, + OutOfBoundsAccess, + ExcessiveAllocation = 0x41, + UninitializedInternalFunction = 0x51, + Unknown, +} + +impl PanicError { + fn from_code(code: u32) -> Self { + match code { + 0 => PanicError::Generic, + 0x01 => PanicError::AssertFailed, + 0x11 => PanicError::ArithmeticOverflow, + 0x12 => PanicError::DivisionByZero, + 0x21 => PanicError::InvalidEnumConversion, + 0x22 => PanicError::InvalidEncoding, + 0x31 => PanicError::EmptyArrayPop, + 0x32 => PanicError::OutOfBoundsAccess, + 0x41 => PanicError::ExcessiveAllocation, + 0x51 => PanicError::UninitializedInternalFunction, + _ => PanicError::Unknown, + } + } +} + +impl std::fmt::Display for PanicError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let desc = match self { + PanicError::Generic => "Generic compiler inserted panic", + PanicError::AssertFailed => "Assertion failed", + PanicError::ArithmeticOverflow => "Arithmetic operation resulted in overflow", + PanicError::DivisionByZero => "Division or modulo by zero", + PanicError::InvalidEnumConversion => "Invalid enum conversion", + PanicError::InvalidEncoding => "Invalid encoding", + PanicError::EmptyArrayPop => "Attempted to pop an empty array", + PanicError::OutOfBoundsAccess => "Out-of-bounds access", + PanicError::ExcessiveAllocation => "Excessive memory allocation", + PanicError::UninitializedInternalFunction => { + "Called an uninitialized internal function" + } + PanicError::Unknown => "Unknown panic", + }; + write!(f, "{desc}") + } +} + +#[cfg(test)] +mod test { + use ethers::{abi::AbiDecode, contract::EthError}; + + use super::Panic; + + fn parse_abi_err_data(err: &str) -> String { + let revert_data = hex::decode( + err.strip_prefix("Contract call reverted with data: 0x") + .unwrap(), + ) + .unwrap(); + if let Ok(p) = Panic::decode(&revert_data) { + p.to_string() + } else if let Some(s) = String::decode_with_selector(&revert_data) { + s + } else { + panic!("failed to decode") + } + } + + #[test] + fn test_sol_revert() { + let err_string = "Contract call reverted with data: 0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001c74657374206661696c656420746f2063726561746520636c69656e7400000000"; + let err = parse_abi_err_data(err_string); + assert_eq!(err, "test failed to create client"); + } + + #[test] + fn test_sol_panic() { + let err_string = "Contract call reverted with data: 0x4e487b710000000000000000000000000000000000000000000000000000000000000012"; + let err = parse_abi_err_data(err_string); + assert_eq!(err, "Panic code: 0x12, Division or modulo by zero"); + } +} diff --git a/crates/relayer/src/chain/axon/utils.rs b/crates/relayer/src/chain/axon/utils.rs index 5d141404f..bdac2b7b5 100644 --- a/crates/relayer/src/chain/axon/utils.rs +++ b/crates/relayer/src/chain/axon/utils.rs @@ -3,14 +3,14 @@ use std::str::FromStr; use axon_tools::types::{Block as AxonBlock, Proof as AxonProof, ValidatorExtend}; use crate::{ - chain::SEC_TO_NANO, + chain::{axon::eth_err::Panic, SEC_TO_NANO}, client_state::{AnyClientState, IdentifiedAnyClientState}, consensus_state::AnyConsensusState, error::Error, event::IbcEventWithHeight, ibc_contract::OwnableIBCHandlerEvents, }; -use ethers::types::H256; +use ethers::{abi::AbiDecode, contract::ContractError, providers::Middleware, types::H256}; use ibc_relayer_types::{ clients::{ ics07_axon::{client_state::AxonClientState, consensus_state::AxonConsensusState}, @@ -29,6 +29,19 @@ pub fn convert_err(err: T) -> Error { Error::other_error(err.to_string()) } +pub fn decode_revert_error(err: ContractError) -> eyre::Report +where + M: Middleware + 'static, +{ + if let Some(r) = err.decode_revert::() { + eyre::eyre!("Contract call reverted: {r}") + } else if let Some(p) = err.as_revert().and_then(|d| Panic::decode(d).ok()) { + eyre::eyre!("Contract call reverted: {p}") + } else { + err.into() + } +} + pub fn to_identified_any_client_state( client_state: ðers::core::types::Bytes, ) -> Result {