diff --git a/Cargo.lock b/Cargo.lock index 6ba9332510129..4fa49c429d0b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1492,10 +1492,12 @@ dependencies = [ "ethers", "eyre", "foundry-utils", + "futures", "glob", "hashbrown 0.12.0", "hex", "once_cell", + "parking_lot 0.12.0", "proptest", "rayon", "regex", diff --git a/forge/Cargo.toml b/forge/Cargo.toml index 37e7235612368..46673f95b1b4c 100644 --- a/forge/Cargo.toml +++ b/forge/Cargo.toml @@ -28,6 +28,8 @@ thiserror = "1.0.29" revm = { package = "revm", git = "https://github.com/onbjerg/revm", branch = "onbjerg/blockhashes", default-features = false, features = ["std", "k256"] } hashbrown = "0.12" once_cell = "1.9.0" +parking_lot = "0.12.0" +futures = "0.3.21" [dev-dependencies] ethers = { git = "https://github.com/gakonst/ethers-rs", default-features = false, features = ["solc-full", "solc-tests"] } diff --git a/forge/src/executor/builder.rs b/forge/src/executor/builder.rs index 4514e65569912..b3effd1c1a5c4 100644 --- a/forge/src/executor/builder.rs +++ b/forge/src/executor/builder.rs @@ -1,6 +1,14 @@ -use revm::{db::EmptyDB, Env, SpecId}; +use ethers::prelude::Provider; +use revm::{ + db::{DatabaseRef, EmptyDB}, + Env, SpecId, +}; -use super::{inspector::InspectorStackConfig, Executor}; +use super::{ + fork::{SharedBackend, SharedMemCache}, + inspector::InspectorStackConfig, + Executor, +}; #[derive(Default)] pub struct ExecutorBuilder { @@ -8,6 +16,72 @@ pub struct ExecutorBuilder { env: Env, /// The configuration used to build an [InspectorStack]. inspector_config: InspectorStackConfig, + fork: Option, +} + +#[derive(Clone, Debug)] +pub struct Fork { + // todo: cache path + /// The URL to a node for fetching remote state + pub url: String, + /// The block to fork against + pub pin_block: Option, +} + +pub enum Backend { + Simple(EmptyDB), + Forked(SharedBackend), +} + +impl Backend { + /// Instantiates a new backend union based on whether there was or not a fork url specified + fn new(fork: Option) -> Self { + if let Some(fork) = fork { + let provider = Provider::try_from(fork.url).unwrap(); + // TOOD: Add reading cache from disk + let backend = SharedBackend::new( + provider, + SharedMemCache::default(), + fork.pin_block.map(Into::into), + ); + Backend::Forked(backend) + } else { + Backend::Simple(EmptyDB()) + } + } +} + +use ethers::types::{H160, H256, U256}; +use revm::AccountInfo; + +impl DatabaseRef for Backend { + fn block_hash(&self, number: U256) -> H256 { + match self { + Backend::Simple(inner) => inner.block_hash(number), + Backend::Forked(inner) => inner.block_hash(number), + } + } + + fn basic(&self, address: H160) -> AccountInfo { + match self { + Backend::Simple(inner) => inner.basic(address), + Backend::Forked(inner) => inner.basic(address), + } + } + + fn code_by_hash(&self, address: H256) -> bytes::Bytes { + match self { + Backend::Simple(inner) => inner.code_by_hash(address), + Backend::Forked(inner) => inner.code_by_hash(address), + } + } + + fn storage(&self, address: H160, index: U256) -> U256 { + match self { + Backend::Simple(inner) => inner.storage(address, index), + Backend::Forked(inner) => inner.storage(address, index), + } + } } impl ExecutorBuilder { @@ -24,6 +98,7 @@ impl ExecutorBuilder { self } + #[must_use] pub fn with_spec(mut self, spec: SpecId) -> Self { self.env.cfg.spec_id = spec; self @@ -36,12 +111,19 @@ impl ExecutorBuilder { self } + /// Configure the executor's forking mode + #[must_use] + pub fn with_fork(mut self, fork: Fork) -> Self { + self.fork = Some(fork); + self + } + /// Builds the executor as configured. - pub fn build(self) -> Executor { - Executor::new(EmptyDB(), self.env, self.inspector_config) + pub fn build(self) -> Executor { + let db = Backend::new(self.fork); + Executor::new(db, self.env, self.inspector_config) } // TODO: add with_traces // TODO: add with_debug(ger?) - // TODO: add forked } diff --git a/forge/src/executor/fork/backend.rs b/forge/src/executor/fork/backend.rs new file mode 100644 index 0000000000000..674781e9f7515 --- /dev/null +++ b/forge/src/executor/fork/backend.rs @@ -0,0 +1,484 @@ +//! Smart caching and deduplication of requests when using a forking provider +use revm::{db::DatabaseRef, AccountInfo, KECCAK_EMPTY}; + +use ethers::{ + core::abi::ethereum_types::BigEndianHash, + providers::Middleware, + types::{Address, BlockId, Bytes, H160, H256, U256}, + utils::keccak256, +}; +use futures::{ + channel::mpsc::{channel, Receiver, Sender}, + stream::{Fuse, Stream, StreamExt}, + task::{Context, Poll}, + Future, FutureExt, +}; +use parking_lot::RwLock; +use std::{ + collections::{hash_map::Entry, BTreeMap, HashMap}, + pin::Pin, + sync::{ + mpsc::{channel as oneshot_channel, Sender as OneshotSender}, + Arc, + }, +}; + +use foundry_utils::RuntimeOrHandle; + +type StorageInfo = BTreeMap; + +// TODO: Add disk flushing on Drop. +/// In Memory cache containing all fetched accounts and storage slots +/// and their values from RPC +#[derive(Clone, Debug, Default)] +pub struct SharedMemCache { + pub accounts: Arc>>, + pub storage: Arc>>, + pub block_hashes: Arc>>, +} + +type AccountFuture = + Pin, Address)> + Send>>; +type StorageFuture = Pin, Address, U256)> + Send>>; +type BlockHashFuture = Pin, u64)> + Send>>; + +/// Request variants that are executed by the provider +enum ProviderRequest { + Account(AccountFuture), + Storage(StorageFuture), + BlockHash(BlockHashFuture), +} + +/// The Request type the Backend listens for +#[derive(Debug)] +enum BackendRequest { + Basic(Address, OneshotSender), + Storage(Address, U256, OneshotSender), + BlockHash(u64, OneshotSender), +} + +/// Handles an internal provider and listens for requests. +/// +/// This handler will remain active as long as it is reachable (request channel still open) and +/// requests are in progress. +#[must_use = "BackendHandler does nothing unless polled."] +struct BackendHandler { + provider: M, + /// Stores the state. + cache: SharedMemCache, + /// Requests currently in progress + pending_requests: Vec>, + /// Listeners that wait for a `get_account` related response + account_requests: HashMap>>, + /// Listeners that wait for a `get_storage_at` response + storage_requests: HashMap<(Address, U256), Vec>>, + /// Listeners that wait for a `get_block` response + block_requests: HashMap>>, + /// Incoming commands. + incoming: Fuse>, + /// The block to fetch data from. + // This is an `Option` so that we can have less code churn in the functions below + block_id: Option, +} + +impl BackendHandler +where + M: Middleware + Clone + 'static, +{ + fn new( + provider: M, + cache: SharedMemCache, + rx: Receiver, + block_id: Option, + ) -> Self { + Self { + provider, + cache, + pending_requests: Default::default(), + account_requests: Default::default(), + storage_requests: Default::default(), + block_requests: Default::default(), + incoming: rx.fuse(), + block_id, + } + } + + /// handle the request in queue in the future. + /// + /// We always check: + /// 1. if the requested value is already stored in the cache, then answer the sender + /// 2. otherwise, fetch it via the provider but check if a request for that value is already in + /// progress (e.g. another Sender just requested the same account) + fn on_request(&mut self, req: BackendRequest) { + match req { + BackendRequest::Basic(addr, sender) => { + let lock = self.cache.accounts.read(); + let basic = lock.get(&addr).cloned(); + // release the lock + drop(lock); + if let Some(basic) = basic { + let _ = sender.send(basic); + } else { + self.request_account(addr, sender); + } + } + BackendRequest::BlockHash(number, sender) => { + let lock = self.cache.block_hashes.read(); + let hash = lock.get(&number).cloned(); + // release the lock + drop(lock); + if let Some(hash) = hash { + let _ = sender.send(hash); + } else { + self.request_hash(number, sender); + } + } + BackendRequest::Storage(addr, idx, sender) => { + let lock = self.cache.storage.read(); + let acc = lock.get(&addr); + let value = acc.and_then(|acc| acc.get(&idx).copied()); + // release the lock + drop(lock); + + // account is already stored in the cache + if let Some(value) = value { + let _ = sender.send(value); + } else { + // account present but not storage -> fetch storage + self.request_account_storage(addr, idx, sender); + } + } + } + } + + /// process a request for account's storage + fn request_account_storage( + &mut self, + address: Address, + idx: U256, + listener: OneshotSender, + ) { + match self.storage_requests.entry((address, idx)) { + Entry::Occupied(mut entry) => { + entry.get_mut().push(listener); + } + Entry::Vacant(entry) => { + entry.insert(vec![listener]); + let provider = self.provider.clone(); + let block_id = self.block_id; + let fut = Box::pin(async move { + // serialize & deserialize back to U256 + let idx_req = H256::from_uint(&idx); + let storage = provider.get_storage_at(address, idx_req, block_id).await; + let storage = + storage.map(|storage| storage.into_uint()).map_err(|err| eyre::eyre!(err)); + (storage, address, idx) + }); + self.pending_requests.push(ProviderRequest::Storage(fut)); + } + } + } + + /// returns the future that fetches the account data + fn get_account_req(&self, address: Address) -> ProviderRequest { + let provider = self.provider.clone(); + let block_id = self.block_id; + let fut = Box::pin(async move { + let balance = provider.get_balance(address, block_id); + let nonce = provider.get_transaction_count(address, block_id); + let code = provider.get_code(address, block_id); + let resp = tokio::try_join!(balance, nonce, code).map_err(|err| eyre::eyre!(err)); + (resp, address) + }); + ProviderRequest::Account(fut) + } + + /// process a request for an account + fn request_account(&mut self, address: Address, listener: OneshotSender) { + match self.account_requests.entry(address) { + Entry::Occupied(mut entry) => { + entry.get_mut().push(listener); + } + Entry::Vacant(entry) => { + entry.insert(vec![listener]); + self.pending_requests.push(self.get_account_req(address)); + } + } + } + + /// process a request for a block hash + fn request_hash(&mut self, number: u64, listener: OneshotSender) { + match self.block_requests.entry(number) { + Entry::Occupied(mut entry) => { + entry.get_mut().push(listener); + } + Entry::Vacant(entry) => { + entry.insert(vec![listener]); + let provider = self.provider.clone(); + let fut = Box::pin(async move { + let res = provider.get_block(number).await; + let block = res.ok().flatten(); + let block_hash = match block { + Some(block) => Ok(block + .hash + .expect("empty block hash on mined block, this should never happen")), + None => Err(eyre::eyre!("block {} not found", number)), + }; + (block_hash, number) + }); + self.pending_requests.push(ProviderRequest::BlockHash(fut)); + } + } + } +} + +impl Future for BackendHandler +where + M: Middleware + Clone + Unpin + 'static, +{ + type Output = (); + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let pin = self.get_mut(); + + // receive new requests to delegate to the underlying provider + while let Poll::Ready(Some(req)) = Pin::new(&mut pin.incoming).poll_next(cx) { + pin.on_request(req) + } + + // poll all requests in progress + for n in (0..pin.pending_requests.len()).rev() { + let mut request = pin.pending_requests.swap_remove(n); + match &mut request { + ProviderRequest::Account(fut) => { + if let Poll::Ready((resp, addr)) = fut.poll_unpin(cx) { + // get the response + let (balance, nonce, code) = resp.unwrap_or_else(|_| { + tracing::trace!("Failed to get account for {}", addr); + Default::default() + }); + + // conver it to revm-style types + let (code, code_hash) = if !code.0.is_empty() { + (Some(code.0.clone()), keccak256(&code).into()) + } else { + (None, KECCAK_EMPTY) + }; + + // update the cache + let acc = AccountInfo { nonce: nonce.as_u64(), balance, code, code_hash }; + pin.cache.accounts.write().insert(addr, acc.clone()); + + // notify all listeners + if let Some(listeners) = pin.account_requests.remove(&addr) { + listeners.into_iter().for_each(|l| { + let _ = l.send(acc.clone()); + }) + } + continue + } + } + ProviderRequest::Storage(fut) => { + if let Poll::Ready((resp, addr, idx)) = fut.poll_unpin(cx) { + let value = resp.unwrap_or_else(|_| { + tracing::trace!("Failed to get storage for {} at {}", addr, idx); + Default::default() + }); + + // update the cache + pin.cache.storage.write().entry(addr).or_default().insert(idx, value); + + // notify all listeners + if let Some(listeners) = pin.storage_requests.remove(&(addr, idx)) { + listeners.into_iter().for_each(|l| { + let _ = l.send(value); + }) + } + continue + } + } + ProviderRequest::BlockHash(fut) => { + if let Poll::Ready((block_hash, number)) = fut.poll_unpin(cx) { + let value = block_hash.unwrap_or_else(|_| { + tracing::trace!("Failed to get block hash for {}", number); + Default::default() + }); + + // update the cache + pin.cache.block_hashes.write().insert(number, value); + + // notify all listeners + if let Some(listeners) = pin.block_requests.remove(&number) { + listeners.into_iter().for_each(|l| { + let _ = l.send(value); + }) + } + continue + } + } + } + // not ready, insert and poll again + pin.pending_requests.push(request); + } + + // the handler is finished if the request channel was closed and all requests are processed + if pin.incoming.is_done() && pin.pending_requests.is_empty() { + Poll::Ready(()) + } else { + Poll::Pending + } + } +} + +/// A cloneable backend type that shares access to the backend data with all its clones. +/// +/// This backend type is connected to the `BackendHandler` via a mpsc channel. The `BackendHandlers` +/// is spawned on a background thread and listens for incoming commands on the receiver half of the +/// channel. A `SharedBackend` holds a sender for that channel, which is `Clone`, so their can be +/// multiple `SharedBackend`s communicating with the same `BackendHandler`, hence this `Backend` +/// type is thread safe. +/// +/// All `Backend` trait functions are delegated as a `BackendRequest` via the channel to the +/// `BackendHandler`. All `BackendRequest` variants include a sender half of an additional channel +/// that is used by the `BackendHandler` to send the result of an executed `BackendRequest` back to +/// `SharedBackend`. +/// +/// The `BackendHandler` holds an ethers `Provider` to look up missing accounts or storage slots +/// from remote (e.g. infura). It detects duplicate requests from multiple `SharedBackend`s and +/// bundles them together, so that always only one provider request is executed. For example, there +/// are two `SharedBackend`s, `A` and `B`, both request the basic account info of account +/// `0xasd9sa7d...` at the same time. After the `BackendHandler` receives the request from `A`, it +/// sends a new provider request to the provider's endpoint, then it reads the identical request +/// from `B` and simply adds it as an additional listener for the request already in progress, +/// instead of sending another one. So that after the provider returns the response all listeners +/// (`A` and `B`) get notified. +#[derive(Debug, Clone)] +pub struct SharedBackend { + backend: Sender, +} + +impl SharedBackend { + /// Spawns a new `BackendHandler` on a background thread that listens for requests from any + /// `SharedBackend`. Missing values get inserted in the `cache`. + /// + /// NOTE: this should be called with `Arc` + pub fn new(provider: M, cache: SharedMemCache, pin_block: Option) -> Self + where + M: Middleware + Unpin + 'static + Clone, + { + let (tx, rx) = channel(1); + let handler = BackendHandler::new(provider, cache, rx, pin_block); + // spawn the provider handler to background + let rt = RuntimeOrHandle::new(); + std::thread::spawn(move || match rt { + RuntimeOrHandle::Runtime(runtime) => runtime.block_on(handler), + RuntimeOrHandle::Handle(handle) => handle.block_on(handler), + }); + + Self { backend: tx } + } + + fn do_get_basic(&self, address: H160) -> eyre::Result { + let (sender, rx) = oneshot_channel(); + let req = BackendRequest::Basic(address, sender); + self.backend.clone().try_send(req).map_err(|e| eyre::eyre!("{:?}", e))?; + Ok(rx.recv()?) + } + + fn do_get_storage(&self, address: H160, index: U256) -> eyre::Result { + let (sender, rx) = oneshot_channel(); + let req = BackendRequest::Storage(address, index, sender); + self.backend.clone().try_send(req).map_err(|e| eyre::eyre!("{:?}", e))?; + Ok(rx.recv()?) + } + + fn do_get_block_hash(&self, number: u64) -> eyre::Result { + let (sender, rx) = oneshot_channel(); + let req = BackendRequest::BlockHash(number, sender); + self.backend.clone().try_send(req).map_err(|e| eyre::eyre!("{:?}", e))?; + Ok(rx.recv()?) + } +} + +impl DatabaseRef for SharedBackend { + fn block_hash(&self, number: U256) -> H256 { + if number > U256::from(u64::MAX) { + return KECCAK_EMPTY + } + let number = number.as_u64(); + self.do_get_block_hash(number).unwrap_or_else(|_| { + tracing::trace!("Failed to send/recv `block_hash` for {}", number); + Default::default() + }) + } + + fn basic(&self, address: H160) -> AccountInfo { + self.do_get_basic(address).unwrap_or_else(|_| { + tracing::trace!("Failed to send/recv `basic` for {}", address); + Default::default() + }) + } + + fn code_by_hash(&self, _address: H256) -> bytes::Bytes { + panic!("Should not be called. Code is already loaded.") + } + + fn storage(&self, address: H160, index: U256) -> U256 { + self.do_get_storage(address, index).unwrap_or_else(|_| { + tracing::trace!("Failed to send/recv `storage` for {} at {}", address, index); + Default::default() + }) + } +} + +#[cfg(test)] +mod tests { + use ethers::{ + providers::{Http, Provider}, + types::Address, + }; + use std::convert::TryFrom; + + use super::*; + + #[test] + fn shared_backend() { + let provider = Provider::::try_from( + "https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27", + ) + .unwrap(); + // some rng contract from etherscan + let address: Address = "63091244180ae240c87d1f528f5f269134cb07b3".parse().unwrap(); + + let cache = SharedMemCache::default(); + let backend = SharedBackend::new(Arc::new(provider), cache.clone(), None); + + let idx = U256::from(0u64); + let value = backend.storage(address, idx); + let account = backend.basic(address); + + let mem_acc = cache.accounts.read().get(&address).unwrap().clone(); + assert_eq!(account.balance, mem_acc.balance); + assert_eq!(account.nonce, mem_acc.nonce); + let slots = cache.storage.read().get(&address).unwrap().clone(); + assert_eq!(slots.len(), 1); + assert_eq!(slots.get(&idx).copied().unwrap(), value); + + let num = U256::from(10u64); + let hash = backend.block_hash(num); + let mem_hash = cache.block_hashes.read().get(&num.as_u64()).unwrap().clone(); + assert_eq!(hash, mem_hash); + + let backend = backend.clone(); + let max_slots = 5; + let handle = std::thread::spawn(move || { + for i in 1..max_slots { + let idx = U256::from(i); + let _ = backend.storage(address, idx); + } + }); + handle.join().unwrap(); + let slots = cache.storage.read().get(&address).unwrap().clone(); + assert_eq!(slots.len() as u64, max_slots); + } +} diff --git a/forge/src/executor/fork/init.rs b/forge/src/executor/fork/init.rs new file mode 100644 index 0000000000000..16355f47df61b --- /dev/null +++ b/forge/src/executor/fork/init.rs @@ -0,0 +1,42 @@ +use ethers::{providers::Middleware, types::Address}; +use revm::{BlockEnv, Env, TxEnv}; + +/// Initializes a REVM block environment based on a forked +/// ethereum provider. +pub async fn environment( + provider: &M, + override_chain_id: Option, + pin_block: Option, + origin: Address, +) -> Result { + let block_number = if let Some(pin_block) = pin_block { + pin_block + } else { + provider.get_block_number().await?.as_u64() + }; + let (gas_price, rpc_chain_id, block) = tokio::try_join!( + provider.get_gas_price(), + provider.get_chainid(), + provider.get_block(block_number) + )?; + let block = block.expect("block not found"); + + Ok(Env { + block: BlockEnv { + number: block.number.expect("block number not found").as_u64().into(), + timestamp: block.timestamp, + coinbase: block.author, + difficulty: block.difficulty, + basefee: block.base_fee_per_gas.unwrap_or_default(), + gas_limit: block.gas_limit, + }, + tx: TxEnv { + caller: origin, + gas_price, + chain_id: Some(override_chain_id.unwrap_or(rpc_chain_id.as_u64())), + gas_limit: block.gas_limit.as_u64(), + ..Default::default() + }, + ..Default::default() + }) +} diff --git a/forge/src/executor/fork/mod.rs b/forge/src/executor/fork/mod.rs new file mode 100644 index 0000000000000..c7ba153c78f6d --- /dev/null +++ b/forge/src/executor/fork/mod.rs @@ -0,0 +1,5 @@ +mod backend; +pub use backend::{SharedBackend, SharedMemCache}; + +mod init; +pub use init::environment; diff --git a/forge/src/executor/fuzz/mod.rs b/forge/src/executor/fuzz/mod.rs index 5131cdac00eee..c3ca85a9c8d36 100644 --- a/forge/src/executor/fuzz/mod.rs +++ b/forge/src/executor/fuzz/mod.rs @@ -217,7 +217,7 @@ pub struct FuzzCase { #[cfg(test)] mod tests { - use super::*; + use crate::CALLER; use crate::test_helpers::{fuzz_executor, test_executor, COMPILED}; @@ -226,9 +226,8 @@ mod tests { let mut executor = test_executor(); let compiled = COMPILED.find("FuzzTests").expect("could not find contract"); - let (addr, _, _, _) = executor - .deploy(Address::zero(), compiled.bytecode().unwrap().0.clone(), 0.into()) - .unwrap(); + let (addr, _, _, _) = + executor.deploy(*CALLER, compiled.bytecode().unwrap().0.clone(), 0.into()).unwrap(); let executor = fuzz_executor(&executor); diff --git a/forge/src/executor/mod.rs b/forge/src/executor/mod.rs index 939e5f81abb54..8a2f5cdb7c246 100644 --- a/forge/src/executor/mod.rs +++ b/forge/src/executor/mod.rs @@ -13,9 +13,12 @@ pub mod opts; /// Executor inspectors pub mod inspector; +/// Forking provider +pub mod fork; + /// Executor builder pub mod builder; -pub use builder::ExecutorBuilder; +pub use builder::{ExecutorBuilder, Fork}; /// Fuzzing wrapper for executors pub mod fuzz; @@ -24,6 +27,7 @@ pub mod fuzz; pub use revm::SpecId; use self::inspector::InspectorStackConfig; +use crate::CALLER; use bytes::Bytes; use ethers::{ abi::{Abi, Detokenize, RawLog, Tokenize}, @@ -136,6 +140,11 @@ where self.db.insert_cache(address, account); } + /// Gets the balance of an account + pub fn get_balance(&self, address: Address) -> U256 { + self.db.basic(address).balance + } + /// Set the nonce of an account. pub fn set_nonce(&mut self, address: Address, nonce: u64) { let mut account = self.db.basic(address); @@ -149,14 +158,8 @@ where &mut self, address: Address, ) -> std::result::Result<(Return, Vec), EvmError> { - let CallResult { status, logs, .. } = self.call_committing::<(), _, _>( - Address::zero(), - address, - "setUp()", - (), - 0.into(), - None, - )?; + let CallResult { status, logs, .. } = + self.call_committing::<(), _, _>(*CALLER, address, "setUp()", (), 0.into(), None)?; Ok((status, logs)) } diff --git a/forge/src/executor/opts.rs b/forge/src/executor/opts.rs index cf27cd0ef4f2f..9a3c09aba8d47 100644 --- a/forge/src/executor/opts.rs +++ b/forge/src/executor/opts.rs @@ -1,7 +1,13 @@ -use ethers::types::{Address, U256}; +use ethers::{ + providers::Provider, + types::{Address, U256}, +}; +use foundry_utils::RuntimeOrHandle; use revm::{BlockEnv, CfgEnv, SpecId, TxEnv}; use serde::{Deserialize, Serialize}; +use super::fork::environment; + #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct EvmOpts { #[serde(flatten)] @@ -63,27 +69,41 @@ pub struct Env { pub block_gas_limit: Option, } -impl Env { +impl EvmOpts { pub fn evm_env(&self) -> revm::Env { - revm::Env { - block: BlockEnv { - number: self.block_number.into(), - coinbase: self.block_coinbase, - timestamp: self.block_timestamp.into(), - difficulty: self.block_difficulty.into(), - basefee: self.block_base_fee_per_gas.into(), - gas_limit: self.block_gas_limit.unwrap_or(self.gas_limit).into(), - }, - cfg: CfgEnv { - chain_id: self.chain_id.unwrap_or(99).into(), - spec_id: SpecId::LONDON, - perf_all_precompiles_have_balance: false, - }, - tx: TxEnv { - gas_price: self.gas_price.into(), - gas_limit: self.block_gas_limit.unwrap_or(self.gas_limit), - ..Default::default() - }, + if let Some(ref fork_url) = self.fork_url { + let rt = RuntimeOrHandle::new(); + let provider = + Provider::try_from(fork_url.as_str()).expect("could not instantiated provider"); + let fut = + environment(&provider, self.env.chain_id, self.fork_block_number, self.sender); + match rt { + RuntimeOrHandle::Runtime(runtime) => runtime.block_on(fut), + RuntimeOrHandle::Handle(handle) => handle.block_on(fut), + } + .expect("could not instantiate forked environment") + } else { + revm::Env { + block: BlockEnv { + number: self.env.block_number.into(), + coinbase: self.env.block_coinbase, + timestamp: self.env.block_timestamp.into(), + difficulty: self.env.block_difficulty.into(), + basefee: self.env.block_base_fee_per_gas.into(), + gas_limit: self.env.block_gas_limit.unwrap_or(self.env.gas_limit).into(), + }, + cfg: CfgEnv { + chain_id: self.env.chain_id.unwrap_or(99).into(), + spec_id: SpecId::LONDON, + perf_all_precompiles_have_balance: false, + }, + tx: TxEnv { + gas_price: self.env.gas_price.into(), + gas_limit: self.env.block_gas_limit.unwrap_or(self.env.gas_limit), + caller: self.sender, + ..Default::default() + }, + } } } } diff --git a/forge/src/lib.rs b/forge/src/lib.rs index b4baec6a7f12e..611b065424ca7 100644 --- a/forge/src/lib.rs +++ b/forge/src/lib.rs @@ -18,12 +18,17 @@ pub trait TestFilter { fn matches_path(&self, path: impl AsRef) -> bool; } +use ethers::types::Address; +use once_cell::sync::Lazy; +static CALLER: Lazy
= Lazy::new(Address::random); + #[cfg(test)] pub mod test_helpers { use crate::executor::fuzz::FuzzedExecutor; use super::{ executor::{ + builder::Backend, opts::{Env, EvmOpts}, Executor, ExecutorBuilder, }, @@ -32,9 +37,9 @@ pub mod test_helpers { use ethers::{ prelude::Lazy, solc::{AggregatedCompilerOutput, Project, ProjectPathsConfig}, - types::{Address, U256}, + types::U256, }; - use revm::db::{DatabaseRef, EmptyDB}; + use revm::db::DatabaseRef; pub static COMPILED: Lazy = Lazy::new(|| { let paths = @@ -49,8 +54,8 @@ pub mod test_helpers { ..Default::default() }); - pub fn test_executor() -> Executor { - ExecutorBuilder::new().with_cheatcodes(false).with_config((*EVM_OPTS).env.evm_env()).build() + pub fn test_executor() -> Executor { + ExecutorBuilder::new().with_cheatcodes(false).with_config((*EVM_OPTS).evm_env()).build() } pub fn fuzz_executor<'a, DB: DatabaseRef>( @@ -58,7 +63,7 @@ pub mod test_helpers { ) -> FuzzedExecutor<'a, DB> { let cfg = proptest::test_runner::Config { failure_persistence: None, ..Default::default() }; - FuzzedExecutor::new(executor, proptest::test_runner::TestRunner::new(cfg), Address::zero()) + FuzzedExecutor::new(executor, proptest::test_runner::TestRunner::new(cfg), *CALLER) } pub mod filter { diff --git a/forge/src/multi_runner.rs b/forge/src/multi_runner.rs index 1735247502e25..2e25ff1ee50f4 100644 --- a/forge/src/multi_runner.rs +++ b/forge/src/multi_runner.rs @@ -1,5 +1,5 @@ use crate::{ - executor::{opts::EvmOpts, Executor, ExecutorBuilder, SpecId}, + executor::{opts::EvmOpts, Executor, ExecutorBuilder, Fork, SpecId}, runner::TestResult, ContractRunner, TestFilter, }; @@ -31,6 +31,8 @@ pub struct MultiContractRunnerBuilder { pub initial_balance: U256, /// The EVM spec to use pub evm_spec: Option, + /// The fork config + pub fork: Option, } pub type DeployableContracts = BTreeMap)>; @@ -194,7 +196,7 @@ impl MultiContractRunner { stream_result: Option)>>, ) -> Result>> { let source_paths = self.source_paths.clone(); - + let env = self.evm_opts.evm_env(); let results = self .contracts .par_iter() @@ -202,12 +204,18 @@ impl MultiContractRunner { .filter(|(name, _)| filter.matches_contract(name)) .filter(|(_, (abi, _, _))| abi.functions().any(|func| filter.matches_test(&func.name))) .map(|(name, (abi, deploy_code, libs))| { - // TODO: Fork mode and "vicinity" - let executor = ExecutorBuilder::new() + let mut builder = ExecutorBuilder::new() .with_cheatcodes(self.evm_opts.ffi) - .with_config(self.evm_opts.env.evm_env()) - .with_spec(self.evm_spec) - .build(); + .with_config(env.clone()) + .with_spec(self.evm_spec); + + if let Some(ref url) = self.evm_opts.fork_url { + let fork = + Fork { url: url.clone(), pin_block: self.evm_opts.fork_block_number }; + builder = builder.with_fork(fork); + } + + let executor = builder.build(); let result = self.run_tests(name, abi, executor, deploy_code.clone(), libs, filter)?; Ok((name.clone(), result)) @@ -234,7 +242,7 @@ impl MultiContractRunner { err, fields(name = %_name) )] - fn run_tests( + fn run_tests( &self, _name: &str, contract: &Abi, diff --git a/forge/src/runner.rs b/forge/src/runner.rs index 765fa70b08b50..46dabfce89537 100644 --- a/forge/src/runner.rs +++ b/forge/src/runner.rs @@ -3,7 +3,7 @@ use crate::{ fuzz::{FuzzError, FuzzTestResult, FuzzedCases, FuzzedExecutor}, CallResult, EvmError, Executor, RawCallResult, }, - TestFilter, + TestFilter, CALLER, }; use rayon::iter::ParallelIterator; use revm::db::DatabaseRef; @@ -198,17 +198,21 @@ impl<'a, DB: DatabaseRef> ContractRunner<'a, DB> { } } -impl<'a, DB: DatabaseRef + Clone + Send + Sync> ContractRunner<'a, DB> { +impl<'a, DB: DatabaseRef + Send + Sync> ContractRunner<'a, DB> { /// Deploys the test contract inside the runner from the sending account, and optionally runs /// the `setUp` function on the test contract. pub fn deploy(&mut self, setup: bool) -> Result<(Address, Vec, bool, Option)> { + // We max out their balance so that they can deploy and make calls. + self.executor.set_balance(self.sender, U256::MAX); + self.executor.set_balance(*CALLER, U256::MAX); + // We set the nonce of the deployer accounts to 1 to get the same addresses as DappTools self.executor.set_nonce(self.sender, 1); // Deploy libraries self.predeploy_libs.iter().for_each(|code| { self.executor - .deploy(Address::zero(), code.0.clone(), 0u32.into()) + .deploy(*CALLER, code.0.clone(), 0u32.into()) .expect("couldn't deploy library"); }); @@ -458,15 +462,17 @@ impl<'a, DB: DatabaseRef + Clone + Send + Sync> ContractRunner<'a, DB> { #[cfg(test)] mod tests { use super::*; - use crate::test_helpers::{filter::Filter, test_executor, COMPILED, EVM_OPTS}; + use crate::{ + executor::builder::Backend, + test_helpers::{filter::Filter, test_executor, COMPILED, EVM_OPTS}, + }; use proptest::test_runner::Config as FuzzConfig; - use revm::db::EmptyDB; pub fn runner<'a>( abi: &'a Abi, code: ethers::prelude::Bytes, libs: &'a mut Vec, - ) -> ContractRunner<'a, EmptyDB> { + ) -> ContractRunner<'a, Backend> { ContractRunner::new( test_executor(), abi,