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
This commit is contained in:
parent
243fb7639e
commit
3105431007
|
@ -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",
|
||||
|
|
|
@ -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<M: Middleware> {
|
|||
#[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")]
|
||||
|
|
|
@ -284,3 +284,10 @@ impl<M: Middleware> Contract<M> {
|
|||
&self.client
|
||||
}
|
||||
}
|
||||
|
||||
impl<M: Middleware> std::ops::Deref for Contract<M> {
|
||||
type Target = BaseContract;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.base_contract
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<Address> for AddressOrBytes {
|
||||
fn from(s: Address) -> Self {
|
||||
Self::Address(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Bytes> for AddressOrBytes {
|
||||
fn from(s: Bytes) -> Self {
|
||||
Self::Bytes(s)
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<HashMap<U256, Address>> = 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<Abi> = 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<M>(Contract<M>);
|
||||
impl<M> std::ops::Deref for DsProxyFactory<M> {
|
||||
type Target = Contract<M>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl<M: Middleware> std::fmt::Debug for DsProxyFactory<M> {
|
||||
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<M> {
|
||||
#[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<T: Into<Address>>(address: T, client: Arc<M>) -> 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<M, bool> {
|
||||
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<M, Address> {
|
||||
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<M, Address> {
|
||||
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<M, CreatedFilter> {
|
||||
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<Token>) -> Result<Self, InvalidOutputType> {
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Provider<Http>, LocalWallet>;
|
||||
///
|
||||
/// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// // instantiate client that can sign transactions.
|
||||
/// let wallet: LocalWallet = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc"
|
||||
/// .parse()?;
|
||||
/// let provider = Provider::<Http>::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::<HttpWallet, Arc<HttpWallet>>(
|
||||
/// 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<M: Middleware, C: Into<Arc<M>>>(
|
||||
client: C,
|
||||
factory: Option<Address>,
|
||||
owner: Address,
|
||||
) -> Result<Self, ContractError<M>> {
|
||||
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<M: Middleware, C: Into<Arc<M>>>(
|
||||
&self,
|
||||
client: C,
|
||||
target: AddressOrBytes,
|
||||
data: Bytes,
|
||||
) -> Result<TxHash, ContractError<M>> {
|
||||
// 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<M, Bytes> = 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<M, (Address, Bytes)> =
|
||||
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<TransactionRequest, TransformerError> {
|
||||
// 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)
|
||||
}
|
||||
}
|
|
@ -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<M, T> {
|
||||
inner: M,
|
||||
transformer: T,
|
||||
}
|
||||
|
||||
impl<M, T> TransformerMiddleware<M, T>
|
||||
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<M: Middleware> {
|
||||
#[error(transparent)]
|
||||
TransformerError(#[from] TransformerError),
|
||||
|
||||
#[error("{0}")]
|
||||
MiddlewareError(M::Error),
|
||||
}
|
||||
|
||||
impl<M: Middleware> FromErr<M::Error> for TransformerMiddlewareError<M> {
|
||||
fn from(src: M::Error) -> TransformerMiddlewareError<M> {
|
||||
TransformerMiddlewareError::MiddlewareError(src)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<M, T> Middleware for TransformerMiddleware<M, T>
|
||||
where
|
||||
M: Middleware,
|
||||
T: Transformer,
|
||||
{
|
||||
type Error = TransformerMiddlewareError<M>;
|
||||
type Provider = M::Provider;
|
||||
type Inner = M;
|
||||
|
||||
fn inner(&self) -> &M {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
async fn send_transaction(
|
||||
&self,
|
||||
mut tx: TransactionRequest,
|
||||
block: Option<BlockNumber>,
|
||||
) -> Result<PendingTransaction<'_, Self::Provider>, 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)
|
||||
}
|
||||
}
|
|
@ -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<TransactionRequest, TransformerError>;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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<Provider<Http>, 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::<Http>::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::<HttpWallet, Arc<HttpWallet>>(
|
||||
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::<Http>::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::<HttpWallet, Arc<HttpWallet>>(
|
||||
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::<HttpWallet, Arc<HttpWallet>>(
|
||||
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));
|
||||
}
|
|
@ -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"]
|
||||
|
|
|
@ -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<P: JsonRpcClient> Middleware for Provider<P> {
|
|||
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
|
||||
|
|
Loading…
Reference in New Issue