diff --git a/Cargo.lock b/Cargo.lock index fae1ec6df31e..1edadb78665e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1535,6 +1535,8 @@ version = "0.1.0" dependencies = [ "ansi_term", "bytes", + "clap", + "clap_complete", "ethers", "evm", "evmodin", @@ -1702,6 +1704,7 @@ dependencies = [ "glob", "hex", "proptest", + "rayon", "regex", "semver", "serde", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 28dc059fd748..e7a41941ee1d 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -76,4 +76,4 @@ doc = false [[bin]] name = "forge" path = "src/forge.rs" -doc = false \ No newline at end of file +doc = false diff --git a/cli/src/cmd/build.rs b/cli/src/cmd/build.rs index 1e2ae25c3c56..fd9edfdce65c 100644 --- a/cli/src/cmd/build.rs +++ b/cli/src/cmd/build.rs @@ -1,12 +1,9 @@ //! build command -use ethers::{ - solc::{ - artifacts::{Optimizer, Settings}, - remappings::Remapping, - MinimalCombinedArtifacts, Project, ProjectCompileOutput, ProjectPathsConfig, SolcConfig, - }, - types::Address, +use ethers::solc::{ + artifacts::{Optimizer, Settings}, + remappings::Remapping, + MinimalCombinedArtifacts, Project, ProjectCompileOutput, ProjectPathsConfig, SolcConfig, }; use std::{ collections::BTreeMap, @@ -17,10 +14,6 @@ use std::{ use crate::{cmd::Cmd, opts::forge::CompilerArgs, utils}; use clap::{Parser, ValueHint}; -#[cfg(feature = "evmodin-evm")] -use evmodin::util::mocked_host::MockedHost; -#[cfg(feature = "sputnik-evm")] -use sputnik::backend::MemoryVicinity; #[derive(Debug, Clone, Parser)] pub struct BuildArgs { @@ -250,108 +243,3 @@ impl BuildArgs { Ok(project) } } - -#[derive(Clone, Debug)] -pub enum EvmType { - #[cfg(feature = "sputnik-evm")] - Sputnik, - #[cfg(feature = "evmodin-evm")] - EvmOdin, -} - -impl FromStr for EvmType { - type Err = eyre::Error; - - fn from_str(s: &str) -> Result { - Ok(match s.to_lowercase().as_str() { - #[cfg(feature = "sputnik-evm")] - "sputnik" => EvmType::Sputnik, - #[cfg(feature = "evmodin-evm")] - "evmodin" => EvmType::EvmOdin, - other => eyre::bail!("unknown EVM type {}", other), - }) - } -} - -#[derive(Debug, Clone, Parser)] -pub struct Env { - #[clap(help = "the block gas limit", long, default_value_t = u64::MAX)] - pub gas_limit: u64, - - #[clap(help = "the chainid opcode value", long, default_value = "1")] - pub chain_id: u64, - - #[clap(help = "the tx.gasprice value during EVM execution", long, default_value = "0")] - pub gas_price: u64, - - #[clap(help = "the base fee in a block", long, default_value = "0")] - pub block_base_fee_per_gas: u64, - - #[clap( - help = "the tx.origin value during EVM execution", - long, - default_value = "0x0000000000000000000000000000000000000000" - )] - pub tx_origin: Address, - - #[clap( - help = "the block.coinbase value during EVM execution", - long, - default_value = "0x0000000000000000000000000000000000000000" - )] - pub block_coinbase: Address, - #[clap( - help = "the block.timestamp value during EVM execution", - long, - default_value = "0", - env = "DAPP_TEST_TIMESTAMP" - )] - pub block_timestamp: u64, - - #[clap(help = "the block.number value during EVM execution", long, default_value = "0")] - #[clap(env = "DAPP_TEST_NUMBER")] - pub block_number: u64, - - #[clap(help = "the block.difficulty value during EVM execution", long, default_value = "0")] - pub block_difficulty: u64, - - #[clap(help = "the block.gaslimit value during EVM execution", long)] - pub block_gas_limit: Option, - // TODO: Add configuration option for base fee. -} - -impl Env { - #[cfg(feature = "sputnik-evm")] - pub fn sputnik_state(&self) -> MemoryVicinity { - MemoryVicinity { - chain_id: self.chain_id.into(), - - gas_price: self.gas_price.into(), - origin: self.tx_origin, - - block_coinbase: self.block_coinbase, - block_number: self.block_number.into(), - block_timestamp: self.block_timestamp.into(), - block_difficulty: self.block_difficulty.into(), - block_base_fee_per_gas: self.block_base_fee_per_gas.into(), - block_gas_limit: self.block_gas_limit.unwrap_or(self.gas_limit).into(), - block_hashes: Vec::new(), - } - } - - #[cfg(feature = "evmodin-evm")] - pub fn evmodin_state(&self) -> MockedHost { - let mut host = MockedHost::default(); - - host.tx_context.chain_id = self.chain_id.into(); - host.tx_context.tx_gas_price = self.gas_price.into(); - host.tx_context.tx_origin = self.tx_origin; - host.tx_context.block_coinbase = self.block_coinbase; - host.tx_context.block_number = self.block_number; - host.tx_context.block_timestamp = self.block_timestamp; - host.tx_context.block_difficulty = self.block_difficulty.into(); - host.tx_context.block_gas_limit = self.block_gas_limit.unwrap_or(self.gas_limit); - - host - } -} diff --git a/cli/src/cmd/run.rs b/cli/src/cmd/run.rs index e33040fd1057..8242a2667a40 100644 --- a/cli/src/cmd/run.rs +++ b/cli/src/cmd/run.rs @@ -1,7 +1,4 @@ -use crate::{ - cmd::{build::BuildArgs, compile, manual_compile, Cmd}, - opts::forge::EvmOpts, -}; +use crate::cmd::{build::BuildArgs, compile, manual_compile, Cmd}; use clap::{Parser, ValueHint}; use ethers::abi::Abi; use forge::ContractRunner; @@ -14,13 +11,15 @@ use ethers::solc::{ MinimalCombinedArtifacts, Project, ProjectPathsConfig, SolcConfig, }; -use evm_adapters::Evm; - use ansi_term::Colour; use ethers::{ prelude::{artifacts::ContractBytecode, Artifact}, solc::artifacts::{CompactContractSome, ContractBytecodeSome}, }; +use evm_adapters::{ + evm_opts::{BackendKind, EvmOpts}, + sputnik::{cheatcodes::debugger::DebugArena, helpers::vm}, +}; #[derive(Debug, Clone, Parser)] pub struct RunArgs { @@ -79,25 +78,40 @@ impl Cmd for RunArgs { let CompactContractSome { abi, bin, .. } = contract; // this should never fail if compilation was successful let bytecode = bin.into_bytes().unwrap(); - - // 2. instantiate the EVM w forked backend if needed / pre-funded account(s) - let mut cfg = crate::utils::sputnik_cfg(self.opts.compiler.evm_version); - let vicinity = self.evm_opts.vicinity()?; - let mut evm = crate::utils::sputnik_helpers::evm(&evm_opts, &mut cfg, &vicinity)?; - - // 3. deploy the contract - let (addr, _, _, logs) = evm.deploy(self.evm_opts.sender, bytecode, 0u32.into())?; - - // 4. set up the runner - let mut runner = - ContractRunner::new(&mut evm, &abi, addr, Some(self.evm_opts.sender), &logs); - - // 5. run the test function & potentially the setup let needs_setup = abi.functions().any(|func| func.name == "setUp"); - let result = runner.run_test(&func, needs_setup, Some(&known_contracts))?; - if self.evm_opts.debug { - // 6. Boot up debugger + let cfg = crate::utils::sputnik_cfg(&self.opts.compiler.evm_version); + let vicinity = evm_opts.vicinity()?; + let backend = evm_opts.backend(&vicinity)?; + + // need to match on the backend type + let result = match backend { + BackendKind::Simple(ref backend) => { + let runner = ContractRunner::new( + &evm_opts, + &cfg, + backend, + &abi, + bytecode, + Some(evm_opts.sender), + ); + runner.run_test(&func, needs_setup, Some(&known_contracts))? + } + BackendKind::Shared(ref backend) => { + let runner = ContractRunner::new( + &evm_opts, + &cfg, + backend, + &abi, + bytecode, + Some(evm_opts.sender), + ); + runner.run_test(&func, needs_setup, Some(&known_contracts))? + } + }; + + if evm_opts.debug { + // 4. Boot up debugger let source_code: BTreeMap = sources .iter() .map(|(id, path)| { @@ -123,7 +137,7 @@ impl Cmd for RunArgs { }) .collect(); - let calls = evm.debug_calls(); + let calls: Vec = result.debug_calls.expect("Debug must be enabled by now"); println!("debugging"); let index = if needs_setup && calls.len() > 1 { 1 } else { 0 }; let mut flattened = Vec::new(); @@ -149,14 +163,14 @@ impl Cmd for RunArgs { if evm_opts.verbosity > 4 || !result.success { // print setup calls as well traces.iter().for_each(|trace| { - trace.pretty_print(0, &known_contracts, &mut ident, runner.evm, ""); + trace.pretty_print(0, &known_contracts, &mut ident, &vm(), ""); }); } else if !traces.is_empty() { traces.last().expect("no last but not empty").pretty_print( 0, &known_contracts, &mut ident, - runner.evm, + &vm(), "", ); } @@ -165,7 +179,7 @@ impl Cmd for RunArgs { println!(); } } else { - // 6. print the result nicely + // 5. print the result nicely if result.success { println!("{}", Colour::Green.paint("Script ran successfully.")); } else { diff --git a/cli/src/cmd/test.rs b/cli/src/cmd/test.rs index e58669300b3f..0fd8f4960f4d 100644 --- a/cli/src/cmd/test.rs +++ b/cli/src/cmd/test.rs @@ -1,16 +1,10 @@ //! Test command -use crate::{ - cmd::{ - build::{BuildArgs, EvmType}, - Cmd, - }, - opts::forge::EvmOpts, - utils, -}; +use crate::cmd::{build::BuildArgs, Cmd}; use ansi_term::Colour; use clap::{AppSettings, Parser}; use ethers::solc::{ArtifactOutput, Project}; +use evm_adapters::{evm_opts::EvmOpts, sputnik::helpers::vm}; use forge::{MultiContractRunnerBuilder, TestFilter}; use std::collections::BTreeMap; @@ -118,35 +112,16 @@ impl Cmd for TestArgs { let project = opts.project()?; // prepare the test builder + let mut evm_cfg = crate::utils::sputnik_cfg(&opts.compiler.evm_version); + evm_cfg.create_contract_limit = None; + let builder = MultiContractRunnerBuilder::default() .fuzzer(fuzzer) .initial_balance(evm_opts.initial_balance) + .evm_cfg(evm_cfg) .sender(evm_opts.sender); - // run the tests depending on the chosen EVM - match evm_opts.evm_type { - #[cfg(feature = "sputnik-evm")] - EvmType::Sputnik => { - let mut cfg = utils::sputnik_cfg(opts.compiler.evm_version); - let vicinity = evm_opts.vicinity()?; - let evm = utils::sputnik_helpers::evm(&evm_opts, &mut cfg, &vicinity)?; - test(builder, project, evm, filter, json, evm_opts.verbosity, allow_failure) - } - #[cfg(feature = "evmodin-evm")] - EvmType::EvmOdin => { - use evm_adapters::evmodin::EvmOdin; - use evmodin::tracing::NoopTracer; - - let revision = utils::evmodin_cfg(opts.compiler.evm_version); - - // TODO: Replace this with a proper host. We'll want this to also be - // provided generically when we add the Forking host(s). - let host = evm_opts.env.evmodin_state(); - - let evm = EvmOdin::new(host, evm_opts.env.gas_limit, revision, NoopTracer); - test(builder, project, evm, filter, json, evm_opts.verbosity, allow_failure) - } - } + test(builder, project, evm_opts, filter, json, allow_failure) } } @@ -221,16 +196,16 @@ impl TestOutcome { } /// Runs all the tests -fn test>( +fn test( builder: MultiContractRunnerBuilder, project: Project, - evm: E, + evm_opts: EvmOpts, filter: Filter, json: bool, - verbosity: u8, allow_failure: bool, ) -> eyre::Result { - let mut runner = builder.build(project, evm)?; + let verbosity = evm_opts.verbosity; + let mut runner = builder.build(project, evm_opts)?; let results = runner.test(&filter)?; @@ -301,7 +276,7 @@ fn test>( 0, &runner.known_contracts, &mut ident, - &runner.evm, + &vm(), "", ); }); @@ -310,7 +285,7 @@ fn test>( 0, &runner.known_contracts, &mut ident, - &runner.evm, + &vm(), "", ); } diff --git a/cli/src/opts/forge.rs b/cli/src/opts/forge.rs index e8977868c72b..19e0d720bd88 100644 --- a/cli/src/opts/forge.rs +++ b/cli/src/opts/forge.rs @@ -3,7 +3,9 @@ use clap::{Parser, Subcommand, ValueHint}; use ethers::{solc::EvmVersion, types::Address}; use std::{path::PathBuf, str::FromStr}; -use crate::cmd::{build::BuildArgs, create::CreateArgs, run::RunArgs, snapshot, test}; +use crate::cmd::{ + build::BuildArgs, create::CreateArgs, remappings::RemappingArgs, run::RunArgs, snapshot, test, +}; #[derive(Debug, Parser)] pub struct Opts { @@ -110,84 +112,6 @@ pub struct CompilerArgs { pub optimize_runs: u32, } -use crate::cmd::{ - build::{Env, EvmType}, - remappings::RemappingArgs, -}; -use ethers::types::U256; - -#[derive(Debug, Clone, Parser)] -pub struct EvmOpts { - #[clap(flatten)] - pub env: Env, - - #[clap( - long, - short, - help = "the EVM type you want to use (e.g. sputnik, evmodin)", - default_value = "sputnik" - )] - pub evm_type: EvmType, - - #[clap(help = "fetch state over a remote instead of starting from empty state", long, short)] - #[clap(alias = "rpc-url")] - pub fork_url: Option, - - #[clap(help = "pins the block number for the state fork", long)] - #[clap(env = "DAPP_FORK_BLOCK")] - pub fork_block_number: Option, - - #[clap( - help = "the initial balance of each deployed test contract", - long, - default_value = "0xffffffffffffffffffffffff" - )] - pub initial_balance: U256, - - #[clap( - help = "the address which will be executing all tests", - long, - default_value = "0x0000000000000000000000000000000000000000", - env = "DAPP_TEST_ADDRESS" - )] - pub sender: Address, - - #[clap(help = "enables the FFI cheatcode", long)] - pub ffi: bool, - - #[clap( - help = r#"Verbosity mode of EVM output as number of occurrences of the `v` flag (-v, -vv, -vvv, etc.) - 3: print test trace for failing tests - 4: always print test trace, print setup for failing tests - 5: always print test trace and setup -"#, - long, - short, - parse(from_occurrences) - )] - pub verbosity: u8, - - #[clap(help = "enable debugger", long)] - pub debug: bool, -} - -impl EvmOpts { - #[cfg(feature = "sputnik-evm")] - pub fn vicinity(&self) -> eyre::Result { - Ok(if let Some(ref url) = self.fork_url { - let provider = ethers::providers::Provider::try_from(url.as_str())?; - let rt = tokio::runtime::Runtime::new().expect("could not start tokio rt"); - rt.block_on(evm_adapters::sputnik::vicinity( - &provider, - self.fork_block_number, - Some(self.env.tx_origin), - ))? - } else { - self.env.sputnik_state() - }) - } -} - /// Represents the common dapp argument pattern for `:` where `:` is /// optional. #[derive(Clone, Debug)] diff --git a/cli/src/utils.rs b/cli/src/utils.rs index 089b1781dc22..2a19f676021a 100644 --- a/cli/src/utils.rs +++ b/cli/src/utils.rs @@ -1,9 +1,5 @@ -use ethers::{ - providers::Provider, - solc::{artifacts::Contract, EvmVersion}, -}; +use ethers::solc::{artifacts::Contract, EvmVersion}; -use evm_adapters::sputnik::Executor; use eyre::{ContextCompat, WrapErr}; use std::{env::VarError, path::PathBuf, process::Command}; @@ -12,8 +8,6 @@ use evmodin::Revision; #[cfg(feature = "sputnik-evm")] use sputnik::Config; -use crate::opts::forge::EvmOpts; - /// Default local RPC endpoint const LOCAL_RPC_URL: &str = "http://127.0.0.1:8545"; @@ -89,7 +83,7 @@ pub fn find_git_root_path() -> eyre::Result { } #[cfg(feature = "sputnik-evm")] -pub fn sputnik_cfg(evm: EvmVersion) -> Config { +pub fn sputnik_cfg(evm: &EvmVersion) -> Config { match evm { EvmVersion::Istanbul => Config::istanbul(), EvmVersion::Berlin => Config::berlin(), @@ -98,61 +92,8 @@ pub fn sputnik_cfg(evm: EvmVersion) -> Config { } } -#[cfg(feature = "sputnik-evm")] -pub mod sputnik_helpers { - use super::*; - use ethers::types::U256; - use sputnik::{ - backend::{Backend, MemoryBackend, MemoryVicinity}, - Config, - }; - use std::sync::Arc; - - use evm_adapters::{ - sputnik::{helpers::TestSputnikVM, ForkMemoryBackend, PRECOMPILES_MAP}, - FAUCET_ACCOUNT, - }; - - /// Creates a new Sputnik EVM given the [`EvmOpts`] (specifying whether to fork or not), a VM - /// Hard Fork config, and the initial state from the memory vicinity. - pub fn evm<'a>( - opts: &EvmOpts, - cfg: &'a mut Config, - vicinity: &'a MemoryVicinity, - ) -> eyre::Result>>> { - // We disable the contract size limit by default, because Solidity - // test smart contracts are likely to be >24kb - cfg.create_contract_limit = None; - - let mut backend = MemoryBackend::new(vicinity, Default::default()); - // max out the balance of the faucet - let faucet = backend.state_mut().entry(*FAUCET_ACCOUNT).or_insert_with(Default::default); - faucet.balance = U256::MAX; - - let backend: Box = if let Some(ref url) = opts.fork_url { - let provider = Provider::try_from(url.as_str())?; - let init_state = backend.state().clone(); - let backend = - ForkMemoryBackend::new(provider, backend, opts.fork_block_number, init_state); - Box::new(backend) - } else { - Box::new(backend) - }; - let backend = Arc::new(backend); - - Ok(Executor::new_with_cheatcodes( - backend, - opts.env.gas_limit, - cfg, - &*PRECOMPILES_MAP, - opts.ffi, - opts.verbosity > 2, - opts.debug, - )) - } -} - #[cfg(feature = "evmodin-evm")] +#[allow(dead_code)] pub fn evmodin_cfg(evm: EvmVersion) -> Revision { match evm { EvmVersion::Istanbul => Revision::Istanbul, diff --git a/evm-adapters/Cargo.toml b/evm-adapters/Cargo.toml index 66b64de90820..35ad56b275b1 100644 --- a/evm-adapters/Cargo.toml +++ b/evm-adapters/Cargo.toml @@ -11,7 +11,7 @@ foundry-utils = { path = "./../utils" } sputnik = { package = "evm", git = "https://github.com/rust-blockchain/evm", optional = true, features = ["tracing"] } -evmodin = { git = "https://github.com/vorot93/evmodin", optional = true } +evmodin = { git = "https://github.com/vorot93/evmodin", optional = true, features = ["util"] } ethers = { git = "https://github.com/gakonst/ethers-rs", default-features = false, features = ["solc-full"] } eyre = "0.6.5" @@ -29,6 +29,15 @@ serde_json = "1.0.72" serde = "1.0.130" ansi_term = "0.12.1" +# for evm config to be usable in clis +clap = { version = "3.0.6", features = [ + "derive", + "env", + "unicode", + "wrap_help", +] } +clap_complete = "3.0.2" + [dev-dependencies] evmodin = { git = "https://github.com/vorot93/evmodin", features = ["util"] } ethers = { git = "https://github.com/gakonst/ethers-rs", default-features = false, features = ["solc-full", "solc-tests"] } diff --git a/evm-adapters/src/evm_opts.rs b/evm-adapters/src/evm_opts.rs new file mode 100644 index 000000000000..061580ed2d17 --- /dev/null +++ b/evm-adapters/src/evm_opts.rs @@ -0,0 +1,254 @@ +use clap::Parser; +use ethers::types::{Address, U256}; +use std::str::FromStr; + +#[cfg(feature = "evmodin")] +use evmodin::util::mocked_host::MockedHost; +#[cfg(feature = "sputnik")] +use sputnik::backend::MemoryVicinity; + +#[derive(Clone, Debug)] +pub enum EvmType { + #[cfg(feature = "sputnik")] + Sputnik, + #[cfg(feature = "evmodin")] + EvmOdin, +} + +#[cfg(any(feature = "sputnik", feature = "evmodin"))] +impl Default for EvmType { + fn default() -> Self { + // if sputnik is enabled, default to it + #[cfg(feature = "sputnik")] + #[rustfmt::skip] + return EvmType::Sputnik; + // if not, fall back to evmodin + #[allow(unreachable_code)] + #[cfg(feature = "evmodin")] + EvmType::EvmOdin + } +} + +impl FromStr for EvmType { + type Err = eyre::Error; + + fn from_str(s: &str) -> Result { + // silence this warning which indicates that if no evm features are + // enabled, the Ok(...) will never be reached. + #[allow(unreachable_code)] + Ok(match s.to_lowercase().as_str() { + #[cfg(feature = "sputnik")] + "sputnik" => EvmType::Sputnik, + #[cfg(feature = "evmodin")] + "evmodin" => EvmType::EvmOdin, + other => eyre::bail!("unknown EVM type {}", other), + }) + } +} + +#[derive(Debug, Clone, Parser)] +#[cfg_attr(any(feature = "sputnik", feature = "evmodin"), derive(Default))] +pub struct EvmOpts { + #[clap(flatten)] + pub env: Env, + + #[clap( + long, + short, + help = "the EVM type you want to use (e.g. sputnik, evmodin)", + default_value = "sputnik" + )] + pub evm_type: EvmType, + + #[clap(help = "fetch state over a remote instead of starting from empty state", long, short)] + #[clap(alias = "rpc-url")] + pub fork_url: Option, + + #[clap(help = "pins the block number for the state fork", long)] + #[clap(env = "DAPP_FORK_BLOCK")] + pub fork_block_number: Option, + + #[clap( + help = "the initial balance of each deployed test contract", + long, + default_value = "0xffffffffffffffffffffffff" + )] + pub initial_balance: U256, + + #[clap( + help = "the address which will be executing all tests", + long, + default_value = "0x00a329c0648769A73afAc7F9381E08FB43dBEA72", + env = "DAPP_TEST_CALLER" + )] + pub sender: Address, + + #[clap(help = "enables the FFI cheatcode", long)] + pub ffi: bool, + + #[clap( + help = r#"Verbosity mode of EVM output as number of occurences of the `v` flag (-v, -vv, -vvv, etc.) + 3: print test trace for failing tests + 4: always print test trace, print setup for failing tests + 5: always print test trace and setup +"#, + long, + short, + parse(from_occurrences) + )] + pub verbosity: u8, + + #[clap(help = "enable debugger", long)] + pub debug: bool, +} + +#[cfg(feature = "sputnik")] +pub use sputnik_helpers::BackendKind; + +// Helper functions for sputnik +#[cfg(feature = "sputnik")] +mod sputnik_helpers { + use super::*; + + use crate::{sputnik::cache::SharedBackend, FAUCET_ACCOUNT}; + use ::sputnik::backend::MemoryBackend; + use ethers::providers::Provider; + + pub enum BackendKind<'a> { + Simple(MemoryBackend<'a>), + Shared(SharedBackend), + } + + impl EvmOpts { + #[cfg(feature = "sputnik")] + pub fn backend<'a>( + &'a self, + vicinity: &'a MemoryVicinity, + ) -> eyre::Result> { + let mut backend = MemoryBackend::new(vicinity, Default::default()); + // max out the balance of the faucet + let faucet = + backend.state_mut().entry(*FAUCET_ACCOUNT).or_insert_with(Default::default); + faucet.balance = U256::MAX; + + let backend = if let Some(ref url) = self.fork_url { + let provider = Provider::try_from(url.as_str())?; + let init_state = backend.state().clone(); + let cache = crate::sputnik::new_shared_cache(init_state); + let backend = SharedBackend::new( + provider, + cache, + vicinity.clone(), + self.fork_block_number.map(Into::into), + ); + BackendKind::Shared(backend) + } else { + BackendKind::Simple(backend) + }; + + Ok(backend) + } + + #[cfg(feature = "sputnik")] + pub fn vicinity(&self) -> eyre::Result { + Ok(if let Some(ref url) = self.fork_url { + let provider = ethers::providers::Provider::try_from(url.as_str())?; + let rt = tokio::runtime::Runtime::new().expect("could not start tokio rt"); + rt.block_on(crate::sputnik::vicinity( + &provider, + self.fork_block_number, + Some(self.env.tx_origin), + ))? + } else { + self.env.sputnik_state() + }) + } + } +} + +#[derive(Debug, Clone, Default, Parser)] +pub struct Env { + // structopt does not let use `u64::MAX`: + // https://doc.rust-lang.org/std/primitive.u64.html#associatedconstant.MAX + #[clap(help = "the block gas limit", long, default_value = "18446744073709551615")] + pub gas_limit: u64, + + #[clap(help = "the chainid opcode value", long, default_value = "1")] + pub chain_id: u64, + + #[clap(help = "the tx.gasprice value during EVM execution", long, default_value = "0")] + pub gas_price: u64, + + #[clap(help = "the base fee in a block", long, default_value = "0")] + pub block_base_fee_per_gas: u64, + + #[clap( + help = "the tx.origin value during EVM execution", + long, + default_value = "0x00a329c0648769A73afAc7F9381E08FB43dBEA72", + env = "DAPP_TEST_ORIGIN" + )] + pub tx_origin: Address, + + #[clap( + help = "the block.coinbase value during EVM execution", + long, + // TODO: It'd be nice if we could use Address::zero() here. + default_value = "0x0000000000000000000000000000000000000000" + )] + pub block_coinbase: Address, + #[clap( + help = "the block.timestamp value during EVM execution", + long, + default_value = "0", + env = "DAPP_TEST_TIMESTAMP" + )] + pub block_timestamp: u64, + + #[clap(help = "the block.number value during EVM execution", long, default_value = "0")] + #[clap(env = "DAPP_TEST_NUMBER")] + pub block_number: u64, + + #[clap(help = "the block.difficulty value during EVM execution", long, default_value = "0")] + pub block_difficulty: u64, + + #[clap(help = "the block.gaslimit value during EVM execution", long)] + pub block_gas_limit: Option, + // TODO: Add configuration option for base fee. +} + +impl Env { + #[cfg(feature = "sputnik")] + pub fn sputnik_state(&self) -> MemoryVicinity { + MemoryVicinity { + chain_id: self.chain_id.into(), + + gas_price: self.gas_price.into(), + origin: self.tx_origin, + + block_coinbase: self.block_coinbase, + block_number: self.block_number.into(), + block_timestamp: self.block_timestamp.into(), + block_difficulty: self.block_difficulty.into(), + block_base_fee_per_gas: self.block_base_fee_per_gas.into(), + block_gas_limit: self.block_gas_limit.unwrap_or(self.gas_limit).into(), + block_hashes: Vec::new(), + } + } + + #[cfg(feature = "evmodin")] + pub fn evmodin_state(&self) -> MockedHost { + let mut host = MockedHost::default(); + + host.tx_context.chain_id = self.chain_id.into(); + host.tx_context.tx_gas_price = self.gas_price.into(); + host.tx_context.tx_origin = self.tx_origin; + host.tx_context.block_coinbase = self.block_coinbase; + host.tx_context.block_number = self.block_number; + host.tx_context.block_timestamp = self.block_timestamp; + host.tx_context.block_difficulty = self.block_difficulty.into(); + host.tx_context.block_gas_limit = self.block_gas_limit.unwrap_or(self.gas_limit); + + host + } +} diff --git a/evm-adapters/src/fuzz.rs b/evm-adapters/src/fuzz.rs index 8a23fe477e35..ebb40a45bde1 100644 --- a/evm-adapters/src/fuzz.rs +++ b/evm-adapters/src/fuzz.rs @@ -30,6 +30,10 @@ pub struct FuzzedExecutor<'a, E, S> { } impl<'a, S, E: Evm> FuzzedExecutor<'a, E, S> { + pub fn into_inner(self) -> &'a mut E { + self.evm.into_inner() + } + /// Returns a mutable reference to the fuzzer's internal EVM instance pub fn as_mut(&self) -> RefMut<'_, &'a mut E> { self.evm.borrow_mut() diff --git a/evm-adapters/src/lib.rs b/evm-adapters/src/lib.rs index cbbdae27df82..ba288618a647 100644 --- a/evm-adapters/src/lib.rs +++ b/evm-adapters/src/lib.rs @@ -18,6 +18,9 @@ pub mod fuzz; pub mod call_tracing; +/// Helpers for easily constructing EVM objects. +pub mod evm_opts; + use ethers::{ abi::{Detokenize, Tokenize}, contract::{decode_function_data, encode_function_data}, diff --git a/evm-adapters/src/sputnik/evm.rs b/evm-adapters/src/sputnik/evm.rs index 5eeb08634f50..ff606ef46edc 100644 --- a/evm-adapters/src/sputnik/evm.rs +++ b/evm-adapters/src/sputnik/evm.rs @@ -218,18 +218,18 @@ pub mod helpers { CheatcodeStackExecutor<'a, 'a, B, BTreeMap>, >; - static CFG: Lazy = Lazy::new(Config::london); + pub static CFG: Lazy = Lazy::new(Config::london); /// London config without a contract size limit. Useful for testing but is a depature from /// mainnet rules. - static CFG_NO_LMT: Lazy = Lazy::new(|| { + pub static CFG_NO_LMT: Lazy = Lazy::new(|| { let mut cfg = Config::london(); cfg.create_contract_limit = None; cfg }); - static VICINITY: Lazy = Lazy::new(new_vicinity); - const GAS_LIMIT: u64 = 30_000_000; + pub static VICINITY: Lazy = Lazy::new(new_vicinity); + pub const GAS_LIMIT: u64 = 30_000_000; /// Instantiates a Sputnik EVM with enabled cheatcodes + FFI and a simple non-forking in memory /// backend and tracing disabled diff --git a/forge/Cargo.toml b/forge/Cargo.toml index 6fdf1ef33ced..b14337dec8a7 100644 --- a/forge/Cargo.toml +++ b/forge/Cargo.toml @@ -8,11 +8,10 @@ license = "MIT OR Apache-2.0" [dependencies] foundry-utils = { path = "./../utils" } -evm-adapters = { path = "./../evm-adapters", default-features = false } +evm-adapters = { path = "./../evm-adapters", features = ["sputnik", "sputnik-helpers"] } # ethers = { version = "0.5.2" } ethers = { git = "https://github.com/gakonst/ethers-rs", default-features = false, features = ["solc-full"] } - eyre = "0.6.5" semver = "1.0.4" serde_json = "1.0.67" @@ -25,9 +24,13 @@ tokio = { version = "1.10.1" } tracing = "0.1.26" tracing-subscriber = "0.2.20" proptest = "1.0.0" +rayon = "1.5" + +# load sputnik for parallel evm usage +sputnik = { package = "evm", git = "https://github.com/rust-blockchain/evm" } +# For now we ignore evmodin in Forge. +# evmodin = { git = "https://github.com/vorot93/evmodin" } [dev-dependencies] -evm-adapters = { path = "./../evm-adapters", features = ["sputnik", "sputnik-helpers", "evmodin", "evmodin-helpers"] } evmodin = { git = "https://github.com/vorot93/evmodin", features = ["util"] } -evm = { git = "https://github.com/rust-blockchain/evm" } ethers = { git = "https://github.com/gakonst/ethers-rs", default-features = false, features = ["solc-full", "solc-tests"] } diff --git a/forge/src/lib.rs b/forge/src/lib.rs index 89c93703c848..a62df5af68e6 100644 --- a/forge/src/lib.rs +++ b/forge/src/lib.rs @@ -15,8 +15,15 @@ pub mod test_helpers { use ethers::{ prelude::Lazy, solc::{CompilerOutput, Project, ProjectPathsConfig}, + types::U256, + }; + use evm_adapters::{ + evm_opts::{Env, EvmOpts, EvmType}, + sputnik::helpers::VICINITY, + FAUCET_ACCOUNT, }; use regex::Regex; + use sputnik::backend::MemoryBackend; pub static COMPILED: Lazy = Lazy::new(|| { // NB: should we add a test-helper function that makes creating these @@ -27,6 +34,21 @@ pub mod test_helpers { project.compile().unwrap().output() }); + pub static EVM_OPTS: Lazy = Lazy::new(|| EvmOpts { + env: Env { gas_limit: 18446744073709551615, chain_id: 1, ..Default::default() }, + initial_balance: U256::MAX, + evm_type: EvmType::Sputnik, + ..Default::default() + }); + + pub static BACKEND: Lazy> = Lazy::new(|| { + let mut backend = MemoryBackend::new(&*VICINITY, Default::default()); + // max out the balance of the faucet + let faucet = backend.state_mut().entry(*FAUCET_ACCOUNT).or_insert_with(Default::default); + faucet.balance = U256::MAX; + backend + }); + pub struct Filter { test_regex: Regex, contract_regex: Regex, diff --git a/forge/src/multi_runner.rs b/forge/src/multi_runner.rs index cd3c1dfbf0b6..90c2e452bf2a 100644 --- a/forge/src/multi_runner.rs +++ b/forge/src/multi_runner.rs @@ -1,9 +1,9 @@ use crate::{runner::TestResult, ContractRunner, TestFilter}; +use evm_adapters::evm_opts::{BackendKind, EvmOpts}; +use sputnik::{backend::Backend, Config}; use ethers::solc::Artifact; -use evm_adapters::Evm; - use ethers::{ abi::Abi, prelude::ArtifactOutput, @@ -14,7 +14,8 @@ use ethers::{ use proptest::test_runner::TestRunner; use eyre::Result; -use std::{collections::BTreeMap, marker::PhantomData}; +use rayon::prelude::*; +use std::collections::BTreeMap; /// Builder used for instantiating the multi-contract runner #[derive(Debug, Default)] @@ -26,20 +27,17 @@ pub struct MultiContractRunnerBuilder { pub sender: Option
, /// The initial balance for each one of the deployed smart contracts pub initial_balance: U256, + /// The EVM Configuration to use + pub evm_cfg: Option, } impl MultiContractRunnerBuilder { /// Given an EVM, proceeds to return a runner which is able to execute all tests /// against that evm - pub fn build( - self, - project: Project, - mut evm: E, - ) -> Result> + pub fn build(self, project: Project, evm_opts: EvmOpts) -> Result where // TODO: Can we remove the static? It's due to the `into_artifacts()` call below A: ArtifactOutput + 'static, - E: Evm, { println!("compiling..."); let output = project.compile()?; @@ -52,14 +50,11 @@ impl MultiContractRunnerBuilder { println!("success."); } - let sender = self.sender.unwrap_or_default(); - let initial_balance = self.initial_balance; - // This is just the contracts compiled, but we need to merge this with the read cached // artifacts let contracts = output.into_artifacts(); let mut known_contracts: BTreeMap)> = Default::default(); - let mut deployed_contracts: BTreeMap)> = + let mut deployable_contracts: BTreeMap = Default::default(); for (fname, contract) in contracts { @@ -70,15 +65,7 @@ impl MultiContractRunnerBuilder { continue } - if abi.constructor.as_ref().map(|c| c.inputs.is_empty()).unwrap_or(true) && - abi.functions().any(|func| func.name.starts_with("test")) - { - let span = tracing::trace_span!("deploying", ?fname); - let _enter = span.enter(); - let (addr, _, _, logs) = evm.deploy(sender, bytecode.clone(), 0u32.into())?; - evm.set_balance(addr, initial_balance); - deployed_contracts.insert(fname.clone(), (abi.clone(), addr, logs)); - } + deployable_contracts.insert(fname.clone(), (abi.clone(), bytecode.clone())); let split = fname.split(':').collect::>(); let contract_name = if split.len() > 1 { split[1] } else { split[0] }; @@ -89,11 +76,11 @@ impl MultiContractRunnerBuilder { } Ok(MultiContractRunner { - contracts: deployed_contracts, + contracts: deployable_contracts, known_contracts, identified_contracts: Default::default(), - evm, - state: PhantomData, + evm_opts, + evm_cfg: self.evm_cfg.unwrap_or_else(Config::london), sender: self.sender, fuzzer: self.fuzzer, }) @@ -116,46 +103,57 @@ impl MultiContractRunnerBuilder { self.fuzzer = Some(fuzzer); self } + + #[must_use] + pub fn evm_cfg(mut self, evm_cfg: Config) -> Self { + self.evm_cfg = Some(evm_cfg); + self + } } /// A multi contract runner receives a set of contracts deployed in an EVM instance and proceeds /// to run all test functions in these contracts. -pub struct MultiContractRunner { - /// Mapping of contract name to compiled bytecode, deployed address and logs emitted during - /// deployment - pub contracts: BTreeMap)>, +pub struct MultiContractRunner { + /// Mapping of contract name to Abi and creation bytecode + pub contracts: BTreeMap, /// Compiled contracts by name that have an Abi and runtime bytecode pub known_contracts: BTreeMap)>, /// Identified contracts by test pub identified_contracts: BTreeMap>, /// The EVM instance used in the test runner - pub evm: E, + pub evm_opts: EvmOpts, + /// The EVM revision config + pub evm_cfg: Config, /// The fuzzer which will be used to run parametric tests (w/ non-0 solidity args) fuzzer: Option, /// The address which will be used as the `from` field in all EVM calls sender: Option
, - /// Market type for the EVM state being used - state: PhantomData, } -impl MultiContractRunner -where - E: Evm, - S: Clone, -{ +impl MultiContractRunner { pub fn test( &mut self, - filter: &impl TestFilter, + filter: &(impl TestFilter + Send + Sync), ) -> Result>> { // TODO: Convert to iterator, ideally parallel one? let contracts = std::mem::take(&mut self.contracts); - let init_state: S = self.evm.state().clone(); + let vicinity = self.evm_opts.vicinity()?; + let backend = self.evm_opts.backend(&vicinity)?; + let results = contracts - .iter() + .par_iter() .filter(|(name, _)| filter.matches_contract(name)) - .map(|(name, (abi, address, logs))| { - let result = self.run_tests(name, abi, *address, logs, filter, &init_state)?; + .map(|(name, (abi, deploy_code))| { + // unavoidable duplication here? + let result = match backend { + BackendKind::Simple(ref backend) => { + self.run_tests(name, abi, backend, deploy_code.clone(), filter)? + } + BackendKind::Shared(ref backend) => { + self.run_tests(name, abi, backend, deploy_code.clone(), filter)? + } + }; Ok((name.clone(), result)) }) .filter_map(|x: Result<_>| x.ok()) @@ -174,25 +172,30 @@ where err, fields(name = %_name) )] - fn run_tests( - &mut self, + fn run_tests( + &self, _name: &str, contract: &Abi, - address: Address, - init_logs: &[String], + backend: &B, + deploy_code: ethers::prelude::Bytes, filter: &impl TestFilter, - init_state: &S, ) -> Result> { - let mut runner = - ContractRunner::new(&mut self.evm, contract, address, self.sender, init_logs); - runner.run_tests(filter, self.fuzzer.as_mut(), init_state, Some(&self.known_contracts)) + let runner = ContractRunner::new( + &self.evm_opts, + &self.evm_cfg, + backend, + contract, + deploy_code, + self.sender, + ); + runner.run_tests(filter, self.fuzzer.clone(), Some(&self.known_contracts)) } } #[cfg(test)] mod tests { use super::*; - use crate::test_helpers::Filter; + use crate::test_helpers::{Filter, EVM_OPTS}; use ethers::solc::ProjectPathsConfig; use std::path::PathBuf; @@ -212,12 +215,12 @@ mod tests { .unwrap() } - fn runner>(evm: E) -> MultiContractRunner { - MultiContractRunnerBuilder::default().build(project(), evm).unwrap() + fn runner() -> MultiContractRunner { + MultiContractRunnerBuilder::default().build(project(), EVM_OPTS.clone()).unwrap() } - fn test_multi_runner>(evm: E) { - let mut runner = runner(evm); + fn test_multi_runner() { + let mut runner = runner(); let results = runner.test(&Filter::new(".*", ".*")).unwrap(); // 8 contracts being built @@ -240,8 +243,8 @@ mod tests { assert!(only_gm["GmTest.json:GmTest"]["testGm()"].success); } - fn test_abstract_contract>(evm: E) { - let mut runner = runner(evm); + fn test_abstract_contract() { + let mut runner = runner(); let results = runner.test(&Filter::new(".*", ".*")).unwrap(); assert!(results.get("Tests.json:Tests").is_none()); assert!(results.get("ATests.json:ATests").is_some()); @@ -250,14 +253,11 @@ mod tests { mod sputnik { use super::*; - use evm_adapters::sputnik::helpers::vm; use std::collections::HashMap; #[test] fn test_sputnik_debug_logs() { - let evm = vm(); - - let mut runner = runner(evm); + let mut runner = runner(); let results = runner.test(&Filter::new(".*", ".*")).unwrap(); let reasons = results["DebugLogsTest.json:DebugLogsTest"] @@ -289,14 +289,12 @@ mod tests { #[test] fn test_sputnik_multi_runner() { - test_multi_runner(vm()); + test_multi_runner(); } #[test] fn test_sputnik_abstract_contract() { - test_abstract_contract(vm()); + test_abstract_contract(); } } - - // TODO: Add EvmOdin tests once we get the Mocked Host working } diff --git a/forge/src/runner.rs b/forge/src/runner.rs index 0c2501951ab0..a8859928e859 100644 --- a/forge/src/runner.rs +++ b/forge/src/runner.rs @@ -1,19 +1,26 @@ use crate::TestFilter; +use evm_adapters::{ + evm_opts::EvmOpts, + sputnik::{helpers::TestSputnikVM, Executor, PRECOMPILES_MAP}, +}; +use rayon::iter::ParallelIterator; +use sputnik::{backend::Backend, Config}; use ethers::{ abi::{Abi, Function, Token}, types::{Address, Bytes}, }; -use evm_adapters::call_tracing::CallTraceArena; - use evm_adapters::{ + call_tracing::CallTraceArena, fuzz::{FuzzTestResult, FuzzedCases, FuzzedExecutor}, + sputnik::cheatcodes::debugger::DebugArena, Evm, EvmError, }; use eyre::Result; -use std::{collections::BTreeMap, fmt, marker::PhantomData, time::Instant}; +use std::{collections::BTreeMap, fmt, time::Instant}; use proptest::test_runner::{TestError, TestRunner}; +use rayon::iter::IntoParallelRefIterator; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Serialize, Deserialize)] @@ -64,6 +71,10 @@ pub struct TestResult { /// Identified contracts pub identified_contracts: Option>, + + /// Debug Steps + #[serde(skip)] + pub debug_calls: Option>, } impl TestResult { @@ -129,52 +140,67 @@ impl TestKind { } } -pub struct ContractRunner<'a, S, E> { - /// Mutable reference to the EVM type. - /// This is a temporary hack to work around the mutability restrictions of - /// [`proptest::TestRunner::run`] which takes a `Fn` preventing interior mutability. [See also](https://github.com/gakonst/dapptools-rs/pull/44). - /// Wrapping it like that allows the `test` function to gain mutable access regardless and - /// since we don't use any parallelized fuzzing yet the `test` function has exclusive access of - /// the mutable reference over time of its existence. - pub evm: &'a mut E, +pub struct ContractRunner<'a, B> { + // EVM Config Options + /// The options used to instantiate a new EVM. + pub evm_opts: &'a EvmOpts, + /// The backend used by the VM. + pub backend: &'a B, + /// The VM Configuration to use for the runner (London, Berlin , ...) + pub evm_cfg: &'a Config, + + // Contract deployment options /// The deployed contract's ABI pub contract: &'a Abi, /// The deployed contract's address - pub address: Address, + // This is cheap to clone due to [`bytes::Bytes`], so OK to own + pub code: ethers::prelude::Bytes, /// The address which will be used as the `from` field in all EVM calls pub sender: Address, - /// Any logs emitted in the constructor of the specific contract - pub init_logs: &'a [String], - // need to constrain the trait generic - state: PhantomData, } -impl<'a, S, E> ContractRunner<'a, S, E> { +impl<'a, B: Backend> ContractRunner<'a, B> { pub fn new( - evm: &'a mut E, + evm_opts: &'a EvmOpts, + evm_cfg: &'a Config, + backend: &'a B, contract: &'a Abi, - address: Address, + code: ethers::prelude::Bytes, sender: Option
, - init_logs: &'a [String], ) -> Self { - Self { - evm, - contract, - address, - init_logs, - state: PhantomData, - sender: sender.unwrap_or_default(), - } + Self { evm_opts, evm_cfg, backend, contract, code, sender: sender.unwrap_or_default() } } } -impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { +// Require that the backend is Cloneable. This allows us to use the `SharedBackend` from +// evm-adapters which is clone-able. +impl<'a, B: Backend + Clone + Send + Sync> ContractRunner<'a, B> { + /// Creates a new EVM and deploys the test contract inside the runner + /// from the sending account. + pub fn new_sputnik_evm(&'a self) -> eyre::Result<(Address, TestSputnikVM<'a, B>, Vec)> { + // create the EVM, clone the backend. + let mut executor = Executor::new_with_cheatcodes( + self.backend.clone(), + self.evm_opts.env.gas_limit, + self.evm_cfg, + &*PRECOMPILES_MAP, + self.evm_opts.ffi, + self.evm_opts.verbosity > 2, + self.evm_opts.debug, + ); + + // deploy an instance of the contract inside the runner in the EVM + let (addr, _, _, logs) = + executor.deploy(self.sender, self.code.clone(), 0u32.into()).expect("couldn't deploy"); + executor.set_balance(addr, self.evm_opts.initial_balance); + Ok((addr, executor, logs)) + } + /// Runs all tests for a contract whose names match the provided regular expression pub fn run_tests( - &mut self, + &self, filter: &impl TestFilter, - fuzzer: Option<&mut TestRunner>, - init_state: &S, + fuzzer: Option, known_contracts: Option<&BTreeMap)>>, ) -> Result> { tracing::info!("starting tests"); @@ -190,11 +216,9 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { // run all unit tests let unit_tests = test_fns - .iter() + .par_iter() .filter(|func| func.inputs.is_empty()) .map(|func| { - // Before each test run executes, ensure we're at our initial state. - self.evm.reset(init_state.clone()); let result = self.run_test(func, needs_setup, known_contracts)?; Ok((func.signature(), result)) }) @@ -202,10 +226,9 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { let map = if let Some(fuzzer) = fuzzer { let fuzz_tests = test_fns - .iter() + .par_iter() .filter(|func| !func.inputs.is_empty()) .map(|func| { - self.evm.reset(init_state.clone()); let result = self.run_fuzz_test(func, needs_setup, fuzzer.clone(), known_contracts)?; Ok((func.signature(), result)) @@ -229,7 +252,7 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { #[tracing::instrument(name = "test", skip_all, fields(name = %func.signature()))] pub fn run_test( - &mut self, + &self, func: &Function, setup: bool, known_contracts: Option<&BTreeMap)>>, @@ -242,9 +265,9 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { let should_fail = func.name.starts_with("testFail"); tracing::debug!(func = ?func.signature(), should_fail, "unit-testing"); - let mut logs = self.init_logs.to_vec(); + let (address, mut evm, init_logs) = self.new_sputnik_evm()?; - self.evm.reset_traces(); + let mut logs = init_logs; let mut traces: Option> = None; let mut identified_contracts: Option> = None; @@ -252,17 +275,18 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { // call the setup function in each test to reset the test's state. if setup { tracing::trace!("setting up"); - let setup_logs = match self.evm.setup(self.address) { + let setup_logs = match evm.setup(address) { Ok((_reason, setup_logs)) => setup_logs, Err(e) => { // if tracing is enabled, just return it as a failed test // otherwise abort - if self.evm.tracing_enabled() { + if evm.tracing_enabled() { self.update_traces( &mut traces, &mut identified_contracts, known_contracts, setup, + &mut evm, ); } @@ -275,40 +299,46 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { kind: TestKind::Standard(0), traces, identified_contracts, + debug_calls: if evm.state().debug_enabled { + Some(evm.debug_calls()) + } else { + None + }, }) } }; logs.extend_from_slice(&setup_logs); } - let (status, reason, gas_used, logs) = match self.evm.call::<(), _, _>( - self.sender, - self.address, - func.clone(), - (), - 0.into(), - ) { - Ok((_, status, gas_used, execution_logs)) => { - logs.extend(execution_logs); - (status, None, gas_used, logs) - } - Err(err) => match err { - EvmError::Execution { reason, gas_used, logs: execution_logs } => { + let (status, reason, gas_used, logs) = + match evm.call::<(), _, _>(self.sender, address, func.clone(), (), 0.into()) { + Ok((_, status, gas_used, execution_logs)) => { logs.extend(execution_logs); - // add reverted logs - logs.extend(self.evm.all_logs()); - (E::revert(), Some(reason), gas_used, logs) + (status, None, gas_used, logs) } - err => { - tracing::error!(?err); - return Err(err.into()) - } - }, - }; + Err(err) => match err { + EvmError::Execution { reason, gas_used, logs: execution_logs } => { + logs.extend(execution_logs); + // add reverted logs + logs.extend(evm.all_logs()); + (revert(&evm), Some(reason), gas_used, logs) + } + err => { + tracing::error!(?err); + return Err(err.into()) + } + }, + }; - self.update_traces(&mut traces, &mut identified_contracts, known_contracts, setup); + self.update_traces( + &mut traces, + &mut identified_contracts, + known_contracts, + setup, + &mut evm, + ); - let success = self.evm.check_success(self.address, &status, should_fail); + let success = evm.check_success(address, &status, should_fail); let duration = Instant::now().duration_since(start); tracing::debug!(?duration, %success, %gas_used); @@ -321,12 +351,13 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { kind: TestKind::Standard(gas_used), traces, identified_contracts, + debug_calls: if evm.state().debug_enabled { Some(evm.debug_calls()) } else { None }, }) } #[tracing::instrument(name = "fuzz-test", skip_all, fields(name = %func.signature()))] pub fn run_fuzz_test( - &mut self, + &self, func: &Function, setup: bool, runner: TestRunner, @@ -337,7 +368,7 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { let should_fail = func.name.starts_with("testFail"); tracing::debug!(func = ?func.signature(), should_fail, "fuzzing"); - self.evm.reset_traces(); + let (address, mut evm, init_logs) = self.new_sputnik_evm()?; let mut traces: Option> = None; let mut identified_contracts: Option> = None; @@ -345,17 +376,18 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { // call the setup function in each test to reset the test's state. if setup { tracing::trace!("setting up"); - match self.evm.setup(self.address) { + match evm.setup(address) { Ok((_reason, _setup_logs)) => {} Err(e) => { // if tracing is enabled, just return it as a failed test // otherwise abort - if self.evm.tracing_enabled() { + if evm.tracing_enabled() { self.update_traces( &mut traces, &mut identified_contracts, known_contracts, setup, + &mut evm, ); } return Ok(TestResult { @@ -367,35 +399,47 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { kind: TestKind::Fuzz(FuzzedCases::new(vec![])), traces, identified_contracts, + debug_calls: if evm.state().debug_enabled { + Some(evm.debug_calls()) + } else { + None + }, }) } } } - let mut logs = self.init_logs.to_vec(); + let mut logs = init_logs; - let prev = self.evm.set_tracing_enabled(false); + let prev = evm.set_tracing_enabled(false); // instantiate the fuzzed evm in line - let evm = FuzzedExecutor::new(self.evm, runner, self.sender); - let FuzzTestResult { cases, test_error } = evm.fuzz(func, self.address, should_fail); + let evm = FuzzedExecutor::new(&mut evm, runner, self.sender); + let FuzzTestResult { cases, test_error } = evm.fuzz(func, address, should_fail); + let evm = evm.into_inner(); if let Some(ref error) = test_error { // we want traces for a failed fuzz if let TestError::Fail(_reason, bytes) = &error.test_error { if prev { - let _ = self.evm.set_tracing_enabled(true); + let _ = evm.set_tracing_enabled(true); } let (_retdata, status, _gas, execution_logs) = - self.evm.call_raw(self.sender, self.address, bytes.clone(), 0.into(), false)?; - if >::is_fail(&status) { + evm.call_raw(self.sender, address, bytes.clone(), 0.into(), false)?; + if is_fail(evm, status) { logs.extend(execution_logs); // add reverted logs - logs.extend(self.evm.all_logs()); + logs.extend(evm.all_logs()); } else { logs.extend(execution_logs); } - self.update_traces(&mut traces, &mut identified_contracts, known_contracts, setup); + self.update_traces( + &mut traces, + &mut identified_contracts, + known_contracts, + setup, + evm, + ); } } @@ -421,8 +465,6 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { let duration = Instant::now().duration_since(start); tracing::debug!(?duration, %success); - // reset tracing to previous value in case next test *isn't* a fuzz test - self.evm.set_tracing_enabled(prev); // from that call? Ok(TestResult { success, @@ -433,18 +475,20 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { kind: TestKind::Fuzz(cases), traces, identified_contracts, + debug_calls: if evm.state().debug_enabled { Some(evm.debug_calls()) } else { None }, }) } - fn update_traces( - &mut self, + fn update_traces>( + &self, traces: &mut Option>, identified_contracts: &mut Option>, known_contracts: Option<&BTreeMap)>>, setup: bool, + evm: &mut E, ) { - let evm_traces = self.evm.traces(); - if !evm_traces.is_empty() && self.evm.tracing_enabled() { + let evm_traces = evm.traces(); + if !evm_traces.is_empty() && evm.tracing_enabled() { let mut ident = BTreeMap::new(); // create an iter over the traces let mut trace_iter = evm_traces.into_iter(); @@ -456,7 +500,7 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { 0, known_contracts.expect("traces enabled but no identified_contracts"), &mut ident, - self.evm, + evm, ); temp_traces.push(setup); } @@ -466,7 +510,7 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { 0, known_contracts.expect("traces enabled but no identified_contracts"), &mut ident, - self.evm, + evm, ); temp_traces.push(test_trace); } @@ -475,49 +519,62 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { *identified_contracts = Some(ident); *traces = Some(temp_traces); } - self.evm.reset_traces(); + evm.reset_traces(); } } +// Helper functions for getting the revert status for a `ReturnReason` without having +// to specify the full EVM signature +fn is_fail + evm_adapters::Evm, T>( + _evm: &mut E, + status: T, +) -> bool { + >::is_fail(&status) +} + +fn revert + evm_adapters::Evm, T>(_evm: &E) -> T { + >::revert() +} + #[cfg(test)] mod tests { use super::*; - use crate::test_helpers::{Filter, COMPILED}; + use crate::test_helpers::{Filter, BACKEND, COMPILED, EVM_OPTS}; use ethers::solc::artifacts::CompactContractRef; - use evm_adapters::sputnik::helpers::vm; mod sputnik { + use ::sputnik::backend::MemoryBackend; + use evm_adapters::sputnik::helpers::CFG_NO_LMT; use foundry_utils::get_func; use proptest::test_runner::Config as FuzzConfig; use super::*; + pub fn runner<'a>( + abi: &'a Abi, + code: ethers::prelude::Bytes, + ) -> ContractRunner<'a, MemoryBackend<'a>> { + ContractRunner::new(&*EVM_OPTS, &*CFG_NO_LMT, &*BACKEND, abi, code, None) + } + #[test] fn test_runner() { let compiled = COMPILED.find("GreeterTest").expect("could not find contract"); - let evm = vm(); - super::test_runner(evm, compiled); + super::test_runner(compiled); } #[test] fn test_function_overriding() { let compiled = COMPILED.find("GreeterTest").expect("could not find contract"); - let mut evm = vm(); - let (addr, _, _, _) = evm - .deploy(Address::zero(), compiled.bytecode().unwrap().clone(), 0.into()) - .unwrap(); - - let init_state = evm.state().clone(); - let mut runner = - ContractRunner::new(&mut evm, compiled.abi.as_ref().unwrap(), addr, None, &[]); + let (_, code, _) = compiled.into_parts_or_default(); + let runner = runner(compiled.abi.as_ref().unwrap(), code); let mut cfg = FuzzConfig::default(); cfg.failure_persistence = None; - let mut fuzzer = TestRunner::new(cfg); - let results = runner - .run_tests(&Filter::new("testGreeting", ".*"), Some(&mut fuzzer), &init_state, None) - .unwrap(); + let fuzzer = TestRunner::new(cfg); + let results = + runner.run_tests(&Filter::new("testGreeting", ".*"), Some(fuzzer), None).unwrap(); assert!(results["testGreeting()"].success); assert!(results["testGreeting(string)"].success); assert!(results["testGreeting(string,string)"].success); @@ -526,22 +583,14 @@ mod tests { #[test] fn test_fuzzing_counterexamples() { let compiled = COMPILED.find("GreeterTest").expect("could not find contract"); - let mut evm = vm(); - let (addr, _, _, _) = evm - .deploy(Address::zero(), compiled.bytecode().unwrap().clone(), 0.into()) - .unwrap(); - - let init_state = evm.state().clone(); - - let mut runner = - ContractRunner::new(&mut evm, compiled.abi.as_ref().unwrap(), addr, None, &[]); + let (_, code, _) = compiled.into_parts_or_default(); + let runner = runner(compiled.abi.as_ref().unwrap(), code); let mut cfg = FuzzConfig::default(); cfg.failure_persistence = None; - let mut fuzzer = TestRunner::new(cfg); - let results = runner - .run_tests(&Filter::new("testFuzz.*", ".*"), Some(&mut fuzzer), &init_state, None) - .unwrap(); + let fuzzer = TestRunner::new(cfg); + let results = + runner.run_tests(&Filter::new("testFuzz.*", ".*"), Some(fuzzer), None).unwrap(); for (_, res) in results { assert!(!res.success); assert!(res.counterexample.is_some()); @@ -551,13 +600,8 @@ mod tests { #[test] fn test_fuzzing_ok() { let compiled = COMPILED.find("GreeterTest").expect("could not find contract"); - let mut evm = vm(); - let (addr, _, _, _) = evm - .deploy(Address::zero(), compiled.bytecode().unwrap().clone(), 0.into()) - .unwrap(); - - let mut runner = - ContractRunner::new(&mut evm, compiled.abi.as_ref().unwrap(), addr, None, &[]); + let (_, code, _) = compiled.into_parts_or_default(); + let runner = runner(compiled.abi.as_ref().unwrap(), code); let mut cfg = FuzzConfig::default(); cfg.failure_persistence = None; @@ -571,13 +615,8 @@ mod tests { #[test] fn test_fuzz_shrinking() { let compiled = COMPILED.find("GreeterTest").expect("could not find contract"); - let mut evm = vm(); - let (addr, _, _, _) = evm - .deploy(Address::zero(), compiled.bytecode().unwrap().clone(), 0.into()) - .unwrap(); - - let mut runner = - ContractRunner::new(&mut evm, compiled.abi.as_ref().unwrap(), addr, None, &[]); + let (_, code, _) = compiled.into_parts_or_default(); + let runner = runner(compiled.abi.as_ref().unwrap(), code); let mut cfg = FuzzConfig::default(); cfg.failure_persistence = None; @@ -611,35 +650,11 @@ mod tests { } } - mod evmodin_test { - use super::*; - use ::evmodin::{tracing::NoopTracer, util::mocked_host::MockedHost, Revision}; - use evm_adapters::evmodin::EvmOdin; - - #[test] - #[ignore] - fn test_runner() { - let revision = Revision::Istanbul; - let compiled = COMPILED.find("GreeterTest").expect("could not find contract"); - - let host = MockedHost::default(); - - let gas_limit = 12_000_000; - let evm = EvmOdin::new(host, gas_limit, revision, NoopTracer); - super::test_runner(evm, compiled); - } - } - - pub fn test_runner>(mut evm: E, compiled: CompactContractRef) { - let (addr, _, _, _) = - evm.deploy(Address::zero(), compiled.bytecode().unwrap().clone(), 0.into()).unwrap(); - - let init_state = evm.state().clone(); - - let mut runner = - ContractRunner::new(&mut evm, compiled.abi.as_ref().unwrap(), addr, None, &[]); + pub fn test_runner(compiled: CompactContractRef) { + let (_, code, _) = compiled.into_parts_or_default(); + let runner = sputnik::runner(compiled.abi.as_ref().unwrap(), code); - let res = runner.run_tests(&Filter::new(".*", ".*"), None, &init_state, None).unwrap(); + let res = runner.run_tests(&Filter::new(".*", ".*"), None, None).unwrap(); assert!(!res.is_empty()); assert!(res.iter().all(|(_, result)| result.success)); }