From 3105431007e372162f7edd8738ac30ce9ad3650a Mon Sep 17 00:00:00 2001 From: Rohit Narurkar Date: Fri, 22 Jan 2021 14:55:22 +0530 Subject: [PATCH] feat: Transformer middleware with DsProxy impl (#165) * feat: basic structure of proxy wallet middleware with DsProxy * feat: build DsProxy contract, minor fixes, naming convention changes * fix: add provider error in contract error * fix: left pad storage value * fix: delete gnosis safe for now * feat(ds_proxy): execute code or target * test(ds_proxy): transformer middleware tests * fix: clippy should be happy * fix(tests): ds proxy execute code * chore: add documentation * chore: formatting --- Cargo.lock | 3 + ethers-contract/src/call.rs | 8 +- ethers-contract/src/contract.rs | 7 + ethers-contract/src/lib.rs | 2 +- ethers-core/src/abi/mod.rs | 2 +- ethers-core/src/types/address_or_bytes.rs | 22 ++ ethers-core/src/types/mod.rs | 3 + ethers-middleware/Cargo.toml | 3 + ethers-middleware/src/lib.rs | 6 + .../src/transformer/ds_proxy/factory.rs | 135 +++++++++++ .../src/transformer/ds_proxy/mod.rs | 210 ++++++++++++++++++ .../src/transformer/middleware.rs | 82 +++++++ ethers-middleware/src/transformer/mod.rs | 35 +++ .../tests/solidity-contracts/DSProxy.sol | 206 +++++++++++++++++ .../solidity-contracts/SimpleStorage.sol | 15 ++ ethers-middleware/tests/transformer.rs | 181 +++++++++++++++ ethers-providers/Cargo.toml | 2 +- ethers-providers/src/provider.rs | 14 +- 18 files changed, 929 insertions(+), 7 deletions(-) create mode 100644 ethers-core/src/types/address_or_bytes.rs create mode 100644 ethers-middleware/src/transformer/ds_proxy/factory.rs create mode 100644 ethers-middleware/src/transformer/ds_proxy/mod.rs create mode 100644 ethers-middleware/src/transformer/middleware.rs create mode 100644 ethers-middleware/src/transformer/mod.rs create mode 100644 ethers-middleware/tests/solidity-contracts/DSProxy.sol create mode 100644 ethers-middleware/tests/solidity-contracts/SimpleStorage.sol create mode 100644 ethers-middleware/tests/transformer.rs diff --git a/Cargo.lock b/Cargo.lock index eb6a5bb6..2ee55cd9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -693,15 +693,18 @@ version = "0.2.0" dependencies = [ "async-trait", "ethers", + "ethers-contract", "ethers-core", "ethers-providers", "ethers-signers", "futures-executor", "futures-util", "hex", + "rand 0.7.3", "reqwest", "serde", "serde-aux", + "serde_json", "thiserror", "tokio", "tracing", diff --git a/ethers-contract/src/call.rs b/ethers-contract/src/call.rs index cbaf1508..35e2040e 100644 --- a/ethers-contract/src/call.rs +++ b/ethers-contract/src/call.rs @@ -3,7 +3,7 @@ use ethers_core::{ abi::{Detokenize, Function, InvalidOutputType}, types::{Address, BlockNumber, Bytes, TransactionRequest, U256}, }; -use ethers_providers::{Middleware, PendingTransaction}; +use ethers_providers::{Middleware, PendingTransaction, ProviderError}; use std::{fmt::Debug, marker::PhantomData, sync::Arc}; @@ -24,10 +24,14 @@ pub enum ContractError { #[error(transparent)] DetokenizationError(#[from] InvalidOutputType), - /// Thrown when a provider call fails + /// Thrown when a middleware call fails #[error("{0}")] MiddlewareError(M::Error), + /// Thrown when a provider call fails + #[error("{0}")] + ProviderError(ProviderError), + /// Thrown during deployment if a constructor argument was passed in the `deploy` /// call but a constructor was not present in the ABI #[error("constructor is not defined in the ABI")] diff --git a/ethers-contract/src/contract.rs b/ethers-contract/src/contract.rs index 8d03da1d..8aa3fddd 100644 --- a/ethers-contract/src/contract.rs +++ b/ethers-contract/src/contract.rs @@ -284,3 +284,10 @@ impl Contract { &self.client } } + +impl std::ops::Deref for Contract { + type Target = BaseContract; + fn deref(&self) -> &Self::Target { + &self.base_contract + } +} diff --git a/ethers-contract/src/lib.rs b/ethers-contract/src/lib.rs index 02af00f4..fe2f0fa8 100644 --- a/ethers-contract/src/lib.rs +++ b/ethers-contract/src/lib.rs @@ -17,7 +17,7 @@ mod contract; pub use contract::Contract; mod base; -pub use base::{decode_function_data, encode_function_data, BaseContract}; +pub use base::{decode_function_data, encode_function_data, AbiError, BaseContract}; mod call; pub use call::ContractError; diff --git a/ethers-core/src/abi/mod.rs b/ethers-core/src/abi/mod.rs index ac49ef01..2a1317b7 100644 --- a/ethers-core/src/abi/mod.rs +++ b/ethers-core/src/abi/mod.rs @@ -9,7 +9,7 @@ mod tokens; pub use tokens::{Detokenize, InvalidOutputType, Tokenizable, TokenizableItem, Tokenize}; mod human_readable; -pub use human_readable::parse as parse_abi; +pub use human_readable::{parse as parse_abi, ParseError}; /// Extension trait for `ethabi::Function`. pub trait FunctionExt { diff --git a/ethers-core/src/types/address_or_bytes.rs b/ethers-core/src/types/address_or_bytes.rs new file mode 100644 index 00000000..c9112c1e --- /dev/null +++ b/ethers-core/src/types/address_or_bytes.rs @@ -0,0 +1,22 @@ +use crate::types::{Address, Bytes}; + +#[derive(Clone, Debug, PartialEq, Eq)] +/// A type that can either be an `Address` or `Bytes`. +pub enum AddressOrBytes { + /// An address type + Address(Address), + /// A bytes type + Bytes(Bytes), +} + +impl From
for AddressOrBytes { + fn from(s: Address) -> Self { + Self::Address(s) + } +} + +impl From for AddressOrBytes { + fn from(s: Bytes) -> Self { + Self::Bytes(s) + } +} diff --git a/ethers-core/src/types/mod.rs b/ethers-core/src/types/mod.rs index 89c7dbdc..47613556 100644 --- a/ethers-core/src/types/mod.rs +++ b/ethers-core/src/types/mod.rs @@ -10,6 +10,9 @@ pub use ethereum_types::{Address, Bloom, H160, H256, U128, U256, U64}; mod transaction; pub use transaction::{Transaction, TransactionReceipt, TransactionRequest}; +mod address_or_bytes; +pub use address_or_bytes::AddressOrBytes; + mod i256; pub use i256::I256; diff --git a/ethers-middleware/Cargo.toml b/ethers-middleware/Cargo.toml index 7771fa2b..c58c3053 100644 --- a/ethers-middleware/Cargo.toml +++ b/ethers-middleware/Cargo.toml @@ -14,6 +14,7 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [dependencies] +ethers-contract = { version = "0.2", path = "../ethers-contract", default-features = false, features = ["abigen"] } ethers-core = { version = "0.2", path = "../ethers-core", default-features = false } ethers-providers = { version = "0.2", path = "../ethers-providers", default-features = false } ethers-signers = { version = "0.2", path = "../ethers-signers", default-features = false } @@ -30,12 +31,14 @@ serde-aux = { version = "2.1.0", default-features = false } reqwest = { version = "0.11.0", default-features = false, features = ["json", "rustls-tls"] } url = { version = "2.2.0", default-features = false } +serde_json = { version = "1.0.61", default-features = false } tokio = { version = "1.0" } [dev-dependencies] ethers = { version = "0.2", path = "../ethers" } futures-executor = { version = "0.3.12", features = ["thread-pool"] } hex = { version = "0.4.2", default-features = false, features = ["std"] } +rand = { version = "0.7.3", default-features = false } tokio = { version = "1.0", default-features = false, features = ["rt", "macros", "time"] } [features] diff --git a/ethers-middleware/src/lib.rs b/ethers-middleware/src/lib.rs index 936f5069..9b7fe7d7 100644 --- a/ethers-middleware/src/lib.rs +++ b/ethers-middleware/src/lib.rs @@ -13,6 +13,8 @@ //! gas prices in the background //! - [`Gas Oracle`](crate::gas_oracle): Allows getting your gas price estimates from //! places other than `eth_gasPrice`. +//! - [`Transformer`](crate::transformer): Allows intercepting and transforming a transaction to +//! be broadcasted via a proxy wallet, e.g. [`DSProxy`](crate::transformer::DsProxy). //! //! ## Example of a middleware stack //! @@ -68,6 +70,10 @@ pub mod gas_oracle; pub mod nonce_manager; pub use nonce_manager::NonceManagerMiddleware; +/// The [Transformer](crate::TransformerMiddleware) is used to intercept transactions and transform +/// them to be sent via various supported transformers, e.g., [DSProxy](crate::transformer::DsProxy) +pub mod transformer; + /// The [Signer](crate::SignerMiddleware) is used to locally sign transactions and messages /// instead of using eth_sendTransaction and eth_sign pub mod signer; diff --git a/ethers-middleware/src/transformer/ds_proxy/factory.rs b/ethers-middleware/src/transformer/ds_proxy/factory.rs new file mode 100644 index 00000000..eefb5739 --- /dev/null +++ b/ethers-middleware/src/transformer/ds_proxy/factory.rs @@ -0,0 +1,135 @@ +use ethers_contract::Lazy; +use ethers_core::types::*; +use std::{collections::HashMap, str::FromStr}; + +/// A lazily computed hash map with the Ethereum network IDs as keys and the corresponding +/// DsProxyFactory 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); + + m +}); + +// Auto-generated type-safe bindings +pub use dsproxyfactory_mod::*; +#[allow(clippy::too_many_arguments)] +mod dsproxyfactory_mod { + #![allow(dead_code)] + #![allow(unused_imports)] + use ethers_contract::{ + builders::{ContractCall, Event}, + Contract, Lazy, + }; + use ethers_core::{ + abi::{parse_abi, Abi, Detokenize, InvalidOutputType, Token, Tokenizable}, + types::*, + }; + use ethers_providers::Middleware; + #[doc = "DsProxyFactory was auto-generated with ethers-rs Abigen. More information at: https://github.com/gakonst/ethers-rs"] + use std::sync::Arc; + pub static DSPROXYFACTORY_ABI: Lazy = Lazy::new(|| { + serde_json :: from_str ("[{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"address\"}],\"name\":\"isProxy\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"cache\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[],\"name\":\"build\",\"outputs\":[{\"name\":\"proxy\",\"type\":\"address\"}],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"owner\",\"type\":\"address\"}],\"name\":\"build\",\"outputs\":[{\"name\":\"proxy\",\"type\":\"address\"}],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"sender\",\"type\":\"address\"},{\"indexed\":true,\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"proxy\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"cache\",\"type\":\"address\"}],\"name\":\"Created\",\"type\":\"event\"}]\n") . expect ("invalid abi") + }); + #[derive(Clone)] + pub struct DsProxyFactory(Contract); + impl std::ops::Deref for DsProxyFactory { + type Target = Contract; + fn deref(&self) -> &Self::Target { + &self.0 + } + } + impl std::fmt::Debug for DsProxyFactory { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.debug_tuple(stringify!(DsProxyFactory)) + .field(&self.address()) + .finish() + } + } + impl<'a, M: Middleware> DsProxyFactory { + #[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>(address: T, client: Arc) -> Self { + let contract = Contract::new(address.into(), DSPROXYFACTORY_ABI.clone(), client); + Self(contract) + } + #[doc = "Calls the contract's `isProxy` (0x29710388) function"] + pub fn is_proxy(&self, p0: Address) -> ContractCall { + self.0 + .method_hash([41, 113, 3, 136], p0) + .expect("method not found (this should never happen)") + } + #[doc = "Calls the contract's `build` (0xf3701da2) function"] + pub fn build(&self, owner: Address) -> ContractCall { + self.0 + .method_hash([243, 112, 29, 162], owner) + .expect("method not found (this should never happen)") + } + #[doc = "Calls the contract's `cache` (0x60c7d295) function"] + pub fn cache(&self) -> ContractCall { + self.0 + .method_hash([96, 199, 210, 149], ()) + .expect("method not found (this should never happen)") + } + #[doc = "Gets the contract's `Created` event"] + pub fn created_filter(&self) -> Event { + self.0 + .event("Created") + .expect("event not found (this should never happen)") + } + } + #[derive(Clone, Debug, Default, Eq, PartialEq)] + pub struct CreatedFilter { + pub sender: Address, + pub owner: Address, + pub proxy: Address, + pub cache: Address, + } + impl CreatedFilter { + #[doc = r" Retrieves the signature for the event this data corresponds to."] + #[doc = r" This signature is the Keccak-256 hash of the ABI signature of"] + #[doc = r" this event."] + pub const fn signature() -> H256 { + H256([ + 37, 155, 48, 202, 57, 136, 92, 109, 128, 26, 11, 93, 188, 152, 134, 64, 243, 194, + 94, 47, 55, 83, 31, 225, 56, 197, 197, 175, 137, 85, 212, 27, + ]) + } + #[doc = r" Retrieves the ABI signature for the event this data corresponds"] + #[doc = r" to. For this event the value should always be:"] + #[doc = r""] + #[doc = "`Created(address,address,address,address)`"] + pub const fn abi_signature() -> &'static str { + "Created(address,address,address,address)" + } + } + impl Detokenize for CreatedFilter { + fn from_tokens(tokens: Vec) -> Result { + if tokens.len() != 4 { + return Err(InvalidOutputType(format!( + "Expected {} tokens, got {}: {:?}", + 4, + tokens.len(), + tokens + ))); + } + #[allow(unused_mut)] + let mut tokens = tokens.into_iter(); + let sender = Tokenizable::from_token(tokens.next().expect("this should never happen"))?; + let owner = Tokenizable::from_token(tokens.next().expect("this should never happen"))?; + let proxy = Tokenizable::from_token(tokens.next().expect("this should never happen"))?; + let cache = Tokenizable::from_token(tokens.next().expect("this should never happen"))?; + Ok(CreatedFilter { + sender, + owner, + proxy, + cache, + }) + } + } +} diff --git a/ethers-middleware/src/transformer/ds_proxy/mod.rs b/ethers-middleware/src/transformer/ds_proxy/mod.rs new file mode 100644 index 00000000..7b716ffb --- /dev/null +++ b/ethers-middleware/src/transformer/ds_proxy/mod.rs @@ -0,0 +1,210 @@ +mod factory; +use factory::{CreatedFilter, DsProxyFactory, ADDRESS_BOOK}; + +use super::{Transformer, TransformerError}; +use ethers_contract::{builders::ContractCall, BaseContract, ContractError}; +use ethers_core::{abi::parse_abi, types::*, utils::id}; +use ethers_providers::Middleware; +use std::sync::Arc; + +/// The function signature of DsProxy's execute function, to execute data on a target address. +const DS_PROXY_EXECUTE_TARGET: &str = + "function execute(address target, bytes memory data) public payable returns (bytes memory response)"; +/// The function signature of DsProxy's execute function, to deploy bytecode and execute data on it. +const DS_PROXY_EXECUTE_CODE: &str = + "function execute(bytes memory code, bytes memory data) public payable returns (address target, bytes memory response)"; + +#[derive(Debug, Clone)] +/// Represents the DsProxy type that implements the [Transformer](super::Transformer) trait. +/// +/// # Example +/// +/// ```no_run +/// use ethers::{ +/// middleware::transformer::DsProxy, +/// prelude::*, +/// }; +/// use std::{convert::TryFrom, sync::Arc}; +/// +/// type HttpWallet = SignerMiddleware, LocalWallet>; +/// +/// # async fn foo() -> Result<(), Box> { +/// // instantiate client that can sign transactions. +/// let wallet: LocalWallet = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc" +/// .parse()?; +/// let provider = Provider::::try_from("http://localhost:8545")?; +/// let client = SignerMiddleware::new(provider, wallet); +/// +/// # let ds_proxy_addr = Address::random(); +/// // instantiate DsProxy by providing its address. +/// let ds_proxy = DsProxy::new(ds_proxy_addr); +/// +/// // execute a transaction via the DsProxy instance. +/// # let target_addr = Address::random(); +/// let target = AddressOrBytes::Address(target_addr); +/// let calldata: Bytes = vec![0u8; 32].into(); +/// let tx_hash = ds_proxy.execute::>( +/// Arc::new(client), +/// target, +/// calldata, +/// ) +/// .await?; +/// +/// # Ok(()) +/// # } +/// ``` +pub struct DsProxy { + address: Address, + contract: BaseContract, +} + +impl DsProxy { + /// Create a new instance of DsProxy by providing the address of the DsProxy contract that has + /// already been deployed to the Ethereum network. + pub fn new(address: Address) -> Self { + let contract = parse_abi(&[DS_PROXY_EXECUTE_TARGET, DS_PROXY_EXECUTE_CODE]) + .expect("could not parse ABI") + .into(); + + Self { address, contract } + } + + /// The address of the DsProxy instance. + pub fn address(&self) -> Address { + self.address + } +} + +impl DsProxy { + /// Deploys a new DsProxy contract to the Ethereum network. + pub async fn build>>( + client: C, + factory: Option
, + owner: Address, + ) -> Result> { + let client = client.into(); + + // Fetch chain id and the corresponding address of DsProxyFactory contract + // preference is given to DsProxyFactory contract's address if provided + // otherwise check the address book for the client's chain ID. + let factory: Address = match factory { + Some(addr) => addr, + None => { + let chain_id = client + .get_chainid() + .await + .map_err(ContractError::MiddlewareError)?; + match ADDRESS_BOOK.get(&chain_id) { + Some(addr) => *addr, + None => panic!( + "Must either be a supported Network ID or provide DsProxyFactory contract address" + ), + } + } + }; + + // broadcast the tx to deploy a new DsProxy. + let ds_proxy_factory = DsProxyFactory::new(factory, client); + let tx_receipt = ds_proxy_factory + .build(owner) + .send() + .await? + .await + .map_err(ContractError::ProviderError)?; + + // decode the event log to get the address of the deployed contract. + if tx_receipt.status == Some(U64::from(1u64)) { + // fetch the appropriate log. Only one event is logged by the DsProxyFactory contract, + // the others are logged by the deployed DsProxy contract and hence can be ignored. + let log = tx_receipt + .logs + .iter() + .find(|i| i.address == factory) + .ok_or(ContractError::ContractNotDeployed)?; + + // decode the log. + let created_filter: CreatedFilter = + ds_proxy_factory.decode_event("Created", log.topics.clone(), log.data.clone())?; + + // instantiate the ABI and return. + let contract = parse_abi(&[DS_PROXY_EXECUTE_TARGET, DS_PROXY_EXECUTE_CODE]) + .expect("could not parse ABI") + .into(); + Ok(Self { + address: created_filter.proxy, + contract, + }) + } else { + Err(ContractError::ContractNotDeployed) + } + } +} + +impl DsProxy { + /// Execute a tx through the DsProxy instance. The target can either be a deployed smart + /// contract's address, or bytecode of a compiled smart contract. Depending on the target, the + /// appropriate `execute` method is called, that is, either + /// [execute(address,bytes)](https://github.com/dapphub/ds-proxy/blob/master/src/proxy.sol#L53-L58) + /// or [execute(bytes,bytes)](https://github.com/dapphub/ds-proxy/blob/master/src/proxy.sol#L39-L42). + pub async fn execute>>( + &self, + client: C, + target: AddressOrBytes, + data: Bytes, + ) -> Result> { + // construct the full contract using DsProxy's address and the injected client. + let ds_proxy = self + .contract + .clone() + .into_contract(self.address, client.into()); + + match target { + // handle the case when the target is an address to a deployed contract. + AddressOrBytes::Address(addr) => { + let selector = id("execute(address,bytes)"); + let args = (addr, data); + let call: ContractCall = ds_proxy.method_hash(selector, args)?; + let pending_tx = call.send().await?; + Ok(*pending_tx) + } + // handle the case when the target is actually bytecode of a contract to be deployed + // and executed on. + AddressOrBytes::Bytes(code) => { + let selector = id("execute(bytes,bytes)"); + let args = (code, data); + let call: ContractCall = + ds_proxy.method_hash(selector, args)?; + let pending_tx = call.send().await?; + Ok(*pending_tx) + } + } + } +} + +impl Transformer for DsProxy { + fn transform(&self, tx: TransactionRequest) -> Result { + // clone the tx into a new proxy tx. + let mut proxy_tx = tx.clone(); + + // the target address cannot be None. + let target = match tx.to { + Some(NameOrAddress::Address(addr)) => Ok(addr), + _ => Err(TransformerError::MissingField("to".into())), + }?; + + // fetch the data field. + let data = tx.data.unwrap_or_else(|| vec![].into()); + + // encode data as the ABI encoded data for DSProxy's execute method. + let selector = id("execute(address,bytes)"); + let encoded_data = self + .contract + .encode_with_selector(selector, (target, data))?; + + // update appropriate fields of the proxy tx. + proxy_tx.data = Some(encoded_data); + proxy_tx.to = Some(NameOrAddress::Address(self.address)); + + Ok(proxy_tx) + } +} diff --git a/ethers-middleware/src/transformer/middleware.rs b/ethers-middleware/src/transformer/middleware.rs new file mode 100644 index 00000000..0a7fc994 --- /dev/null +++ b/ethers-middleware/src/transformer/middleware.rs @@ -0,0 +1,82 @@ +use super::{Transformer, TransformerError}; +use async_trait::async_trait; +use ethers_core::types::*; +use ethers_providers::{FromErr, Middleware, PendingTransaction}; +use thiserror::Error; + +#[derive(Debug)] +/// Middleware used for intercepting transaction requests and transforming them to be executed by +/// the underneath `Transformer` instance. +pub struct TransformerMiddleware { + inner: M, + transformer: T, +} + +impl TransformerMiddleware +where + M: Middleware, + T: Transformer, +{ + /// Creates a new TransformerMiddleware that intercepts transactions, modifying them to be sent + /// through the Transformer. + pub fn new(inner: M, transformer: T) -> Self { + Self { inner, transformer } + } +} + +#[derive(Error, Debug)] +pub enum TransformerMiddlewareError { + #[error(transparent)] + TransformerError(#[from] TransformerError), + + #[error("{0}")] + MiddlewareError(M::Error), +} + +impl FromErr for TransformerMiddlewareError { + fn from(src: M::Error) -> TransformerMiddlewareError { + TransformerMiddlewareError::MiddlewareError(src) + } +} + +#[async_trait] +impl Middleware for TransformerMiddleware +where + M: Middleware, + T: Transformer, +{ + type Error = TransformerMiddlewareError; + type Provider = M::Provider; + type Inner = M; + + fn inner(&self) -> &M { + &self.inner + } + + async fn send_transaction( + &self, + mut tx: TransactionRequest, + block: Option, + ) -> Result, Self::Error> { + // resolve the to field if that's an ENS name. + if let Some(ref to) = tx.to { + if let NameOrAddress::Name(ens_name) = to { + let addr = self + .inner + .resolve_name(&ens_name) + .await + .map_err(TransformerMiddlewareError::MiddlewareError)?; + tx.to = Some(addr.into()) + } + } + + // construct the appropriate proxy tx. + let proxy_tx = self.transformer.transform(tx)?; + + // send the proxy tx. + self.inner + .send_transaction(proxy_tx, block) + .await + .map_err(TransformerMiddlewareError::MiddlewareError) + } +} diff --git a/ethers-middleware/src/transformer/mod.rs b/ethers-middleware/src/transformer/mod.rs new file mode 100644 index 00000000..7cf123aa --- /dev/null +++ b/ethers-middleware/src/transformer/mod.rs @@ -0,0 +1,35 @@ +mod ds_proxy; +pub use ds_proxy::DsProxy; + +mod middleware; +pub use middleware::TransformerMiddleware; + +use ethers_contract::AbiError; +use ethers_core::{abi::ParseError, types::*}; +use thiserror::Error; + +#[derive(Error, Debug)] +/// Errors thrown from the types that implement the `Transformer` trait. +pub enum TransformerError { + #[error("The field `{0}` is missing")] + MissingField(String), + + #[error(transparent)] + AbiParseError(#[from] ParseError), + + #[error(transparent)] + AbiError(#[from] AbiError), +} + +/// `Transformer` is a trait to be implemented by a proxy wallet, eg. [`DSProxy`], that intends to +/// intercept a transaction request and transform it into one that is instead sent via the proxy +/// contract. +/// +/// [`DSProxy`]: struct@crate::ds_proxy::DsProxy +pub trait Transformer: Send + Sync + std::fmt::Debug { + /// Transforms a [`transaction request`] into one that can be broadcasted and execute via the + /// proxy contract. + /// + /// [`transaction request`]: struct@ethers_core::types::TransactionRequest + fn transform(&self, tx: TransactionRequest) -> Result; +} diff --git a/ethers-middleware/tests/solidity-contracts/DSProxy.sol b/ethers-middleware/tests/solidity-contracts/DSProxy.sol new file mode 100644 index 00000000..9bf010f5 --- /dev/null +++ b/ethers-middleware/tests/solidity-contracts/DSProxy.sol @@ -0,0 +1,206 @@ +pragma solidity >=0.6.0; + +pragma solidity >=0.4.23; + +interface DSAuthority { + function canCall( + address src, address dst, bytes4 sig + ) external view returns (bool); +} + +contract DSAuthEvents { + event LogSetAuthority (address indexed authority); + event LogSetOwner (address indexed owner); +} + +contract DSAuth is DSAuthEvents { + DSAuthority public authority; + address public owner; + + constructor() public { + owner = msg.sender; + emit LogSetOwner(msg.sender); + } + + function setOwner(address owner_) + public + auth + { + owner = owner_; + emit LogSetOwner(owner); + } + + function setAuthority(DSAuthority authority_) + public + auth + { + authority = authority_; + emit LogSetAuthority(address(authority)); + } + + modifier auth { + require(isAuthorized(msg.sender, msg.sig), "ds-auth-unauthorized"); + _; + } + + function isAuthorized(address src, bytes4 sig) internal view returns (bool) { + if (src == address(this)) { + return true; + } else if (src == owner) { + return true; + } else if (authority == DSAuthority(0)) { + return false; + } else { + return authority.canCall(src, address(this), sig); + } + } +} + +contract DSNote { + event LogNote( + bytes4 indexed sig, + address indexed guy, + bytes32 indexed foo, + bytes32 indexed bar, + uint256 wad, + bytes fax + ) anonymous; + + modifier note { + bytes32 foo; + bytes32 bar; + uint256 wad; + + assembly { + foo := calldataload(4) + bar := calldataload(36) + wad := callvalue() + } + + _; + + emit LogNote(msg.sig, msg.sender, foo, bar, wad, msg.data); + } +} + +// DSProxy +// Allows code execution using a persistant identity This can be very +// useful to execute a sequence of atomic actions. Since the owner of +// the proxy can be changed, this allows for dynamic ownership models +// i.e. a multisig +contract DSProxy is DSAuth, DSNote { + DSProxyCache public cache; // global cache for contracts + + constructor(address _cacheAddr) public { + require(setCache(_cacheAddr)); + } + + fallback() external payable { + } + + receive() external payable { + } + + // use the proxy to execute calldata _data on contract _code + function execute(bytes memory _code, bytes memory _data) + public + payable + returns (address target, bytes32 response) + { + target = cache.read(_code); + if (target == address(0x0)) { + // deploy contract & store its address in cache + target = cache.write(_code); + } + + response = execute(target, _data); + } + + function execute(address _target, bytes memory _data) + public + auth + note + payable + returns (bytes32 response) + { + require(_target != address(0x0)); + + // call contract in current context + assembly { + let succeeded := delegatecall(sub(gas(), 5000), _target, add(_data, 0x20), mload(_data), 0, 32) + response := mload(0) // load delegatecall output + switch iszero(succeeded) + case 1 { + // throw if delegatecall failed + revert(0, 0) + } + } + } + + //set new cache + function setCache(address _cacheAddr) + public + auth + note + returns (bool) + { + require(_cacheAddr != address(0x0)); // invalid cache address + cache = DSProxyCache(_cacheAddr); // overwrite cache + return true; + } +} + +// DSProxyFactory +// This factory deploys new proxy instances through build() +// Deployed proxy addresses are logged +contract DSProxyFactory { + event Created(address indexed sender, address indexed owner, address proxy, address cache); + mapping(address=>bool) public isProxy; + DSProxyCache public cache = new DSProxyCache(); + + // deploys a new proxy instance + // sets owner of proxy to caller + function build() public returns (DSProxy proxy) { + proxy = build(msg.sender); + } + + // deploys a new proxy instance + // sets custom owner of proxy + function build(address owner) public returns (DSProxy proxy) { + proxy = new DSProxy(address(cache)); + emit Created(msg.sender, owner, address(proxy), address(cache)); + proxy.setOwner(owner); + isProxy[address(proxy)] = true; + } +} + +// DSProxyCache +// This global cache stores addresses of contracts previously deployed +// by a proxy. This saves gas from repeat deployment of the same +// contracts and eliminates blockchain bloat. + +// By default, all proxies deployed from the same factory store +// contracts in the same cache. The cache a proxy instance uses can be +// changed. The cache uses the sha3 hash of a contract's bytecode to +// lookup the address +contract DSProxyCache { + mapping(bytes32 => address) cache; + + function read(bytes memory _code) public view returns (address) { + bytes32 hash = keccak256(_code); + return cache[hash]; + } + + function write(bytes memory _code) public returns (address target) { + assembly { + target := create(0, add(_code, 0x20), mload(_code)) + switch iszero(extcodesize(target)) + case 1 { + // throw if contract failed to deploy + revert(0, 0) + } + } + bytes32 hash = keccak256(_code); + cache[hash] = target; + } +} diff --git a/ethers-middleware/tests/solidity-contracts/SimpleStorage.sol b/ethers-middleware/tests/solidity-contracts/SimpleStorage.sol new file mode 100644 index 00000000..592913e1 --- /dev/null +++ b/ethers-middleware/tests/solidity-contracts/SimpleStorage.sol @@ -0,0 +1,15 @@ +pragma solidity >=0.4.24; + +contract SimpleStorage { + + event ValueChanged(address indexed author, address indexed oldAuthor, uint256 oldValue, uint256 newValue); + + address public lastSender; + uint256 public value; + + function setValue(uint256 _value) public { + emit ValueChanged(msg.sender, lastSender, value, _value); + value = _value; + lastSender = msg.sender; + } +} diff --git a/ethers-middleware/tests/transformer.rs b/ethers-middleware/tests/transformer.rs new file mode 100644 index 00000000..4042893c --- /dev/null +++ b/ethers-middleware/tests/transformer.rs @@ -0,0 +1,181 @@ +use ethers_contract::{BaseContract, ContractFactory}; +use ethers_core::{ + types::*, + utils::{Ganache, Solc}, +}; +use ethers_middleware::{ + transformer::{DsProxy, TransformerMiddleware}, + SignerMiddleware, +}; +use ethers_providers::{Http, Middleware, PendingTransaction, Provider}; +use ethers_signers::LocalWallet; +use rand::Rng; +use std::{convert::TryFrom, sync::Arc, time::Duration}; + +type HttpWallet = SignerMiddleware, LocalWallet>; + +#[tokio::test] +#[cfg(not(feature = "celo"))] +async fn ds_proxy_transformer() { + // randomness + let mut rng = rand::thread_rng(); + + // spawn ganache and instantiate a signer middleware. + let ganache = Ganache::new().spawn(); + let wallet: LocalWallet = ganache.keys()[0].clone().into(); + let provider = Provider::::try_from(ganache.endpoint()) + .unwrap() + .interval(Duration::from_millis(10u64)); + let signer_middleware = SignerMiddleware::new(provider.clone(), wallet); + let wallet_addr = signer_middleware.address(); + let provider = Arc::new(signer_middleware.clone()); + + // deploy DsProxyFactory which we'll use to deploy a new DsProxy contract. + let compiled = Solc::new("./tests/solidity-contracts/DSProxy.sol") + .build() + .expect("could not compile DSProxyFactory"); + let contract = compiled + .get("DSProxyFactory") + .expect("could not find DSProxyFactory"); + let factory = ContractFactory::new( + contract.abi.clone(), + contract.bytecode.clone(), + Arc::clone(&provider), + ); + let ds_proxy_factory = factory.deploy(()).unwrap().send().await.unwrap(); + + // deploy a new DsProxy contract. + let ds_proxy = DsProxy::build::>( + Arc::clone(&provider), + Some(ds_proxy_factory.address()), + provider.address(), + ) + .await + .unwrap(); + let ds_proxy_addr = ds_proxy.address(); + + // deploy SimpleStorage and try to update its value via transformer middleware. + let compiled = Solc::new("./tests/solidity-contracts/SimpleStorage.sol") + .build() + .expect("could not compile SimpleStorage"); + let contract = compiled + .get("SimpleStorage") + .expect("could not find SimpleStorage"); + let factory = ContractFactory::new( + contract.abi.clone(), + contract.bytecode.clone(), + Arc::clone(&provider), + ); + let simple_storage = factory.deploy(()).unwrap().send().await.unwrap(); + + // instantiate a new transformer middleware. + let provider = TransformerMiddleware::new(signer_middleware, ds_proxy.clone()); + + // broadcast the setValue tx via transformer middleware (first wallet). + let expected_value: u64 = rng.gen(); + let calldata = simple_storage + .encode("setValue", U256::from(expected_value)) + .expect("could not get ABI encoded data"); + let tx = TransactionRequest::new() + .to(simple_storage.address()) + .data(calldata); + provider + .send_transaction(tx, None) + .await + .unwrap() + .await + .unwrap(); + + // verify that DsProxy's state was updated. + let last_sender = provider + .get_storage_at(ds_proxy_addr, H256::zero(), None) + .await + .unwrap(); + let last_value = provider + .get_storage_at(ds_proxy_addr, H256::from_low_u64_be(1u64), None) + .await + .unwrap(); + assert_eq!(last_sender, wallet_addr.into()); + assert_eq!(last_value, H256::from_low_u64_be(expected_value)); +} + +#[tokio::test] +#[cfg(not(feature = "celo"))] +async fn ds_proxy_code() { + // randomness + let mut rng = rand::thread_rng(); + + // spawn ganache and instantiate a signer middleware. + let ganache = Ganache::new().spawn(); + let wallet: LocalWallet = ganache.keys()[1].clone().into(); + let provider = Provider::::try_from(ganache.endpoint()) + .unwrap() + .interval(Duration::from_millis(10u64)); + let signer_middleware = SignerMiddleware::new(provider.clone(), wallet); + let wallet_addr = signer_middleware.address(); + let provider = Arc::new(signer_middleware.clone()); + + // deploy DsProxyFactory which we'll use to deploy a new DsProxy contract. + let compiled = Solc::new("./tests/solidity-contracts/DSProxy.sol") + .build() + .expect("could not compile DSProxyFactory"); + let contract = compiled + .get("DSProxyFactory") + .expect("could not find DSProxyFactory"); + let factory = ContractFactory::new( + contract.abi.clone(), + contract.bytecode.clone(), + Arc::clone(&provider), + ); + let ds_proxy_factory = factory.deploy(()).unwrap().send().await.unwrap(); + + // deploy a new DsProxy contract. + let ds_proxy = DsProxy::build::>( + Arc::clone(&provider), + Some(ds_proxy_factory.address()), + provider.address(), + ) + .await + .unwrap(); + let ds_proxy_addr = ds_proxy.address(); + + // compile the SimpleStorage contract which we will use to interact via DsProxy. + let compiled = Solc::new("./tests/solidity-contracts/SimpleStorage.sol") + .build() + .expect("could not compile SimpleStorage"); + let ss = compiled + .get("SimpleStorage") + .expect("could not find SimpleStorage"); + let ss_base_contract: BaseContract = ss.abi.clone().into(); + let expected_value: u64 = rng.gen(); + let calldata = ss_base_contract + .encode("setValue", U256::from(expected_value)) + .expect("could not get ABI encoded data"); + + // execute code via the deployed DsProxy contract. + let tx_hash = ds_proxy + .execute::>( + Arc::clone(&provider), + AddressOrBytes::Bytes(ss.bytecode.clone()), + calldata, + ) + .await + .expect("could not execute code via DSProxy"); + + // wait for the tx to be confirmed. + PendingTransaction::new(tx_hash, provider.provider()) + .await + .expect("could not confirm pending tx"); + + // verify that DsProxy's state was updated. + let last_sender = provider + .get_storage_at(ds_proxy_addr, H256::zero(), None) + .await + .unwrap(); + let last_value = provider + .get_storage_at(ds_proxy_addr, H256::from_low_u64_be(1u64), None) + .await + .unwrap(); + assert_eq!(last_sender, wallet_addr.into()); + assert_eq!(last_value, H256::from_low_u64_be(expected_value)); +} diff --git a/ethers-providers/Cargo.toml b/ethers-providers/Cargo.toml index 46ed05e5..5102d457 100644 --- a/ethers-providers/Cargo.toml +++ b/ethers-providers/Cargo.toml @@ -17,6 +17,7 @@ rustdoc-args = ["--cfg", "docsrs"] ethers-core = { version = "0.2", path = "../ethers-core", default-features = false } async-trait = { version = "0.1.42", default-features = false } +hex = { version = "0.4.2", default-features = false, features = ["std"] } reqwest = { version = "0.11.0", default-features = false, features = ["json", "rustls-tls"] } serde = { version = "1.0.119", default-features = false, features = ["derive"] } serde_json = { version = "1.0.60", default-features = false } @@ -42,7 +43,6 @@ tokio-tungstenite = { version = "0.13.0", default-features = false, features = [ [dev-dependencies] ethers = { version = "0.2", path = "../ethers" } tokio = { version = "1.0", default-features = false, features = ["rt", "macros"] } -hex = { version = "0.4.2", default-features = false, features = ["std"] } [features] default = ["ws"] diff --git a/ethers-providers/src/provider.rs b/ethers-providers/src/provider.rs index 010e2af1..9d923c3e 100644 --- a/ethers-providers/src/provider.rs +++ b/ethers-providers/src/provider.rs @@ -17,6 +17,7 @@ use ethers_core::{ use crate::Middleware; use async_trait::async_trait; +use hex::FromHex; use serde::{de::DeserializeOwned, Serialize}; use thiserror::Error; use url::{ParseError, Url}; @@ -75,6 +76,9 @@ pub enum ProviderError { #[error(transparent)] SerdeJson(#[from] serde_json::Error), + + #[error(transparent)] + HexError(#[from] hex::FromHexError), } /// Types of filters supported by the JSON-RPC. @@ -414,8 +418,14 @@ impl Middleware for Provider

{ let from = utils::serialize(&from); let location = utils::serialize(&location); let block = utils::serialize(&block.unwrap_or(BlockNumber::Latest)); - self.request("eth_getStorageAt", [from, location, block]) - .await + + // get the hex encoded value. + let value: String = self + .request("eth_getStorageAt", [from, location, block]) + .await?; + // get rid of the 0x prefix and left pad it with zeroes. + let value = format!("{:0>64}", value.replace("0x", "")); + Ok(H256::from_slice(&Vec::from_hex(value)?)) } /// Returns the deployed code at a given address