Repository: gakonst/ethers-rs
Pull request #2182 (2023-02-24 03:30:52)

feature: contract revert trait
## Motivation

Followup work to #2172. Allows easier decoding of error types for generated contracts

## Solution

- Introduce the `ContractRevert` trait
- Auto-derive `ContractRevert` on the error enum in `abigen!` contract modules
- Add `RevertString(String)` to contract error enum in `abigen!` contract modules
- Add `ContractError::decode_contract_revert<Err: ContractRevert(&self) -> Option<Err>`
- (drive-by) add missing `FromIterator` impls to `ethers::core::types::Bytes`


This is a breaing change to `abigen!` generated bindings (again) but i promise this one is also really good :)

## PR Checklist

-   [ ] Added Tests
-   [x] Added Documentation
-   [ ] Updated the changelog
-   [x] Breaking changes


prestwich: will address these comments tomorrow. ledger issue has worn my brain out
prestwich: this is now based on latest master 👍 
gakonst: @prestwich seems to have broken in the Seaport codegen

```

---- abigen::can_generate_seaport_1_0 stdout ----
error: test failed, to rerun pass `-p ethers-contract --test it`
thread 'abigen::can_generate_seaport_1_0' panicked at 'checked by len: TryFromSliceError(())', /home/runner/work/ethers-rs/ethers-rs/ethers-contract/src/error.rs:32:40
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```
prestwich: @gakonst seaport is fixed now 👍 

Patch:
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<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 {
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<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) {
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<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)]
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-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<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`.
     ///


Test patch:
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::<Provider<MockProvider>>::Revert(with_selector);
+
+    assert_eq!(contract_err.decode_contract_revert(), Some(err));
 
     let _err = SeaportErrors::ConsiderationNotMet(ConsiderationNotMet {
         order_index: U256::zero(),