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 <me@gakonst.com>
This commit is contained in:
James Prestwich 2023-02-27 23:59:32 -08:00 committed by GitHub
parent 9e443fc216
commit ded611e714
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 106 additions and 9 deletions

View File

@ -104,11 +104,20 @@ impl Context {
#[derive(Clone, #ethers_contract::EthAbiType, #derives)] #[derive(Clone, #ethers_contract::EthAbiType, #derives)]
pub enum #enum_name { pub enum #enum_name {
#( #variants(#variants), )* #( #variants(#variants), )*
/// The standard solidity revert string, with selector
/// Error(string) -- 0x08c379a0
RevertString(::std::string::String),
} }
impl #ethers_core::abi::AbiDecode for #enum_name { impl #ethers_core::abi::AbiDecode for #enum_name {
fn decode(data: impl AsRef<[u8]>) -> ::core::result::Result<Self, #ethers_core::abi::AbiError> { fn decode(data: impl AsRef<[u8]>) -> ::core::result::Result<Self, #ethers_core::abi::AbiError> {
let data = data.as_ref(); 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) { if let Ok(decoded) = <#variants as #ethers_core::abi::AbiDecode>::decode(data) {
return Ok(Self::#variants(decoded)) return Ok(Self::#variants(decoded))
@ -124,20 +133,42 @@ impl Context {
#( #(
Self::#variants(element) => #ethers_core::abi::AbiEncode::encode(element), 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 { impl ::core::fmt::Display for #enum_name {
fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
match self { 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 { impl ::core::convert::From<#variants> for #enum_name {
fn from(value: #variants) -> Self { fn from(value: #variants) -> Self {

View File

@ -1,6 +1,6 @@
#![allow(clippy::return_self_not_must_use)] #![allow(clippy::return_self_not_must_use)]
use crate::EthError; use crate::{error::ContractRevert, EthError};
use super::base::{decode_function_data, AbiError}; use super::base::{decode_function_data, AbiError};
use ethers_core::{ use ethers_core::{
@ -115,6 +115,15 @@ impl<M: Middleware> ContractError<M> {
self.as_revert().and_then(|data| Err::decode_with_selector(data)) 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<Err: ContractRevert>(&self) -> Option<Err> {
self.as_revert().and_then(|data| Err::decode_with_selector(data))
}
/// Convert a [`MiddlewareError`] to a `ContractError` /// Convert a [`MiddlewareError`] to a `ContractError`
pub fn from_middleware_error(e: M::Error) -> Self { pub fn from_middleware_error(e: M::Error) -> Self {
if let Some(data) = e.as_error_response().and_then(JsonRpcError::as_revert_data) { if let Some(data) = e.as_error_response().and_then(JsonRpcError::as_revert_data) {

View File

@ -1,11 +1,46 @@
use ethers_core::{ use ethers_core::{
abi::{AbiDecode, AbiEncode, Tokenizable}, abi::{AbiDecode, AbiEncode, Tokenizable},
types::{Bytes, Selector}, types::Selector,
utils::id, utils::id,
}; };
use ethers_providers::JsonRpcError; use ethers_providers::JsonRpcError;
use std::borrow::Cow; 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<Self> {
if data.len() < 4 {
return None
}
let selector = data[..4].try_into().expect("checked by len");
if !Self::valid_selector(selector) {
return None
}
<Self as AbiDecode>::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 /// A helper trait for types that represents a custom error type
pub trait EthError: Tokenizable + AbiDecode + AbiEncode + Send + Sync { pub trait EthError: Tokenizable + AbiDecode + AbiEncode + Send + Sync {
/// Attempt to decode from a [`JsonRpcError`] by extracting revert data /// 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 /// Decode the error from EVM revert data including an Error selector
fn decode_with_selector(data: &Bytes) -> Option<Self> { fn decode_with_selector(data: &[u8]) -> Option<Self> {
// This will return none if selector mismatch. // This will return none if selector mismatch.
<Self as AbiDecode>::decode(data.strip_prefix(&Self::selector())?).ok() <Self as AbiDecode>::decode(data.strip_prefix(&Self::selector())?).ok()
} }
@ -41,6 +76,10 @@ impl EthError for String {
fn abi_signature() -> Cow<'static, str> { fn abi_signature() -> Cow<'static, str> {
Cow::Borrowed("Error(string)") Cow::Borrowed("Error(string)")
} }
fn selector() -> Selector {
[0x08, 0xc3, 0x79, 0xa0]
}
} }
#[cfg(test)] #[cfg(test)]

View File

@ -13,7 +13,7 @@ mod call;
pub use call::{ContractCall, ContractError, EthCall, FunctionCall}; pub use call::{ContractCall, ContractError, EthCall, FunctionCall};
mod error; mod error;
pub use error::EthError; pub use error::{ContractRevert, EthError};
mod factory; mod factory;
pub use factory::{ContractDeployer, ContractDeploymentTx, ContractFactory, DeploymentTxFactory}; pub use factory::{ContractDeployer, ContractDeploymentTx, ContractFactory, DeploymentTxFactory};

View File

@ -1,9 +1,9 @@
//! Test cases to validate the `abigen!` macro //! Test cases to validate the `abigen!` macro
use ethers_contract::{abigen, EthCall, EthEvent}; use ethers_contract::{abigen, ContractError, EthCall, EthError, EthEvent};
use ethers_core::{ use ethers_core::{
abi::{AbiDecode, AbiEncode, Address, Tokenizable}, abi::{AbiDecode, AbiEncode, Address, Tokenizable},
types::{transaction::eip2718::TypedTransaction, Eip1559TransactionRequest, U256}, types::{transaction::eip2718::TypedTransaction, Bytes, Eip1559TransactionRequest, U256},
utils::Anvil, utils::Anvil,
}; };
use ethers_providers::{MockProvider, Provider}; use ethers_providers::{MockProvider, Provider};
@ -648,7 +648,13 @@ fn can_generate_seaport_1_0() {
let err = SeaportErrors::BadContractSignature(BadContractSignature::default()); let err = SeaportErrors::BadContractSignature(BadContractSignature::default());
let encoded = err.clone().encode(); 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::<Provider<MockProvider>>::Revert(with_selector);
assert_eq!(contract_err.decode_contract_revert(), Some(err));
let _err = SeaportErrors::ConsiderationNotMet(ConsiderationNotMet { let _err = SeaportErrors::ConsiderationNotMet(ConsiderationNotMet {
order_index: U256::zero(), order_index: U256::zero(),

View File

@ -16,6 +16,18 @@ pub struct Bytes(
pub bytes::Bytes, pub bytes::Bytes,
); );
impl FromIterator<u8> for Bytes {
fn from_iter<T: IntoIterator<Item = u8>>(iter: T) -> Self {
iter.into_iter().collect::<bytes::Bytes>().into()
}
}
impl<'a> FromIterator<&'a u8> for Bytes {
fn from_iter<T: IntoIterator<Item = &'a u8>>(iter: T) -> Self {
iter.into_iter().copied().collect::<bytes::Bytes>().into()
}
}
impl Bytes { impl Bytes {
/// Creates a new empty `Bytes`. /// Creates a new empty `Bytes`.
/// ///