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:
parent
9e443fc216
commit
ded611e714
|
@ -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<Self, #ethers_core::abi::AbiError> {
|
||||
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 {
|
||||
|
|
|
@ -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<M: Middleware> ContractError<M> {
|
|||
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`
|
||||
pub fn from_middleware_error(e: M::Error) -> Self {
|
||||
if let Some(data) = e.as_error_response().and_then(JsonRpcError::as_revert_data) {
|
||||
|
|
|
@ -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<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
|
||||
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<Self> {
|
||||
fn decode_with_selector(data: &[u8]) -> Option<Self> {
|
||||
// This will return none if selector mismatch.
|
||||
<Self as AbiDecode>::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)]
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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::<Provider<MockProvider>>::Revert(with_selector);
|
||||
|
||||
assert_eq!(contract_err.decode_contract_revert(), Some(err));
|
||||
|
||||
let _err = SeaportErrors::ConsiderationNotMet(ConsiderationNotMet {
|
||||
order_index: U256::zero(),
|
||||
|
|
|
@ -16,6 +16,18 @@ pub struct 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 {
|
||||
/// Creates a new empty `Bytes`.
|
||||
///
|
||||
|
|
Loading…
Reference in New Issue