Skip to content
This repository has been archived by the owner on Oct 19, 2024. It is now read-only.

Commit

Permalink
Expose contract revert errors in the ContractError struct (#2172)
Browse files Browse the repository at this point in the history
* feature: spelunk for revert errors

* feature: bubble up revert to contract error

* feature: bubble up reverts to multicall

* fix: correctly remove signature when deserializing EthErrors

* chore: remove redundant test

* chore: clippy

* fix: allow empty revert string

* docs: add all missing rustdoc for ethers-contract

* chore: rustfmt

* chore: Changelog

* fix: danipope test comment
  • Loading branch information
prestwich authored Feb 22, 2023
1 parent ee5e3e5 commit 2090bf5
Show file tree
Hide file tree
Showing 18 changed files with 492 additions and 231 deletions.
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 {
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

0 comments on commit 2090bf5

Please sign in to comment.