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:
Rohit Narurkar 2021-01-22 14:55:22 +05:30 committed by GitHub
parent 243fb7639e
commit 3105431007
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 929 additions and 7 deletions

3
Cargo.lock generated
View File

@ -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",

View File

@ -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")]

View File

@ -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
}
}

View File

@ -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;

View File

@ -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 {

View File

@ -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)
}
}

View File

@ -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;

View File

@ -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]

View File

@ -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;

View File

@ -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,
})
}
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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>;
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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));
}

View File

@ -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"]

View File

@ -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