From a9bb98b5a787143398f40f3eb9759ba9ea7007bb Mon Sep 17 00:00:00 2001 From: Rohit Narurkar Date: Fri, 3 Jul 2020 22:22:09 +0530 Subject: [PATCH] Implement Multicall functionality for batched calls (#43) * Implement Multicall functionality for batched calls * Documentation, some modifications as suggested in the review * (Abigen) handle single input arg and set output irrespective of mutability * implement send functionality and allow clearing calls * Fix detokenization, dont require pre-processing anymore * panic when more than supported number of calls are pushed * add doc for panics in case of add_call * (multicall) eth_balance support, update bindings * refactor: move multicall to its own directory * fix: add infura api key * ci: ensure CI runs on PRs from forks * test(multicall): re-use aggregate call * contract: make multicall docs compile and remove redundant clones * ci: add public etherscan API key so that forks don't get rate limited * chore: adjust test contract naming Co-authored-by: Georgios Konstantopoulos --- .github/workflows/ci.yml | 8 +- ethers-contract/Cargo.toml | 1 + .../src/contract/methods.rs | 69 +++- ethers-contract/src/lib.rs | 3 + ethers-contract/src/multicall/mod.rs | 379 ++++++++++++++++++ .../src/multicall/multicall_contract.rs | 96 +++++ ethers-contract/tests/common/mod.rs | 12 +- ethers-contract/tests/contract.rs | 159 +++++++- .../tests/solidity-contracts/Multicall.sol | 45 +++ .../solidity-contracts/NotSoSimpleStorage.sol | 28 ++ .../SimpleStorage.sol} | 0 ethers-core/src/abi/tokens.rs | 37 ++ 12 files changed, 810 insertions(+), 27 deletions(-) create mode 100644 ethers-contract/src/multicall/mod.rs create mode 100644 ethers-contract/src/multicall/multicall_contract.rs create mode 100644 ethers-contract/tests/solidity-contracts/Multicall.sol create mode 100644 ethers-contract/tests/solidity-contracts/NotSoSimpleStorage.sol rename ethers-contract/tests/{contract.sol => solidity-contracts/SimpleStorage.sol} (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49128872..3e6a2ab3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,10 +1,12 @@ -on: push +on: pull_request name: Tests -# set for fetching ABIs for abigen from etherscan +# Yeah I know it's bad practice to have API keys, this is a read-only API key +# so that we do not get rate limited by Etherscan (and it's free to generate as +# many as you want) env: - ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }} + ETHERSCAN_API_KEY: 76XKCZ4QKZYTJS8PBFUDZ292JBKEKS4974 jobs: tests: diff --git a/ethers-contract/Cargo.toml b/ethers-contract/Cargo.toml index 81df360b..4d2552a5 100644 --- a/ethers-contract/Cargo.toml +++ b/ethers-contract/Cargo.toml @@ -18,6 +18,7 @@ ethers-signers = { version = "0.1.3", path = "../ethers-signers" } ethers-core = { version = "0.1.3", path = "../ethers-core" } serde = { version = "1.0.110", default-features = false } +serde_json = "1.0.55" rustc-hex = { version = "2.1.0", default-features = false } thiserror = { version = "1.0.15", default-features = false } once_cell = { version = "1.3.1", default-features = false } diff --git a/ethers-contract/ethers-contract-abigen/src/contract/methods.rs b/ethers-contract/ethers-contract-abigen/src/contract/methods.rs index bf9c4df0..1f166a03 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract/methods.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract/methods.rs @@ -40,15 +40,7 @@ fn expand_function(function: &Function, alias: Option) -> Result } - } else { - quote! { ContractCall } - }; + let result = quote! { ContractCall }; let arg = expand_inputs_call_arg(&function.inputs); let doc = util::expand_doc(&format!( @@ -85,8 +77,13 @@ pub(crate) fn expand_inputs_call_arg(inputs: &[Param]) -> TokenStream { let names = inputs .iter() .enumerate() - .map(|(i, param)| util::expand_input_name(i, ¶m.name)); - quote! { ( #( #names ,)* ) } + .map(|(i, param)| util::expand_input_name(i, ¶m.name)) + .collect::>(); + match names.len() { + 0 => quote! { () }, + 1 => quote! { #( #names )* }, + _ => quote! { ( #(#names, )* ) }, + } } fn expand_fn_outputs(outputs: &[Param]) -> Result { @@ -113,6 +110,54 @@ mod tests { use super::*; use ethers_core::abi::ParamType; + #[test] + fn test_expand_inputs_call_arg() { + // no inputs + let params = vec![]; + let token_stream = expand_inputs_call_arg(¶ms); + assert_eq!(token_stream.to_string(), "( )"); + + // single input + let params = vec![Param { + name: "arg_a".to_string(), + kind: ParamType::Address, + }]; + let token_stream = expand_inputs_call_arg(¶ms); + assert_eq!(token_stream.to_string(), "arg_a"); + + // two inputs + let params = vec![ + Param { + name: "arg_a".to_string(), + kind: ParamType::Address, + }, + Param { + name: "arg_b".to_string(), + kind: ParamType::Uint(256usize), + }, + ]; + let token_stream = expand_inputs_call_arg(¶ms); + assert_eq!(token_stream.to_string(), "( arg_a , arg_b , )"); + + // three inputs + let params = vec![ + Param { + name: "arg_a".to_string(), + kind: ParamType::Address, + }, + Param { + name: "arg_b".to_string(), + kind: ParamType::Uint(128usize), + }, + Param { + name: "arg_c".to_string(), + kind: ParamType::Bool, + }, + ]; + let token_stream = expand_inputs_call_arg(¶ms); + assert_eq!(token_stream.to_string(), "( arg_a , arg_b , arg_c , )"); + } + #[test] fn expand_inputs_empty() { assert_quote!(expand_inputs(&[]).unwrap().to_string(), {},); @@ -157,7 +202,7 @@ mod tests { } #[test] - fn expand_fn_outputs_muliple() { + fn expand_fn_outputs_multiple() { assert_quote!( expand_fn_outputs(&[ Param { diff --git a/ethers-contract/src/lib.rs b/ethers-contract/src/lib.rs index fb0679df..0aedc737 100644 --- a/ethers-contract/src/lib.rs +++ b/ethers-contract/src/lib.rs @@ -24,6 +24,9 @@ pub use factory::ContractFactory; mod event; +mod multicall; +pub use multicall::Multicall; + /// This module exposes low lever builder structures which are only consumed by the /// type-safe ABI bindings generators. pub mod builders { diff --git a/ethers-contract/src/multicall/mod.rs b/ethers-contract/src/multicall/mod.rs new file mode 100644 index 00000000..975bf331 --- /dev/null +++ b/ethers-contract/src/multicall/mod.rs @@ -0,0 +1,379 @@ +use ethers_core::{ + abi::{Detokenize, Function, Token}, + types::{Address, BlockNumber, NameOrAddress, TxHash, U256}, +}; +use ethers_providers::JsonRpcClient; +use ethers_signers::{Client, Signer}; + +use std::{collections::HashMap, str::FromStr, sync::Arc}; + +use crate::{ + call::{ContractCall, ContractError}, + Lazy, +}; + +mod multicall_contract; +use multicall_contract::MulticallContract; + +/// A lazily computed hash map with the Ethereum network IDs as keys and the corresponding +/// Multicall smart contract addresses as values +pub static ADDRESS_BOOK: Lazy> = Lazy::new(|| { + let mut m = HashMap::new(); + + // mainnet + let addr = + Address::from_str("eefba1e63905ef1d7acba5a8513c70307c1ce441").expect("Decoding failed"); + m.insert(U256::from(1u8), addr); + + // rinkeby + let addr = + Address::from_str("42ad527de7d4e9d9d011ac45b31d8551f8fe9821").expect("Decoding failed"); + m.insert(U256::from(4u8), addr); + + // goerli + let addr = + Address::from_str("77dca2c955b15e9de4dbbcf1246b4b85b651e50e").expect("Decoding failed"); + m.insert(U256::from(5u8), addr); + + // kovan + let addr = + Address::from_str("2cc8688c5f75e365aaeeb4ea8d6a480405a48d2a").expect("Decoding failed"); + m.insert(U256::from(42u8), addr); + + m +}); + +/// 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/0xeefba1e63905ef1d7acba5a8513c70307c1ce441#code) +/// and the user provided list of transactions to be made. +/// +/// `Multicall` can instantiate the Multicall contract instance from the chain ID of the client +/// supplied to [`new`]. It supports the Ethereum mainnet, as well as testnets +/// [Rinkeby](https://rinkeby.etherscan.io/address/0x42ad527de7d4e9d9d011ac45b31d8551f8fe9821#code), +/// [Goerli](https://goerli.etherscan.io/address/0x77dca2c955b15e9de4dbbcf1246b4b85b651e50e) and +/// [Kovan](https://kovan.etherscan.io/address/0x2cc8688c5f75e365aaeeb4ea8d6a480405a48d2a#code). +/// +/// Additionally, the `block` number can be provided for the call by using the [`block`] method. +/// Build on the `Multicall` instance by adding calls using the [`add_call`] method. +/// +/// # Example +/// +/// ```no_run +/// use ethers::{ +/// abi::Abi, +/// contract::{Contract, Multicall}, +/// providers::{Http, Provider}, +/// signers::{Client, Wallet}, +/// types::{Address, H256, U256}, +/// }; +/// use std::{convert::TryFrom, sync::Arc}; +/// +/// # async fn bar() -> Result<(), Box> { +/// // this is a dummy address used for illustration purpose +/// let address = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse::
()?; +/// +/// // (ugly way to write the ABI inline, you can otherwise read it from a file) +/// 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 provider = Provider::::try_from("https://kovan.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27")?; +/// let client = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc" +/// .parse::()?.connect(provider); +/// +/// // create the contract object. This will be used to construct the calls for multicall +/// let contract = Contract::new(address, abi, client.clone()); +/// +/// // note that these [`ContractCall`]s are futures, and need to be `.await`ed to resolve. +/// // But we will let `Multicall` to take care of that for us +/// 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 +/// // 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 multicall = Multicall::new(client.clone(), None) +/// .await? +/// .add_call(first_call) +/// .add_call(second_call); +/// +/// // `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?; +/// +/// // 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. +/// let first_broadcast = contract.method::<_, H256>("setValue", "some value".to_owned())?; +/// let second_broadcast = contract.method::<_, H256>("setValue", "new value".to_owned())?; +/// let multicall = multicall +/// .clear_calls() +/// .add_call(first_broadcast) +/// .add_call(second_broadcast); +/// +/// // `await`ing the `send` method waits for the transaction to be broadcast, which also +/// // returns the transaction hash +/// let tx_hash = multicall.send().await?; +/// let _tx_receipt = client.provider().pending_transaction(tx_hash).await?; +/// +/// // you can also query ETH balances of multiple addresses +/// let address_1 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".parse::
()?; +/// let address_2 = "ffffffffffffffffffffffffffffffffffffffff".parse::
()?; +/// let multicall = multicall +/// .clear_calls() +/// .eth_balance_of(address_1) +/// .eth_balance_of(address_2); +/// let _balances: (U256, U256) = multicall.call().await?; +/// # Ok(()) +/// # } +/// ``` +/// +/// [`new`]: method@crate::Multicall::new +/// [`block`]: method@crate::Multicall::block +/// [`add_call`]: methond@crate::Multicall::add_call +#[derive(Clone)] +pub struct Multicall { + calls: Vec, + block: Option, + contract: MulticallContract, +} + +#[derive(Clone)] +/// Helper struct for managing calls to be made to the `function` in smart contract `target` +/// with `data` +pub struct Call { + target: Address, + data: Vec, + function: Function, +} + +impl Multicall +where + P: JsonRpcClient, + S: Signer, +{ + /// Creates a new Multicall instance from the provided client. If provided with an `address`, + /// it instantiates the Multicall contract with that address. Otherwise it fetches the address + /// from the address book. + /// + /// # Panics + /// If a `None` address is provided, and the provided client also does not belong to one of + /// the supported network IDs (mainnet, kovan, rinkeby and goerli) + pub async fn new>>>( + client: C, + address: Option
, + ) -> Result { + let client = client.into(); + + // Fetch chain id and the corresponding address of Multicall contract + // preference is given to Multicall contract's address if provided + // otherwise check the address book for the client's chain ID + let address: Address = match address { + Some(addr) => addr, + None => { + let chain_id = client.get_chainid().await?; + match ADDRESS_BOOK.get(&chain_id) { + Some(addr) => *addr, + None => panic!( + "Must either be a supported Network ID or provide Multicall contract address" + ), + } + } + }; + + // Instantiate the multicall contract + let contract = MulticallContract::new(address, client); + + Ok(Self { + calls: vec![], + block: None, + contract, + }) + } + + /// Sets the `block` field for the multicall aggregate call + pub fn block>(mut self, block: T) -> Self { + self.block = Some(block.into()); + self + } + + /// Appends a `call` to the list of calls for the Multicall instance + /// + /// # Panics + /// + /// If more than the maximum number of supported calls are added. The maximum + /// limits is constrained due to tokenization/detokenization support for tuples + pub fn add_call(mut self, call: ContractCall) -> Self { + if self.calls.len() >= 16 { + panic!("Cannot support more than {} calls", 16); + } + + match (call.tx.to, call.tx.data) { + (Some(NameOrAddress::Address(target)), Some(data)) => { + let call = Call { + target, + data: data.0, + function: call.function, + }; + self.calls.push(call); + self + } + _ => self, + } + } + + /// Appends a `call` to the list of calls for the Multicall instance for querying + /// the ETH balance of an address + /// + /// # Panics + /// + /// If more than the maximum number of supported calls are added. The maximum + /// limits is constrained due to tokenization/detokenization support for tuples + pub fn eth_balance_of(self, addr: Address) -> Self { + let call = self.contract.get_eth_balance(addr); + self.add_call(call) + } + + /// Clear the batch of calls from the Multicall instance. Re-use the already instantiated + /// Multicall, to send a different batch of transactions or do another aggregate query + /// + /// ```no_run + /// # async fn foo() -> Result<(), Box> { + /// # use ethers::prelude::*; + /// # use std::{sync::Arc, convert::TryFrom}; + /// # + /// # let provider = Provider::::try_from("http://localhost:8545")?; + /// # let client = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc" + /// # .parse::()?.connect(provider); + /// # let client = Arc::new(client); + /// # + /// # let abi = serde_json::from_str("")?; + /// # let address = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse::
()?; + /// # let contract = Contract::new(address, abi, client.clone()); + /// # + /// # let broadcast_1 = contract.method::<_, H256>("setValue", "some value".to_owned())?; + /// # let broadcast_2 = contract.method::<_, H256>("setValue", "new value".to_owned())?; + /// # + /// let multicall = Multicall::new(client, None) + /// .await? + /// .add_call(broadcast_1) + /// .add_call(broadcast_2); + /// + /// let _tx_hash = multicall.send().await?; + /// + /// # let call_1 = contract.method::<_, String>("getValue", ())?; + /// # let call_2 = contract.method::<_, Address>("lastSender", ())?; + /// let multicall = multicall + /// .clear_calls() + /// .add_call(call_1) + /// .add_call(call_2); + /// let return_data: (String, Address) = multicall.call().await?; + /// # Ok(()) + /// # } + /// ``` + pub fn clear_calls(mut self) -> Self { + self.calls.clear(); + self + } + + /// Queries the Ethereum blockchain via an `eth_call`, but via the Multicall contract. + /// + /// It returns a [`ContractError`] if there is any error in the RPC call or while + /// detokenizing the tokens back to the expected return type. The return type must be + /// annonated while calling this method. + /// + /// ```no_run + /// # async fn foo() -> Result<(), Box> { + /// # use ethers::prelude::*; + /// # use std::convert::TryFrom; + /// # + /// # let provider = Provider::::try_from("http://localhost:8545")?; + /// # let client = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc" + /// # .parse::()?.connect(provider); + /// # + /// # let multicall = Multicall::new(client, None).await?; + /// // If the Solidity function calls has the following return types: + /// // 1. `returns (uint256)` + /// // 2. `returns (string, address)` + /// // 3. `returns (bool)` + /// let result: (U256, (String, Address), bool) = multicall.call().await?; + /// # Ok(()) + /// # } + /// ``` + /// + /// Note: this method _does not_ send a transaction from your account + /// + /// [`ContractError`]: crate::ContractError + pub async fn call(&self) -> Result { + let contract_call = self.as_contract_call(); + + // Fetch response from the Multicall contract + let (_block_number, return_data) = contract_call.call().await?; + + // Decode return data into ABI tokens + let tokens = self + .calls + .iter() + .zip(&return_data) + .map(|(call, bytes)| { + let tokens: Vec = call.function.decode_output(&bytes)?; + + Ok(match tokens.len() { + 0 => Token::Tuple(vec![]), + 1 => tokens[0].clone(), + _ => Token::Tuple(tokens), + }) + }) + .collect::, ContractError>>()?; + + // Form tokens that represent tuples + let tokens = vec![Token::Tuple(tokens)]; + + // Detokenize from the tokens into the provided tuple D + let data = D::from_tokens(tokens)?; + + Ok(data) + } + + /// Signs and broadcasts a batch of transactions by using the Multicall contract as proxy. + /// + /// ```no_run + /// # async fn foo() -> Result<(), Box> { + /// # use ethers::prelude::*; + /// # use std::convert::TryFrom; + /// # let provider = Provider::::try_from("http://localhost:8545")?; + /// # let client = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc" + /// # .parse::()?.connect(provider); + /// # let multicall = Multicall::new(client, None).await?; + /// let tx_hash = multicall.send().await?; + /// # Ok(()) + /// # } + /// ``` + /// + /// Note: this method sends a transaction from your account, and will return an error + /// if you do not have sufficient funds to pay for gas + pub async fn send(&self) -> Result { + let contract_call = self.as_contract_call(); + + // Broadcast transaction and return the transaction hash + let tx_hash = contract_call.send().await?; + + Ok(tx_hash) + } + + fn as_contract_call(&self) -> ContractCall>)> { + // Map the Multicall struct into appropriate types for `aggregate` function + let calls: Vec<(Address, Vec)> = self + .calls + .iter() + .map(|call| (call.target, call.data.clone())) + .collect(); + + // Construct the ContractCall for `aggregate` function to broadcast the transaction + let contract_call = self.contract.aggregate(calls); + if let Some(block) = self.block { + contract_call.block(block) + } else { + contract_call + } + } +} diff --git a/ethers-contract/src/multicall/multicall_contract.rs b/ethers-contract/src/multicall/multicall_contract.rs new file mode 100644 index 00000000..6280c688 --- /dev/null +++ b/ethers-contract/src/multicall/multicall_contract.rs @@ -0,0 +1,96 @@ +pub use multicallcontract_mod::*; +mod multicallcontract_mod { + #![allow(dead_code)] + #![allow(unused_imports)] + use crate::{ + builders::{ContractCall, Event}, + Contract, Lazy, + }; + use ethers_core::{ + abi::{Abi, Detokenize, InvalidOutputType, Token, Tokenizable}, + types::*, + }; + use ethers_providers::JsonRpcClient; + use ethers_signers::{Client, Signer}; + #[doc = "MulticallContract was auto-generated with ethers-rs Abigen. More information at: https://github.com/gakonst/ethers-rs"] + use std::sync::Arc; + pub static MULTICALLCONTRACT_ABI: Lazy = Lazy::new(|| { + serde_json :: from_str ( "[{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"callData\",\"type\":\"bytes\"}],\"internalType\":\"struct MulticallContract.Call[]\",\"name\":\"calls\",\"type\":\"tuple[]\"}],\"name\":\"aggregate\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"},{\"internalType\":\"bytes[]\",\"name\":\"returnData\",\"type\":\"bytes[]\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"}],\"name\":\"getBlockHash\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"blockHash\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getCurrentBlockCoinbase\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"coinbase\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getCurrentBlockDifficulty\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"difficulty\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getCurrentBlockGasLimit\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"gaslimit\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getCurrentBlockTimestamp\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"timestamp\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"addr\",\"type\":\"address\"}],\"name\":\"getEthBalance\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"balance\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getLastBlockHash\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"blockHash\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]" ) . expect ( "invalid abi" ) + }); + #[derive(Clone)] + pub struct MulticallContract(Contract); + impl std::ops::Deref for MulticallContract { + type Target = Contract; + fn deref(&self) -> &Self::Target { + &self.0 + } + } + impl std::fmt::Debug for MulticallContract { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.debug_tuple(stringify!(MulticallContract)) + .field(&self.address()) + .finish() + } + } + impl<'a, P: JsonRpcClient, S: Signer> MulticallContract { + #[doc = r" Creates a new contract instance with the specified `ethers`"] + #[doc = r" client at the given `Address`. The contract derefs to a `ethers::Contract`"] + #[doc = r" object"] + pub fn new, C: Into>>>(address: T, client: C) -> Self { + let contract = + Contract::new(address.into(), MULTICALLCONTRACT_ABI.clone(), client.into()); + Self(contract) + } + #[doc = "Calls the contract's `aggregate` (0x252dba42) function"] + pub fn aggregate( + &self, + calls: Vec<(Address, Vec)>, + ) -> ContractCall>)> { + self.0 + .method_hash([37, 45, 186, 66], calls) + .expect("method not found (this should never happen)") + } + #[doc = "Calls the contract's `getCurrentBlockDifficulty` (0x72425d9d) function"] + pub fn get_current_block_difficulty(&self) -> ContractCall { + self.0 + .method_hash([114, 66, 93, 157], ()) + .expect("method not found (this should never happen)") + } + #[doc = "Calls the contract's `getCurrentBlockGasLimit` (0x86d516e8) function"] + pub fn get_current_block_gas_limit(&self) -> ContractCall { + self.0 + .method_hash([134, 213, 22, 232], ()) + .expect("method not found (this should never happen)") + } + #[doc = "Calls the contract's `getCurrentBlockTimestamp` (0x0f28c97d) function"] + pub fn get_current_block_timestamp(&self) -> ContractCall { + self.0 + .method_hash([15, 40, 201, 125], ()) + .expect("method not found (this should never happen)") + } + #[doc = "Calls the contract's `getCurrentBlockCoinbase` (0xa8b0574e) function"] + pub fn get_current_block_coinbase(&self) -> ContractCall { + self.0 + .method_hash([168, 176, 87, 78], ()) + .expect("method not found (this should never happen)") + } + #[doc = "Calls the contract's `getBlockHash` (0xee82ac5e) function"] + pub fn get_block_hash(&self, block_number: U256) -> ContractCall { + self.0 + .method_hash([238, 130, 172, 94], block_number) + .expect("method not found (this should never happen)") + } + #[doc = "Calls the contract's `getEthBalance` (0x4d2301cc) function"] + pub fn get_eth_balance(&self, addr: Address) -> ContractCall { + self.0 + .method_hash([77, 35, 1, 204], addr) + .expect("method not found (this should never happen)") + } + #[doc = "Calls the contract's `getLastBlockHash` (0x27e86d6e) function"] + pub fn get_last_block_hash(&self) -> ContractCall { + self.0 + .method_hash([39, 232, 109, 110], ()) + .expect("method not found (this should never happen)") + } + } +} diff --git a/ethers-contract/tests/common/mod.rs b/ethers-contract/tests/common/mod.rs index e62afb9d..c22802b0 100644 --- a/ethers-contract/tests/common/mod.rs +++ b/ethers-contract/tests/common/mod.rs @@ -34,12 +34,12 @@ impl Detokenize for ValueChanged { } } -/// compiles the test contract -pub fn compile() -> (Abi, Bytes) { - let compiled = Solc::new("./tests/contract.sol").build().unwrap(); - let contract = compiled - .get("SimpleStorage") - .expect("could not find contract"); +/// compiles the given contract and returns the ABI and Bytecode +pub fn compile_contract(name: &str, filename: &str) -> (Abi, Bytes) { + let compiled = Solc::new(&format!("./tests/solidity-contracts/{}", filename)) + .build() + .unwrap(); + let contract = compiled.get(name).expect("could not find contract"); (contract.abi.clone(), contract.bytecode.clone()) } diff --git a/ethers-contract/tests/contract.rs b/ethers-contract/tests/contract.rs index f3586076..05a29382 100644 --- a/ethers-contract/tests/contract.rs +++ b/ethers-contract/tests/contract.rs @@ -7,16 +7,17 @@ pub use common::*; mod eth_tests { use super::*; use ethers::{ + contract::Multicall, providers::{Http, Provider, StreamExt}, signers::Client, - types::Address, + types::{Address, U256}, utils::Ganache, }; use std::{convert::TryFrom, sync::Arc}; #[tokio::test] async fn deploy_and_call_contract() { - let (abi, bytecode) = compile(); + let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol"); // launch ganache let ganache = Ganache::new().spawn(); @@ -85,7 +86,7 @@ mod eth_tests { #[tokio::test] async fn get_past_events() { - let (abi, bytecode) = compile(); + let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol"); let ganache = Ganache::new().spawn(); let client = connect(&ganache, 0); let contract = deploy(client.clone(), abi, bytecode).await; @@ -114,7 +115,7 @@ mod eth_tests { #[tokio::test] async fn watch_events() { - let (abi, bytecode) = compile(); + let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol"); let ganache = Ganache::new().spawn(); let client = connect(&ganache, 0); let contract = deploy(client, abi, bytecode).await; @@ -149,7 +150,7 @@ mod eth_tests { #[tokio::test] async fn signer_on_node() { - let (abi, bytecode) = compile(); + let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol"); // spawn ganache let ganache = Ganache::new().spawn(); @@ -180,6 +181,152 @@ mod eth_tests { .unwrap(); assert_eq!(value, "hi"); } + + #[tokio::test] + async fn multicall_aggregate() { + // get ABI and bytecode for the Multcall contract + let (multicall_abi, multicall_bytecode) = compile_contract("Multicall", "Multicall.sol"); + + // get ABI and bytecode for the NotSoSimpleStorage contract + let (not_so_simple_abi, not_so_simple_bytecode) = + compile_contract("NotSoSimpleStorage", "NotSoSimpleStorage.sol"); + + // get ABI and bytecode for the SimpleStorage contract + let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol"); + + // launch ganache + let ganache = Ganache::new().spawn(); + + // Instantiate the clients. We assume that clients consume the provider and the wallet + // (which makes sense), so for multi-client tests, you must clone the provider. + // `client` is used to deploy the Multicall contract + // `client2` is used to deploy the first SimpleStorage contract + // `client3` is used to deploy the second SimpleStorage contract + // `client4` is used to make the aggregate call + let client = connect(&ganache, 0); + let client2 = connect(&ganache, 1); + let client3 = connect(&ganache, 2); + let client4 = connect(&ganache, 3); + + // create a factory which will be used to deploy instances of the contract + let multicall_factory = + ContractFactory::new(multicall_abi, multicall_bytecode, client.clone()); + let simple_factory = ContractFactory::new(abi.clone(), bytecode.clone(), client2.clone()); + let not_so_simple_factory = + ContractFactory::new(not_so_simple_abi, not_so_simple_bytecode, client3.clone()); + + let multicall_contract = multicall_factory.deploy(()).unwrap().send().await.unwrap(); + let addr = multicall_contract.address(); + + let simple_contract = simple_factory + .deploy("the first one".to_string()) + .unwrap() + .send() + .await + .unwrap(); + let not_so_simple_contract = not_so_simple_factory + .deploy("the second one".to_string()) + .unwrap() + .send() + .await + .unwrap(); + + // Client2 and Client3 broadcast txs to set the values for both contracts + simple_contract + .connect(client2.clone()) + .method::<_, H256>("setValue", "reset first".to_owned()) + .unwrap() + .send() + .await + .unwrap(); + not_so_simple_contract + .connect(client3.clone()) + .method::<_, H256>("setValue", "reset second".to_owned()) + .unwrap() + .send() + .await + .unwrap(); + + // get the calls for `value` and `last_sender` for both SimpleStorage contracts + let value = simple_contract.method::<_, String>("getValue", ()).unwrap(); + let value2 = not_so_simple_contract + .method::<_, (String, Address)>("getValues", ()) + .unwrap(); + let last_sender = simple_contract + .method::<_, Address>("lastSender", ()) + .unwrap(); + let last_sender2 = not_so_simple_contract + .method::<_, Address>("lastSender", ()) + .unwrap(); + + // initiate the Multicall instance and add calls one by one in builder style + let multicall = Multicall::new(client4.clone(), Some(addr)) + .await + .unwrap() + .add_call(value) + .add_call(value2) + .add_call(last_sender) + .add_call(last_sender2); + + let return_data: (String, (String, Address), Address, Address) = + multicall.call().await.unwrap(); + + assert_eq!(return_data.0, "reset first"); + assert_eq!((return_data.1).0, "reset second"); + assert_eq!((return_data.1).1, client3.address()); + assert_eq!(return_data.2, client2.address()); + assert_eq!(return_data.3, client3.address()); + + // construct broadcast transactions that will be batched and broadcast via Multicall + let broadcast = simple_contract + .connect(client4.clone()) + .method::<_, H256>("setValue", "first reset again".to_owned()) + .unwrap(); + let broadcast2 = not_so_simple_contract + .connect(client4.clone()) + .method::<_, H256>("setValue", "second reset again".to_owned()) + .unwrap(); + + // use the already initialised Multicall instance, clearing the previous calls and adding + // new calls. Previously we used the `.call()` functionality to do a batch of calls in one + // go. Now we will use the `.send()` functionality to broadcast a batch of transactions + // in one go + let multicall_send = multicall + .clone() + .clear_calls() + .add_call(broadcast) + .add_call(broadcast2); + + // broadcast the transaction and wait for it to be mined + let tx_hash = multicall_send.send().await.unwrap(); + let _tx_receipt = client4.pending_transaction(tx_hash).await.unwrap(); + + // Do another multicall to check the updated return values + // The `getValue` calls should return the last value we set in the batched broadcast + // The `lastSender` calls should return the address of the Multicall contract, as it is + // the one acting as proxy and calling our SimpleStorage contracts (msg.sender) + let return_data: (String, (String, Address), Address, Address) = + multicall.call().await.unwrap(); + + assert_eq!(return_data.0, "first reset again"); + assert_eq!((return_data.1).0, "second reset again"); + assert_eq!((return_data.1).1, multicall_contract.address()); + assert_eq!(return_data.2, multicall_contract.address()); + assert_eq!(return_data.3, multicall_contract.address()); + + // query ETH balances of multiple addresses + // these keys haven't been used to do any tx + // so should have 100 ETH + let multicall = multicall + .clear_calls() + .eth_balance_of(Address::from(&ganache.keys()[4])) + .eth_balance_of(Address::from(&ganache.keys()[5])) + .eth_balance_of(Address::from(&ganache.keys()[6])); + let balances: (U256, U256, U256) = multicall.call().await.unwrap(); + assert_eq!(balances.0, U256::from(100000000000000000000u128)); + assert_eq!(balances.1, U256::from(100000000000000000000u128)); + assert_eq!(balances.2, U256::from(100000000000000000000u128)); + } } #[cfg(feature = "celo")] @@ -194,7 +341,7 @@ mod celo_tests { #[tokio::test] async fn deploy_and_call_contract() { - let (abi, bytecode) = compile(); + let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol"); // Celo testnet let provider = diff --git a/ethers-contract/tests/solidity-contracts/Multicall.sol b/ethers-contract/tests/solidity-contracts/Multicall.sol new file mode 100644 index 00000000..7e6cd973 --- /dev/null +++ b/ethers-contract/tests/solidity-contracts/Multicall.sol @@ -0,0 +1,45 @@ +pragma solidity >=0.5.0; +pragma experimental ABIEncoderV2; + +/// @title Multicall - Aggregate results from multiple read-only function calls +/// @author Michael Elliot +/// @author Joshua Levine +/// @author Nick Johnson + +contract Multicall { + struct Call { + address target; + bytes callData; + } + function aggregate(Call[] memory calls) public returns (uint256 blockNumber, bytes[] memory returnData) { + blockNumber = block.number; + returnData = new bytes[](calls.length); + for(uint256 i = 0; i < calls.length; i++) { + (bool success, bytes memory ret) = calls[i].target.call(calls[i].callData); + require(success); + returnData[i] = ret; + } + } + // Helper functions + function getEthBalance(address addr) public view returns (uint256 balance) { + balance = addr.balance; + } + function getBlockHash(uint256 blockNumber) public view returns (bytes32 blockHash) { + blockHash = blockhash(blockNumber); + } + function getLastBlockHash() public view returns (bytes32 blockHash) { + blockHash = blockhash(block.number - 1); + } + function getCurrentBlockTimestamp() public view returns (uint256 timestamp) { + timestamp = block.timestamp; + } + function getCurrentBlockDifficulty() public view returns (uint256 difficulty) { + difficulty = block.difficulty; + } + function getCurrentBlockGasLimit() public view returns (uint256 gaslimit) { + gaslimit = block.gaslimit; + } + function getCurrentBlockCoinbase() public view returns (address coinbase) { + coinbase = block.coinbase; + } +} diff --git a/ethers-contract/tests/solidity-contracts/NotSoSimpleStorage.sol b/ethers-contract/tests/solidity-contracts/NotSoSimpleStorage.sol new file mode 100644 index 00000000..2e85a0d2 --- /dev/null +++ b/ethers-contract/tests/solidity-contracts/NotSoSimpleStorage.sol @@ -0,0 +1,28 @@ +pragma solidity >=0.4.24; + +contract NotSoSimpleStorage { + + event ValueChanged(address indexed author, address indexed oldAuthor, string oldValue, string newValue); + + address public lastSender; + string _value; + + constructor(string memory value) public { + emit ValueChanged(msg.sender, address(0), _value, value); + _value = value; + } + + function getValue() view public returns (string memory) { + return _value; + } + + function getValues() view public returns (string memory, address) { + return (_value, lastSender); + } + + function setValue(string memory value) public { + emit ValueChanged(msg.sender, lastSender, _value, value); + _value = value; + lastSender = msg.sender; + } +} diff --git a/ethers-contract/tests/contract.sol b/ethers-contract/tests/solidity-contracts/SimpleStorage.sol similarity index 100% rename from ethers-contract/tests/contract.sol rename to ethers-contract/tests/solidity-contracts/SimpleStorage.sol diff --git a/ethers-core/src/abi/tokens.rs b/ethers-core/src/abi/tokens.rs index 8a994dea..4c69ce75 100644 --- a/ethers-core/src/abi/tokens.rs +++ b/ethers-core/src/abi/tokens.rs @@ -566,4 +566,41 @@ mod tests { assert_eq!((-4i64).into_token(), Token::Int(U256::MAX - 3)); assert_eq!((-5i128).into_token(), Token::Int(U256::MAX - 4)); } + + #[test] + fn should_detokenize() { + // handle tuple of one element + let tokens = vec![Token::FixedBytes(vec![1, 2, 3, 4]), Token::Bool(true)]; + let tokens = vec![Token::Tuple(tokens)]; + let data: ([u8; 4], bool) = Detokenize::from_tokens(tokens).unwrap(); + assert_eq!(data.0[0], 1); + assert_eq!(data.0[1], 2); + assert_eq!(data.0[2], 3); + assert_eq!(data.0[3], 4); + assert_eq!(data.1, true); + + // 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_eq!(data.1, 13u8); + + // handle more than two tuples + let tokens1 = vec![Token::FixedBytes(vec![1, 2, 3, 4]), Token::Bool(true)]; + let tokens2 = vec![Token::Bool(false), Token::Uint(U256::from(13u8))]; + let tokens = vec![Token::Tuple(tokens1), Token::Tuple(tokens2)]; + let data: (([u8; 4], bool), (bool, u8)) = Detokenize::from_tokens(tokens).unwrap(); + assert_eq!((data.0).0[0], 1); + 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_eq!((data.1).1, 13u8); + + // error if no tokens in the vector + let tokens = vec![]; + let data: Result = Detokenize::from_tokens(tokens); + assert!(data.is_err()); + } }