Repository: gakonst/ethers-rs
Pull request #2172 (2023-02-21 01:24:20)

Expose contract revert errors in the ContractError struct
## Motivation

Users don't have access to revert data. That sucks. I'm fixing it.

## Solution

- Add logic to spelunk for json rpc revert data, inspired by ethers-rs
- Add logic to contract
- Break old usages (and direct instantiation) of middleware error and provider error variants of contract error enum


TODO: 
- [x] test this code

## PR Checklist

-   [x] Added Tests
-   [x] Added Documentation
-   [x] Updated the changelog
-   [x] Breaking changes


prestwich: looks like linter error is related to #2171
prestwich: cc @DaniPopes this touches your recent work 😅 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f7c22e40..255d604d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -302,6 +302,11 @@
 
 ### Unreleased
 
+- (Breaking) Add `Revert` to `ContractError`. Add `impl EthError for String`.
+  Modify existing `ContractError` variants to prevent accidental improper
+  usage. Change `MulticallError` to use `ContractError::Revert`. Add
+  convenience methods to decode errors from reverts.
+  [#2172](https://github.com/gakonst/ethers-rs/pull/2172)
 - (Breaking) Improve Multicall result handling
   [#2164](https://github.com/gakonst/ethers-rs/pull/2105)
 - (Breaking) Make `Event` objects generic over borrow & remove lifetime
diff --git a/ethers-contract/src/base.rs b/ethers-contract/src/base.rs
index 6f0c4395..be648b52 100644
--- a/ethers-contract/src/base.rs
+++ b/ethers-contract/src/base.rs
@@ -159,6 +159,8 @@ impl BaseContract {
         decode_function_data(function, bytes, true)
     }
 
+    /// Decode the provided ABI encoded bytes as the output of the provided
+    /// function selector
     pub fn decode_output_with_selector<D: Detokenize, T: AsRef<[u8]>>(
         &self,
         signature: Selector,
diff --git a/ethers-contract/src/call.rs b/ethers-contract/src/call.rs
index ad931078..016ba73e 100644
--- a/ethers-contract/src/call.rs
+++ b/ethers-contract/src/call.rs
@@ -1,5 +1,7 @@
 #![allow(clippy::return_self_not_must_use)]
 
+use crate::EthError;
+
 use super::base::{decode_function_data, AbiError};
 use ethers_core::{
     abi::{AbiDecode, AbiEncode, Detokenize, Function, InvalidOutputType, Tokenizable},
@@ -11,7 +13,7 @@ use ethers_core::{
 };
 use ethers_providers::{
     call_raw::{CallBuilder, RawCall},
-    Middleware, PendingTransaction, ProviderError,
+    JsonRpcError, Middleware, MiddlewareError, PendingTransaction, ProviderError,
 };
 
 use std::{
@@ -54,12 +56,22 @@ pub enum ContractError<M: Middleware> {
     DetokenizationError(#[from] InvalidOutputType),
 
     /// Thrown when a middleware call fails
-    #[error("{0}")]
-    MiddlewareError(M::Error),
+    #[error("{e}")]
+    MiddlewareError {
+        /// The underlying error
+        e: M::Error,
+    },
 
     /// Thrown when a provider call fails
-    #[error("{0}")]
-    ProviderError(ProviderError),
+    #[error("{e}")]
+    ProviderError {
+        /// The underlying error
+        e: ProviderError,
+    },
+
+    /// Contract reverted
+    #[error("Contract call reverted with data: {0}")]
+    Revert(Bytes),
 
     /// Thrown during deployment if a constructor argument was passed in the `deploy`
     /// call but a constructor was not present in the ABI
@@ -72,6 +84,83 @@ pub enum ContractError<M: Middleware> {
     ContractNotDeployed,
 }
 
+impl<M: Middleware> ContractError<M> {
+    /// If this `ContractError` is a revert, this method will retrieve a
+    /// reference to the underlying revert data. This ABI-encoded data could be
+    /// a String, or a custom Solidity error type.
+    ///
+    /// ## Returns
+    ///
+    /// `None` if the error is not a revert
+    /// `Some(data)` with the revert data, if the error is a revert
+    ///
+    /// ## Note
+    ///
+    /// To skip this step, consider using [`ContractError::decode_revert`]
+    pub fn as_revert(&self) -> Option<&Bytes> {
+        match self {
+            ContractError::Revert(data) => Some(data),
+            _ => None,
+        }
+    }
+
+    /// True if the error is a revert, false otherwise
+    pub fn is_revert(&self) -> bool {
+        matches!(self, ContractError::Revert(_))
+    }
+
+    /// Decode revert data into an [`EthError`] type. Returns `None` if
+    /// decoding fails, or if this is not a revert
+    pub fn decode_revert<Err: EthError>(&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) {
+            ContractError::Revert(data)
+        } else {
+            ContractError::MiddlewareError { e }
+        }
+    }
+
+    /// Convert a `ContractError` to a [`MiddlewareError`] if possible.
+    pub fn as_middleware_error(&self) -> Option<&M::Error> {
+        match self {
+            ContractError::MiddlewareError { e } => Some(e),
+            _ => None,
+        }
+    }
+
+    /// True if the error is a middleware error
+    pub fn is_middleware_error(&self) -> bool {
+        matches!(self, ContractError::MiddlewareError { .. })
+    }
+
+    /// Convert a `ContractError` to a [`ProviderError`] if possible.
+    pub fn as_provider_error(&self) -> Option<&ProviderError> {
+        match self {
+            ContractError::ProviderError { e } => Some(e),
+            _ => None,
+        }
+    }
+
+    /// True if the error is a provider error
+    pub fn is_provider_error(&self) -> bool {
+        matches!(self, ContractError::ProviderError { .. })
+    }
+}
+
+impl<M: Middleware> From<ProviderError> for ContractError<M> {
+    fn from(e: ProviderError) -> Self {
+        if let Some(data) = e.as_error_response().and_then(JsonRpcError::as_revert_data) {
+            ContractError::Revert(data)
+        } else {
+            ContractError::ProviderError { e }
+        }
+    }
+}
+
 /// `ContractCall` is a [`FunctionCall`] object with an [`std::sync::Arc`] middleware.
 /// This type alias exists to preserve backwards compatibility with
 /// less-abstract Contracts.
@@ -177,7 +266,7 @@ where
             .borrow()
             .estimate_gas(&self.tx, self.block)
             .await
-            .map_err(ContractError::MiddlewareError)
+            .map_err(ContractError::from_middleware_error)
     }
 
     /// Queries the blockchain via an `eth_call` for the provided transaction.
@@ -190,9 +279,12 @@ where
     ///
     /// Note: this function _does not_ send a transaction from your account
     pub async fn call(&self) -> Result<D, ContractError<M>> {
-        let client: &M = self.client.borrow();
-        let bytes =
-            client.call(&self.tx, self.block).await.map_err(ContractError::MiddlewareError)?;
+        let bytes = self
+            .client
+            .borrow()
+            .call(&self.tx, self.block)
+            .await
+            .map_err(ContractError::from_middleware_error)?;
 
         // decode output
         let data = decode_function_data(&self.function, &bytes, false)?;
@@ -211,7 +303,7 @@ where
     ) -> impl RawCall<'_> + Future<Output = Result<D, ContractError<M>>> + Debug {
         let call = self.call_raw_bytes();
         call.map(move |res: Result<Bytes, ProviderError>| {
-            let bytes = res.map_err(ContractError::ProviderError)?;
+            let bytes = res?;
             decode_function_data(&self.function, &bytes, false).map_err(From::from)
         })
     }
@@ -237,7 +329,7 @@ where
             .borrow()
             .send_transaction(self.tx.clone(), self.block)
             .await
-            .map_err(ContractError::MiddlewareError)
+            .map_err(ContractError::from_middleware_error)
     }
 }
 
diff --git a/ethers-contract/src/error.rs b/ethers-contract/src/error.rs
index cfac0651..b0f4f707 100644
--- a/ethers-contract/src/error.rs
+++ b/ethers-contract/src/error.rs
@@ -1,12 +1,26 @@
 use ethers_core::{
     abi::{AbiDecode, AbiEncode, Tokenizable},
-    types::Selector,
+    types::{Bytes, Selector},
     utils::id,
 };
+use ethers_providers::JsonRpcError;
 use std::borrow::Cow;
 
 /// 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
+    ///
+    /// Fails if the error is not a revert, or decoding fails
+    fn from_rpc_response(response: &JsonRpcError) -> Option<Self> {
+        Self::decode_with_selector(&response.as_revert_data()?)
+    }
+
+    /// Decode the error from EVM revert data including an Error selector
+    fn decode_with_selector(data: &Bytes) -> Option<Self> {
+        // This will return none if selector mismatch.
+        <Self as AbiDecode>::decode(data.strip_prefix(&Self::selector())?).ok()
+    }
+
     /// The name of the error
     fn error_name() -> Cow<'static, str>;
 
@@ -18,3 +32,30 @@ pub trait EthError: Tokenizable + AbiDecode + AbiEncode + Send + Sync {
         id(Self::abi_signature())
     }
 }
+
+impl EthError for String {
+    fn error_name() -> Cow<'static, str> {
+        Cow::Borrowed("Error")
+    }
+
+    fn abi_signature() -> Cow<'static, str> {
+        Cow::Borrowed("Error(string)")
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use ethers_core::types::Bytes;
+
+    use super::EthError;
+
+    #[test]
+    fn string_error() {
+        let multicall_revert_string: Bytes = "0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000174d756c746963616c6c333a2063616c6c206661696c6564000000000000000000".parse().unwrap();
+        assert_eq!(String::selector().as_slice(), &multicall_revert_string[0..4]);
+        assert_eq!(
+            String::decode_with_selector(&multicall_revert_string).unwrap().as_str(),
+            "Multicall3: call failed"
+        );
+    }
+}
diff --git a/ethers-contract/src/event.rs b/ethers-contract/src/event.rs
index df529e6d..1f771f79 100644
--- a/ethers-contract/src/event.rs
+++ b/ethers-contract/src/event.rs
@@ -192,7 +192,7 @@ where
             .borrow()
             .watch(&self.filter)
             .await
-            .map_err(ContractError::MiddlewareError)?;
+            .map_err(ContractError::from_middleware_error)?;
         Ok(EventStream::new(filter.id, filter, Box::new(move |log| Ok(parse_log(log)?))))
     }
 
@@ -209,7 +209,7 @@ where
             .borrow()
             .watch(&self.filter)
             .await
-            .map_err(ContractError::MiddlewareError)?;
+            .map_err(ContractError::from_middleware_error)?;
         Ok(EventStream::new(
             filter.id,
             filter,
@@ -243,10 +243,11 @@ where
             .borrow()
             .subscribe_logs(&self.filter)
             .await
-            .map_err(ContractError::MiddlewareError)?;
+            .map_err(ContractError::from_middleware_error)?;
         Ok(EventStream::new(filter.id, filter, Box::new(move |log| Ok(parse_log(log)?))))
     }
 
+    /// As [`Self::subscribe`], but includes event metadata
     pub async fn subscribe_with_meta(
         &self,
     ) -> Result<
@@ -259,7 +260,7 @@ where
             .borrow()
             .subscribe_logs(&self.filter)
             .await
-            .map_err(ContractError::MiddlewareError)?;
+            .map_err(ContractError::from_middleware_error)?;
         Ok(EventStream::new(
             filter.id,
             filter,
@@ -285,7 +286,7 @@ where
             .borrow()
             .get_logs(&self.filter)
             .await
-            .map_err(ContractError::MiddlewareError)?;
+            .map_err(ContractError::from_middleware_error)?;
         let events = logs
             .into_iter()
             .map(|log| Ok(parse_log(log)?))
@@ -301,7 +302,7 @@ where
             .borrow()
             .get_logs(&self.filter)
             .await
-            .map_err(ContractError::MiddlewareError)?;
+            .map_err(ContractError::from_middleware_error)?;
         let events = logs
             .into_iter()
             .map(|log| {
diff --git a/ethers-contract/src/factory.rs b/ethers-contract/src/factory.rs
index d3248150..6aa08eab 100644
--- a/ethers-contract/src/factory.rs
+++ b/ethers-contract/src/factory.rs
@@ -79,6 +79,7 @@ where
         self
     }
 
+    /// Sets the block at which RPC requests are made
     pub fn block<T: Into<BlockNumber>>(mut self, block: T) -> Self {
         self.deployer.block = block.into();
         self
@@ -222,6 +223,7 @@ where
         self
     }
 
+    /// Set the block at which requests are made
     pub fn block<T: Into<BlockNumber>>(mut self, block: T) -> Self {
         self.block = block.into();
         self
@@ -247,7 +249,7 @@ where
             .borrow()
             .call(&self.tx, Some(self.block.into()))
             .await
-            .map_err(ContractError::MiddlewareError)?;
+            .map_err(ContractError::from_middleware_error)?;
 
         // TODO: It would be nice to handle reverts in a structured way.
         Ok(())
@@ -282,7 +284,7 @@ where
             .borrow()
             .send_transaction(self.tx, Some(self.block.into()))
             .await
-            .map_err(ContractError::MiddlewareError)?;
+            .map_err(ContractError::from_middleware_error)?;
 
         // TODO: Should this be calculated "optimistically" by address/nonce?
         let receipt = pending_tx
@@ -382,6 +384,8 @@ where
         Self { client, abi, bytecode, _m: PhantomData }
     }
 
+    /// Create a deployment tx using the provided tokens as constructor
+    /// arguments
     pub fn deploy_tokens(self, params: Vec<Token>) -> Result<Deployer<B, M>, ContractError<M>>
     where
         B: Clone,
diff --git a/ethers-contract/src/lib.rs b/ethers-contract/src/lib.rs
index 002f07c9..238ae951 100644
--- a/ethers-contract/src/lib.rs
+++ b/ethers-contract/src/lib.rs
@@ -1,6 +1,7 @@
 #![cfg_attr(docsrs, feature(doc_cfg))]
 #![doc = include_str!("../README.md")]
 #![deny(unsafe_code)]
+#![warn(missing_docs)]
 
 mod contract;
 pub use contract::{Contract, ContractInstance};
@@ -31,8 +32,10 @@ mod multicall;
 #[cfg(any(test, feature = "abigen"))]
 #[cfg_attr(docsrs, doc(cfg(feature = "abigen")))]
 pub use multicall::{
-    contract as multicall_contract, Call, Multicall, MulticallContract, MulticallError,
-    MulticallVersion, MULTICALL_ADDRESS, MULTICALL_SUPPORTED_CHAIN_IDS,
+    constants::{MULTICALL_ADDRESS, MULTICALL_SUPPORTED_CHAIN_IDS},
+    contract as multicall_contract,
+    error::MulticallError,
+    Call, Multicall, MulticallContract, MulticallVersion,
 };
 
 /// This module exposes low lever builder structures which are only consumed by the
diff --git a/ethers-contract/src/multicall/constants.rs b/ethers-contract/src/multicall/constants.rs
new file mode 100644
index 00000000..7f111061
--- /dev/null
+++ b/ethers-contract/src/multicall/constants.rs
@@ -0,0 +1,74 @@
+use ethers_core::types::{Chain, H160};
+
+/// The Multicall3 contract address that is deployed in [`MULTICALL_SUPPORTED_CHAIN_IDS`]:
+/// [`0xcA11bde05977b3631167028862bE2a173976CA11`](https://etherscan.io/address/0xcA11bde05977b3631167028862bE2a173976CA11)
+pub const MULTICALL_ADDRESS: H160 = H160([
+    0xca, 0x11, 0xbd, 0xe0, 0x59, 0x77, 0xb3, 0x63, 0x11, 0x67, 0x02, 0x88, 0x62, 0xbe, 0x2a, 0x17,
+    0x39, 0x76, 0xca, 0x11,
+]);
+
+/// The chain IDs that [`MULTICALL_ADDRESS`] has been deployed to.
+///
+/// Taken from: <https://github.com/mds1/multicall#multicall3-contract-addresses>
+pub const MULTICALL_SUPPORTED_CHAIN_IDS: &[u64] = {
+    use Chain::*;
+    &[
+        Mainnet as u64,                  // Mainnet
+        Kovan as u64,                    // Kovan
+        Rinkeby as u64,                  // Rinkeby
+        Goerli as u64,                   // Görli
+        Ropsten as u64,                  // Ropsten
+        Sepolia as u64,                  // Sepolia
+        Optimism as u64,                 // Optimism
+        OptimismKovan as u64,            // Optimism Kovan
+        OptimismGoerli as u64,           // Optimism Görli
+        Arbitrum as u64,                 // Arbitrum
+        ArbitrumNova as u64,             // Arbitrum Nova
+        ArbitrumGoerli as u64,           // Arbitrum Görli
+        ArbitrumTestnet as u64,          // Arbitrum Rinkeby
+        Polygon as u64,                  // Polygon
+        PolygonMumbai as u64,            // Polygon Mumbai
+        XDai as u64,                     // Gnosis Chain
+        Avalanche as u64,                // Avalanche
+        AvalancheFuji as u64,            // Avalanche Fuji
+        FantomTestnet as u64,            // Fantom Testnet
+        Fantom as u64,                   // Fantom Opera
+        BinanceSmartChain as u64,        // BNB Smart Chain
+        BinanceSmartChainTestnet as u64, // BNB Smart Chain Testnet
+        Moonbeam as u64,                 // Moonbeam
+        Moonriver as u64,                // Moonriver
+        Moonbase as u64,                 // Moonbase
+        1666600000,                      // Harmony0
+        1666600001,                      // Harmony1
+        1666600002,                      // Harmony2
+        1666600003,                      // Harmony3
+        Cronos as u64,                   // Cronos
+        122,                             // Fuse
+        14,                              // Flare Mainnet
+        19,                              // Songbird Canary Network
+        16,                              // Coston Testnet
+        114,                             // Coston2 Testnet
+        288,                             // Boba
+        Aurora as u64,                   // Aurora
+        592,                             // Astar
+        66,                              // OKC
+        128,                             // Heco Chain
+        1088,                            // Metis
+        Rsk as u64,                      // Rsk
+        31,                              // Rsk Testnet
+        Evmos as u64,                    // Evmos
+        EvmosTestnet as u64,             // Evmos Testnet
+        Oasis as u64,                    // Oasis
+        42261,                           // Oasis Emerald ParaTime Testnet
+        42262,                           // Oasis Emerald ParaTime
+        Celo as u64,                     // Celo
+        CeloAlfajores as u64,            // Celo Alfajores Testnet
+        71402,                           // Godwoken
+        71401,                           // Godwoken Testnet
+        8217,                            // Klaytn
+        2001,                            // Milkomeda
+        321,                             // KCC
+        106,                             // Velas
+        40,                              // Telos
+    ]
+};
diff --git a/ethers-contract/src/multicall/contract.rs b/ethers-contract/src/multicall/contract.rs
new file mode 100644
index 00000000..a18fdb4e
--- /dev/null
+++ b/ethers-contract/src/multicall/contract.rs
@@ -0,0 +1,4 @@
+#![allow(missing_docs)]
+use ethers_contract_derive::abigen;
+
+abigen!(Multicall3, "src/multicall/multicall_abi.json");
diff --git a/ethers-contract/src/multicall/error.rs b/ethers-contract/src/multicall/error.rs
new file mode 100644
index 00000000..e9ec06b9
--- /dev/null
+++ b/ethers-contract/src/multicall/error.rs
@@ -0,0 +1,98 @@
+use ethers_core::{
+    abi::{self, InvalidOutputType},
+    types::Bytes,
+};
+
+use ethers_providers::{Middleware, ProviderError};
+
+use crate::{ContractError, EthError};
+
+/// Errors using the [`crate::Multicall`] system
+#[derive(Debug, thiserror::Error)]
+pub enum MulticallError<M: Middleware> {
+    /// Contract call returned an error
+    #[error(transparent)]
+    ContractError(#[from] ContractError<M>),
+
+    /// Unsupported chain
+    #[error("Chain ID {0} is currently not supported by Multicall. Provide an address instead.")]
+    InvalidChainId(u64),
+
+    /// Contract call reverted when not allowed
+    #[error("Illegal revert: Multicall2 call reverted when it wasn't allowed to.")]
+    IllegalRevert,
+}
+
+impl<M: Middleware> From<abi::Error> for MulticallError<M> {
+    fn from(value: abi::Error) -> Self {
+        Self::ContractError(ContractError::DecodingError(value))
+    }
+}
+
+impl<M: Middleware> From<InvalidOutputType> for MulticallError<M> {
+    fn from(value: InvalidOutputType) -> Self {
+        Self::ContractError(ContractError::DetokenizationError(value))
+    }
+}
+
+impl<M: Middleware> MulticallError<M> {
+    /// Convert a `MulticallError` to a the underlying error if possible.
+    pub fn as_contract_error(&self) -> Option<&ContractError<M>> {
+        match self {
+            MulticallError::ContractError(e) => Some(e),
+            _ => None,
+        }
+    }
+
+    /// True if the underlying error is a [`ContractError`]
+    pub fn is_contract_error(&self) -> bool {
+        matches!(self, MulticallError::ContractError(_))
+    }
+
+    /// Convert a `MulticallError` to a the underlying error if possible.
+    pub fn as_middleware_error(&self) -> Option<&M::Error> {
+        self.as_contract_error().and_then(ContractError::as_middleware_error)
+    }
+
+    /// True if the underlying error is a MiddlewareError
+    pub fn is_middleware_error(&self) -> bool {
+        self.as_contract_error().map(ContractError::is_middleware_error).unwrap_or_default()
+    }
+
+    /// Convert a `MulticallError` to a [`ProviderError`] if possible.
+    pub fn as_provider_error(&self) -> Option<&ProviderError> {
+        self.as_contract_error().and_then(ContractError::as_provider_error)
+    }
+
+    /// True if the error is a provider error
+    pub fn is_provider_error(&self) -> bool {
+        self.as_contract_error().map(ContractError::is_provider_error).unwrap_or_default()
+    }
+
+    /// If this `MulticallError` is a revert, this method will retrieve a
+    /// reference to the underlying revert data. This ABI-encoded data could be
+    /// a String, or a custom Solidity error type.
+    ///
+    /// ## Returns
+    ///
+    /// `None` if the error is not a revert
+    /// `Some(data)` with the revert data, if the error is a revert
+    ///
+    /// ## Note
+    ///
+    /// To skip this step, consider using [`MulticallError::decode_revert`]
+    pub fn as_revert(&self) -> Option<&Bytes> {
+        self.as_contract_error().and_then(ContractError::as_revert)
+    }
+
+    /// True if the error is a revert, false otherwise
+    pub fn is_revert(&self) -> bool {
+        self.as_contract_error().map(ContractError::is_revert).unwrap_or_default()
+    }
+
+    /// Decode revert data into an [`EthError`] type. Returns `None` if
+    /// decoding fails, or if this is not a revert
+    pub fn decode_revert<Err: EthError>(&self) -> Option<Err> {
+        self.as_revert().and_then(|data| Err::decode_with_selector(data))
+    }
+}
diff --git a/ethers-contract/src/multicall/mod.rs b/ethers-contract/src/multicall/mod.rs
index 85af2faa..14f7cd55 100644
--- a/ethers-contract/src/multicall/mod.rs
+++ b/ethers-contract/src/multicall/mod.rs
@@ -1,151 +1,28 @@
 use crate::call::{ContractCall, ContractError};
 use ethers_core::{
-    abi::{AbiDecode, Detokenize, Function, InvalidOutputType, Token, Tokenizable},
+    abi::{Detokenize, Function, Token, Tokenizable},
     types::{
-        transaction::eip2718::TypedTransaction, Address, BlockNumber, Bytes, Chain, NameOrAddress,
-        H160, U256,
+        transaction::eip2718::TypedTransaction, Address, BlockNumber, Bytes, NameOrAddress, U256,
     },
 };
 use ethers_providers::{Middleware, PendingTransaction};
 use std::{convert::TryFrom, fmt, result::Result as StdResult, sync::Arc};
 
 /// The Multicall contract bindings. Auto-generated with `abigen`.
-pub mod contract {
-    ethers_contract_derive::abigen!(Multicall3, "src/multicall/multicall_abi.json");
-}
+pub mod contract;
 pub use contract::Multicall3 as MulticallContract;
 use contract::{
     Call as Multicall1Call, Call3 as Multicall3Call, Call3Value as Multicall3CallValue,
     Result as MulticallResult,
 };
 
-/// The Multicall3 contract address that is deployed in [`MULTICALL_SUPPORTED_CHAIN_IDS`]:
-/// [`0xcA11bde05977b3631167028862bE2a173976CA11`](https://etherscan.io/address/0xcA11bde05977b3631167028862bE2a173976CA11)
-pub const MULTICALL_ADDRESS: Address = H160([
-    0xca, 0x11, 0xbd, 0xe0, 0x59, 0x77, 0xb3, 0x63, 0x11, 0x67, 0x02, 0x88, 0x62, 0xbe, 0x2a, 0x17,
-    0x39, 0x76, 0xca, 0x11,
-]);
-
-/// The chain IDs that [`MULTICALL_ADDRESS`] has been deployed to.
-///
-/// Taken from: <https://github.com/mds1/multicall#multicall3-contract-addresses>
-pub const MULTICALL_SUPPORTED_CHAIN_IDS: &[u64] = {
-    use Chain::*;
-    &[
-        Mainnet as u64,                  // Mainnet
-        Kovan as u64,                    // Kovan
-        Rinkeby as u64,                  // Rinkeby
-        Goerli as u64,                   // Görli
-        Ropsten as u64,                  // Ropsten
-        Sepolia as u64,                  // Sepolia
-        Optimism as u64,                 // Optimism
-        OptimismKovan as u64,            // Optimism Kovan
-        OptimismGoerli as u64,           // Optimism Görli
-        Arbitrum as u64,                 // Arbitrum
-        ArbitrumNova as u64,             // Arbitrum Nova
-        ArbitrumGoerli as u64,           // Arbitrum Görli
-        ArbitrumTestnet as u64,          // Arbitrum Rinkeby
-        Polygon as u64,                  // Polygon
-        PolygonMumbai as u64,            // Polygon Mumbai
-        XDai as u64,                     // Gnosis Chain
-        Avalanche as u64,                // Avalanche
-        AvalancheFuji as u64,            // Avalanche Fuji
-        FantomTestnet as u64,            // Fantom Testnet
-        Fantom as u64,                   // Fantom Opera
-        BinanceSmartChain as u64,        // BNB Smart Chain
-        BinanceSmartChainTestnet as u64, // BNB Smart Chain Testnet
-        Moonbeam as u64,                 // Moonbeam
-        Moonriver as u64,                // Moonriver
-        Moonbase as u64,                 // Moonbase
-        1666600000,                      // Harmony0
-        1666600001,                      // Harmony1
-        1666600002,                      // Harmony2
-        1666600003,                      // Harmony3
-        Cronos as u64,                   // Cronos
-        122,                             // Fuse
-        14,                              // Flare Mainnet
-        19,                              // Songbird Canary Network
-        16,                              // Coston Testnet
-        114,                             // Coston2 Testnet
-        288,                             // Boba
-        Aurora as u64,                   // Aurora
-        592,                             // Astar
-        66,                              // OKC
-        128,                             // Heco Chain
-        1088,                            // Metis
-        Rsk as u64,                      // Rsk
-        31,                              // Rsk Testnet
-        Evmos as u64,                    // Evmos
-        EvmosTestnet as u64,             // Evmos Testnet
-        Oasis as u64,                    // Oasis
-        42261,                           // Oasis Emerald ParaTime Testnet
-        42262,                           // Oasis Emerald ParaTime
-        Celo as u64,                     // Celo
-        CeloAlfajores as u64,            // Celo Alfajores Testnet
-        71402,                           // Godwoken
-        71401,                           // Godwoken Testnet
-        8217,                            // Klaytn
-        2001,                            // Milkomeda
-        321,                             // KCC
-        106,                             // Velas
-        40,                              // Telos
-    ]
-};
+pub mod constants;
 
 /// Type alias for `Result<T, MulticallError<M>>`
-pub type Result<T, M> = StdResult<T, MulticallError<M>>;
-
-#[derive(Debug, thiserror::Error)]
-pub enum MulticallError<M: Middleware> {
-    #[error(transparent)]
-    ContractError(#[from] ContractError<M>),
-
-    #[error("Chain ID {0} is currently not supported by Multicall. Provide an address instead.")]
-    InvalidChainId(u64),
-
-    #[error("Illegal revert: Multicall2 call reverted when it wasn't allowed to.")]
-    IllegalRevert,
-
-    #[error("Call reverted with data: \"{}\"", decode_error(_0))]
-    CallReverted(Bytes),
-}
+pub type Result<T, M> = StdResult<T, error::MulticallError<M>>;
 
-impl<M: Middleware> From<ethers_core::abi::Error> for MulticallError<M> {
-    fn from(value: ethers_core::abi::Error) -> Self {
-        Self::ContractError(ContractError::DecodingError(value))
-    }
-}
-
-impl<M: Middleware> From<InvalidOutputType> for MulticallError<M> {
-    fn from(value: InvalidOutputType) -> Self {
-        Self::ContractError(ContractError::DetokenizationError(value))
-    }
-}
-
-impl<M: Middleware> MulticallError<M> {
-    pub fn into_bytes(self) -> Result<Bytes, M> {
-        match self {
-            Self::CallReverted(bytes) => Ok(bytes),
-            e => Err(e),
-        }
-    }
-
-    /// Returns the bytes that the call reverted with.
-    pub fn get_bytes(&self) -> Option<&Bytes> {
-        match self {
-            Self::CallReverted(bytes) => Some(bytes),
-            _ => None,
-        }
-    }
-
-    /// Formats the bytes that the call reverted with.
-    pub fn format_bytes(&self) -> Option<String> {
-        match self {
-            Self::CallReverted(bytes) => Some(decode_error(bytes)),
-            _ => None,
-        }
-    }
-}
+/// MultiCall error type
+pub mod error;
 
 /// Helper struct for managing calls to be made to the `function` in smart contract `target`
 /// with `data`.
@@ -171,8 +48,11 @@ pub struct Call {
 #[repr(u8)]
 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
 pub enum MulticallVersion {
+    /// V1
     Multicall = 1,
+    /// V2
     Multicall2 = 2,
+    /// V3
     #[default]
     Multicall3 = 3,
 }
@@ -196,16 +76,19 @@ impl TryFrom<u8> for MulticallVersion {
 }
 
 impl MulticallVersion {
+    /// True if call is v1
     #[inline]
     pub fn is_v1(&self) -> bool {
         matches!(self, Self::Multicall)
     }
 
+    /// True if call is v2
     #[inline]
     pub fn is_v2(&self) -> bool {
         matches!(self, Self::Multicall2)
     }
 
+    /// True if call is v3
     #[inline]
     pub fn is_v3(&self) -> bool {
         matches!(self, Self::Multicall3)
@@ -218,7 +101,7 @@ impl MulticallVersion {
 ///
 /// `Multicall` can be instantiated asynchronously from the chain ID of the provided client using
 /// [`new`] or synchronously by providing a chain ID in [`new_with_chain`]. This, by default, uses
-/// [`MULTICALL_ADDRESS`], but can be overridden by providing `Some(address)`.
+/// [`constants::MULTICALL_ADDRESS`], but can be overridden by providing `Some(address)`.
 /// A list of all the supported chains is available [`here`](https://github.com/mds1/multicall#multicall3-contract-addresses).
 ///
 /// Set the contract's version by using [`version`].
@@ -356,17 +239,17 @@ impl<M> fmt::Debug for Multicall<M> {
 impl<M: Middleware> Multicall<M> {
     /// Creates a new Multicall instance from the provided client. If provided with an `address`,
     /// it instantiates the Multicall contract with that address, otherwise it defaults to
-    /// [`MULTICALL_ADDRESS`].
+    /// [`constants::MULTICALL_ADDRESS`].
     ///
     /// # Errors
     ///
-    /// Returns a [`MulticallError`] if the provider returns an error while getting
+    /// Returns a [`error::MulticallError`] if the provider returns an error while getting
     /// `network_version`.
     ///
     /// # Panics
     ///
     /// If a `None` address is provided and the client's network is
-    /// [not supported](MULTICALL_SUPPORTED_CHAIN_IDS).
+    /// [not supported](constants::MULTICALL_SUPPORTED_CHAIN_IDS).
     pub async fn new(client: impl Into<Arc<M>>, address: Option<Address>) -> Result<Self, M> {
         let client = client.into();
 
@@ -376,12 +259,15 @@ impl<M: Middleware> Multicall<M> {
         let address: Address = match address {
             Some(addr) => addr,
             None => {
-                let chain_id =
-                    client.get_chainid().await.map_err(ContractError::MiddlewareError)?.as_u64();
-                if !MULTICALL_SUPPORTED_CHAIN_IDS.contains(&chain_id) {
-                    return Err(MulticallError::InvalidChainId(chain_id))
+                let chain_id = client
+                    .get_chainid()
+                    .await
+                    .map_err(ContractError::from_middleware_error)?
+                    .as_u64();
+                if !constants::MULTICALL_SUPPORTED_CHAIN_IDS.contains(&chain_id) {
+                    return Err(error::MulticallError::InvalidChainId(chain_id))
                 }
-                MULTICALL_ADDRESS
+                constants::MULTICALL_ADDRESS
             }
         };
 
@@ -398,12 +284,13 @@ impl<M: Middleware> Multicall<M> {
     }
 
     /// Creates a new Multicall instance synchronously from the provided client and address or chain
-    /// ID. Uses the [default multicall address](MULTICALL_ADDRESS) if no address is provided.
+    /// ID. Uses the [default multicall address](constants::MULTICALL_ADDRESS) if no address is
+    /// provided.
     ///
     /// # Errors
     ///
-    /// Returns a [`MulticallError`] if the provided chain_id is not in the
-    /// [supported networks](MULTICALL_SUPPORTED_CHAIN_IDS).
+    /// Returns a [`error::MulticallError`] if the provided chain_id is not in the
+    /// [supported networks](constants::MULTICALL_SUPPORTED_CHAIN_IDS).
     ///
     /// # Panics
     ///
@@ -421,10 +308,10 @@ impl<M: Middleware> Multicall<M> {
             (Some(addr), _) => addr,
             (_, Some(chain_id)) => {
                 let chain_id = chain_id.into();
-                if !MULTICALL_SUPPORTED_CHAIN_IDS.contains(&chain_id) {
-                    return Err(MulticallError::InvalidChainId(chain_id))
+                if !constants::MULTICALL_SUPPORTED_CHAIN_IDS.contains(&chain_id) {
+                    return Err(error::MulticallError::InvalidChainId(chain_id))
                 }
-                MULTICALL_ADDRESS
+                constants::MULTICALL_ADDRESS
             }
             _ => {
                 // Can't fetch chain_id from provider since we're not in an async function so we
@@ -460,8 +347,9 @@ impl<M: Middleware> Multicall<M> {
     /// to use (so you can fit more calls into a single request), and it adds an aggregate3 method
     /// so you can specify whether calls are allowed to fail on a per-call basis.
     ///
-    /// Note: all these versions are available in the same contract address ([`MULTICALL_ADDRESS`])
-    /// so changing version just changes the methods used, not the contract address.
+    /// Note: all these versions are available in the same contract address
+    /// ([`constants::MULTICALL_ADDRESS`]) so changing version just changes the methods used,
+    /// not the contract address.
     pub fn version(mut self, version: MulticallVersion) -> Self {
         self.version = version;
         self
@@ -673,8 +561,8 @@ impl<M: Middleware> Multicall<M> {
     ///
     /// # Errors
     ///
-    /// Returns a [`MulticallError`] if there are any errors in the RPC call or while detokenizing
-    /// the tokens back to the expected return type.
+    /// Returns a [`error::MulticallError`] if there are any errors in the RPC call or while
+    /// detokenizing the tokens back to the expected return type.
     ///
     /// Returns an error if any call failed, even if `allow_failure` was set, or if the return data
     /// was empty.
@@ -707,7 +595,11 @@ impl<M: Middleware> Multicall<M> {
         let results = self.call_raw().await?;
         let tokens = results
             .into_iter()
-            .map(|res| res.map_err(MulticallError::CallReverted))
+            .map(|res| {
+                res.map_err(|data| {
+                    error::MulticallError::ContractError(ContractError::Revert(data))
+                })
+            })
             .collect::<Result<_, _>>()?;
         T::from_token(Token::Tuple(tokens)).map_err(Into::into)
     }
@@ -717,8 +609,8 @@ impl<M: Middleware> Multicall<M> {
     ///
     /// # Errors
     ///
-    /// Returns a [`MulticallError`] if there are any errors in the RPC call or while detokenizing
-    /// the tokens back to the expected return type.
+    /// Returns a [`error::MulticallError`] if there are any errors in the RPC call or while
+    /// detokenizing the tokens back to the expected return type.
     ///
     /// Returns an error if any call failed, even if `allow_failure` was set, or if the return data
     /// was empty.
@@ -747,8 +639,10 @@ impl<M: Middleware> Multicall<M> {
             .await?
             .into_iter()
             .map(|res| {
-                res.map_err(MulticallError::CallReverted)
-                    .and_then(|token| T::from_token(token).map_err(Into::into))
+                res.map_err(|data| {
+                    error::MulticallError::ContractError(ContractError::Revert(data))
+                })
+                .and_then(|token| T::from_token(token).map_err(Into::into))
             })
             .collect()
     }
@@ -763,7 +657,7 @@ impl<M: Middleware> Multicall<M> {
     ///
     /// # Errors
     ///
-    /// Returns a [`MulticallError`] if there are any errors in the RPC call.
+    /// Returns a [`error::MulticallError`] if there are any errors in the RPC call.
     ///
     /// # Examples
     ///
@@ -824,7 +718,7 @@ impl<M: Middleware> Multicall<M> {
                 // still do so because of other calls that are in the same multicall
                 // aggregate.
                 if !success && !call.allow_failure {
-                    return Err(MulticallError::IllegalRevert)
+                    return Err(error::MulticallError::IllegalRevert)
                 }
 
                 Err(return_data)
@@ -849,7 +743,7 @@ impl<M: Middleware> Multicall<M> {
     ///
     /// # Errors
     ///
-    /// Returns a [`MulticallError`] if there are any errors in the RPC call.
+    /// Returns a [`error::MulticallError`] if there are any errors in the RPC call.
     ///
     /// # Examples
     ///
@@ -871,10 +765,9 @@ impl<M: Middleware> Multicall<M> {
             MulticallVersion::Multicall3 => self.as_aggregate_3_value().tx,
         };
         let client: &M = self.contract.client_ref();
-        client
-            .send_transaction(tx, self.block.map(Into::into))
-            .await
-            .map_err(|e| MulticallError::ContractError(ContractError::MiddlewareError(e)))
+        client.send_transaction(tx, self.block.map(Into::into)).await.map_err(|e| {
+            error::MulticallError::ContractError(ContractError::from_middleware_error(e))
+        })
     }
 
     /// v1
@@ -984,13 +877,3 @@ impl<M: Middleware> Multicall<M> {
         }
     }
 }
-
-fn decode_error(bytes: &Bytes) -> String {
-    // Try decoding with "Error(string)" (0x08c379a0)
-    if bytes.len() >= 4 && bytes[..4] == [0x08, 0xc3, 0x79, 0xa0] {
-        if let Ok(string) = String::decode(&bytes[4..]) {
-            return string
-        }
-    }
-    bytes.to_string()
-}
diff --git a/ethers-contract/src/stream.rs b/ethers-contract/src/stream.rs
index e258ea4c..cbd5685d 100644
--- a/ethers-contract/src/stream.rs
+++ b/ethers-contract/src/stream.rs
@@ -1,3 +1,6 @@
+//! Contains the `EventStream` type which aids in streaming access to contract
+//! events
+
 use crate::LogMeta;
 use ethers_core::types::{Log, U256};
 use futures_util::{
@@ -19,6 +22,7 @@ type MapEvent<'a, R, E> = Box<dyn Fn(Log) -> Result<R, E> + 'a + Send + Sync>;
 /// We use this wrapper type instead of `StreamExt::map` in order to preserve
 /// information about the filter/subscription's id.
 pub struct EventStream<'a, T, R, E> {
+    /// The stream ID, provided by the RPC server
     pub id: U256,
     #[pin]
     stream: T,
@@ -33,6 +37,9 @@ impl<'a, T, R, E> EventStream<'a, T, R, E> {
 }
 
 impl<'a, T, R, E> EventStream<'a, T, R, E> {
+    /// Instantiate a new `EventStream`
+    ///
+    /// Typically users should not call this directly
     pub fn new(id: U256, stream: T, parse: MapEvent<'a, R, E>) -> Self {
         Self { id, stream, parse }
     }
@@ -128,8 +135,10 @@ where
     }
 }
 
+/// A stream of two items
 pub type SelectEither<'a, L, R> = Pin<Box<dyn Stream<Item = Either<L, R>> + 'a>>;
 
+/// Stream for [`EventStream::select`]
 #[pin_project]
 pub struct SelectEvent<T>(#[pin] T);
 
diff --git a/ethers-core/src/abi/tokens.rs b/ethers-core/src/abi/tokens.rs
index e6f11b88..38d1a284 100644
--- a/ethers-core/src/abi/tokens.rs
+++ b/ethers-core/src/abi/tokens.rs
@@ -143,7 +143,7 @@ impl Tokenizable for String {
     fn from_token(token: Token) -> Result<Self, InvalidOutputType> {
         match token {
             Token::String(s) => Ok(s),
-            other => Err(InvalidOutputType(format!("Expected `String`, got {:?}", other))),
+            other => Err(InvalidOutputType(format!("Expected `String`, got {other:?}"))),
         }
     }
 
@@ -156,7 +156,7 @@ impl Tokenizable for Bytes {
     fn from_token(token: Token) -> Result<Self, InvalidOutputType> {
         match token {
             Token::Bytes(s) => Ok(s.into()),
-            other => Err(InvalidOutputType(format!("Expected `Bytes`, got {:?}", other))),
+            other => Err(InvalidOutputType(format!("Expected `Bytes`, got {other:?}"))),
         }
     }
 
@@ -169,7 +169,7 @@ impl Tokenizable for bytes::Bytes {
     fn from_token(token: Token) -> Result<Self, InvalidOutputType> {
         match token {
             Token::Bytes(s) => Ok(s.into()),
-            other => Err(InvalidOutputType(format!("Expected `Bytes`, got {:?}", other))),
+            other => Err(InvalidOutputType(format!("Expected `Bytes`, got {other:?}"))),
         }
     }
 
@@ -183,7 +183,7 @@ impl Tokenizable for H256 {
         match token {
             Token::FixedBytes(mut s) => {
                 if s.len() != 32 {
-                    return Err(InvalidOutputType(format!("Expected `H256`, got {:?}", s)))
+                    return Err(InvalidOutputType(format!("Expected `H256`, got {s:?}")))
                 }
                 let mut data = [0; 32];
                 for (idx, val) in s.drain(..).enumerate() {
@@ -191,7 +191,7 @@ impl Tokenizable for H256 {
                 }
                 Ok(data.into())
             }
-            other => Err(InvalidOutputType(format!("Expected `H256`, got {:?}", other))),
+            other => Err(InvalidOutputType(format!("Expected `H256`, got {other:?}"))),
         }
     }
 
@@ -204,7 +204,7 @@ impl Tokenizable for Address {
     fn from_token(token: Token) -> Result<Self, InvalidOutputType> {
         match token {
             Token::Address(data) => Ok(data),
-            other => Err(InvalidOutputType(format!("Expected `Address`, got {:?}", other))),
+            other => Err(InvalidOutputType(format!("Expected `Address`, got {other:?}"))),
         }
     }
 
@@ -217,7 +217,7 @@ impl Tokenizable for bool {
     fn from_token(token: Token) -> Result<Self, InvalidOutputType> {
         match token {
             Token::Bool(data) => Ok(data),
-            other => Err(InvalidOutputType(format!("Expected `bool`, got {:?}", other))),
+            other => Err(InvalidOutputType(format!("Expected `bool`, got {other:?}"))),
         }
     }
     fn into_token(self) -> Token {
@@ -298,7 +298,7 @@ impl Tokenizable for Vec<u8> {
             Token::Bytes(data) => Ok(data),
             Token::Array(data) => data.into_iter().map(u8::from_token).collect(),
             Token::FixedBytes(data) => Ok(data),
-            other => Err(InvalidOutputType(format!("Expected `bytes`, got {:?}", other))),
+            other => Err(InvalidOutputType(format!("Expected `bytes`, got {other:?}"))),
         }
     }
 
@@ -313,7 +313,7 @@ impl<T: TokenizableItem> Tokenizable for Vec<T> {
             Token::FixedArray(tokens) | Token::Array(tokens) => {
                 tokens.into_iter().map(Tokenizable::from_token).collect()
             }
-            other => Err(InvalidOutputType(format!("Expected `Array`, got {:?}", other))),
+            other => Err(InvalidOutputType(format!("Expected `Array`, got {other:?}"))),
         }
     }
 
@@ -338,9 +338,7 @@ impl<const N: usize> Tokenizable for [u8; N] {
                 arr.copy_from_slice(&bytes);
                 Ok(arr)
             }
-            other => {
-                Err(InvalidOutputType(format!("Expected `FixedBytes({})`, got {:?}", N, other)))
-            }
+            other => Err(InvalidOutputType(format!("Expected `FixedBytes({N})`, got {other:?}"))),
         }
     }
 
@@ -372,9 +370,7 @@ impl<T: TokenizableItem + Clone, const N: usize> Tokenizable for [T; N] {
                     Err(_) => panic!("All elements inserted so the array is full; qed"),
                 }
             }
-            other => {
-                Err(InvalidOutputType(format!("Expected `FixedArray({})`, got {:?}", N, other)))
-            }
+            other => Err(InvalidOutputType(format!("Expected `FixedArray({N})`, got {other:?}"))),
         }
     }
 
@@ -483,6 +479,7 @@ mod tests {
 
         let _tuple: (Address, Vec<Vec<u8>>) = assert_detokenize();
         let _vec_of_tuple: Vec<(Address, String)> = assert_detokenize();
+        #[allow(clippy::type_complexity)]
         let _vec_of_tuple_5: Vec<(Address, Vec<Vec<u8>>, String, U256, bool)> = assert_detokenize();
     }
 
@@ -542,12 +539,12 @@ mod tests {
         assert_eq!(data.0[1], 2);
         assert_eq!(data.0[2], 3);
         assert_eq!(data.0[3], 4);
-        assert_eq!(data.1, true);
+        assert!(data.1);
 
         // handle vector of more than one elements
         let tokens = vec![Token::Bool(false), Token::Uint(U256::from(13u8))];
         let data: (bool, u8) = Detokenize::from_tokens(tokens).unwrap();
-        assert_eq!(data.0, false);
+        assert!(!data.0);
         assert_eq!(data.1, 13u8);
 
         // handle more than two tuples
@@ -559,8 +556,8 @@ mod tests {
         assert_eq!((data.0).0[1], 2);
         assert_eq!((data.0).0[2], 3);
         assert_eq!((data.0).0[3], 4);
-        assert_eq!((data.0).1, true);
-        assert_eq!((data.1).0, false);
+        assert!((data.0).1);
+        assert!(!(data.1).0);
         assert_eq!((data.1).1, 13u8);
 
         // error if no tokens in the vector
diff --git a/ethers-middleware/src/transformer/ds_proxy/mod.rs b/ethers-middleware/src/transformer/ds_proxy/mod.rs
index 7251ab2b..d97e7a16 100644
--- a/ethers-middleware/src/transformer/ds_proxy/mod.rs
+++ b/ethers-middleware/src/transformer/ds_proxy/mod.rs
@@ -95,7 +95,7 @@ impl DsProxy {
             Some(addr) => addr,
             None => {
                 let chain_id =
-                    client.get_chainid().await.map_err(ContractError::MiddlewareError)?;
+                    client.get_chainid().await.map_err(ContractError::from_middleware_error)?;
                 match ADDRESS_BOOK.get(&chain_id) {
                     Some(addr) => *addr,
                     None => panic!(
@@ -112,8 +112,7 @@ impl DsProxy {
             .legacy()
             .send()
             .await?
-            .await
-            .map_err(ContractError::ProviderError)?
+            .await?
             .ok_or(ContractError::ContractNotDeployed)?;
 
         // decode the event log to get the address of the deployed contract.
diff --git a/ethers-providers/src/rpc/transports/common.rs b/ethers-providers/src/rpc/transports/common.rs
index 02591928..9caab6b1 100644
--- a/ethers-providers/src/rpc/transports/common.rs
+++ b/ethers-providers/src/rpc/transports/common.rs
@@ -1,7 +1,10 @@
 // Code adapted from: https://github.com/althea-net/guac_rs/tree/master/web3/src/jsonrpc
 
 use base64::{engine::general_purpose, Engine};
-use ethers_core::types::U256;
+use ethers_core::{
+    abi::AbiDecode,
+    types::{Bytes, U256},
+};
 use serde::{
     de::{self, MapAccess, Unexpected, Visitor},
     Deserialize, Serialize,
@@ -21,6 +24,48 @@ pub struct JsonRpcError {
     pub data: Option<Value>,
 }
 
+/// Recursively traverses the value, looking for hex data that it can extract
+/// Inspired by ethers-js logic:
+/// https://github.com/ethers-io/ethers.js/blob/9f990c57f0486728902d4b8e049536f2bb3487ee/packages/providers/src.ts/json-rpc-provider.ts#L25-L53
+fn spelunk_revert(value: &Value) -> Option<Bytes> {
+    match value {
+        Value::String(s) => s.parse().ok(),
+        Value::Object(o) => o.values().flat_map(spelunk_revert).next(),
+        _ => None,
+    }
+}
+
+impl JsonRpcError {
+    /// Determine if the error output of the `eth_call` RPC request is a revert
+    ///
+    /// Note that this may return false positives if called on an error from
+    /// other RPC requests
+    pub fn is_revert(&self) -> bool {
+        // Ganache says "revert" not "reverted"
+        self.message.contains("revert")
+    }
+
+    /// Attempt to extract revert data from the JsonRpcError be recursively
+    /// traversing the error's data field
+    ///
+    /// This returns the first hex it finds in the data object, and its
+    /// behavior may change with `serde_json` internal changes.
+    ///
+    /// If no hex object is found, it will return an empty bytes IFF the error
+    /// is a revert
+    ///
+    /// Inspired by ethers-js logic:
+    /// <https://github.com/ethers-io/ethers.js/blob/9f990c57f0486728902d4b8e049536f2bb3487ee/packages/providers/src.ts/json-rpc-provider.ts#L25-L53>
+    pub fn as_revert_data(&self) -> Option<Bytes> {
+        self.is_revert().then(|| self.data.as_ref().and_then(spelunk_revert).unwrap_or_default())
+    }
+
+    /// Decode revert data (if any) into a decodeable type
+    pub fn decode_revert_data<E: AbiDecode>(&self) -> Option<E> {
+        E::decode(&self.as_revert_data()?).ok()
+    }
+}
+
 impl fmt::Display for JsonRpcError {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         write!(f, "(code: {}, message: {}, data: {:?})", self.code, self.message, self.data)
diff --git a/examples/events/examples/filtering.rs b/examples/events/examples/filtering.rs
index 5791934e..d1ab9092 100644
--- a/examples/events/examples/filtering.rs
+++ b/examples/events/examples/filtering.rs
@@ -4,7 +4,6 @@ use ethers::{
 };
 use eyre::Result;
 use std::sync::Arc;
-use tokio;
 
 const HTTP_URL: &str = "https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27";
 const V3FACTORY_ADDRESS: &str = "0x1F98431c8aD98523631AE4a59f267346ea31F984";
@@ -40,8 +39,7 @@ async fn main() -> Result<()> {
         let tick_spacing = U256::from_big_endian(&log.data[29..32]);
         let pool = Address::from(&log.data[44..64].try_into()?);
         println!(
-            "pool = {}, token0 = {}, token1 = {}, fee = {}, spacing = {}",
-            pool, token0, token1, fee_tier, tick_spacing,
+            "pool = {pool}, token0 = {token0}, token1 = {token1}, fee = {fee_tier}, spacing = {tick_spacing}"
         );
     }
     Ok(())


Test patch:
diff --git a/ethers-contract/tests/it/contract.rs b/ethers-contract/tests/it/contract.rs
index e33c522e..a8cc689c 100644
--- a/ethers-contract/tests/it/contract.rs
+++ b/ethers-contract/tests/it/contract.rs
@@ -722,19 +722,24 @@ mod eth_tests {
             .unwrap();
 
         // .call reverts
-        // don't allow revert -> entire call reverts
-        multicall.clear_calls().add_call(get_value_reverting_call.clone(), false);
-        assert!(matches!(
-            multicall.call::<(String,)>().await.unwrap_err(),
-            MulticallError::ContractError(_)
-        ));
+        // don't allow revert
+        multicall
+            .clear_calls()
+            .add_call(get_value_reverting_call.clone(), false)
+            .add_call(get_value_call.clone(), false);
+        let res = multicall.call::<(String, String)>().await;
+        let err = res.unwrap_err();
+
+        assert!(err.is_revert());
+        let message = err.decode_revert::<String>().unwrap();
+        assert!(message.contains("Multicall3: call failed"));
 
         // allow revert -> call doesn't revert, but returns Err(_) in raw tokens
         let expected = Bytes::from_static(b"getValue revert").encode();
         multicall.clear_calls().add_call(get_value_reverting_call.clone(), true);
         assert_eq!(multicall.call_raw().await.unwrap()[0].as_ref().unwrap_err()[4..], expected[..]);
         assert_eq!(
-            multicall.call::<(String,)>().await.unwrap_err().into_bytes().unwrap()[4..],
+            multicall.call::<(String,)>().await.unwrap_err().as_revert().unwrap()[4..],
             expected[..]
         );
 
@@ -774,14 +779,14 @@ mod eth_tests {
         // empty revert
         let empty_revert = reverting_contract.method::<_, H256>("emptyRevert", ()).unwrap();
         multicall.clear_calls().add_call(empty_revert.clone(), true);
-        assert!(multicall.call::<(String,)>().await.unwrap_err().into_bytes().unwrap().is_empty());
+        assert!(multicall.call::<(String,)>().await.unwrap_err().as_revert().unwrap().is_empty());
 
         // string revert
         let string_revert =
             reverting_contract.method::<_, H256>("stringRevert", ("String".to_string())).unwrap();
         multicall.clear_calls().add_call(string_revert, true);
         assert_eq!(
-            multicall.call::<(String,)>().await.unwrap_err().into_bytes().unwrap()[4..],
+            multicall.call::<(String,)>().await.unwrap_err().as_revert().unwrap()[4..],
             Bytes::from_static(b"String").encode()[..]
         );
 
@@ -789,7 +794,7 @@ mod eth_tests {
         let custom_error = reverting_contract.method::<_, H256>("customError", ()).unwrap();
         multicall.clear_calls().add_call(custom_error, true);
         assert_eq!(
-            multicall.call::<(Bytes,)>().await.unwrap_err().into_bytes().unwrap()[..],
+            multicall.call::<(Bytes,)>().await.unwrap_err().as_revert().unwrap()[..],
             keccak256("CustomError()")[..4]
         );
 
@@ -798,7 +803,8 @@ mod eth_tests {
             .method::<_, H256>("customErrorWithData", ("Data".to_string()))
             .unwrap();
         multicall.clear_calls().add_call(custom_error_with_data, true);
-        let bytes = multicall.call::<(Bytes,)>().await.unwrap_err().into_bytes().unwrap();
+        let err = multicall.call::<(Bytes,)>().await.unwrap_err();
+        let bytes = err.as_revert().unwrap();
         assert_eq!(bytes[..4], keccak256("CustomErrorWithData(string)")[..4]);
         assert_eq!(bytes[4..], encode(&[Token::String("Data".to_string())]));
     }
diff --git a/ethers-contract/tests/it/main.rs b/ethers-contract/tests/it/main.rs
index dda43f81..bdf63ef0 100644
--- a/ethers-contract/tests/it/main.rs
+++ b/ethers-contract/tests/it/main.rs
@@ -1,4 +1,4 @@
-#![allow(clippy::extra_unused_type_parameters)]
+// #![allow(clippy::extra_unused_type_parameters)]
 
 #[cfg(feature = "abigen")]
 mod abigen;