diff --git a/CHANGELOG.md b/CHANGELOG.md index 190fa258..f7c22e40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -302,6 +302,8 @@ ### Unreleased +- (Breaking) Improve Multicall result handling + [#2164](https://github.com/gakonst/ethers-rs/pull/2105) - (Breaking) Make `Event` objects generic over borrow & remove lifetime [#2105](https://github.com/gakonst/ethers-rs/pull/2105) - Make `Factory` objects generic over the borrow trait, to allow non-arc mware diff --git a/ethers-contract/src/call.rs b/ethers-contract/src/call.rs index 97923ebd..ad931078 100644 --- a/ethers-contract/src/call.rs +++ b/ethers-contract/src/call.rs @@ -190,12 +190,9 @@ where /// /// Note: this function _does not_ send a transaction from your account pub async fn call(&self) -> Result> { - let bytes = self - .client - .borrow() - .call(&self.tx, self.block) - .await - .map_err(ContractError::MiddlewareError)?; + let client: &M = self.client.borrow(); + let bytes = + client.call(&self.tx, self.block).await.map_err(ContractError::MiddlewareError)?; // decode output let data = decode_function_data(&self.function, &bytes, false)?; diff --git a/ethers-contract/src/lib.rs b/ethers-contract/src/lib.rs index 4a14760b..002f07c9 100644 --- a/ethers-contract/src/lib.rs +++ b/ethers-contract/src/lib.rs @@ -31,8 +31,8 @@ mod multicall; #[cfg(any(test, feature = "abigen"))] #[cfg_attr(docsrs, doc(cfg(feature = "abigen")))] pub use multicall::{ - multicall_contract, Call, Multicall, MulticallContract, MulticallError, MulticallVersion, - MULTICALL_ADDRESS, MULTICALL_SUPPORTED_CHAIN_IDS, + contract as multicall_contract, Call, Multicall, MulticallContract, MulticallError, + MulticallVersion, MULTICALL_ADDRESS, MULTICALL_SUPPORTED_CHAIN_IDS, }; /// This module exposes low lever builder structures which are only consumed by the diff --git a/ethers-contract/src/multicall/mod.rs b/ethers-contract/src/multicall/mod.rs index 513f1776..85af2faa 100644 --- a/ethers-contract/src/multicall/mod.rs +++ b/ethers-contract/src/multicall/mod.rs @@ -1,21 +1,24 @@ use crate::call::{ContractCall, ContractError}; use ethers_core::{ - abi::{AbiDecode, Detokenize, Function, Token}, - types::{Address, BlockNumber, Bytes, Chain, NameOrAddress, H160, U256}, + abi::{AbiDecode, Detokenize, Function, InvalidOutputType, Token, Tokenizable}, + types::{ + transaction::eip2718::TypedTransaction, Address, BlockNumber, Bytes, Chain, NameOrAddress, + H160, U256, + }, }; - use ethers_providers::{Middleware, PendingTransaction}; -use std::{convert::TryFrom, fmt, sync::Arc}; +use std::{convert::TryFrom, fmt, result::Result as StdResult, sync::Arc}; -pub mod multicall_contract; -use multicall_contract::multicall_3::{ +/// The Multicall contract bindings. Auto-generated with `abigen`. +pub mod contract { + ethers_contract_derive::abigen!(Multicall3, "src/multicall/multicall_abi.json"); +} +pub use contract::Multicall3 as MulticallContract; +use contract::{ Call as Multicall1Call, Call3 as Multicall3Call, Call3Value as Multicall3CallValue, Result as MulticallResult, }; -// Export the contract interface -pub use multicall_contract::multicall_3::Multicall3 as MulticallContract; - /// The Multicall3 contract address that is deployed in [`MULTICALL_SUPPORTED_CHAIN_IDS`]: /// [`0xcA11bde05977b3631167028862bE2a173976CA11`](https://etherscan.io/address/0xcA11bde05977b3631167028862bE2a173976CA11) pub const MULTICALL_ADDRESS: Address = H160([ @@ -89,6 +92,9 @@ pub const MULTICALL_SUPPORTED_CHAIN_IDS: &[u64] = { ] }; +/// Type alias for `Result>` +pub type Result = StdResult>; + #[derive(Debug, thiserror::Error)] pub enum MulticallError { #[error(transparent)] @@ -99,9 +105,47 @@ pub enum MulticallError { #[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 = std::result::Result>; +impl From for MulticallError { + fn from(value: ethers_core::abi::Error) -> Self { + Self::ContractError(ContractError::DecodingError(value)) + } +} + +impl From for MulticallError { + fn from(value: InvalidOutputType) -> Self { + Self::ContractError(ContractError::DetokenizationError(value)) + } +} + +impl MulticallError { + pub fn into_bytes(self) -> Result { + 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 { + match self { + Self::CallReverted(bytes) => Some(decode_error(bytes)), + _ => None, + } + } +} /// Helper struct for managing calls to be made to the `function` in smart contract `target` /// with `data`. @@ -141,7 +185,7 @@ impl From for u8 { impl TryFrom for MulticallVersion { type Error = String; - fn try_from(v: u8) -> std::result::Result { + fn try_from(v: u8) -> StdResult { match v { 1 => Ok(MulticallVersion::Multicall), 2 => Ok(MulticallVersion::Multicall2), @@ -151,6 +195,23 @@ impl TryFrom for MulticallVersion { } } +impl MulticallVersion { + #[inline] + pub fn is_v1(&self) -> bool { + matches!(self, Self::Multicall) + } + + #[inline] + pub fn is_v2(&self) -> bool { + matches!(self, Self::Multicall2) + } + + #[inline] + pub fn is_v3(&self) -> bool { + matches!(self, Self::Multicall3) + } +} + /// A Multicall is an abstraction for sending batched calls/transactions to the Ethereum blockchain. /// It stores an instance of the [`Multicall` smart contract](https://etherscan.io/address/0xcA11bde05977b3631167028862bE2a173976CA11#code) /// and the user provided list of transactions to be called or executed on chain. @@ -190,7 +251,7 @@ impl TryFrom for MulticallVersion { /// let abi: Abi = serde_json::from_str(r#"[{"inputs":[{"internalType":"string","name":"value","type":"string"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"author","type":"address"},{"indexed":true,"internalType":"address","name":"oldAuthor","type":"address"},{"indexed":false,"internalType":"string","name":"oldValue","type":"string"},{"indexed":false,"internalType":"string","name":"newValue","type":"string"}],"name":"ValueChanged","type":"event"},{"inputs":[],"name":"getValue","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"lastSender","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"value","type":"string"}],"name":"setValue","outputs":[],"stateMutability":"nonpayable","type":"function"}]"#)?; /// /// // connect to the network -/// let client = Provider::::try_from("https://kovan.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27")?; +/// let client = Provider::::try_from("https://goerli.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27")?; /// /// // create the contract object. This will be used to construct the calls for multicall /// let client = Arc::new(client); @@ -201,24 +262,18 @@ impl TryFrom for MulticallVersion { /// let first_call = contract.method::<_, String>("getValue", ())?; /// let second_call = contract.method::<_, Address>("lastSender", ())?; /// -/// // Since this example connects to the Kovan testnet, we need not provide an address for +/// // Since this example connects to a known chain, we need not provide an address for /// // the Multicall contract and we set that to `None`. If you wish to provide the address /// // for the Multicall contract, you can pass the `Some(multicall_addr)` argument. /// // Construction of the `Multicall` instance follows the builder pattern: -/// let mut multicall = Multicall::new(client.clone(), None).await?.version(MulticallVersion::Multicall); +/// let mut multicall = Multicall::new(client.clone(), None).await?; /// multicall /// .add_call(first_call, false) /// .add_call(second_call, false); /// /// // `await`ing on the `call` method lets us fetch the return values of both the above calls /// // in one single RPC call -/// let _return_data: (String, Address) = multicall.call().await?; -/// -/// // using Multicall2 (version 2) or Multicall3 (version 3) differs when parsing `.call()` results -/// multicall = multicall.version(MulticallVersion::Multicall3); -/// -/// // each call returns the results in a tuple, with the success status as the first element -/// let _return_data: ((bool, String), (bool, Address)) = multicall.call().await?; +/// let return_data: (String, Address) = multicall.call().await?; /// /// // the same `Multicall` instance can be re-used to do a different batch of transactions. /// // Say we wish to broadcast (send) a couple of transactions via the Multicall contract. @@ -231,27 +286,17 @@ impl TryFrom for MulticallVersion { /// /// // `await`ing the `send` method waits for the transaction to be broadcast, which also /// // returns the transaction hash -/// let _tx_receipt = multicall.send().await?.await.expect("tx dropped"); +/// let tx_receipt = multicall.send().await?.await.expect("tx dropped"); /// /// // you can also query ETH balances of multiple addresses /// let address_1 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".parse::
()?; /// let address_2 = "ffffffffffffffffffffffffffffffffffffffff".parse::
()?; /// -/// // using version 1 -/// multicall = multicall.version(MulticallVersion::Multicall); /// multicall /// .clear_calls() /// .add_get_eth_balance(address_1, false) /// .add_get_eth_balance(address_2, false); -/// let _balances: (U256, U256) = multicall.call().await?; -/// -/// // or with version 2 and above -/// multicall = multicall.version(MulticallVersion::Multicall3); -/// multicall -/// .clear_calls() -/// .add_get_eth_balance(address_1, false) -/// .add_get_eth_balance(address_2, false); -/// let _balances: ((bool, U256), (bool, U256)) = multicall.call().await?; +/// let balances: (U256, U256) = multicall.call().await?; /// /// # Ok(()) /// # } @@ -269,9 +314,17 @@ impl TryFrom for MulticallVersion { pub struct Multicall { /// The Multicall contract interface. pub contract: MulticallContract, - version: MulticallVersion, - legacy: bool, - block: Option, + + /// The version of which methods to use when making the contract call. + pub version: MulticallVersion, + + /// Whether to use a legacy or a EIP-1559 transaction. + pub legacy: bool, + + /// The `block` field of the Multicall aggregate call. + pub block: Option, + + /// The internal call vector. calls: Vec, } @@ -429,10 +482,10 @@ impl Multicall { /// Appends a `call` to the list of calls of the Multicall instance. /// /// Version specific details: - /// - 1: `allow_failure` is ignored. - /// - >=2: `allow_failure` specifies whether or not this call is allowed to revert in the + /// - `1`: `allow_failure` is ignored. + /// - `>=2`: `allow_failure` specifies whether or not this call is allowed to revert in the /// multicall. - /// - 3: Transaction values are used when broadcasting transactions with [`send`], otherwise + /// - `3`: Transaction values are used when broadcasting transactions with [`send`], otherwise /// they are always ignored. /// /// [`send`]: #method.send @@ -441,32 +494,32 @@ impl Multicall { call: ContractCall, allow_failure: bool, ) -> &mut Self { - match (call.tx.to(), call.tx.data()) { - (Some(NameOrAddress::Address(target)), Some(data)) => { - let call = Call { - target: *target, - data: data.clone(), - value: call.tx.value().cloned().unwrap_or_default(), - allow_failure, - function: call.function, - }; - self.calls.push(call); - self - } - _ => self, + let (to, data, value) = match call.tx { + TypedTransaction::Legacy(tx) => (tx.to, tx.data, tx.value), + TypedTransaction::Eip2930(tx) => (tx.tx.to, tx.tx.data, tx.tx.value), + TypedTransaction::Eip1559(tx) => (tx.to, tx.data, tx.value), + }; + if data.is_none() && !call.function.outputs.is_empty() { + return self } + if let Some(NameOrAddress::Address(target)) = to { + let call = Call { + target, + data: data.unwrap_or_default(), + value: value.unwrap_or_default(), + allow_failure, + function: call.function, + }; + self.calls.push(call); + } + self } /// Appends multiple `call`s to the list of calls of the Multicall instance. /// - /// Version specific details: - /// - 1: `allow_failure` is ignored. - /// - >=2: `allow_failure` specifies whether or not this call is allowed to revert in the - /// multicall. - /// - 3: Transaction values are used when broadcasting transactions with [`send`], otherwise - /// they are always ignored. + /// See [`add_call`] for more details. /// - /// [`send`]: #method.send + /// [`add_call`]: #method.add_call pub fn add_calls( &mut self, allow_failure: bool, @@ -611,21 +664,24 @@ impl Multicall { /// Queries the Ethereum blockchain using `eth_call`, but via the Multicall contract. /// - /// Note: this method _does not_ send a transaction from your account. + /// For handling calls that have the same result type, see [`call_array`]. + /// + /// For handling each call's result individually, see [`call_raw`]. + /// + /// [`call_raw`]: #method.call_raw + /// [`call_array`]: #method.call_array /// /// # Errors /// /// Returns a [`MulticallError`] if there are any errors in the RPC call or while detokenizing /// the tokens back to the expected return type. /// - /// # Panics - /// - /// If more than the maximum number of supported calls are added (16). The maximum limit is - /// constrained due to tokenization/detokenization support for tuples. + /// Returns an error if any call failed, even if `allow_failure` was set, or if the return data + /// was empty. /// /// # Examples /// - /// The return type must be annotated while calling this method: + /// The return type must be annotated as a tuple when calling this method: /// /// ```no_run /// # async fn foo() -> Result<(), Box> { @@ -641,31 +697,32 @@ impl Multicall { /// // 1. `returns (uint256)` /// // 2. `returns (string, address)` /// // 3. `returns (bool)` - /// // Version 1: /// let result: (U256, (String, Address), bool) = multicall.call().await?; - /// // Version 2 and above (each call returns also the success status as the first element): - /// let result: ((bool, U256), (bool, (String, Address)), (bool, bool)) = multicall.call().await?; + /// // or using the turbofish syntax: + /// let result = multicall.call::<(U256, (String, Address), bool)>().await?; /// # Ok(()) /// # } /// ``` - pub async fn call(&self) -> Result { - assert!(self.calls.len() <= 16, "Cannot decode more than 16 calls"); - let tokens = self.call_raw().await?; - let tokens = vec![Token::Tuple(tokens)]; - let data = D::from_tokens(tokens).map_err(ContractError::DetokenizationError)?; - Ok(data) + pub async fn call(&self) -> Result { + let results = self.call_raw().await?; + let tokens = results + .into_iter() + .map(|res| res.map_err(MulticallError::CallReverted)) + .collect::>()?; + T::from_token(Token::Tuple(tokens)).map_err(Into::into) } /// Queries the Ethereum blockchain using `eth_call`, but via the Multicall contract, assuming - /// that every call returns same data type. - /// - /// Note: this method _does not_ send a transaction from your account. + /// that every call returns same type. /// /// # 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 an error if any call failed, even if `allow_failure` was set, or if the return data + /// was empty. + /// /// # Examples /// /// The return type must be annotated while calling this method: @@ -685,18 +742,24 @@ impl Multicall { /// # Ok(()) /// # } /// ``` - pub async fn call_array(&self) -> Result, M> { - let tokens = self.call_raw().await?; - let res: std::result::Result, ContractError> = tokens + pub async fn call_array(&self) -> Result, M> { + self.call_raw() + .await? .into_iter() - .map(|token| D::from_tokens(vec![token]).map_err(ContractError::DetokenizationError)) - .collect(); - - Ok(res?) + .map(|res| { + res.map_err(MulticallError::CallReverted) + .and_then(|token| T::from_token(token).map_err(Into::into)) + }) + .collect() } - /// Queries the Ethereum blockchain using `eth_call`, but via the Multicall contract and - /// without detokenization. + /// Queries the Ethereum blockchain using `eth_call`, but via the Multicall contract. + /// + /// Returns a vector of `Result` for each call added to the Multicall: + /// `Err(Bytes)` if the individual call failed while allowed or the return data was empty, + /// `Ok(Token)` otherwise. + /// + /// If the Multicall version is 1, this will always be a vector of `Ok`. /// /// # Errors /// @@ -715,97 +778,67 @@ impl Multicall { /// # /// # let multicall = Multicall::new(client, None).await?; /// // The consumer of the API is responsible for detokenizing the results - /// // as the results will be a Vec /// let tokens = multicall.call_raw().await?; /// # Ok(()) /// # } /// ``` - /// - /// Note: this method _does not_ send a transaction from your account - /// - /// [`ContractError`]: crate::ContractError - pub async fn call_raw(&self) -> Result, M> { + pub async fn call_raw(&self) -> Result>, M> { // Different call result types based on version - let tokens: Vec = match self.version { + match self.version { + // Wrap the return data with `success: true` since version 1 reverts if any call failed MulticallVersion::Multicall => { let call = self.as_aggregate(); - let (_, return_data) = call.call().await?; - self.calls - .iter() - .zip(&return_data) - .map(|(call, bytes)| { - // Always return an empty Bytes token for calls that return no data - if bytes.is_empty() { - Ok(Token::Bytes(Default::default())) - } else { - let mut tokens = call - .function - .decode_output(bytes) - .map_err(ContractError::DecodingError)?; - Ok(match tokens.len() { - 0 => Token::Tuple(vec![]), - 1 => tokens.remove(0), - _ => Token::Tuple(tokens), - }) - } - }) - .collect::, M>>()? + let (_, bytes) = ContractCall::call(&call).await?; + self.parse_call_result( + bytes + .into_iter() + .map(|return_data| MulticallResult { success: true, return_data }), + ) } // Same result type (`MulticallResult`) - v @ (MulticallVersion::Multicall2 | MulticallVersion::Multicall3) => { - let is_v2 = v == MulticallVersion::Multicall2; - let call = if is_v2 { self.as_try_aggregate() } else { self.as_aggregate_3() }; - let return_data = ContractCall::call(&call).await?; - self.calls - .iter() - .zip(return_data.into_iter()) - .map(|(call, res)| { - let bytes = &res.return_data; - // Always return an empty Bytes token for calls that return no data - let res_token: Token = if bytes.is_empty() { - Token::Bytes(Default::default()) - } else if res.success { - // Decode using call.function - let mut res_tokens = call - .function - .decode_output(bytes) - .map_err(ContractError::DecodingError)?; - match res_tokens.len() { - 0 => Token::Tuple(vec![]), - 1 => res_tokens.remove(0), - _ => Token::Tuple(res_tokens), - } - } else { - // Call reverted - - // v2: In the function call to `tryAggregate`, the `allow_failure` check - // is done on a per-transaction basis, and we set this transaction-wide - // check to true when *any* call is allowed to fail. If this is true - // then a call that is not allowed to revert (`call.allow_failure`) may - // still do so because of other calls that are in the same multicall - // aggregate. - if !call.allow_failure { - return Err(MulticallError::IllegalRevert) - } - - // Decode with "Error(string)" (0x08c379a0) - if bytes.len() >= 4 && bytes[..4] == [0x08, 0xc3, 0x79, 0xa0] { - Token::String( - String::decode(&bytes[4..]).map_err(ContractError::AbiError)?, - ) - } else { - Token::Bytes(bytes.to_vec()) - } - }; - - // (bool, (...)) - Ok(Token::Tuple(vec![Token::Bool(res.success), res_token])) - }) - .collect::, M>>()? + MulticallVersion::Multicall2 | MulticallVersion::Multicall3 => { + let call = if self.version.is_v2() { + self.as_try_aggregate() + } else { + self.as_aggregate_3() + }; + let results = ContractCall::call(&call).await?; + self.parse_call_result(results.into_iter()) } - }; + } + } - Ok(tokens) + /// For each call and its `return_data`: if `success` is true, parses `return_data` with the + /// call's function outputs, otherwise returns the bytes in `Err`. + fn parse_call_result( + &self, + return_data: impl Iterator, + ) -> Result>, M> { + let mut results = Vec::with_capacity(self.calls.len()); + for (call, MulticallResult { success, return_data }) in self.calls.iter().zip(return_data) { + let result = if !success || return_data.is_empty() { + // v2: In the function call to `tryAggregate`, the `allow_failure` check + // is done on a per-transaction basis, and we set this transaction-wide + // check to true when *any* call is allowed to fail. If this is true + // then a call that is not allowed to revert (`call.allow_failure`) may + // still do so because of other calls that are in the same multicall + // aggregate. + if !success && !call.allow_failure { + return Err(MulticallError::IllegalRevert) + } + + Err(return_data) + } else { + let mut res_tokens = call.function.decode_output(return_data.as_ref())?; + Ok(if res_tokens.len() == 1 { + res_tokens.pop().unwrap() + } else { + Token::Tuple(res_tokens) + }) + }; + results.push(result); + } + Ok(results) } /// Signs and broadcasts a batch of transactions by using the Multicall contract as proxy, @@ -837,15 +870,15 @@ impl Multicall { MulticallVersion::Multicall2 => self.as_try_aggregate().tx, MulticallVersion::Multicall3 => self.as_aggregate_3_value().tx, }; - - self.contract - .client_ref() + 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))) } /// v1 + #[inline] fn as_aggregate(&self) -> ContractCall)> { // Map the calls vector into appropriate types for `aggregate` function let calls: Vec = self @@ -862,6 +895,7 @@ impl Multicall { } /// v2 + #[inline] fn as_try_aggregate(&self) -> ContractCall> { let mut allow_failure = false; // Map the calls vector into appropriate types for `try_aggregate` function @@ -885,6 +919,7 @@ impl Multicall { } /// v3 + #[inline] fn as_aggregate_3(&self) -> ContractCall> { // Map the calls vector into appropriate types for `aggregate_3` function let calls: Vec = self @@ -905,6 +940,7 @@ impl Multicall { } /// v3 + values (only .send()) + #[inline] fn as_aggregate_3_value(&self) -> ContractCall> { // Map the calls vector into appropriate types for `aggregate_3_value` function let mut total_value = U256::zero(); @@ -938,13 +974,23 @@ impl Multicall { /// Sets the block and legacy flags on a [ContractCall] if they were set on Multicall. fn set_call_flags(&self, mut call: ContractCall) -> ContractCall { if let Some(block) = self.block { - call = call.block(block); + call.block = Some(block.into()); } if self.legacy { - call = call.legacy(); + call.legacy() + } else { + call } - - call } } + +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/multicall/multicall_contract.rs b/ethers-contract/src/multicall/multicall_contract.rs deleted file mode 100644 index 82878124..00000000 --- a/ethers-contract/src/multicall/multicall_contract.rs +++ /dev/null @@ -1 +0,0 @@ -ethers_contract_derive::abigen!(Multicall3, "src/multicall/multicall_abi.json"); diff --git a/ethers-contract/tests/it/contract.rs b/ethers-contract/tests/it/contract.rs index e170dca9..e33c522e 100644 --- a/ethers-contract/tests/it/contract.rs +++ b/ethers-contract/tests/it/contract.rs @@ -7,9 +7,11 @@ use ethers_core::types::{Filter, ValueOrArray, H256}; #[cfg(not(feature = "celo"))] mod eth_tests { use super::*; - use ethers_contract::{ContractInstance, EthEvent, LogMeta, Multicall, MulticallVersion}; + use ethers_contract::{ + ContractInstance, EthEvent, LogMeta, Multicall, MulticallError, MulticallVersion, + }; use ethers_core::{ - abi::{encode, Detokenize, Token, Tokenizable}, + abi::{encode, AbiEncode, Detokenize, Token, Tokenizable}, types::{transaction::eip712::Eip712, Address, BlockId, Bytes, H160, I256, U256}, utils::{keccak256, Anvil}, }; @@ -635,21 +637,18 @@ mod eth_tests { .await .unwrap(); - // build up a list of calls greater than the 16 max restriction multicall.add_calls( false, std::iter::repeat(simple_contract.method::<_, String>("getValue", ()).unwrap()) - .take(17), // .collect(), + .take(17), ); - // must use `call_raw` as `.calls` > 16 let tokens = multicall.call_raw().await.unwrap(); - // if want to use, must detokenize manually let results: Vec = tokens - .iter() - .map(|token| { + .into_iter() + .map(|result| { // decode manually using Tokenizable method - String::from_token(token.to_owned()).unwrap() + String::from_token(result.unwrap()).unwrap() }) .collect(); assert_eq!(results, ["many"; 17]); @@ -685,11 +684,11 @@ mod eth_tests { .unwrap(); let get_value_call = reverting_contract .connect(client2.clone()) - .method::<_, String>("getValue", (false)) + .method::<_, String>("getValue", false) .unwrap(); let get_value_reverting_call = reverting_contract .connect(client.clone()) - .method::<_, String>("getValue", (true)) + .method::<_, String>("getValue", true) .unwrap(); // .send reverts @@ -723,36 +722,31 @@ mod eth_tests { .unwrap(); // .call reverts - // 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::<((bool, String), (bool, String))>().await; - let err = res.unwrap_err(); - assert!(err.to_string().contains("Multicall3: call failed")); + // 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(_) + )); - // allow revert - multicall - .clear_calls() - .add_call(get_value_reverting_call.clone(), true) - .add_call(get_value_call.clone(), false); - let res = multicall.call().await; - let data: ((bool, String), (bool, String)) = res.unwrap(); + // 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..], + expected[..] + ); - assert!(!data.0 .0); // first call reverted - assert_eq!(data.0 .1, "getValue revert"); // first call revert data - assert!(data.1 .0); // second call didn't revert - assert_eq!(data.1 .1, "reset third again"); // second call return data - - // test v2 illegal revert + // v2 illegal revert multicall .clear_calls() .add_call(get_value_reverting_call.clone(), false) // don't allow revert .add_call(get_value_call.clone(), true); // true here will result in `tryAggregate(false, ...)` - let res = multicall.call::<((bool, String), (bool, String))>().await; - let err = res.unwrap_err(); - assert!(err.to_string().contains("Illegal revert")); + assert!(matches!( + multicall.call::<(String, String)>().await.unwrap_err(), + MulticallError::IllegalRevert + )); // test version 3 // aggregate3 is the same as try_aggregate except with allowing failure on a per-call basis. @@ -764,71 +758,49 @@ mod eth_tests { let value_tx = reverting_contract.method::<_, H256>("deposit", ()).unwrap().value(amount); let rc_addr = reverting_contract.address(); - // add a second call because we can't decode using a single element tuple - // ((bool, U256)) == (bool, U256) - let bal_before: ((bool, U256), (bool, U256)) = multicall - .clear_calls() - .add_get_eth_balance(rc_addr, false) - .add_get_eth_balance(rc_addr, false) - .call() - .await - .unwrap(); + let (bal_before,): (U256,) = + multicall.clear_calls().add_get_eth_balance(rc_addr, false).call().await.unwrap(); // send 2 value_tx multicall.clear_calls().add_call(value_tx.clone(), false).add_call(value_tx.clone(), false); multicall.send().await.unwrap(); - let bal_after: ((bool, U256), (bool, U256)) = multicall - .clear_calls() - .add_get_eth_balance(rc_addr, false) - .add_get_eth_balance(rc_addr, false) - .call() - .await - .unwrap(); + let (bal_after,): (U256,) = + multicall.clear_calls().add_get_eth_balance(rc_addr, false).call().await.unwrap(); - assert_eq!(bal_after.0 .1, bal_before.0 .1 + U256::from(2) * amount); + assert_eq!(bal_after, bal_before + U256::from(2) * amount); // test specific revert cases // empty revert let empty_revert = reverting_contract.method::<_, H256>("emptyRevert", ()).unwrap(); - multicall - .clear_calls() - .add_call(empty_revert.clone(), true) - .add_call(empty_revert.clone(), true); - let res: ((bool, Bytes), (bool, Bytes)) = multicall.call().await.unwrap(); - assert!(!res.0 .0); - assert_eq!(res.0 .1, Bytes::default()); + multicall.clear_calls().add_call(empty_revert.clone(), true); + assert!(multicall.call::<(String,)>().await.unwrap_err().into_bytes().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).add_call(empty_revert.clone(), true); - let res: ((bool, String), (bool, Bytes)) = multicall.call().await.unwrap(); - assert!(!res.0 .0); - assert_eq!(res.0 .1, "String"); + multicall.clear_calls().add_call(string_revert, true); + assert_eq!( + multicall.call::<(String,)>().await.unwrap_err().into_bytes().unwrap()[4..], + Bytes::from_static(b"String").encode()[..] + ); // custom error revert let custom_error = reverting_contract.method::<_, H256>("customError", ()).unwrap(); - multicall.clear_calls().add_call(custom_error, true).add_call(empty_revert.clone(), true); - let res: ((bool, Bytes), (bool, Bytes)) = multicall.call().await.unwrap(); - let selector = &keccak256("CustomError()")[..4]; - assert!(!res.0 .0); - assert_eq!(res.0 .1.len(), 4); - assert_eq!(&res.0 .1[..4], selector); + multicall.clear_calls().add_call(custom_error, true); + assert_eq!( + multicall.call::<(Bytes,)>().await.unwrap_err().into_bytes().unwrap()[..], + keccak256("CustomError()")[..4] + ); // custom error with data revert let custom_error_with_data = reverting_contract .method::<_, H256>("customErrorWithData", ("Data".to_string())) .unwrap(); - multicall - .clear_calls() - .add_call(custom_error_with_data, true) - .add_call(empty_revert.clone(), true); - let res: ((bool, Bytes), (bool, Bytes)) = multicall.call().await.unwrap(); - let selector = &keccak256("CustomErrorWithData(string)")[..4]; - assert!(!res.0 .0); - assert_eq!(&res.0 .1[..4], selector); - assert_eq!(&res.0 .1[4..], encode(&[Token::String("Data".to_string())])); + multicall.clear_calls().add_call(custom_error_with_data, true); + let bytes = multicall.call::<(Bytes,)>().await.unwrap_err().into_bytes().unwrap(); + assert_eq!(bytes[..4], keccak256("CustomErrorWithData(string)")[..4]); + assert_eq!(bytes[4..], encode(&[Token::String("Data".to_string())])); } #[tokio::test] diff --git a/ethers-core/src/abi/tokens.rs b/ethers-core/src/abi/tokens.rs index 8839d14d..e6f11b88 100644 --- a/ethers-core/src/abi/tokens.rs +++ b/ethers-core/src/abi/tokens.rs @@ -1,11 +1,11 @@ //! Contract Functions Output types. -// Adapted from: [rust-web3](https://github.com/tomusdrw/rust-web3/blob/master/src/contract/tokens.rs) -#![allow(clippy::all)] +//! +//! Adapted from [rust-web3](https://github.com/tomusdrw/rust-web3/blob/master/src/contract/tokens.rs). + use crate::{ abi::Token, types::{Address, Bytes, H256, I256, U128, U256}, }; - use arrayvec::ArrayVec; use thiserror::Error; @@ -22,41 +22,38 @@ pub trait Detokenize { } impl Detokenize for () { - fn from_tokens(_: Vec) -> std::result::Result - where - Self: Sized, - { + fn from_tokens(_: Vec) -> std::result::Result { Ok(()) } } impl Detokenize for T { fn from_tokens(mut tokens: Vec) -> Result { - let token = match tokens.len() { - 0 => Token::Tuple(vec![]), - 1 => tokens.remove(0), - _ => Token::Tuple(tokens), - }; - + let token = if tokens.len() == 1 { tokens.pop().unwrap() } else { Token::Tuple(tokens) }; Self::from_token(token) } } -/// Tokens conversion trait +/// Convert types into [`Token`]s. pub trait Tokenize { - /// Convert to list of tokens + /// Converts `self` into a `Vec`. fn into_tokens(self) -> Vec; } impl<'a> Tokenize for &'a [Token] { fn into_tokens(self) -> Vec { - flatten_tokens(self.to_vec()) + let mut tokens = self.to_vec(); + if tokens.len() == 1 { + flatten_token(tokens.pop().unwrap()) + } else { + tokens + } } } impl Tokenize for T { fn into_tokens(self) -> Vec { - flatten_tokens(vec![self.into_token()]) + flatten_token(self.into_token()) } } @@ -72,13 +69,15 @@ pub trait Tokenizable { fn from_token(token: Token) -> Result where Self: Sized; + /// Converts a specified type back into token. fn into_token(self) -> Token; } macro_rules! impl_tuples { - ($num: expr, $( $ty: ident : $no: tt, )+) => { - impl<$($ty, )+> Tokenizable for ($($ty,)+) where + ($num:expr, $( $ty:ident : $no:tt ),+ $(,)?) => { + impl<$( $ty ),+> Tokenizable for ($( $ty, )+) + where $( $ty: Tokenizable, )+ @@ -88,11 +87,12 @@ macro_rules! impl_tuples { Token::Tuple(mut tokens) => { let mut it = tokens.drain(..); Ok(($( - $ty::from_token(it.next().expect("All elements are in vector; qed"))?, + $ty::from_token(it.next().expect("All elements are in vector; qed"))?, )+)) }, other => Err(InvalidOutputType(format!( - "Expected `Tuple`, got {:?}", + "Expected `Tuple` of length {}, got {:?}", + $num, other, ))), } @@ -133,6 +133,7 @@ impl Tokenizable for Token { fn from_token(token: Token) -> Result { Ok(token) } + fn into_token(self) -> Token { self } @@ -212,6 +213,18 @@ impl Tokenizable for Address { } } +impl Tokenizable for bool { + fn from_token(token: Token) -> Result { + match token { + Token::Bool(data) => Ok(data), + other => Err(InvalidOutputType(format!("Expected `bool`, got {:?}", other))), + } + } + fn into_token(self) -> Token { + Token::Bool(self) + } +} + macro_rules! eth_uint_tokenizable { ($uint: ident, $name: expr) => { impl Tokenizable for $uint { @@ -279,20 +292,99 @@ int_tokenizable!(u32, Uint); int_tokenizable!(u64, Uint); int_tokenizable!(u128, Uint); -impl Tokenizable for bool { +impl Tokenizable for Vec { fn from_token(token: Token) -> Result { match token { - Token::Bool(data) => Ok(data), - other => Err(InvalidOutputType(format!("Expected `bool`, got {:?}", other))), + 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))), } } + fn into_token(self) -> Token { - Token::Bool(self) + Token::Array(self.into_iter().map(Tokenizable::into_token).collect()) } } -/// Marker trait for `Tokenizable` types that are can tokenized to and from a -/// `Token::Array` and `Token:FixedArray`. +impl Tokenizable for Vec { + fn from_token(token: Token) -> Result { + match token { + Token::FixedArray(tokens) | Token::Array(tokens) => { + tokens.into_iter().map(Tokenizable::from_token).collect() + } + other => Err(InvalidOutputType(format!("Expected `Array`, got {:?}", other))), + } + } + + fn into_token(self) -> Token { + Token::Array(self.into_iter().map(Tokenizable::into_token).collect()) + } +} + +impl Tokenizable for [u8; N] { + fn from_token(token: Token) -> Result { + match token { + Token::FixedBytes(bytes) => { + if bytes.len() != N { + return Err(InvalidOutputType(format!( + "Expected `FixedBytes({})`, got FixedBytes({})", + N, + bytes.len() + ))) + } + + let mut arr = [0; N]; + arr.copy_from_slice(&bytes); + Ok(arr) + } + other => { + Err(InvalidOutputType(format!("Expected `FixedBytes({})`, got {:?}", N, other))) + } + } + } + + fn into_token(self) -> Token { + Token::FixedBytes(self.to_vec()) + } +} + +impl Tokenizable for [T; N] { + fn from_token(token: Token) -> Result { + match token { + Token::FixedArray(tokens) => { + if tokens.len() != N { + return Err(InvalidOutputType(format!( + "Expected `FixedArray({})`, got FixedArray({})", + N, + tokens.len() + ))) + } + + let mut arr = ArrayVec::::new(); + let mut it = tokens.into_iter().map(T::from_token); + for _ in 0..N { + arr.push(it.next().expect("Length validated in guard; qed")?); + } + // Can't use expect here because [T; N]: Debug is not satisfied. + match arr.into_inner() { + Ok(arr) => Ok(arr), + Err(_) => panic!("All elements inserted so the array is full; qed"), + } + } + other => { + Err(InvalidOutputType(format!("Expected `FixedArray({})`, got {:?}", N, other))) + } + } + } + + fn into_token(self) -> Token { + Token::FixedArray(ArrayVec::from(self).into_iter().map(T::into_token).collect()) + } +} + +/// Marker trait for `Tokenizable` types that are can tokenized to and from a `Token::Array` and +/// `Token:FixedArray`. pub trait TokenizableItem: Tokenizable {} macro_rules! tokenizable_item { @@ -308,9 +400,16 @@ tokenizable_item! { i8, i16, i32, i64, i128, u16, u32, u64, u128, Bytes, bytes::Bytes, } +impl TokenizableItem for Vec {} + +impl TokenizableItem for [u8; N] {} + +impl TokenizableItem for [T; N] {} + macro_rules! impl_tokenizable_item_tuple { - ($( $ty: ident , )+) => { - impl<$($ty, )+> TokenizableItem for ($($ty,)+) where + ($( $ty:ident ),+ $(,)?) => { + impl<$( $ty ),+> TokenizableItem for ($( $ty, )+) + where $( $ty: Tokenizable, )+ @@ -340,117 +439,15 @@ impl_tokenizable_item_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, impl_tokenizable_item_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T,); impl_tokenizable_item_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U,); -impl Tokenizable for Vec { - fn from_token(token: Token) -> Result { - match token { - 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))), - } - } - fn into_token(self) -> Token { - Token::Array(self.into_iter().map(Tokenizable::into_token).collect()) - } -} - -impl Tokenizable for Vec { - fn from_token(token: Token) -> Result { - match token { - Token::FixedArray(tokens) | Token::Array(tokens) => { - tokens.into_iter().map(Tokenizable::from_token).collect() - } - other => Err(InvalidOutputType(format!("Expected `Array`, got {:?}", other))), - } - } - - fn into_token(self) -> Token { - Token::Array(self.into_iter().map(Tokenizable::into_token).collect()) - } -} - -impl TokenizableItem for Vec {} - -impl Tokenizable for [u8; N] { - fn from_token(token: Token) -> Result { - match token { - Token::FixedBytes(bytes) => { - if bytes.len() != N { - return Err(InvalidOutputType(format!( - "Expected `FixedBytes({})`, got FixedBytes({})", - N, - bytes.len() - ))) - } - - let mut arr = [0; N]; - arr.copy_from_slice(&bytes); - Ok(arr) - } - other => { - Err(InvalidOutputType(format!("Expected `FixedBytes({})`, got {:?}", N, other)) - .into()) - } - } - } - - fn into_token(self) -> Token { - Token::FixedBytes(self.to_vec()) - } -} - -impl TokenizableItem for [u8; N] {} - -impl Tokenizable for [T; N] { - fn from_token(token: Token) -> Result { - match token { - Token::FixedArray(tokens) => { - if tokens.len() != N { - return Err(InvalidOutputType(format!( - "Expected `FixedArray({})`, got FixedArray({})", - N, - tokens.len() - ))) - } - - let mut arr = ArrayVec::::new(); - let mut it = tokens.into_iter().map(T::from_token); - for _ in 0..N { - arr.push(it.next().expect("Length validated in guard; qed")?); - } - // Can't use expect here because [T; N]: Debug is not satisfied. - match arr.into_inner() { - Ok(arr) => Ok(arr), - Err(_) => panic!("All elements inserted so the array is full; qed"), - } - } - other => { - Err(InvalidOutputType(format!("Expected `FixedArray({})`, got {:?}", N, other)) - .into()) - } - } - } - - fn into_token(self) -> Token { - Token::FixedArray(ArrayVec::from(self).into_iter().map(T::into_token).collect()) - } -} - -impl TokenizableItem for [T; N] {} - -/// Helper for flattening non-nested tokens into their inner -/// types, e.g. (A, B, C ) would get tokenized to Tuple([A, B, C]) -/// when in fact we need [A, B, C]. -fn flatten_tokens(mut tokens: Vec) -> Vec { - if tokens.len() == 1 { - // flatten the tokens if required - // and there is no nesting - match tokens.remove(0) { - Token::Tuple(inner) => inner, - other => vec![other], - } - } else { - tokens +/// Helper for flattening non-nested tokens into their inner types; +/// +/// e.g. `(A, B, C)` would get tokenized to `Tuple([A, B, C])` when in fact we need `[A, B, C]`. +#[inline] +fn flatten_token(token: Token) -> Vec { + // flatten the tokens if required and there is no nesting + match token { + Token::Tuple(inner) => inner, + token => vec![token], } } @@ -460,33 +457,33 @@ mod tests { use crate::types::{Address, U256}; use ethabi::Token; - fn output() -> R { + fn assert_detokenize() -> T { unimplemented!() } #[test] #[ignore] fn should_be_able_to_compile() { - let _tokens: Vec = output(); - let _uint: U256 = output(); - let _address: Address = output(); - let _string: String = output(); - let _bool: bool = output(); - let _bytes: Vec = output(); + let _tokens: Vec = assert_detokenize(); + let _uint: U256 = assert_detokenize(); + let _address: Address = assert_detokenize(); + let _string: String = assert_detokenize(); + let _bool: bool = assert_detokenize(); + let _bytes: Vec = assert_detokenize(); - let _pair: (U256, bool) = output(); - let _vec: Vec = output(); - let _array: [U256; 4] = output(); - let _bytes: Vec<[[u8; 1]; 64]> = output(); + let _pair: (U256, bool) = assert_detokenize(); + let _vec: Vec = assert_detokenize(); + let _array: [U256; 4] = assert_detokenize(); + let _bytes: Vec<[[u8; 1]; 64]> = assert_detokenize(); - let _mixed: (Vec>, [U256; 4], Vec, U256) = output(); + let _mixed: (Vec>, [U256; 4], Vec, U256) = assert_detokenize(); - let _ints: (i16, i32, i64, i128) = output(); - let _uints: (u16, u32, u64, u128) = output(); + let _ints: (i16, i32, i64, i128) = assert_detokenize(); + let _uints: (u16, u32, u64, u128) = assert_detokenize(); - let _tuple: (Address, Vec>) = output(); - let _vec_of_tuple: Vec<(Address, String)> = output(); - let _vec_of_tuple_5: Vec<(Address, Vec>, String, U256, bool)> = output(); + let _tuple: (Address, Vec>) = assert_detokenize(); + let _vec_of_tuple: Vec<(Address, String)> = assert_detokenize(); + let _vec_of_tuple_5: Vec<(Address, Vec>, String, U256, bool)> = assert_detokenize(); } #[test]