Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose contract revert errors in the ContractError struct #2172

Merged
merged 11 commits into from
Feb 22, 2023
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions ethers-contract/src/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
114 changes: 103 additions & 11 deletions ethers-contract/src/call.rs
Original file line number Diff line number Diff line change
@@ -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},
Expand All @@ -11,7 +13,7 @@ use ethers_core::{
};
use ethers_providers::{
call_raw::{CallBuilder, RawCall},
Middleware, PendingTransaction, ProviderError,
JsonRpcError, Middleware, MiddlewareError, PendingTransaction, ProviderError,
};

use std::{
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't make From<M::Error> because it potentially conflicts with blanket From<T> for T in cases where M::Error and ContractError<M> are the same type

yes this is weird

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this would be fixed with specialization, but alas

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.
Expand Down Expand Up @@ -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.
Expand All @@ -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)?;
Expand All @@ -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)
})
}
Expand All @@ -237,7 +329,7 @@ where
.borrow()
.send_transaction(self.tx.clone(), self.block)
.await
.map_err(ContractError::MiddlewareError)
.map_err(ContractError::from_middleware_error)
}
}

Expand Down
43 changes: 42 additions & 1 deletion ethers-contract/src/error.rs
Original file line number Diff line number Diff line change
@@ -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>;

Expand All @@ -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"
);
}
}
13 changes: 7 additions & 6 deletions ethers-contract/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?))))
}

Expand All @@ -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,
Expand Down Expand Up @@ -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<
Expand All @@ -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,
Expand All @@ -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)?))
Expand All @@ -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| {
Expand Down
8 changes: 6 additions & 2 deletions ethers-contract/src/factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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(())
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
7 changes: 5 additions & 2 deletions ethers-contract/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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
Expand Down
Loading