From ded611e714e4b50bc9db5fb8841df367d596d495 Mon Sep 17 00:00:00 2001 From: James Prestwich <10149425+prestwich@users.noreply.github.com> Date: Mon, 27 Feb 2023 23:59:32 -0800 Subject: [PATCH] feature: contract revert trait (#2182) * feature: contract revert trait * fix: proper link to abigen in docs * fix: don't borrow Bytes, better valid_slector * fix: mattsse's nits * opt: hardcode selector for Error(string) * fix: add docstring to RevertString * docs: enhance docs on ContractRevert * chore: add doc on decoding error reverts as strings * docs: more docstring on ContractRevert * fix: fix try_into invocation --------- Co-authored-by: Georgios Konstantopoulos --- .../src/contract/errors.rs | 35 ++++++++++++++- ethers-contract/src/call.rs | 11 ++++- ethers-contract/src/error.rs | 43 ++++++++++++++++++- ethers-contract/src/lib.rs | 2 +- ethers-contract/tests/it/abigen.rs | 12 ++++-- ethers-core/src/types/bytes.rs | 12 ++++++ 6 files changed, 106 insertions(+), 9 deletions(-) diff --git a/ethers-contract/ethers-contract-abigen/src/contract/errors.rs b/ethers-contract/ethers-contract-abigen/src/contract/errors.rs index c6ba41f6..ad2d0b2a 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract/errors.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract/errors.rs @@ -104,11 +104,20 @@ impl Context { #[derive(Clone, #ethers_contract::EthAbiType, #derives)] pub enum #enum_name { #( #variants(#variants), )* + /// The standard solidity revert string, with selector + /// Error(string) -- 0x08c379a0 + RevertString(::std::string::String), } impl #ethers_core::abi::AbiDecode for #enum_name { fn decode(data: impl AsRef<[u8]>) -> ::core::result::Result { let data = data.as_ref(); + // NB: This implementation does not include selector information, and ABI encoded types + // are incredibly ambiguous, so it's possible to have bad false positives. Instead, we default + // to a String to minimize amount of decoding attempts + if let Ok(decoded) = <::std::string::String as #ethers_core::abi::AbiDecode>::decode(data) { + return Ok(Self::RevertString(decoded)) + } #( if let Ok(decoded) = <#variants as #ethers_core::abi::AbiDecode>::decode(data) { return Ok(Self::#variants(decoded)) @@ -124,20 +133,42 @@ impl Context { #( Self::#variants(element) => #ethers_core::abi::AbiEncode::encode(element), )* + Self::RevertString(s) => #ethers_core::abi::AbiEncode::encode(s), } } } + impl #ethers_contract::ContractRevert for #enum_name { + fn valid_selector(selector: [u8; 4]) -> bool { + match selector { + // Error(string) -- 0x08c379a0 -- standard solidity revert + [0x08, 0xc3, 0x79, 0xa0] => true, + #( + _ if selector == <#variants as #ethers_contract::EthError>::selector() => true, + )* + _ => false, + } + } + } + + impl ::core::fmt::Display for #enum_name { fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { match self { #( - Self::#variants(element) => ::core::fmt::Display::fmt(element, f) - ),* + Self::#variants(element) => ::core::fmt::Display::fmt(element, f), + )* + Self::RevertString(s) => ::core::fmt::Display::fmt(s, f), } } } + impl ::core::convert::From<::std::string::String> for #enum_name { + fn from(value: String) -> Self { + Self::RevertString(value) + } + } + #( impl ::core::convert::From<#variants> for #enum_name { fn from(value: #variants) -> Self { diff --git a/ethers-contract/src/call.rs b/ethers-contract/src/call.rs index 016ba73e..16b7b1b8 100644 --- a/ethers-contract/src/call.rs +++ b/ethers-contract/src/call.rs @@ -1,6 +1,6 @@ #![allow(clippy::return_self_not_must_use)] -use crate::EthError; +use crate::{error::ContractRevert, EthError}; use super::base::{decode_function_data, AbiError}; use ethers_core::{ @@ -115,6 +115,15 @@ impl ContractError { self.as_revert().and_then(|data| Err::decode_with_selector(data)) } + /// Decode revert data into a [`ContractRevert`] type. Returns `None` if + /// decoding fails, or if this is not a revert + /// + /// This is intended to be used with error enum outputs from `abigen!` + /// contracts + pub fn decode_contract_revert(&self) -> Option { + self.as_revert().and_then(|data| Err::decode_with_selector(data)) + } + /// Convert a [`MiddlewareError`] to a `ContractError` pub fn from_middleware_error(e: M::Error) -> Self { if let Some(data) = e.as_error_response().and_then(JsonRpcError::as_revert_data) { diff --git a/ethers-contract/src/error.rs b/ethers-contract/src/error.rs index b0f4f707..e7373c8a 100644 --- a/ethers-contract/src/error.rs +++ b/ethers-contract/src/error.rs @@ -1,11 +1,46 @@ use ethers_core::{ abi::{AbiDecode, AbiEncode, Tokenizable}, - types::{Bytes, Selector}, + types::Selector, utils::id, }; use ethers_providers::JsonRpcError; use std::borrow::Cow; +/// A trait for enums unifying [`EthError`] types. This trait is usually used +/// to represent the errors that a specific contract might throw. I.e. all +/// solidity custom errors + revert strings. +/// +/// This trait should be accessed via +/// [`crate::ContractError::decode_contract_revert`]. It is generally +/// unnecessary to import this trait into your code. +/// +/// # Implementor's Note +/// +/// We do not recommend manual implementations of this trait. Instead, use the +/// automatically generated implementation in the [`crate::abigen`] macro +/// +/// However, sophisticated users may wish to represent the errors of multiple +/// contracts as a single unified enum. E.g. if your contract calls Uniswap, +/// you may wish to implement this on `pub enum MyContractOrUniswapErrors`. +/// In that case, it should be straightforward to delegate to the inner types. +pub trait ContractRevert: AbiDecode + AbiEncode + Send + Sync { + /// Decode the error from EVM revert data including an Error selector + fn decode_with_selector(data: &[u8]) -> Option { + if data.len() < 4 { + return None + } + let selector = data[..4].try_into().expect("checked by len"); + if !Self::valid_selector(selector) { + return None + } + ::decode(&data[4..]).ok() + } + + /// `true` if the selector corresponds to an error that this contract can + /// revert. False otherwise + fn valid_selector(selector: Selector) -> bool; +} + /// A helper trait for types that represents a custom error type pub trait EthError: Tokenizable + AbiDecode + AbiEncode + Send + Sync { /// Attempt to decode from a [`JsonRpcError`] by extracting revert data @@ -16,7 +51,7 @@ pub trait EthError: Tokenizable + AbiDecode + AbiEncode + Send + Sync { } /// Decode the error from EVM revert data including an Error selector - fn decode_with_selector(data: &Bytes) -> Option { + fn decode_with_selector(data: &[u8]) -> Option { // This will return none if selector mismatch. ::decode(data.strip_prefix(&Self::selector())?).ok() } @@ -41,6 +76,10 @@ impl EthError for String { fn abi_signature() -> Cow<'static, str> { Cow::Borrowed("Error(string)") } + + fn selector() -> Selector { + [0x08, 0xc3, 0x79, 0xa0] + } } #[cfg(test)] diff --git a/ethers-contract/src/lib.rs b/ethers-contract/src/lib.rs index 238ae951..4d4058d9 100644 --- a/ethers-contract/src/lib.rs +++ b/ethers-contract/src/lib.rs @@ -13,7 +13,7 @@ mod call; pub use call::{ContractCall, ContractError, EthCall, FunctionCall}; mod error; -pub use error::EthError; +pub use error::{ContractRevert, EthError}; mod factory; pub use factory::{ContractDeployer, ContractDeploymentTx, ContractFactory, DeploymentTxFactory}; diff --git a/ethers-contract/tests/it/abigen.rs b/ethers-contract/tests/it/abigen.rs index 75a4ee2c..45ca640a 100644 --- a/ethers-contract/tests/it/abigen.rs +++ b/ethers-contract/tests/it/abigen.rs @@ -1,9 +1,9 @@ //! Test cases to validate the `abigen!` macro -use ethers_contract::{abigen, EthCall, EthEvent}; +use ethers_contract::{abigen, ContractError, EthCall, EthError, EthEvent}; use ethers_core::{ abi::{AbiDecode, AbiEncode, Address, Tokenizable}, - types::{transaction::eip2718::TypedTransaction, Eip1559TransactionRequest, U256}, + types::{transaction::eip2718::TypedTransaction, Bytes, Eip1559TransactionRequest, U256}, utils::Anvil, }; use ethers_providers::{MockProvider, Provider}; @@ -648,7 +648,13 @@ fn can_generate_seaport_1_0() { let err = SeaportErrors::BadContractSignature(BadContractSignature::default()); let encoded = err.clone().encode(); - assert_eq!(err, SeaportErrors::decode(encoded).unwrap()); + assert_eq!(err, SeaportErrors::decode(encoded.clone()).unwrap()); + + let with_selector: Bytes = + BadContractSignature::selector().into_iter().chain(encoded).collect(); + let contract_err = ContractError::>::Revert(with_selector); + + assert_eq!(contract_err.decode_contract_revert(), Some(err)); let _err = SeaportErrors::ConsiderationNotMet(ConsiderationNotMet { order_index: U256::zero(), diff --git a/ethers-core/src/types/bytes.rs b/ethers-core/src/types/bytes.rs index f09c9170..3db0e04e 100644 --- a/ethers-core/src/types/bytes.rs +++ b/ethers-core/src/types/bytes.rs @@ -16,6 +16,18 @@ pub struct Bytes( pub bytes::Bytes, ); +impl FromIterator for Bytes { + fn from_iter>(iter: T) -> Self { + iter.into_iter().collect::().into() + } +} + +impl<'a> FromIterator<&'a u8> for Bytes { + fn from_iter>(iter: T) -> Self { + iter.into_iter().copied().collect::().into() + } +} + impl Bytes { /// Creates a new empty `Bytes`. ///