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 = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"ethers",
|
"ethers",
|
||||||
|
"ethers-contract",
|
||||||
"ethers-core",
|
"ethers-core",
|
||||||
"ethers-providers",
|
"ethers-providers",
|
||||||
"ethers-signers",
|
"ethers-signers",
|
||||||
"futures-executor",
|
"futures-executor",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"hex",
|
"hex",
|
||||||
|
"rand 0.7.3",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde-aux",
|
"serde-aux",
|
||||||
|
"serde_json",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|
|
@ -3,7 +3,7 @@ use ethers_core::{
|
||||||
abi::{Detokenize, Function, InvalidOutputType},
|
abi::{Detokenize, Function, InvalidOutputType},
|
||||||
types::{Address, BlockNumber, Bytes, TransactionRequest, U256},
|
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};
|
use std::{fmt::Debug, marker::PhantomData, sync::Arc};
|
||||||
|
|
||||||
|
@ -24,10 +24,14 @@ pub enum ContractError<M: Middleware> {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
DetokenizationError(#[from] InvalidOutputType),
|
DetokenizationError(#[from] InvalidOutputType),
|
||||||
|
|
||||||
/// Thrown when a provider call fails
|
/// Thrown when a middleware call fails
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
MiddlewareError(M::Error),
|
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`
|
/// Thrown during deployment if a constructor argument was passed in the `deploy`
|
||||||
/// call but a constructor was not present in the ABI
|
/// call but a constructor was not present in the ABI
|
||||||
#[error("constructor is not defined in the ABI")]
|
#[error("constructor is not defined in the ABI")]
|
||||||
|
|
|
@ -284,3 +284,10 @@ impl<M: Middleware> Contract<M> {
|
||||||
&self.client
|
&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;
|
pub use contract::Contract;
|
||||||
|
|
||||||
mod base;
|
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;
|
mod call;
|
||||||
pub use call::ContractError;
|
pub use call::ContractError;
|
||||||
|
|
|
@ -9,7 +9,7 @@ mod tokens;
|
||||||
pub use tokens::{Detokenize, InvalidOutputType, Tokenizable, TokenizableItem, Tokenize};
|
pub use tokens::{Detokenize, InvalidOutputType, Tokenizable, TokenizableItem, Tokenize};
|
||||||
|
|
||||||
mod human_readable;
|
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`.
|
/// Extension trait for `ethabi::Function`.
|
||||||
pub trait FunctionExt {
|
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;
|
mod transaction;
|
||||||
pub use transaction::{Transaction, TransactionReceipt, TransactionRequest};
|
pub use transaction::{Transaction, TransactionReceipt, TransactionRequest};
|
||||||
|
|
||||||
|
mod address_or_bytes;
|
||||||
|
pub use address_or_bytes::AddressOrBytes;
|
||||||
|
|
||||||
mod i256;
|
mod i256;
|
||||||
pub use i256::I256;
|
pub use i256::I256;
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ all-features = true
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|
||||||
[dependencies]
|
[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-core = { version = "0.2", path = "../ethers-core", default-features = false }
|
||||||
ethers-providers = { version = "0.2", path = "../ethers-providers", 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 }
|
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"] }
|
reqwest = { version = "0.11.0", default-features = false, features = ["json", "rustls-tls"] }
|
||||||
url = { version = "2.2.0", default-features = false }
|
url = { version = "2.2.0", default-features = false }
|
||||||
|
|
||||||
|
serde_json = { version = "1.0.61", default-features = false }
|
||||||
tokio = { version = "1.0" }
|
tokio = { version = "1.0" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
ethers = { version = "0.2", path = "../ethers" }
|
ethers = { version = "0.2", path = "../ethers" }
|
||||||
futures-executor = { version = "0.3.12", features = ["thread-pool"] }
|
futures-executor = { version = "0.3.12", features = ["thread-pool"] }
|
||||||
hex = { version = "0.4.2", default-features = false, features = ["std"] }
|
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"] }
|
tokio = { version = "1.0", default-features = false, features = ["rt", "macros", "time"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|
|
@ -13,6 +13,8 @@
|
||||||
//! gas prices in the background
|
//! gas prices in the background
|
||||||
//! - [`Gas Oracle`](crate::gas_oracle): Allows getting your gas price estimates from
|
//! - [`Gas Oracle`](crate::gas_oracle): Allows getting your gas price estimates from
|
||||||
//! places other than `eth_gasPrice`.
|
//! 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
|
//! ## Example of a middleware stack
|
||||||
//!
|
//!
|
||||||
|
@ -68,6 +70,10 @@ pub mod gas_oracle;
|
||||||
pub mod nonce_manager;
|
pub mod nonce_manager;
|
||||||
pub use nonce_manager::NonceManagerMiddleware;
|
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
|
/// The [Signer](crate::SignerMiddleware) is used to locally sign transactions and messages
|
||||||
/// instead of using eth_sendTransaction and eth_sign
|
/// instead of using eth_sendTransaction and eth_sign
|
||||||
pub mod signer;
|
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 }
|
ethers-core = { version = "0.2", path = "../ethers-core", default-features = false }
|
||||||
|
|
||||||
async-trait = { version = "0.1.42", 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"] }
|
reqwest = { version = "0.11.0", default-features = false, features = ["json", "rustls-tls"] }
|
||||||
serde = { version = "1.0.119", default-features = false, features = ["derive"] }
|
serde = { version = "1.0.119", default-features = false, features = ["derive"] }
|
||||||
serde_json = { version = "1.0.60", default-features = false }
|
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]
|
[dev-dependencies]
|
||||||
ethers = { version = "0.2", path = "../ethers" }
|
ethers = { version = "0.2", path = "../ethers" }
|
||||||
tokio = { version = "1.0", default-features = false, features = ["rt", "macros"] }
|
tokio = { version = "1.0", default-features = false, features = ["rt", "macros"] }
|
||||||
hex = { version = "0.4.2", default-features = false, features = ["std"] }
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["ws"]
|
default = ["ws"]
|
||||||
|
|
|
@ -17,6 +17,7 @@ use ethers_core::{
|
||||||
|
|
||||||
use crate::Middleware;
|
use crate::Middleware;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use hex::FromHex;
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use url::{ParseError, Url};
|
use url::{ParseError, Url};
|
||||||
|
@ -75,6 +76,9 @@ pub enum ProviderError {
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
SerdeJson(#[from] serde_json::Error),
|
SerdeJson(#[from] serde_json::Error),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
HexError(#[from] hex::FromHexError),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Types of filters supported by the JSON-RPC.
|
/// 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 from = utils::serialize(&from);
|
||||||
let location = utils::serialize(&location);
|
let location = utils::serialize(&location);
|
||||||
let block = utils::serialize(&block.unwrap_or(BlockNumber::Latest));
|
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
|
/// Returns the deployed code at a given address
|
||||||
|
|
Loading…
Reference in New Issue