From a532eb4d29c963f5b7baa404c3a821e26561ed94 Mon Sep 17 00:00:00 2001 From: Gio <102917377+gio256@users.noreply.github.com> Date: Sat, 4 Jun 2022 15:18:25 -0600 Subject: [PATCH] Add call builder and eth_call state overrides (#1340) * Add call builder and eth_call state overrides * Fix ::poll impl * Move call overrides to ethers_provider::call_raw * Add example for call_raw overrides * Add support for contract call overrides * Documentation and convenience impls for call_raw types * Test for eth_call state overrides * ci: install geth Co-authored-by: Georgios Konstantopoulos --- .github/workflows/ci.yml | 10 + ethers-contract/src/call.rs | 38 +- ethers-contract/src/factory.rs | 26 +- ethers-providers/src/call_raw.rs | 620 +++++++++++++++++++++++++++++++ ethers-providers/src/lib.rs | 1 + ethers-providers/src/provider.rs | 41 ++ examples/call_override.rs | 70 ++++ 7 files changed, 801 insertions(+), 5 deletions(-) create mode 100644 ethers-providers/src/call_raw.rs create mode 100644 examples/call_override.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 829f3edc..65c0c23e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -177,6 +177,16 @@ jobs: - name: Checkout sources uses: actions/checkout@v2 + - name: Install geth (for state overrides example) + run: | + mkdir -p "$HOME/bin" + wget -q https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.9.23-8c2f2715.tar.gz + tar -xvf geth-linux-amd64-1.9.23-8c2f2715.tar.gz + mv geth-linux-amd64-1.9.23-8c2f2715/geth $HOME/bin/geth + chmod u+x "$HOME/bin/geth" + export PATH=$HOME/bin:$PATH + geth version + - name: Install Anvil uses: foundry-rs/foundry-toolchain@v1 with: diff --git a/ethers-contract/src/call.rs b/ethers-contract/src/call.rs index c8eb7e1c..8dcc4fba 100644 --- a/ethers-contract/src/call.rs +++ b/ethers-contract/src/call.rs @@ -9,9 +9,12 @@ use ethers_core::{ }, utils::id, }; -use ethers_providers::{Middleware, PendingTransaction, ProviderError}; +use ethers_providers::{ + call_raw::{CallBuilder, RawCall}, + Middleware, PendingTransaction, ProviderError, +}; -use std::{borrow::Cow, fmt::Debug, marker::PhantomData, sync::Arc}; +use std::{borrow::Cow, fmt::Debug, future::Future, marker::PhantomData, sync::Arc}; use thiserror::Error as ThisError; @@ -169,6 +172,37 @@ where Ok(data) } + /// Returns an implementer of [`RawCall`] which can be `.await`d to query the blockchain via + /// `eth_call`, returning the deoded return data. + /// + /// The returned call can also be used to override the input parameters to `eth_call`. + /// + /// Note: this function _does not_ send a transaction from your account + pub fn call_raw( + &self, + ) -> impl RawCall<'_> + Future>> + Debug { + let call = self.call_raw_bytes(); + call.map(move |res: Result| { + let bytes = res.map_err(ContractError::ProviderError)?; + decode_function_data(&self.function, &bytes, false).map_err(From::from) + }) + } + + /// Returns a [`CallBuilder`] which can be `.await`d to query the blochcain via `eth_call`, + /// returning the raw bytes from the transaction. + /// + /// The returned call can also be used to override the input parameters to `eth_call`. + /// + /// Note: this function _does not_ send a transaction from your account + pub fn call_raw_bytes(&self) -> CallBuilder<'_, M::Provider> { + let call = self.client.provider().call_raw(&self.tx); + if let Some(block) = self.block { + call.block(block) + } else { + call + } + } + /// Signs and broadcasts the provided transaction pub async fn send(&self) -> Result, ContractError> { self.client diff --git a/ethers-contract/src/factory.rs b/ethers-contract/src/factory.rs index cb493e0b..7f51e8d3 100644 --- a/ethers-contract/src/factory.rs +++ b/ethers-contract/src/factory.rs @@ -1,5 +1,4 @@ use crate::{Contract, ContractError}; -use std::marker::PhantomData; use ethers_core::{ abi::{Abi, Token, Tokenize}, @@ -8,12 +7,15 @@ use ethers_core::{ TransactionReceipt, TransactionRequest, U256, U64, }, }; -use ethers_providers::Middleware; +use ethers_providers::{ + call_raw::{CallBuilder, RawCall}, + Middleware, +}; #[cfg(not(feature = "legacy"))] use ethers_core::types::Eip1559TransactionRequest; -use std::sync::Arc; +use std::{marker::PhantomData, sync::Arc}; /// Helper which manages the deployment transaction of a smart contract. /// @@ -114,6 +116,15 @@ impl>> ContractDeployer { self.deployer.call().await } + /// Returns a CallBuilder, which when awaited executes the deployment of this contract via + /// `eth_call`. This call resolves to the returned data which would have been stored at the + /// destination address had the deploy transaction been executed via `send()`. + /// + /// Note: this function _does not_ send a transaction from your account + pub fn call_raw(&self) -> CallBuilder<'_, M::Provider> { + self.deployer.call_raw() + } + /// Broadcasts the contract deployment transaction and after waiting for it to /// be sufficiently confirmed (default: 1), it returns a new instance of the contract type at /// the deployed contract's address. @@ -203,6 +214,15 @@ impl Deployer { Ok(()) } + /// Returns a CallBuilder, which when awaited executes the deployment of this contract via + /// `eth_call`. This call resolves to the returned data which would have been stored at the + /// destination address had the deploy transaction been executed via `send()`. + /// + /// Note: this function _does not_ send a transaction from your account + pub fn call_raw(&self) -> CallBuilder<'_, M::Provider> { + self.client.provider().call_raw(&self.tx).block(self.block.into()) + } + /// Broadcasts the contract deployment transaction and after waiting for it to /// be sufficiently confirmed (default: 1), it returns a [`Contract`](crate::Contract) /// struct at the deployed contract's address. diff --git a/ethers-providers/src/call_raw.rs b/ethers-providers/src/call_raw.rs new file mode 100644 index 00000000..e0b95bad --- /dev/null +++ b/ethers-providers/src/call_raw.rs @@ -0,0 +1,620 @@ +//! Overrides for the `eth_call` rpc method + +use crate::{JsonRpcClient, PinBoxFut, Provider, ProviderError}; +use ethers_core::{ + types::{ + transaction::eip2718::TypedTransaction, Address, BlockId, BlockNumber, Bytes, H256, U256, + U64, + }, + utils, +}; +use pin_project::pin_project; +use serde::{ser::SerializeTuple, Deserialize, Serialize}; +use std::{ + fmt, + future::Future, + pin::Pin, + task::{Context, Poll}, +}; + +pub use spoof::{balance, code, nonce, state, storage}; + +/// Provides methods for overriding parameters to the `eth_call` rpc method +pub trait RawCall<'a> { + /// Sets the block number to execute against + fn block(self, id: BlockId) -> Self; + /// Sets the [state override set](https://geth.ethereum.org/docs/rpc/ns-eth#3-object---state-override-set). + /// Note that not all client implementations will support this as a parameter. + fn state(self, state: &'a spoof::State) -> Self; + + /// Maps a closure `f` over the result of `.await`ing this call + fn map(self, f: F) -> Map + where + Self: Sized, + { + Map::new(self, f) + } +} + +/// A builder which implements [`RawCall`] methods for overriding `eth_call` parameters. +/// +/// `CallBuilder` also implements [`std::future::Future`], so `.await`ing a `CallBuilder` will +/// resolve to the result of executing the `eth_call`. +#[must_use = "call_raw::CallBuilder does nothing unless you `.await` or poll it"] +pub enum CallBuilder<'a, P> { + /// The primary builder which exposes [`RawCall`] methods. + Build(Caller<'a, P>), + /// Used by the [`std::future::Future`] implementation. You are unlikely to encounter this + /// variant unless you are constructing your own [`RawCall`] wrapper type. + Wait(PinBoxFut<'a, Bytes>), +} + +impl fmt::Debug for CallBuilder<'_, P> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Build(call) => f.debug_tuple("Build").field(call).finish(), + Self::Wait(_) => f.debug_tuple("Wait").field(&"< Future >").finish(), + } + } +} + +impl<'a, P> CallBuilder<'a, P> { + pub fn new(provider: &'a Provider

, tx: &'a TypedTransaction) -> Self { + Self::Build(Caller::new(provider, tx)) + } + + /// Applies a closure `f` to a `CallBuilder::Build`. Does nothing for `CallBuilder::Wait`. + pub fn map_input(self, f: F) -> Self + where + F: FnOnce(&mut Caller<'a, P>), + { + match self { + Self::Build(mut call) => { + f(&mut call); + Self::Build(call) + } + wait => wait, + } + } + + /// Returns the inner `Caller` from a `CallBuilder::Build`. Panics if the `CallBuilder` future + /// has already been polled. + pub fn unwrap(self) -> Caller<'a, P> { + match self { + Self::Build(b) => b, + _ => panic!("CallBuilder::unwrap on a Wait value"), + } + } +} + +impl<'a, P> RawCall<'a> for CallBuilder<'a, P> { + /// Sets the block number to execute against + fn block(self, id: BlockId) -> Self { + self.map_input(|mut call| call.input.block = Some(id)) + } + /// Sets the [state override set](https://geth.ethereum.org/docs/rpc/ns-eth#3-object---state-override-set). + /// Note that not all client implementations will support this as a parameter. + fn state(self, state: &'a spoof::State) -> Self { + self.map_input(|mut call| call.input.state = Some(state)) + } +} + +impl<'a, P: JsonRpcClient> Future for CallBuilder<'a, P> { + type Output = Result; + + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { + let pin = self.get_mut(); + loop { + match pin { + CallBuilder::Build(ref call) => { + let fut = Box::pin(call.execute()); + *pin = CallBuilder::Wait(fut); + } + CallBuilder::Wait(ref mut fut) => return fut.as_mut().poll(cx), + } + } + } +} + +/// Holds the inputs to the `eth_call` rpc method along with the rpc provider. +/// This type is constructed by [`CallBuilder::new`]. +#[derive(Clone, Debug)] +pub struct Caller<'a, P> { + provider: &'a Provider

, + input: CallInput<'a>, +} + +impl<'a, P> Caller<'a, P> { + pub fn new(provider: &'a Provider

, tx: &'a TypedTransaction) -> Self { + Self { provider, input: CallInput::new(tx) } + } +} +impl<'a, P: JsonRpcClient> Caller<'a, P> { + /// Executes an `eth_call` rpc request with the overriden parameters. Returns a future that + /// resolves to the result of the request. + fn execute(&self) -> impl Future> + 'a { + self.provider.request("eth_call", utils::serialize(&self.input)) + } +} + +/// The input parameters to the `eth_call` rpc method +#[derive(Clone, Debug, PartialEq, Eq)] +struct CallInput<'a> { + tx: &'a TypedTransaction, + block: Option, + state: Option<&'a spoof::State>, +} + +impl<'a> CallInput<'a> { + fn new(tx: &'a TypedTransaction) -> Self { + Self { tx, block: None, state: None } + } +} + +impl<'a> Serialize for CallInput<'a> { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::ser::Serializer, + { + let len = 2 + self.state.is_some() as usize; + + let mut tup = serializer.serialize_tuple(len)?; + tup.serialize_element(self.tx)?; + + let block = self.block.unwrap_or_else(|| BlockNumber::Latest.into()); + tup.serialize_element(&block)?; + + if let Some(state) = self.state { + tup.serialize_element(state)?; + } + tup.end() + } +} + +/// An implementer of [`RawCall`] that maps a function `f` over the output of the inner future. +/// +/// This struct is created by the [`map`] method on [`RawCall`]. +/// +/// [`map`]: RawCall::map +#[must_use = "call_raw::Map does nothing unless you `.await` or poll it"] +#[derive(Clone)] +#[pin_project] +pub struct Map { + #[pin] + inner: T, + f: F, +} + +impl fmt::Debug for Map { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Map").field("inner", &self.inner).finish() + } +} + +impl Map { + pub fn new(inner: T, f: F) -> Self { + Self { inner, f } + } +} + +impl<'a, T, F> RawCall<'a> for Map +where + T: RawCall<'a>, +{ + /// Sets the block number to execute against + fn block(self, id: BlockId) -> Self { + Self { inner: self.inner.block(id), f: self.f } + } + + /// Sets the [state override set](https://geth.ethereum.org/docs/rpc/ns-eth#3-object---state-override-set). + /// Note that not all client implementations will support this as a parameter. + fn state(self, state: &'a spoof::State) -> Self { + Self { inner: self.inner.state(state), f: self.f } + } +} + +impl Future for Map +where + T: Future, + F: FnMut(T::Output) -> Y, +{ + type Output = Y; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let pin = self.project(); + let x = futures_util::ready!(pin.inner.poll(cx)); + Poll::Ready((pin.f)(x)) + } +} + +/// Provides types and methods for constructing an `eth_call` +/// [state override set](https://geth.ethereum.org/docs/rpc/ns-eth#3-object---state-override-set) +pub mod spoof { + use super::*; + use std::collections::HashMap; + + /// The state elements to override for a particular account. + #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] + pub struct Account { + #[serde(skip_serializing_if = "Option::is_none")] + pub nonce: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub balance: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub code: Option, + #[serde(flatten, skip_serializing_if = "Option::is_none")] + pub storage: Option, + } + + impl Account { + /// Override the account nonce + pub fn nonce(&mut self, nonce: U64) -> &mut Self { + self.nonce = Some(nonce); + self + } + /// Override the account balance + pub fn balance(&mut self, bal: U256) -> &mut Self { + self.balance = Some(bal); + self + } + /// Override the code at the account + pub fn code(&mut self, code: Bytes) -> &mut Self { + self.code = Some(code); + self + } + /// Override the value of the account storage at the given storage `key` + pub fn store(&mut self, key: H256, val: H256) -> &mut Self { + self.storage.get_or_insert_with(Default::default).insert(key, val); + self + } + } + + /// Wraps a map from storage slot to the overriden value. + /// + /// Storage overrides can either replace the existing state of an account or they can be treated + /// as a diff on the existing state. + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] + pub enum Storage { + #[serde(rename = "stateDiff")] + Diff(HashMap), + #[serde(rename = "state")] + Replace(HashMap), + } + + /// The default storage override is a diff on the existing state of the account. + impl Default for Storage { + fn default() -> Self { + Self::Diff(Default::default()) + } + } + impl std::ops::Deref for Storage { + type Target = HashMap; + fn deref(&self) -> &Self::Target { + match self { + Self::Diff(map) => map, + Self::Replace(map) => map, + } + } + } + impl std::ops::DerefMut for Storage { + fn deref_mut(&mut self) -> &mut Self::Target { + match self { + Self::Diff(map) => map, + Self::Replace(map) => map, + } + } + } + + /// A wrapper type that holds a complete state override set. + #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] + #[serde(transparent)] + pub struct State(#[serde(skip_serializing_if = "HashMap::is_empty")] HashMap); + + impl State { + /// Returns a mutable reference to the [`Account`] in the map. + pub fn account(&mut self, adr: Address) -> &mut Account { + self.0.entry(adr).or_default() + } + } + + /// Returns an empty state override set. + /// + /// # Example + /// ```no_run + /// # use ethers_core::{ + /// # types::{Address, TransactionRequest, H256}, + /// # utils::{parse_ether, Geth}, + /// # }; + /// # use ethers_providers::{Provider, Http, Middleware, call_raw::{spoof, RawCall}}; + /// # use std::convert::TryFrom; + /// # + /// # #[tokio::main(flavor = "current_thread")] + /// # async fn main() -> Result<(), Box> { + /// let geth = Geth::new().spawn(); + /// let provider = Provider::::try_from(geth.endpoint()).unwrap(); + /// + /// let adr1: Address = "0x6fC21092DA55B392b045eD78F4732bff3C580e2c".parse().unwrap(); + /// let adr2: Address = "0x295a70b2de5e3953354a6a8344e616ed314d7251".parse().unwrap(); + /// let key = H256::from_low_u64_be(1); + /// let val = H256::from_low_u64_be(17); + /// + /// let tx = TransactionRequest::default().to(adr2).from(adr1).into(); + /// + /// // override the storage at `adr2` + /// let mut state = spoof::state(); + /// state.account(adr2).store(key, val); + /// + /// // override the nonce at `adr1` + /// state.account(adr1).nonce(2.into()); + /// + /// provider.call_raw(&tx).state(&state).await.unwrap(); + /// # Ok(()) + /// # } + /// ``` + pub fn state() -> State { + Default::default() + } + + /// Returns a state override set with a single element setting the balance of the address. + /// + /// # Example + /// ```no_run + /// # use ethers_core::{ + /// # types::{Address, TransactionRequest, H256}, + /// # utils::{parse_ether, Geth}, + /// # }; + /// # use ethers_providers::{Provider, Http, Middleware, call_raw::{RawCall, spoof}}; + /// # use std::convert::TryFrom; + /// # + /// # #[tokio::main(flavor = "current_thread")] + /// # async fn main() -> Result<(), Box> { + /// let geth = Geth::new().spawn(); + /// let provider = Provider::::try_from(geth.endpoint()).unwrap(); + /// + /// let adr1: Address = "0x6fC21092DA55B392b045eD78F4732bff3C580e2c".parse()?; + /// let adr2: Address = "0x295a70b2de5e3953354a6a8344e616ed314d7251".parse()?; + /// let pay_amt = parse_ether(1u64)?; + /// + /// // Not enough ether to pay for the transaction + /// let tx = TransactionRequest::pay(adr2, pay_amt).from(adr1).into(); + /// + /// // override the sender's balance for the call + /// let state = spoof::balance(adr1, pay_amt * 2); + /// provider.call_raw(&tx).state(&state).await?; + /// # Ok(()) + /// # } + /// ``` + pub fn balance(adr: Address, bal: U256) -> State { + let mut state = State::default(); + state.account(adr).balance(bal); + state + } + + /// Returns a state override set with a single element setting the nonce of the address. + /// + /// # Example + /// ```no_run + /// # use ethers_core::{ + /// # types::{Address, TransactionRequest, H256}, + /// # utils::{parse_ether, Geth}, + /// # }; + /// # use ethers_providers::{Provider, Http, Middleware, call_raw::{RawCall, spoof}}; + /// # use std::convert::TryFrom; + /// # + /// # #[tokio::main(flavor = "current_thread")] + /// # async fn main() -> Result<(), Box> { + /// let geth = Geth::new().spawn(); + /// let provider = Provider::::try_from(geth.endpoint()).unwrap(); + /// + /// let adr: Address = "0x6fC21092DA55B392b045eD78F4732bff3C580e2c".parse()?; + /// let pay_amt = parse_ether(1u64)?; + /// + /// let tx = TransactionRequest::default().from(adr).into(); + /// + /// // override the sender's nonce for the call + /// let state = spoof::nonce(adr, 72.into()); + /// provider.call_raw(&tx).state(&state).await?; + /// # Ok(()) + /// # } + /// ``` + pub fn nonce(adr: Address, nonce: U64) -> State { + let mut state = State::default(); + state.account(adr).nonce(nonce); + state + } + + /// Returns a state override set with a single element setting the code at the address. + /// + /// # Example + /// ```no_run + /// # use ethers_core::{ + /// # types::{Address, TransactionRequest, H256}, + /// # utils::{parse_ether, Geth}, + /// # }; + /// # use ethers_providers::{Provider, Http, Middleware, call_raw::{RawCall, spoof}}; + /// # use std::convert::TryFrom; + /// # + /// # #[tokio::main(flavor = "current_thread")] + /// # async fn main() -> Result<(), Box> { + /// let geth = Geth::new().spawn(); + /// let provider = Provider::::try_from(geth.endpoint()).unwrap(); + /// + /// let adr: Address = "0x6fC21092DA55B392b045eD78F4732bff3C580e2c".parse()?; + /// let pay_amt = parse_ether(1u64)?; + /// + /// let tx = TransactionRequest::default().to(adr).into(); + /// + /// // override the code at the target address + /// let state = spoof::code(adr, "0x00".parse()?); + /// provider.call_raw(&tx).state(&state).await?; + /// # Ok(()) + /// # } + /// ``` + pub fn code(adr: Address, code: Bytes) -> State { + let mut state = State::default(); + state.account(adr).code(code); + state + } + + /// Returns a state override set with a single element setting the storage at the given address + /// and key. + /// + /// # Example + /// ```no_run + /// # use ethers_core::{ + /// # types::{Address, TransactionRequest, H256}, + /// # utils::{parse_ether, Geth}, + /// # }; + /// # use ethers_providers::{Provider, Http, Middleware, call_raw::{RawCall, spoof}}; + /// # use std::convert::TryFrom; + /// # + /// # #[tokio::main(flavor = "current_thread")] + /// # async fn main() -> Result<(), Box> { + /// let geth = Geth::new().spawn(); + /// let provider = Provider::::try_from(geth.endpoint()).unwrap(); + /// + /// let adr: Address = "0x6fC21092DA55B392b045eD78F4732bff3C580e2c".parse()?; + /// let key = H256::from_low_u64_be(1); + /// let val = H256::from_low_u64_be(17); + /// + /// let tx = TransactionRequest::default().to(adr).into(); + /// + /// // override the storage slot `key` at `adr` + /// let state = spoof::storage(adr, key, val); + /// provider.call_raw(&tx).state(&state).await?; + /// # Ok(()) + /// # } + /// ``` + pub fn storage(adr: Address, key: H256, val: H256) -> State { + let mut state = State::default(); + state.account(adr).store(key, val); + state + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Http, Middleware, Provider}; + use ethers_core::{ + types::TransactionRequest, + utils::{get_contract_address, keccak256, parse_ether, Anvil, Geth}, + }; + use std::convert::TryFrom; + + // Deserializes eth_call parameters as owned data for testing serialization + #[derive(Debug, Deserialize)] + struct CallInputOwned( + TypedTransaction, + Option, + #[serde(default)] Option, + ); + impl<'a> From<&'a CallInputOwned> for CallInput<'a> { + fn from(src: &'a CallInputOwned) -> Self { + Self { tx: &src.0, block: src.1, state: src.2.as_ref() } + } + } + + // Tests "roundtrip" serialization of calls: deserialize(serialize(call)) == call + fn test_encode<'a, P>(call: CallBuilder<'a, P>) { + let input = call.unwrap().input; + let ser = utils::serialize(&input).to_string(); + let de: CallInputOwned = serde_json::from_str(&ser).unwrap(); + let de = CallInput::from(&de); + + assert_eq!(input.tx, de.tx); + assert_eq!(input.state, de.state); + + let block = input.block.or_else(|| Some(BlockNumber::Latest.into())); + assert_eq!(block, de.block); + } + + #[test] + fn test_serialize() { + let adr1: Address = "0x6fC21092DA55B392b045eD78F4732bff3C580e2c".parse().unwrap(); + let adr2: Address = "0x295a70b2de5e3953354a6a8344e616ed314d7251".parse().unwrap(); + let k1 = utils::keccak256("foo").into(); + let v1 = H256::from_low_u64_be(534); + let k2 = utils::keccak256("bar").into(); + let v2 = H256::from_low_u64_be(8675309); + + let tx = TypedTransaction::default(); + let (provider, _) = Provider::mocked(); + + let call = provider.call_raw(&tx); + test_encode(call); + + let mut state = spoof::state(); + state.account(adr1).nonce(1.into()).balance(2.into()).store(k1, v1).store(k2, v2); + let call = provider.call_raw(&tx).block(100.into()).state(&state); + test_encode(call); + + let mut state = spoof::state(); + state.account(adr1).nonce(1.into()); + state.account(adr2).nonce(7.into()); + let call = provider.call_raw(&tx).state(&state); + test_encode(call); + + // State override with an empty acccount should be encoded as "0xab..": {} + let mut state = spoof::state(); + state.account(adr1); + let call = provider.call_raw(&tx).state(&state); + test_encode(call); + } + + #[tokio::test] + async fn test_state_overrides() { + let geth = Geth::new().spawn(); + let provider = Provider::::try_from(geth.endpoint()).unwrap(); + + let adr1: Address = "0x6fC21092DA55B392b045eD78F4732bff3C580e2c".parse().unwrap(); + let adr2: Address = "0x295a70b2de5e3953354a6a8344e616ed314d7251".parse().unwrap(); + let pay_amt = parse_ether(1u64).unwrap(); + + // Not enough ether to pay for the transaction + let tx = TransactionRequest::pay(adr2, pay_amt).from(adr1).into(); + + // assert that overriding the sender's balance works + let state = spoof::balance(adr1, pay_amt * 2); + provider.call_raw(&tx).state(&state).await.expect("eth_call success"); + + // bytecode that returns the result of the SELFBALANCE opcode + const RETURN_BALANCE: &str = "0x4760005260206000f3"; + let bytecode = RETURN_BALANCE.parse().unwrap(); + let balance = 100.into(); + + let tx = TransactionRequest::default().to(adr2).into(); + let mut state = spoof::state(); + state.account(adr2).code(bytecode).balance(balance); + + // assert that overriding the code and balance at adr2 works + let bytes = provider.call_raw(&tx).state(&state).await.unwrap(); + assert_eq!(U256::from_big_endian(bytes.as_ref()), balance); + + // bytecode that deploys a contract and returns the deployed address + const DEPLOY_CONTRACT: &str = "0x6000600052602060006000f060005260206000f3"; + let bytecode = DEPLOY_CONTRACT.parse().unwrap(); + let nonce = 17.into(); + + let mut state = spoof::state(); + state.account(adr2).code(bytecode).nonce(nonce); + + // assert that overriding nonce works (contract is deployed to expected address) + let bytes = provider.call_raw(&tx).state(&state).await.unwrap(); + let deployed = Address::from_slice(&bytes.as_ref()[12..]); + assert_eq!(deployed, get_contract_address(adr2, nonce.as_u64())); + + // bytecode that returns the value of storage slot 1 + const RETURN_STORAGE: &str = "0x60015460005260206000f3"; + let bytecode = RETURN_STORAGE.parse().unwrap(); + let slot = H256::from_low_u64_be(1); + let val = keccak256("foo").into(); + + let mut state = spoof::state(); + state.account(adr2).code(bytecode).store(slot, val); + + // assert that overriding storage works + let bytes = provider.call_raw(&tx).state(&state).await.unwrap(); + assert_eq!(H256::from_slice(bytes.as_ref()), val); + } +} diff --git a/ethers-providers/src/lib.rs b/ethers-providers/src/lib.rs index f82a91f8..7743df89 100644 --- a/ethers-providers/src/lib.rs +++ b/ethers-providers/src/lib.rs @@ -28,6 +28,7 @@ pub use stream::{interval, FilterWatcher, TransactionStream, DEFAULT_POLL_INTERV mod pubsub; pub use pubsub::{PubsubClient, SubscriptionStream}; +pub mod call_raw; pub mod erc; use async_trait::async_trait; diff --git a/ethers-providers/src/provider.rs b/ethers-providers/src/provider.rs index 687ff607..08ed88cb 100644 --- a/ethers-providers/src/provider.rs +++ b/ethers-providers/src/provider.rs @@ -1,4 +1,5 @@ use crate::{ + call_raw::CallBuilder, ens, erc, maybe, pubsub::{PubsubClient, SubscriptionStream}, stream::{FilterWatcher, DEFAULT_POLL_INTERVAL}, @@ -230,6 +231,46 @@ impl Provider

{ } }) } + + /// Analogous to [`Middleware::call`], but returns a [`CallBuilder`] that can either be + /// `.await`d or used to override the parameters sent to `eth_call`. + /// + /// See the [`call_raw::spoof`] for functions to construct state override parameters. + /// + /// Note: this method _does not_ send a transaction from your account + /// + /// [`call_raw::spoof`]: crate::call_raw::spoof + /// + /// # Example + /// ```no_run + /// # use ethers_core::{ + /// # types::{Address, TransactionRequest, H256}, + /// # utils::{parse_ether, Geth}, + /// # }; + /// # use ethers_providers::{Provider, Http, Middleware, call_raw::{RawCall, spoof}}; + /// # use std::convert::TryFrom; + /// # + /// # #[tokio::main(flavor = "current_thread")] + /// # async fn main() -> Result<(), Box> { + /// let geth = Geth::new().spawn(); + /// let provider = Provider::::try_from(geth.endpoint()).unwrap(); + /// + /// let adr1: Address = "0x6fC21092DA55B392b045eD78F4732bff3C580e2c".parse()?; + /// let adr2: Address = "0x295a70b2de5e3953354a6a8344e616ed314d7251".parse()?; + /// let pay_amt = parse_ether(1u64)?; + /// + /// // Not enough ether to pay for the transaction + /// let tx = TransactionRequest::pay(adr2, pay_amt).from(adr1).into(); + /// + /// // override the sender's balance for the call + /// let mut state = spoof::balance(adr1, pay_amt * 2); + /// provider.call_raw(&tx).state(&state).await?; + /// # Ok(()) + /// # } + /// ``` + pub fn call_raw<'a>(&'a self, tx: &'a TypedTransaction) -> CallBuilder<'a, P> { + CallBuilder::new(self, tx) + } } #[cfg(feature = "celo")] diff --git a/examples/call_override.rs b/examples/call_override.rs new file mode 100644 index 00000000..2237ee1e --- /dev/null +++ b/examples/call_override.rs @@ -0,0 +1,70 @@ +use ethers::{ + prelude::*, + providers::call_raw::RawCall, + utils::{parse_ether, Geth}, +}; +use std::sync::Arc; + +abigen!(Greeter, "ethers-contract/tests/solidity-contracts/greeter.json",); + +#[tokio::main] +async fn main() -> eyre::Result<()> { + let geth = Geth::new().spawn(); + let provider = Provider::::try_from(geth.endpoint()).unwrap(); + let client = Arc::new(provider); + + // Both empty accounts + let target: Address = "0x6fC21092DA55B392b045eD78F4732bff3C580e2c".parse()?; + let from: Address = "0x295a70b2de5e3953354a6a8344e616ed314d7251".parse()?; + + // Override the sender's balance for the call + let pay_amt = parse_ether(1u64)?; + let tx = TransactionRequest::pay(target, pay_amt).from(from); + let state = call_raw::balance(from, pay_amt * 2); + + // The call succeeds as if the sender had sufficient balance + client.call_raw(&tx.into()).state(&state).await.expect("balance override"); + + // Get the runtime bytecode for the Greeter contract using eth_call to + // simulate the deploy transaction + let deploy = Greeter::deploy(client.clone(), "Hi".to_string())?; + let runtime_bytecode = deploy.call_raw().await?; + + // Instantiate a Greeter, though no bytecode exists at the target address + let greeter = Greeter::new(target, client.clone()); + + // Override the target account's code, simulating a call to Greeter.greet() + // as if the Greeter contract was deployed at the target address + let state = call_raw::code(target, runtime_bytecode.clone()); + let res = greeter.greet().call_raw().state(&state).await?; + + // greet() returns the empty string, because the target account's storage is empty + assert_eq!(&res, ""); + + // Encode the greeting string as solidity expects it to be stored + let greeting = "Hello, world"; + let greet_slot = H256::zero(); + let greet_val = encode_string_for_storage(greeting); + + // Override the target account's code and storage + let mut state = call_raw::state(); + state.account(target).code(runtime_bytecode.clone()).store(greet_slot, greet_val); + let res = greeter.greet().call_raw().state(&state).await?; + + // The call returns "Hello, world" + assert_eq!(&res, greeting); + + Ok(()) +} + +// Solidity stores strings shorter than 32 bytes in a single storage slot, +// with the lowest order byte storing 2 * length and the higher order +// bytes storing the string data (left aligned) +fn encode_string_for_storage(s: &str) -> H256 { + let mut bytes = s.as_bytes().to_vec(); + let len = bytes.len(); + assert!(len < 32, "longer strings aren't stored in a single slot"); + bytes.resize(31, 0); + bytes.push(len as u8 * 2); + H256::from_slice(&bytes) +}