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)]
|
#[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 {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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};
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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`.
|
||||||
///
|
///
|
||||||
|
|
Loading…
Reference in New Issue