From 8360c6bd5335e00570a5849e513bc1f1abe92d06 Mon Sep 17 00:00:00 2001 From: Nisheeth Barthwal Date: Tue, 3 Dec 2024 16:23:36 +0100 Subject: [PATCH 1/3] initial wip --- Cargo.lock | 29 + Cargo.toml | 4 + crates/cast/bin/cmd/call.rs | 6 +- crates/cast/bin/cmd/run.rs | 5 +- crates/chisel/bin/main.rs | 11 +- crates/chisel/src/cmd.rs | 3 +- crates/chisel/src/dispatcher.rs | 23 +- crates/chisel/src/executor.rs | 52 +- crates/chisel/src/runner.rs | 12 +- crates/chisel/src/session.rs | 9 +- crates/chisel/src/session_source.rs | 20 +- crates/chisel/tests/cache.rs | 61 +- crates/evm/core/src/backend/cow.rs | 39 +- crates/evm/core/src/backend/mod.rs | 646 +++++++++--------- crates/evm/core/src/backend/strategy.rs | 192 ++++++ crates/evm/evm/Cargo.toml | 3 +- crates/evm/evm/src/executors/builder.rs | 4 +- crates/evm/evm/src/executors/fuzz/mod.rs | 11 +- crates/evm/evm/src/executors/invariant/mod.rs | 30 +- .../evm/evm/src/executors/invariant/replay.rs | 11 +- .../evm/evm/src/executors/invariant/result.rs | 14 +- .../evm/evm/src/executors/invariant/shrink.rs | 10 +- crates/evm/evm/src/executors/mod.rs | 43 +- crates/evm/evm/src/executors/trace.rs | 29 +- crates/forge/Cargo.toml | 2 + crates/forge/bin/cmd/coverage.rs | 9 +- crates/forge/bin/cmd/test/mod.rs | 25 +- crates/forge/src/multi_runner.rs | 26 +- crates/forge/src/runner.rs | 18 +- crates/forge/tests/it/config.rs | 13 +- crates/forge/tests/it/repros.rs | 5 +- crates/forge/tests/it/test_helpers.rs | 34 +- crates/forge/tests/it/zk/repros.rs | 3 +- crates/script/src/broadcast.rs | 8 +- crates/script/src/build.rs | 28 +- crates/script/src/execute.rs | 33 +- crates/script/src/lib.rs | 31 +- crates/script/src/runner.rs | 14 +- crates/script/src/simulate.rs | 46 +- crates/script/src/verify.rs | 6 +- crates/strategy/core/Cargo.toml | 16 + crates/strategy/core/src/lib.rs | 26 + crates/strategy/zksync/Cargo.toml | 30 + crates/strategy/zksync/src/lib.rs | 318 +++++++++ crates/verify/src/bytecode.rs | 8 +- crates/verify/src/utils.rs | 20 +- 46 files changed, 1365 insertions(+), 621 deletions(-) create mode 100644 crates/evm/core/src/backend/strategy.rs create mode 100644 crates/strategy/core/Cargo.toml create mode 100644 crates/strategy/core/src/lib.rs create mode 100644 crates/strategy/zksync/Cargo.toml create mode 100644 crates/strategy/zksync/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index f60dac45e..ea7b4e756 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4481,6 +4481,8 @@ dependencies = [ "foundry-evm", "foundry-evm-abi", "foundry-linking", + "foundry-strategy-core", + "foundry-strategy-zksync", "foundry-test-utils", "foundry-wallets", "foundry-zksync-compiler", @@ -5099,6 +5101,7 @@ dependencies = [ "revm", "revm-inspectors", "serde", + "serde_json", "thiserror", "tracing", ] @@ -5263,6 +5266,32 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "foundry-strategy-core" +version = "0.0.2" +dependencies = [ + "foundry-evm-core", +] + +[[package]] +name = "foundry-strategy-zksync" +version = "0.0.2" +dependencies = [ + "alloy-primitives", + "eyre", + "foundry-common", + "foundry-compilers", + "foundry-evm-core", + "foundry-evm-traces", + "foundry-strategy-core", + "foundry-zksync-core", + "revm", + "revm-inspectors", + "serde", + "serde_json", + "tracing", +] + [[package]] name = "foundry-test-utils" version = "0.0.2" diff --git a/Cargo.toml b/Cargo.toml index cad9fb0db..5c3a24c68 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,8 @@ members = [ "crates/script-sequence/", "crates/macros/", "crates/test-utils/", + "crates/strategy/core/", + "crates/strategy/zksync/", ] resolver = "2" @@ -173,6 +175,8 @@ foundry-linking = { path = "crates/linking" } foundry-zksync-core = { path = "crates/zksync/core" } foundry-zksync-compiler = { path = "crates/zksync/compiler" } foundry-zksync-inspectors = { path = "crates/zksync/inspectors" } +foundry-strategy-core = { path = "crates/strategy/core" } +foundry-strategy-zksync = { path = "crates/strategy/zksync" } # solc & compilation utilities # foundry-block-explorers = { version = "0.7.3", default-features = false } diff --git a/crates/cast/bin/cmd/call.rs b/crates/cast/bin/cmd/call.rs index aefc5f1c0..275fb813a 100644 --- a/crates/cast/bin/cmd/call.rs +++ b/crates/cast/bin/cmd/call.rs @@ -1,7 +1,7 @@ use crate::tx::{CastTxBuilder, SenderKind}; use alloy_primitives::{TxKind, U256}; use alloy_rpc_types::{BlockId, BlockNumberOrTag}; -use cast::{traces::TraceKind, Cast}; +use cast::{backend::strategy::{BackendStrategy, EvmBackendStrategy}, traces::TraceKind, Cast}; use clap::Parser; use eyre::Result; use foundry_cli::{ @@ -171,14 +171,14 @@ impl CallArgs { } let (mut env, fork, chain, alphanet) = - TracingExecutor::get_fork_material(&config, evm_opts).await?; + TracingExecutor::::get_fork_material(&config, evm_opts).await?; // modify settings that usually set in eth_call env.cfg.disable_block_gas_limit = true; env.block.gas_limit = U256::MAX; let mut executor = - TracingExecutor::new(env, fork, evm_version, debug, decode_internal, alphanet); + TracingExecutor::new(env, fork, evm_version, debug, decode_internal, alphanet, EvmBackendStrategy::new()); let value = tx.value.unwrap_or_default(); let input = tx.inner.input.into_input().unwrap_or_default(); diff --git a/crates/cast/bin/cmd/run.rs b/crates/cast/bin/cmd/run.rs index 11c7b507e..a83f328de 100644 --- a/crates/cast/bin/cmd/run.rs +++ b/crates/cast/bin/cmd/run.rs @@ -1,7 +1,7 @@ use alloy_primitives::U256; use alloy_provider::Provider; use alloy_rpc_types::BlockTransactions; -use cast::revm::primitives::EnvWithHandlerCfg; +use cast::{backend::strategy::{BackendStrategy, EvmBackendStrategy}, revm::primitives::EnvWithHandlerCfg}; use clap::Parser; use eyre::{Result, WrapErr}; use foundry_cli::{ @@ -133,7 +133,7 @@ impl RunArgs { config.fork_block_number = Some(tx_block_number - 1); let (mut env, fork, chain, alphanet) = - TracingExecutor::get_fork_material(&config, evm_opts).await?; + TracingExecutor::::get_fork_material(&config, evm_opts).await?; let mut evm_version = self.evm_version; @@ -164,6 +164,7 @@ impl RunArgs { self.debug, self.decode_internal, alphanet, + EvmBackendStrategy::new(), ); let mut env = EnvWithHandlerCfg::new_with_spec_id(Box::new(env.clone()), executor.spec_id()); diff --git a/crates/chisel/bin/main.rs b/crates/chisel/bin/main.rs index dca99d4a5..46ab06cd5 100644 --- a/crates/chisel/bin/main.rs +++ b/crates/chisel/bin/main.rs @@ -22,6 +22,7 @@ use foundry_config::{ }, Config, }; +use foundry_evm::backend::strategy::{BackendStrategy, EvmBackendStrategy}; use rustyline::{config::Configurer, error::ReadlineError, Editor}; use std::path::PathBuf; use tracing::debug; @@ -132,7 +133,7 @@ async fn main_args(args: Chisel) -> eyre::Result<()> { let (config, evm_opts) = args.load_config_and_evm_opts()?; // Create a new cli dispatcher - let mut dispatcher = ChiselDispatcher::new(chisel::session_source::SessionSourceConfig { + let mut dispatcher = ChiselDispatcher::::new(chisel::session_source::SessionSourceConfig { // Enable traces if any level of verbosity was passed traces: config.verbosity > 0, foundry_config: config, @@ -265,7 +266,7 @@ impl Provider for Chisel { } /// Evaluate a single Solidity line. -async fn dispatch_repl_line(dispatcher: &mut ChiselDispatcher, line: &str) -> eyre::Result { +async fn dispatch_repl_line(dispatcher: &mut ChiselDispatcher, line: &str) -> eyre::Result { let r = dispatcher.dispatch(line).await; match &r { DispatchResult::Success(msg) | DispatchResult::CommandSuccess(msg) => { @@ -288,8 +289,8 @@ async fn dispatch_repl_line(dispatcher: &mut ChiselDispatcher, line: &str) -> ey /// Evaluate multiple Solidity source files contained within a /// Chisel prelude directory. -async fn evaluate_prelude( - dispatcher: &mut ChiselDispatcher, +async fn evaluate_prelude( + dispatcher: &mut ChiselDispatcher, maybe_prelude: Option, ) -> eyre::Result<()> { let Some(prelude_dir) = maybe_prelude else { return Ok(()) }; @@ -314,7 +315,7 @@ async fn evaluate_prelude( } /// Loads a single Solidity file into the prelude. -async fn load_prelude_file(dispatcher: &mut ChiselDispatcher, file: PathBuf) -> eyre::Result<()> { +async fn load_prelude_file(dispatcher: &mut ChiselDispatcher, file: PathBuf) -> eyre::Result<()> { let prelude = fs::read_to_string(file) .wrap_err("Could not load source file. Are you sure this path is correct?")?; dispatch_repl_line(dispatcher, &prelude).await?; diff --git a/crates/chisel/src/cmd.rs b/crates/chisel/src/cmd.rs index c13272bc9..4f5d56ed3 100644 --- a/crates/chisel/src/cmd.rs +++ b/crates/chisel/src/cmd.rs @@ -5,6 +5,7 @@ use crate::prelude::ChiselDispatcher; use std::{error::Error, str::FromStr}; +use foundry_evm::backend::strategy::EvmBackendStrategy; use strum::EnumIter; /// Builtin chisel command variants @@ -79,7 +80,7 @@ impl FromStr for ChiselCommand { "exec" | "e" => Ok(Self::Exec), "rawstack" | "rs" => Ok(Self::RawStack), "edit" => Ok(Self::Edit), - _ => Err(ChiselDispatcher::make_error(format!( + _ => Err(ChiselDispatcher::::make_error(format!( "Unknown command \"{s}\"! See available commands with `!help`.", )) .into()), diff --git a/crates/chisel/src/dispatcher.rs b/crates/chisel/src/dispatcher.rs index d69de3bf5..e8d339f7e 100644 --- a/crates/chisel/src/dispatcher.rs +++ b/crates/chisel/src/dispatcher.rs @@ -15,12 +15,11 @@ use alloy_primitives::{hex, Address}; use forge_fmt::FormatterConfig; use foundry_config::{Config, RpcEndpoint}; use foundry_evm::{ - decode::decode_console_logs, - traces::{ + backend::strategy::BackendStrategy, decode::decode_console_logs, traces::{ decode_trace_arena, identifier::{SignaturesIdentifier, TraceIdentifiers}, render_trace_arena, CallTraceDecoder, CallTraceDecoderBuilder, TraceKind, - }, + } }; use regex::Regex; use reqwest::Url; @@ -58,9 +57,9 @@ static ADDRESS_RE: LazyLock = LazyLock::new(|| { /// Chisel input dispatcher #[derive(Debug)] -pub struct ChiselDispatcher { +pub struct ChiselDispatcher { /// A Chisel Session - pub session: ChiselSession, + pub session: ChiselSession, } /// Chisel dispatch result variants @@ -134,9 +133,9 @@ pub fn format_source(source: &str, config: FormatterConfig) -> eyre::Result ChiselDispatcher where B: BackendStrategy { /// Associated public function to create a new Dispatcher instance - pub fn new(config: SessionSourceConfig) -> eyre::Result { + pub fn new(config: SessionSourceConfig) -> eyre::Result { ChiselSession::new(config).map(|session| Self { session }) } @@ -146,12 +145,12 @@ impl ChiselDispatcher { } /// Returns the [`SessionSource`]. - pub fn source(&self) -> &SessionSource { + pub fn source(&self) -> &SessionSource { &self.session.session_source } /// Returns the [`SessionSource`]. - pub fn source_mut(&mut self) -> &mut SessionSource { + pub fn source_mut(&mut self) -> &mut SessionSource { &mut self.session.session_source } @@ -302,7 +301,7 @@ impl ChiselDispatcher { DispatchResult::CommandFailed(Self::make_error("Failed to load session!")) } } - ChiselCommand::ListSessions => match ChiselSession::list_sessions() { + ChiselCommand::ListSessions => match ChiselSession::::list_sessions() { Ok(sessions) => DispatchResult::CommandSuccess(Some(format!( "{}\n{}", format!("{CHISEL_CHAR} Chisel Sessions").cyan(), @@ -326,7 +325,7 @@ impl ChiselDispatcher { DispatchResult::CommandFailed(String::from("Failed to format session source")) } }, - ChiselCommand::ClearCache => match ChiselSession::clear_cache() { + ChiselCommand::ClearCache => match ChiselSession::::clear_cache() { Ok(_) => { self.session.id = None; DispatchResult::CommandSuccess(Some(String::from("Cleared chisel cache!"))) @@ -908,7 +907,7 @@ impl ChiselDispatcher { /// /// Optionally, a [CallTraceDecoder] pub async fn decode_traces( - session_config: &SessionSourceConfig, + session_config: &SessionSourceConfig, result: &mut ChiselResult, // known_contracts: &ContractsByArtifact, ) -> eyre::Result { diff --git a/crates/chisel/src/executor.rs b/crates/chisel/src/executor.rs index 91ed0decc..29839d1ed 100644 --- a/crates/chisel/src/executor.rs +++ b/crates/chisel/src/executor.rs @@ -12,8 +12,11 @@ use core::fmt::Debug; use eyre::{Result, WrapErr}; use foundry_compilers::Artifact; use foundry_evm::{ - backend::Backend, decode::decode_console_logs, executors::ExecutorBuilder, - inspectors::CheatsConfig, traces::TraceMode, + backend::{strategy::BackendStrategy, Backend}, + decode::decode_console_logs, + executors::ExecutorBuilder, + inspectors::CheatsConfig, + traces::TraceMode, }; use solang_parser::pt::{self, CodeLocation}; use std::str::FromStr; @@ -23,7 +26,10 @@ use yansi::Paint; const USIZE_MAX_AS_U256: U256 = U256::from_limbs([usize::MAX as u64, 0, 0, 0]); /// Executor implementation for [SessionSource] -impl SessionSource { +impl SessionSource +where + B: BackendStrategy, +{ /// Runs the source with the [ChiselRunner] /// /// ### Returns @@ -216,8 +222,10 @@ impl SessionSource { let Some((stack, memory, _)) = &res.state else { // Show traces and logs, if there are any, and return an error - if let Ok(decoder) = ChiselDispatcher::decode_traces(&source.config, &mut res).await { - ChiselDispatcher::show_traces(&decoder, &mut res).await?; + if let Ok(decoder) = + ChiselDispatcher::::decode_traces(&source.config, &mut res).await + { + ChiselDispatcher::::show_traces(&decoder, &mut res).await?; } let decoded_logs = decode_console_logs(&res.logs); if !decoded_logs.is_empty() { @@ -311,7 +319,7 @@ impl SessionSource { /// ### Returns /// /// A configured [ChiselRunner] - async fn prepare_runner(&mut self, final_pc: usize) -> ChiselRunner { + async fn prepare_runner(&mut self, final_pc: usize) -> ChiselRunner { let env = self.config.evm_opts.evm_env().await.expect("Could not instantiate fork environment"); @@ -320,7 +328,7 @@ impl SessionSource { Some(backend) => backend, None => { let fork = self.config.evm_opts.get_fork(&self.config.foundry_config, env.clone()); - let backend = Backend::spawn(fork); + let backend = Backend::spawn(fork, B::new()); self.config.backend = Some(backend.clone()); backend } @@ -1422,6 +1430,7 @@ impl Iterator for InstructionIter<'_> { mod tests { use super::*; use foundry_compilers::{error::SolcError, solc::Solc}; + use foundry_evm::backend::strategy::EvmBackendStrategy; use semver::Version; use std::sync::Mutex; @@ -1517,7 +1526,7 @@ mod tests { ] }; - let source = &mut source(); + let source = &mut source::(); let array_expressions: &[(&str, DynSolType)] = &[ ("[1, 2, 3]", fixed_array(DynSolType::Uint(256), 3)), @@ -1592,8 +1601,8 @@ mod tests { )); } - generic_type_test(&mut source(), TYPES); - generic_type_test(&mut source(), &types); + generic_type_test(&mut source::(), TYPES); + generic_type_test(&mut source::(), &types); } #[test] @@ -1692,11 +1701,11 @@ mod tests { ] }; - generic_type_test(&mut source(), global_variables); + generic_type_test(&mut source::(), global_variables); } #[track_caller] - fn source() -> SessionSource { + fn source() -> SessionSource { // synchronize solc install static PRE_INSTALL_SOLC_LOCK: Mutex = Mutex::new(false); @@ -1739,7 +1748,11 @@ mod tests { DynSolType::FixedArray(Box::new(ty), len) } - fn parse(s: &mut SessionSource, input: &str, clear: bool) -> IntermediateOutput { + fn parse( + s: &mut SessionSource, + input: &str, + clear: bool, + ) -> IntermediateOutput { if clear { s.drain_run(); s.drain_top_level_code(); @@ -1770,8 +1783,8 @@ mod tests { } } - fn get_type( - s: &mut SessionSource, + fn get_type( + s: &mut SessionSource, input: &str, clear: bool, ) -> (Option, IntermediateOutput) { @@ -1781,15 +1794,20 @@ mod tests { (Type::from_expression(&expr).map(Type::map_special), intermediate) } - fn get_type_ethabi(s: &mut SessionSource, input: &str, clear: bool) -> Option { + fn get_type_ethabi( + s: &mut SessionSource, + input: &str, + clear: bool, + ) -> Option { let (ty, intermediate) = get_type(s, input, clear); ty.and_then(|ty| ty.try_as_ethabi(Some(&intermediate))) } - fn generic_type_test<'a, T, I>(s: &mut SessionSource, input: I) + fn generic_type_test<'a, T, I, B>(s: &mut SessionSource, input: I) where T: AsRef + std::fmt::Display + 'a, I: IntoIterator + 'a, + B: BackendStrategy, { for (input, expected) in input.into_iter() { let input = input.as_ref(); diff --git a/crates/chisel/src/runner.rs b/crates/chisel/src/runner.rs index 72b083e1f..c4fa4b282 100644 --- a/crates/chisel/src/runner.rs +++ b/crates/chisel/src/runner.rs @@ -6,6 +6,7 @@ use alloy_primitives::{map::AddressHashMap, Address, Bytes, Log, U256}; use eyre::Result; use foundry_evm::{ + backend::strategy::BackendStrategy, executors::{DeployResult, Executor, RawCallResult}, traces::{TraceKind, Traces}, }; @@ -19,9 +20,9 @@ static RUN_SELECTOR: [u8; 4] = [0xc0, 0x40, 0x62, 0x26]; /// Based off of foundry's forge cli runner for scripting. /// See: [runner](cli::cmd::forge::script::runner.rs) #[derive(Debug)] -pub struct ChiselRunner { +pub struct ChiselRunner { /// The Executor - pub executor: Executor, + pub executor: Executor, /// An initial balance pub initial_balance: U256, /// The sender @@ -52,7 +53,10 @@ pub struct ChiselResult { } /// ChiselRunner implementation -impl ChiselRunner { +impl ChiselRunner +where + B: BackendStrategy, +{ /// Create a new [ChiselRunner] /// /// ### Takes @@ -63,7 +67,7 @@ impl ChiselRunner { /// /// A new [ChiselRunner] pub fn new( - executor: Executor, + executor: Executor, initial_balance: U256, sender: Address, input: Option>, diff --git a/crates/chisel/src/session.rs b/crates/chisel/src/session.rs index 2f293c1cd..bb0b5ab9e 100644 --- a/crates/chisel/src/session.rs +++ b/crates/chisel/src/session.rs @@ -5,21 +5,22 @@ use crate::prelude::{SessionSource, SessionSourceConfig}; use eyre::Result; +use foundry_evm::backend::strategy::BackendStrategy; use serde::{Deserialize, Serialize}; use std::path::Path; use time::{format_description, OffsetDateTime}; /// A Chisel REPL Session #[derive(Debug, Serialize, Deserialize)] -pub struct ChiselSession { +pub struct ChiselSession { /// The `SessionSource` object that houses the REPL session. - pub session_source: SessionSource, + pub session_source: SessionSource, /// The current session's identifier pub id: Option, } // ChiselSession Common Associated Functions -impl ChiselSession { +impl ChiselSession where B: BackendStrategy { /// Create a new `ChiselSession` with a specified `solc` version and configuration. /// /// ### Takes @@ -29,7 +30,7 @@ impl ChiselSession { /// ### Returns /// /// A new instance of [ChiselSession] - pub fn new(config: SessionSourceConfig) -> Result { + pub fn new(config: SessionSourceConfig) -> Result { let solc = config.solc()?; // Return initialized ChiselSession with set solc version Ok(Self { session_source: SessionSource::new(solc, config), id: None }) diff --git a/crates/chisel/src/session_source.rs b/crates/chisel/src/session_source.rs index 128873167..d740554f5 100644 --- a/crates/chisel/src/session_source.rs +++ b/crates/chisel/src/session_source.rs @@ -12,7 +12,7 @@ use foundry_compilers::{ compilers::solc::Solc, }; use foundry_config::{Config, SolcReq}; -use foundry_evm::{backend::Backend, opts::EvmOpts}; +use foundry_evm::{backend::{strategy::BackendStrategy, Backend}, opts::EvmOpts}; use semver::Version; use serde::{Deserialize, Serialize}; use solang_parser::{diagnostics::Diagnostic, pt}; @@ -68,7 +68,7 @@ pub struct GeneratedOutput { /// Configuration for the [SessionSource] #[derive(Clone, Debug, Default, Serialize, Deserialize)] -pub struct SessionSourceConfig { +pub struct SessionSourceConfig { /// Foundry configuration pub foundry_config: Config, /// EVM Options @@ -77,14 +77,14 @@ pub struct SessionSourceConfig { pub no_vm: bool, #[serde(skip)] /// In-memory REVM db for the session's runner. - pub backend: Option, + pub backend: Option>, /// Optionally enable traces for the REPL contract execution pub traces: bool, /// Optionally set calldata for the REPL contract execution pub calldata: Option>, } -impl SessionSourceConfig { +impl SessionSourceConfig { /// Returns the solc version to use /// /// Solc version precedence @@ -131,7 +131,7 @@ impl SessionSourceConfig { /// /// Heavily based on soli's [`ConstructedSource`](https://github.com/jpopesculian/soli/blob/master/src/main.rs#L166) #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct SessionSource { +pub struct SessionSource { /// The file name pub file_name: PathBuf, /// The contract name @@ -152,10 +152,10 @@ pub struct SessionSource { /// The generated output pub generated_output: Option, /// Session Source configuration - pub config: SessionSourceConfig, + pub config: SessionSourceConfig, } -impl SessionSource { +impl SessionSource where B: BackendStrategy { /// Creates a new source given a solidity compiler version /// /// # Panics @@ -171,7 +171,7 @@ impl SessionSource { /// /// A new instance of [SessionSource] #[track_caller] - pub fn new(solc: Solc, mut config: SessionSourceConfig) -> Self { + pub fn new(solc: Solc, mut config: SessionSourceConfig) -> Self { if solc.version < MIN_VM_VERSION && !config.no_vm { tracing::info!(version=%solc.version, minimum=%MIN_VM_VERSION, "Disabling VM injection"); config.no_vm = true; @@ -639,9 +639,9 @@ pub enum ParseTreeFragment { /// Parses a fragment of solidity code with solang_parser and assigns /// it a scope within the [SessionSource]. -pub fn parse_fragment( +pub fn parse_fragment( solc: Solc, - config: SessionSourceConfig, + config: SessionSourceConfig, buffer: &str, ) -> Option { let mut base = SessionSource::new(solc, config); diff --git a/crates/chisel/tests/cache.rs b/crates/chisel/tests/cache.rs index 7016bce09..0e1f86845 100644 --- a/crates/chisel/tests/cache.rs +++ b/crates/chisel/tests/cache.rs @@ -1,6 +1,7 @@ use chisel::session::ChiselSession; use foundry_compilers::artifacts::EvmVersion; use foundry_config::Config; +use foundry_evm::backend::strategy::EvmBackendStrategy; use serial_test::serial; use std::path::Path; @@ -9,7 +10,7 @@ use std::path::Path; fn test_cache_directory() { // Get the cache dir // Should be ~/.foundry/cache/chisel - let cache_dir = ChiselSession::cache_dir().unwrap(); + let cache_dir = ChiselSession::::cache_dir().unwrap(); // Validate the cache directory let home_dir = dirs::home_dir().unwrap(); @@ -20,10 +21,10 @@ fn test_cache_directory() { #[serial] fn test_create_cache_directory() { // Get the cache dir - let cache_dir = ChiselSession::cache_dir().unwrap(); + let cache_dir = ChiselSession::::cache_dir().unwrap(); // Create the cache directory - ChiselSession::create_cache_dir().unwrap(); + ChiselSession::::create_cache_dir().unwrap(); // Validate the cache directory assert!(Path::new(&cache_dir).exists()); @@ -33,14 +34,14 @@ fn test_create_cache_directory() { #[serial] fn test_write_session() { // Create the cache directory if it doesn't exist - let cache_dir = ChiselSession::cache_dir().unwrap(); - ChiselSession::create_cache_dir().unwrap(); + let cache_dir = ChiselSession::::cache_dir().unwrap(); + ChiselSession::::create_cache_dir().unwrap(); // Force the solc version to be 0.8.19 let foundry_config = Config { evm_version: EvmVersion::London, ..Default::default() }; // Create a new session - let mut env = ChiselSession::new(chisel::session_source::SessionSourceConfig { + let mut env = ChiselSession::::new(chisel::session_source::SessionSourceConfig { foundry_config, ..Default::default() }) @@ -61,14 +62,14 @@ fn test_write_session() { #[serial] fn test_write_session_with_name() { // Create the cache directory if it doesn't exist - let cache_dir = ChiselSession::cache_dir().unwrap(); - ChiselSession::create_cache_dir().unwrap(); + let cache_dir = ChiselSession::::cache_dir().unwrap(); + ChiselSession::::create_cache_dir().unwrap(); // Force the solc version to be 0.8.19 let foundry_config = Config { evm_version: EvmVersion::London, ..Default::default() }; // Create a new session - let mut env = ChiselSession::new(chisel::session_source::SessionSourceConfig { + let mut env = ChiselSession::::new(chisel::session_source::SessionSourceConfig { foundry_config, ..Default::default() }) @@ -86,13 +87,13 @@ fn test_write_session_with_name() { #[serial] fn test_clear_cache() { // Create a session to validate clearing a non-empty cache directory - let cache_dir = ChiselSession::cache_dir().unwrap(); + let cache_dir = ChiselSession::::cache_dir().unwrap(); // Force the solc version to be 0.8.19 let foundry_config = Config { evm_version: EvmVersion::London, ..Default::default() }; - ChiselSession::create_cache_dir().unwrap(); - let mut env = ChiselSession::new(chisel::session_source::SessionSourceConfig { + ChiselSession::::create_cache_dir().unwrap(); + let mut env = ChiselSession::::new(chisel::session_source::SessionSourceConfig { foundry_config, ..Default::default() }) @@ -100,7 +101,7 @@ fn test_clear_cache() { env.write().unwrap(); // Clear the cache - ChiselSession::clear_cache().unwrap(); + ChiselSession::::clear_cache().unwrap(); // Validate there are no items in the cache dir let num_items = std::fs::read_dir(cache_dir).unwrap().count(); @@ -111,14 +112,14 @@ fn test_clear_cache() { #[serial] fn test_list_sessions() { // Create and clear the cache directory - ChiselSession::create_cache_dir().unwrap(); - ChiselSession::clear_cache().unwrap(); + ChiselSession::::create_cache_dir().unwrap(); + ChiselSession::::clear_cache().unwrap(); // Force the solc version to be 0.8.19 let foundry_config = Config { evm_version: EvmVersion::London, ..Default::default() }; // Create a new session - let mut env = ChiselSession::new(chisel::session_source::SessionSourceConfig { + let mut env = ChiselSession::::new(chisel::session_source::SessionSourceConfig { foundry_config, ..Default::default() }) @@ -127,7 +128,7 @@ fn test_list_sessions() { env.write().unwrap(); // List the sessions - let sessions = ChiselSession::list_sessions().unwrap(); + let sessions = ChiselSession::::list_sessions().unwrap(); // Validate the sessions assert_eq!(sessions.len(), 1); @@ -138,14 +139,14 @@ fn test_list_sessions() { #[serial] fn test_load_cache() { // Create and clear the cache directory - ChiselSession::create_cache_dir().unwrap(); - ChiselSession::clear_cache().unwrap(); + ChiselSession::::create_cache_dir().unwrap(); + ChiselSession::::clear_cache().unwrap(); // Force the solc version to be 0.8.19 let foundry_config = Config { evm_version: EvmVersion::London, ..Default::default() }; // Create a new session - let mut env = ChiselSession::new(chisel::session_source::SessionSourceConfig { + let mut env = ChiselSession::::new(chisel::session_source::SessionSourceConfig { foundry_config, ..Default::default() }) @@ -153,7 +154,7 @@ fn test_load_cache() { env.write().unwrap(); // Load the session - let new_env = ChiselSession::load("0"); + let new_env = ChiselSession::::load("0"); // Validate the session assert!(new_env.is_ok()); @@ -166,14 +167,14 @@ fn test_load_cache() { #[serial] fn test_write_same_session_multiple_times() { // Create and clear the cache directory - ChiselSession::create_cache_dir().unwrap(); - ChiselSession::clear_cache().unwrap(); + ChiselSession::::create_cache_dir().unwrap(); + ChiselSession::::clear_cache().unwrap(); // Force the solc version to be 0.8.19 let foundry_config = Config { evm_version: EvmVersion::London, ..Default::default() }; // Create a new session - let mut env = ChiselSession::new(chisel::session_source::SessionSourceConfig { + let mut env = ChiselSession::::new(chisel::session_source::SessionSourceConfig { foundry_config, ..Default::default() }) @@ -182,21 +183,21 @@ fn test_write_same_session_multiple_times() { env.write().unwrap(); env.write().unwrap(); env.write().unwrap(); - assert_eq!(ChiselSession::list_sessions().unwrap().len(), 1); + assert_eq!(ChiselSession::::list_sessions().unwrap().len(), 1); } #[test] #[serial] fn test_load_latest_cache() { // Create and clear the cache directory - ChiselSession::create_cache_dir().unwrap(); - ChiselSession::clear_cache().unwrap(); + ChiselSession::::create_cache_dir().unwrap(); + ChiselSession::::clear_cache().unwrap(); // Force the solc version to be 0.8.19 let foundry_config = Config { evm_version: EvmVersion::London, ..Default::default() }; // Create sessions - let mut env = ChiselSession::new(chisel::session_source::SessionSourceConfig { + let mut env = ChiselSession::::new(chisel::session_source::SessionSourceConfig { foundry_config: foundry_config.clone(), ..Default::default() }) @@ -206,7 +207,7 @@ fn test_load_latest_cache() { let wait_time = std::time::Duration::from_millis(100); std::thread::sleep(wait_time); - let mut env2 = ChiselSession::new(chisel::session_source::SessionSourceConfig { + let mut env2 = ChiselSession::::new(chisel::session_source::SessionSourceConfig { foundry_config, ..Default::default() }) @@ -214,7 +215,7 @@ fn test_load_latest_cache() { env2.write().unwrap(); // Load the latest session - let new_env = ChiselSession::latest().unwrap(); + let new_env = ChiselSession::::latest().unwrap(); // Validate the session assert_eq!(new_env.id.unwrap(), "1"); diff --git a/crates/evm/core/src/backend/cow.rs b/crates/evm/core/src/backend/cow.rs index 41b28ed75..dda22e888 100644 --- a/crates/evm/core/src/backend/cow.rs +++ b/crates/evm/core/src/backend/cow.rs @@ -1,6 +1,6 @@ //! A wrapper around `Backend` that is clone-on-write used for fuzzing. -use super::{BackendError, ForkInfo}; +use super::{strategy::BackendStrategy, BackendError, ForkInfo}; use crate::{ backend::{ diagnostic::RevertDiagnostic, Backend, DatabaseExt, LocalForkId, RevertStateSnapshotAction, @@ -44,20 +44,23 @@ use std::{ /// which would add significant overhead for large fuzz sets even if the Database is not big after /// setup. #[derive(Clone, Debug)] -pub struct CowBackend<'a> { +pub struct CowBackend<'a, S: Clone> { /// The underlying `Backend`. /// /// No calls on the `CowBackend` will ever persistently modify the `backend`'s state. - pub backend: Cow<'a, Backend>, + pub backend: Cow<'a, Backend>, /// Keeps track of whether the backed is already initialized is_initialized: bool, /// The [SpecId] of the current backend. spec_id: SpecId, } -impl<'a> CowBackend<'a> { +impl<'a, S> CowBackend<'a, S> +where + S: BackendStrategy, +{ /// Creates a new `CowBackend` with the given `Backend`. - pub fn new(backend: &'a Backend) -> Self { + pub fn new(backend: &'a Backend) -> Self { Self { backend: Cow::Borrowed(backend), is_initialized: false, spec_id: SpecId::LATEST } } @@ -105,7 +108,7 @@ impl<'a> CowBackend<'a> { Ok(res) } - pub fn new_borrowed(backend: &'a Backend) -> Self { + pub fn new_borrowed(backend: &'a Backend) -> Self { Self { backend: Cow::Borrowed(backend), is_initialized: false, spec_id: SpecId::LATEST } } @@ -119,7 +122,7 @@ impl<'a> CowBackend<'a> { /// Returns a mutable instance of the Backend. /// /// If this is the first time this is called, the backed is cloned and initialized. - fn backend_mut(&mut self, env: &Env) -> &mut Backend { + fn backend_mut(&mut self, env: &Env) -> &mut Backend { if !self.is_initialized { let backend = self.backend.to_mut(); let env = EnvWithHandlerCfg::new_with_spec_id(Box::new(env.clone()), self.spec_id); @@ -131,7 +134,7 @@ impl<'a> CowBackend<'a> { } /// Returns a mutable instance of the Backend if it is initialized. - fn initialized_backend_mut(&mut self) -> Option<&mut Backend> { + fn initialized_backend_mut(&mut self) -> Option<&mut Backend> { if self.is_initialized { return Some(self.backend.to_mut()) } @@ -139,7 +142,10 @@ impl<'a> CowBackend<'a> { } } -impl DatabaseExt for CowBackend<'_> { +impl DatabaseExt for CowBackend<'_, S> +where + S: BackendStrategy, +{ fn get_fork_info(&mut self, id: LocalForkId) -> eyre::Result { self.backend.to_mut().get_fork_info(id) } @@ -316,7 +322,10 @@ impl DatabaseExt for CowBackend<'_> { } } -impl DatabaseRef for CowBackend<'_> { +impl DatabaseRef for CowBackend<'_, S> +where + S: BackendStrategy, +{ type Error = DatabaseError; fn basic_ref(&self, address: Address) -> Result, Self::Error> { @@ -336,7 +345,10 @@ impl DatabaseRef for CowBackend<'_> { } } -impl Database for CowBackend<'_> { +impl Database for CowBackend<'_, S> +where + S: BackendStrategy, +{ type Error = DatabaseError; fn basic(&mut self, address: Address) -> Result, Self::Error> { @@ -356,7 +368,10 @@ impl Database for CowBackend<'_> { } } -impl DatabaseCommit for CowBackend<'_> { +impl DatabaseCommit for CowBackend<'_, S> +where + S: BackendStrategy, +{ fn commit(&mut self, changes: Map) { self.backend.to_mut().commit(changes) } diff --git a/crates/evm/core/src/backend/mod.rs b/crates/evm/core/src/backend/mod.rs index e0042f9d7..e9f9ef9ec 100644 --- a/crates/evm/core/src/backend/mod.rs +++ b/crates/evm/core/src/backend/mod.rs @@ -14,11 +14,7 @@ use alloy_serde::WithOtherFields; use eyre::Context; use foundry_common::{is_known_system_sender, SYSTEM_TRANSACTION_TYPE}; pub use foundry_fork_db::{cache::BlockchainDbMeta, BlockchainDb, SharedBackend}; -use foundry_zksync_core::{ - convert::ConvertH160, PaymasterParams, ACCOUNT_CODE_STORAGE_ADDRESS, - IMMUTABLE_SIMULATOR_STORAGE_ADDRESS, KNOWN_CODES_STORAGE_ADDRESS, L2_BASE_TOKEN_ADDRESS, - NONCE_HOLDER_ADDRESS, -}; +use foundry_zksync_core::PaymasterParams; use itertools::Itertools; use revm::{ db::{CacheDB, DatabaseRef}, @@ -31,9 +27,11 @@ use revm::{ Database, DatabaseCommit, JournaledState, }; use std::{ - collections::{hash_map::Entry, BTreeMap, HashSet}, + collections::{BTreeMap, HashSet}, + sync::{Arc, Mutex}, time::Instant, }; +use strategy::{BackendStrategy, BackendStrategyForkInfo, EvmBackendStrategy}; mod diagnostic; pub use diagnostic::RevertDiagnostic; @@ -53,8 +51,10 @@ pub use snapshot::{BackendStateSnapshot, RevertStateSnapshotAction, StateSnapsho mod fork_type; pub use fork_type::{CachedForkType, ForkType}; +pub mod strategy; + // A `revm::Database` that is used in forking mode -type ForkDB = CacheDB; +pub type ForkDB = CacheDB; /// Represents a numeric `ForkId` valid only for the existence of the `Backend`. /// @@ -464,7 +464,10 @@ struct _ObjectSafe(dyn DatabaseExt); /// after reverting the snapshot. #[derive(Clone, Debug)] #[must_use] -pub struct Backend { +pub struct Backend { + /// Custom backend strategy + pub strategy: Arc>, + /// The access point for managing forks forks: MultiFork, // The default in memory db @@ -494,24 +497,21 @@ pub struct Backend { inner: BackendInner, /// Keeps track of the fork type fork_url_type: CachedForkType, - /// TODO: Ensure this parameter is updated on `select_fork`. - /// - /// Keeps track if the backend is in ZK mode. - /// This is required to correctly merge storage when selecting another ZK fork. - /// The balance, nonce and code are stored under zkSync's respective system contract - /// storages. These need to be merged into the forked storage. - pub is_zk: bool, /// Store storage keys per contract address for immutable variables. + /// TODO(zk): Move this to strategy zk_recorded_immutable_keys: HashMap>, } -impl Backend { +impl Backend +where + S: BackendStrategy, +{ /// Creates a new Backend with a spawned multi fork thread. /// /// If `fork` is `Some` this will use a `fork` database, otherwise with an in-memory /// database. - pub fn spawn(fork: Option) -> Self { - Self::new(MultiFork::spawn(), fork) + pub fn spawn(fork: Option, strategy: Arc>) -> Self { + Self::new(MultiFork::spawn(), fork, strategy) } /// Creates a new instance of `Backend` @@ -520,7 +520,7 @@ impl Backend { /// database. /// /// Prefer using [`spawn`](Self::spawn) instead. - pub fn new(forks: MultiFork, fork: Option) -> Self { + pub fn new(forks: MultiFork, fork: Option, strategy: Arc>) -> Self { trace!(target: "backend", forking_mode=?fork.is_some(), "creating executor backend"); // Note: this will take of registering the `fork` let inner = BackendInner { @@ -529,13 +529,13 @@ impl Backend { }; let mut backend = Self { + strategy, forks, mem_db: CacheDB::new(Default::default()), fork_init_journaled_state: inner.new_journaled_state(), active_fork_ids: None, inner, fork_url_type: Default::default(), - is_zk: false, zk_recorded_immutable_keys: Default::default(), }; @@ -559,8 +559,13 @@ impl Backend { /// Creates a new instance of `Backend` with fork added to the fork database and sets the fork /// as active - pub(crate) fn new_with_fork(id: &ForkId, fork: Fork, journaled_state: JournaledState) -> Self { - let mut backend = Self::spawn(None); + pub(crate) fn new_with_fork( + id: &ForkId, + fork: Fork, + journaled_state: JournaledState, + strategy: Arc>, + ) -> Self { + let mut backend = Self::spawn(None, strategy); let fork_ids = backend.inner.insert_new_fork(id.clone(), fork.db, journaled_state); backend.inner.launched_with_fork = Some((id.clone(), fork_ids.0, fork_ids.1)); backend.active_fork_ids = Some(fork_ids); @@ -570,13 +575,13 @@ impl Backend { /// Creates a new instance with a `BackendDatabase::InMemory` cache layer for the `CacheDB` pub fn clone_empty(&self) -> Self { Self { + strategy: self.strategy.clone(), forks: self.forks.clone(), mem_db: CacheDB::new(Default::default()), fork_init_journaled_state: self.inner.new_journaled_state(), active_fork_ids: None, inner: Default::default(), fork_url_type: Default::default(), - is_zk: false, zk_recorded_immutable_keys: Default::default(), } } @@ -679,41 +684,41 @@ impl Backend { self.inner.has_state_snapshot_failure = has_state_snapshot_failure } - /// When creating or switching forks, we update the AccountInfo of the contract - pub(crate) fn update_fork_db( - &self, - active_journaled_state: &mut JournaledState, - target_fork: &mut Fork, - zk_state: Option, - ) { - self.update_fork_db_contracts( - self.inner.persistent_accounts.iter().copied(), - active_journaled_state, - target_fork, - zk_state, - ) - } - - /// Merges the state of all `accounts` from the currently active db into the given `fork` - pub(crate) fn update_fork_db_contracts( - &self, - accounts: impl IntoIterator, - active_journaled_state: &mut JournaledState, - target_fork: &mut Fork, - zk_state: Option, - ) { - if let Some(db) = self.active_fork_db() { - merge_account_data(accounts, db, active_journaled_state, target_fork, zk_state) - } else { - merge_account_data( - accounts, - &self.mem_db, - active_journaled_state, - target_fork, - zk_state, - ) - } - } + // /// When creating or switching forks, we update the AccountInfo of the contract + // pub(crate) fn update_fork_db( + // &self, + // active_journaled_state: &mut JournaledState, + // target_fork: &mut Fork, + // zk_state: Option, + // ) { + // self.update_fork_db_contracts( + // self.inner.persistent_accounts.iter().copied(), + // active_journaled_state, + // target_fork, + // zk_state, + // ) + // } + + // /// Merges the state of all `accounts` from the currently active db into the given `fork` + // pub(crate) fn update_fork_db_contracts( + // &self, + // accounts: impl IntoIterator, + // active_journaled_state: &mut JournaledState, + // target_fork: &mut Fork, + // zk_state: Option, + // ) { + // if let Some(db) = self.active_fork_db() { + // merge_account_data(accounts, db, active_journaled_state, target_fork, zk_state) + // } else { + // merge_account_data( + // accounts, + // &self.mem_db, + // active_journaled_state, + // target_fork, + // zk_state, + // ) + // } + // } /// Returns the memory db used if not in forking mode pub fn mem_db(&self) -> &FoundryEvmInMemoryDB { @@ -805,7 +810,7 @@ impl Backend { /// Initializes settings we need to keep track of. /// /// We need to track these mainly to prevent issues when switching between different evms - pub(crate) fn initialize(&mut self, env: &EnvWithHandlerCfg) { + pub fn initialize(&mut self, env: &EnvWithHandlerCfg) { self.set_caller(env.tx.caller); self.set_spec_id(env.handler_cfg.spec_id); } @@ -824,15 +829,20 @@ impl Backend { &mut self, env: &mut EnvWithHandlerCfg, inspector: &mut I, + extra: Option>, ) -> eyre::Result { self.initialize(env); - let mut evm = crate::utils::new_evm_with_inspector(self, env.clone(), inspector); - let res = evm.transact().wrap_err("backend: failed while inspecting")?; + let strategy = self.strategy.clone(); + let mut guard = strategy.lock().unwrap(); + guard.inspect(self, env, inspector, extra) + // let mut evm = crate::utils::new_evm_with_inspector(self, env.clone(), inspector); + + // let res = evm.transact().wrap_err("backend: failed while inspecting")?; - env.env = evm.context.evm.inner.env; + // env.env = evm.context.evm.inner.env; - Ok(res) + // Ok(res) } /// Executes the configured test call of the `env` without committing state changes @@ -989,7 +999,10 @@ impl Backend { } } -impl DatabaseExt for Backend { +impl DatabaseExt for Backend +where + S: BackendStrategy, +{ fn get_fork_info(&mut self, id: LocalForkId) -> eyre::Result { let fork_id = self.ensure_fork_id(id).cloned()?; let fork_env = self @@ -1158,23 +1171,23 @@ impl DatabaseExt for Backend { let fork_id = self.ensure_fork_id(id).cloned()?; let idx = self.inner.ensure_fork_index(&fork_id)?; - let is_current_zk_fork = if let Some(active_fork_id) = self.active_fork_id() { + let current_fork_type = if let Some(active_fork_id) = self.active_fork_id() { self.forks .get_fork_url(self.ensure_fork_id(active_fork_id).cloned()?)? - .map(|url| self.fork_url_type.get(&url).is_zk()) - .unwrap_or_default() + .map(|url| self.fork_url_type.get(&url)) + .unwrap_or(ForkType::Evm) } else { - self.is_zk + ForkType::Zk }; - let is_target_zk_fork = self + let target_fork_type = self .forks .get_fork_url(fork_id.clone())? - .map(|url| self.fork_url_type.get(&url).is_zk()) - .unwrap_or_default(); - let merge_zk_db = is_current_zk_fork && is_target_zk_fork; - let zk_state = merge_zk_db.then(|| ZkMergeState { - persistent_immutable_keys: self.zk_recorded_immutable_keys.clone(), - }); + .map(|url| self.fork_url_type.get(&url)) + .unwrap_or(ForkType::Evm); + // let merge_zk_db = is_current_zk_fork && is_target_zk_fork; + // let zk_state = merge_zk_db.then(|| ZkMergeState { + // persistent_immutable_keys: self.zk_recorded_immutable_keys.clone(), + // }); let fork_env = self .forks @@ -1243,7 +1256,18 @@ impl DatabaseExt for Backend { caller_account.into() }); - self.update_fork_db(active_journaled_state, &mut fork, zk_state); + // TODO(zk): add zk_state by using it inside strategy + self.strategy.lock().unwrap().update_fork_db( + BackendStrategyForkInfo { + active_fork: self.active_fork(), + active_type: current_fork_type, + target_type: target_fork_type, + }, + &self.mem_db, + &self.inner, + active_journaled_state, + &mut fork, + ); // insert the fork back self.inner.set_fork(idx, fork); @@ -1292,7 +1316,11 @@ impl DatabaseExt for Backend { active.journaled_state.depth = journaled_state.depth; for addr in persistent_addrs { - merge_journaled_state_data(addr, journaled_state, &mut active.journaled_state); + strategy::merge_journaled_state_data( + addr, + journaled_state, + &mut active.journaled_state, + ); } // Ensure all previously loaded accounts are present in the journaled state to @@ -1305,7 +1333,7 @@ impl DatabaseExt for Backend { for (addr, acc) in journaled_state.state.iter() { if acc.is_created() { if acc.is_touched() { - merge_journaled_state_data( + strategy::merge_journaled_state_data( *addr, journaled_state, &mut active.journaled_state, @@ -1598,7 +1626,10 @@ impl DatabaseExt for Backend { } } -impl DatabaseRef for Backend { +impl DatabaseRef for Backend +where + S: BackendStrategy, +{ type Error = DatabaseError; fn basic_ref(&self, address: Address) -> Result, Self::Error> { @@ -1634,7 +1665,10 @@ impl DatabaseRef for Backend { } } -impl DatabaseCommit for Backend { +impl DatabaseCommit for Backend +where + S: BackendStrategy, +{ fn commit(&mut self, changes: Map) { if let Some(db) = self.active_fork_db_mut() { db.commit(changes) @@ -1644,7 +1678,10 @@ impl DatabaseCommit for Backend { } } -impl Database for Backend { +impl Database for Backend +where + S: BackendStrategy, +{ type Error = DatabaseError; fn basic(&mut self, address: Address) -> Result, Self::Error> { if let Some(db) = self.active_fork_db_mut() { @@ -1691,8 +1728,8 @@ pub enum BackendDatabaseSnapshot { /// Represents a fork #[derive(Clone, Debug)] pub struct Fork { - db: ForkDB, - journaled_state: JournaledState, + pub db: ForkDB, + pub journaled_state: JournaledState, } impl Fork { @@ -1875,7 +1912,7 @@ impl BackendInner { // we initialize a _new_ `ForkDB` but keep the state of persistent accounts let mut new_db = ForkDB::new(backend); for addr in self.persistent_accounts.iter().copied() { - merge_db_account_data(addr, &active.db, &mut new_db); + strategy::merge_db_account_data(addr, &active.db, &mut new_db); } active.db = new_db; } @@ -1961,227 +1998,221 @@ pub(crate) fn update_current_env_with_fork_env(current: &mut Env, fork: Env) { current.tx.chain_id = fork.tx.chain_id; } -/// Defines the zksync specific state to help during merge. -#[derive(Debug, Default)] -pub(crate) struct ZkMergeState { - persistent_immutable_keys: HashMap>, -} - -/// Clones the data of the given `accounts` from the `active` database into the `fork_db` -/// This includes the data held in storage (`CacheDB`) and kept in the `JournaledState`. -pub(crate) fn merge_account_data( - accounts: impl IntoIterator, - active: &CacheDB, - active_journaled_state: &mut JournaledState, - target_fork: &mut Fork, - zk_state: Option, -) { - for addr in accounts.into_iter() { - merge_db_account_data(addr, active, &mut target_fork.db); - if let Some(zk_state) = &zk_state { - merge_zk_account_data(addr, active, &mut target_fork.db, zk_state); - } - merge_journaled_state_data(addr, active_journaled_state, &mut target_fork.journaled_state); - if let Some(zk_state) = &zk_state { - merge_zk_journaled_state_data( - addr, - active_journaled_state, - &mut target_fork.journaled_state, - zk_state, - ); - } - } - - // need to mock empty journal entries in case the current checkpoint is higher than the existing - // journal entries - while active_journaled_state.journal.len() > target_fork.journaled_state.journal.len() { - target_fork.journaled_state.journal.push(Default::default()); - } - - *active_journaled_state = target_fork.journaled_state.clone(); -} - -/// Clones the account data from the `active_journaled_state` into the `fork_journaled_state` -fn merge_journaled_state_data( - addr: Address, - active_journaled_state: &JournaledState, - fork_journaled_state: &mut JournaledState, -) { - if let Some(mut acc) = active_journaled_state.state.get(&addr).cloned() { - trace!(?addr, "updating journaled_state account data"); - if let Some(fork_account) = fork_journaled_state.state.get_mut(&addr) { - // This will merge the fork's tracked storage with active storage and update values - fork_account.storage.extend(std::mem::take(&mut acc.storage)); - // swap them so we can insert the account as whole in the next step - std::mem::swap(&mut fork_account.storage, &mut acc.storage); - } - fork_journaled_state.state.insert(addr, acc); - } -} - -/// Clones the account data from the `active` db into the `ForkDB` -fn merge_db_account_data( - addr: Address, - active: &CacheDB, - fork_db: &mut ForkDB, -) { - trace!(?addr, "merging database data"); - - let Some(acc) = active.accounts.get(&addr) else { return }; - - // port contract cache over - if let Some(code) = active.contracts.get(&acc.info.code_hash) { - trace!("merging contract cache"); - fork_db.contracts.insert(acc.info.code_hash, code.clone()); - } - - // port account storage over - match fork_db.accounts.entry(addr) { - Entry::Vacant(vacant) => { - trace!("target account not present - inserting from active"); - // if the fork_db doesn't have the target account - // insert the entire thing - vacant.insert(acc.clone()); - } - Entry::Occupied(mut occupied) => { - trace!("target account present - merging storage slots"); - // if the fork_db does have the system, - // extend the existing storage (overriding) - let fork_account = occupied.get_mut(); - fork_account.storage.extend(&acc.storage); - } - } -} - -/// Clones the zk account data from the `active` db into the `ForkDB` -fn merge_zk_account_data( - addr: Address, - active: &CacheDB, - fork_db: &mut ForkDB, - _zk_state: &ZkMergeState, -) { - let merge_system_contract_entry = - |fork_db: &mut ForkDB, system_contract: Address, slot: U256| { - let Some(acc) = active.accounts.get(&system_contract) else { return }; - - // port contract cache over - if let Some(code) = active.contracts.get(&acc.info.code_hash) { - trace!("merging contract cache"); - fork_db.contracts.insert(acc.info.code_hash, code.clone()); - } - - // prepare only the specified slot in account storage - let mut new_acc = acc.clone(); - new_acc.storage = Default::default(); - if let Some(value) = acc.storage.get(&slot) { - new_acc.storage.insert(slot, *value); - } - - // port account storage over - match fork_db.accounts.entry(system_contract) { - Entry::Vacant(vacant) => { - trace!("target account not present - inserting from active"); - // if the fork_db doesn't have the target account - // insert the entire thing - vacant.insert(new_acc); - } - Entry::Occupied(mut occupied) => { - trace!("target account present - merging storage slots"); - // if the fork_db does have the system, - // extend the existing storage (overriding) - let fork_account = occupied.get_mut(); - fork_account.storage.extend(&new_acc.storage); - } - } - }; - - merge_system_contract_entry( - fork_db, - L2_BASE_TOKEN_ADDRESS.to_address(), - foundry_zksync_core::get_balance_key(addr), - ); - merge_system_contract_entry( - fork_db, - ACCOUNT_CODE_STORAGE_ADDRESS.to_address(), - foundry_zksync_core::get_account_code_key(addr), - ); - merge_system_contract_entry( - fork_db, - NONCE_HOLDER_ADDRESS.to_address(), - foundry_zksync_core::get_nonce_key(addr), - ); - - if let Some(acc) = active.accounts.get(&addr) { - merge_system_contract_entry( - fork_db, - KNOWN_CODES_STORAGE_ADDRESS.to_address(), - U256::from_be_slice(&acc.info.code_hash.0[..]), - ); - } -} - -/// Clones the account data from the `active_journaled_state` into the `fork_journaled_state` for -/// zksync storage. -fn merge_zk_journaled_state_data( - addr: Address, - active_journaled_state: &JournaledState, - fork_journaled_state: &mut JournaledState, - zk_state: &ZkMergeState, -) { - let merge_system_contract_entry = - |fork_journaled_state: &mut JournaledState, system_contract: Address, slot: U256| { - if let Some(acc) = active_journaled_state.state.get(&system_contract) { - // prepare only the specified slot in account storage - let mut new_acc = acc.clone(); - new_acc.storage = Default::default(); - if let Some(value) = acc.storage.get(&slot).cloned() { - new_acc.storage.insert(slot, value); - } - - match fork_journaled_state.state.entry(system_contract) { - Entry::Vacant(vacant) => { - vacant.insert(new_acc); - } - Entry::Occupied(mut occupied) => { - let fork_account = occupied.get_mut(); - fork_account.storage.extend(new_acc.storage); - } - } - } - }; - - merge_system_contract_entry( - fork_journaled_state, - L2_BASE_TOKEN_ADDRESS.to_address(), - foundry_zksync_core::get_balance_key(addr), - ); - merge_system_contract_entry( - fork_journaled_state, - ACCOUNT_CODE_STORAGE_ADDRESS.to_address(), - foundry_zksync_core::get_account_code_key(addr), - ); - merge_system_contract_entry( - fork_journaled_state, - NONCE_HOLDER_ADDRESS.to_address(), - foundry_zksync_core::get_nonce_key(addr), - ); - - if let Some(acc) = active_journaled_state.state.get(&addr) { - merge_system_contract_entry( - fork_journaled_state, - KNOWN_CODES_STORAGE_ADDRESS.to_address(), - U256::from_be_slice(&acc.info.code_hash.0[..]), - ); - } - - // merge immutable storage. - let immutable_simulator_addr = IMMUTABLE_SIMULATOR_STORAGE_ADDRESS.to_address(); - if let Some(immutable_storage_keys) = zk_state.persistent_immutable_keys.get(&addr) { - for slot_key in immutable_storage_keys { - merge_system_contract_entry(fork_journaled_state, immutable_simulator_addr, *slot_key); - } - } -} +// /// Clones the data of the given `accounts` from the `active` database into the `fork_db` +// /// This includes the data held in storage (`CacheDB`) and kept in the `JournaledState`. +// pub(crate) fn merge_account_data( +// accounts: impl IntoIterator, +// active: &CacheDB, +// active_journaled_state: &mut JournaledState, +// target_fork: &mut Fork, +// zk_state: Option, +// ) { +// for addr in accounts.into_iter() { +// merge_db_account_data(addr, active, &mut target_fork.db); +// if let Some(zk_state) = &zk_state { +// merge_zk_account_data(addr, active, &mut target_fork.db, zk_state); +// } +// merge_journaled_state_data(addr, active_journaled_state, &mut +// target_fork.journaled_state); if let Some(zk_state) = &zk_state { +// merge_zk_journaled_state_data( +// addr, +// active_journaled_state, +// &mut target_fork.journaled_state, +// zk_state, +// ); +// } +// } + +// // need to mock empty journal entries in case the current checkpoint is higher than the +// existing // journal entries +// while active_journaled_state.journal.len() > target_fork.journaled_state.journal.len() { +// target_fork.journaled_state.journal.push(Default::default()); +// } + +// *active_journaled_state = target_fork.journaled_state.clone(); +// } + +// /// Clones the account data from the `active_journaled_state` into the `fork_journaled_state` +// fn merge_journaled_state_data( +// addr: Address, +// active_journaled_state: &JournaledState, +// fork_journaled_state: &mut JournaledState, +// ) { +// if let Some(mut acc) = active_journaled_state.state.get(&addr).cloned() { +// trace!(?addr, "updating journaled_state account data"); +// if let Some(fork_account) = fork_journaled_state.state.get_mut(&addr) { +// // This will merge the fork's tracked storage with active storage and update values +// fork_account.storage.extend(std::mem::take(&mut acc.storage)); +// // swap them so we can insert the account as whole in the next step +// std::mem::swap(&mut fork_account.storage, &mut acc.storage); +// } +// fork_journaled_state.state.insert(addr, acc); +// } +// } + +// /// Clones the account data from the `active` db into the `ForkDB` +// fn merge_db_account_data( +// addr: Address, +// active: &CacheDB, +// fork_db: &mut ForkDB, +// ) { +// trace!(?addr, "merging database data"); + +// let Some(acc) = active.accounts.get(&addr) else { return }; + +// // port contract cache over +// if let Some(code) = active.contracts.get(&acc.info.code_hash) { +// trace!("merging contract cache"); +// fork_db.contracts.insert(acc.info.code_hash, code.clone()); +// } + +// // port account storage over +// match fork_db.accounts.entry(addr) { +// Entry::Vacant(vacant) => { +// trace!("target account not present - inserting from active"); +// // if the fork_db doesn't have the target account +// // insert the entire thing +// vacant.insert(acc.clone()); +// } +// Entry::Occupied(mut occupied) => { +// trace!("target account present - merging storage slots"); +// // if the fork_db does have the system, +// // extend the existing storage (overriding) +// let fork_account = occupied.get_mut(); +// fork_account.storage.extend(&acc.storage); +// } +// } +// } + +// /// Clones the zk account data from the `active` db into the `ForkDB` +// fn merge_zk_account_data( +// addr: Address, +// active: &CacheDB, +// fork_db: &mut ForkDB, +// _zk_state: &ZkMergeState, +// ) { +// let merge_system_contract_entry = +// |fork_db: &mut ForkDB, system_contract: Address, slot: U256| { +// let Some(acc) = active.accounts.get(&system_contract) else { return }; + +// // port contract cache over +// if let Some(code) = active.contracts.get(&acc.info.code_hash) { +// trace!("merging contract cache"); +// fork_db.contracts.insert(acc.info.code_hash, code.clone()); +// } + +// // prepare only the specified slot in account storage +// let mut new_acc = acc.clone(); +// new_acc.storage = Default::default(); +// if let Some(value) = acc.storage.get(&slot) { +// new_acc.storage.insert(slot, *value); +// } + +// // port account storage over +// match fork_db.accounts.entry(system_contract) { +// Entry::Vacant(vacant) => { +// trace!("target account not present - inserting from active"); +// // if the fork_db doesn't have the target account +// // insert the entire thing +// vacant.insert(new_acc); +// } +// Entry::Occupied(mut occupied) => { +// trace!("target account present - merging storage slots"); +// // if the fork_db does have the system, +// // extend the existing storage (overriding) +// let fork_account = occupied.get_mut(); +// fork_account.storage.extend(&new_acc.storage); +// } +// } +// }; + +// merge_system_contract_entry( +// fork_db, +// L2_BASE_TOKEN_ADDRESS.to_address(), +// foundry_zksync_core::get_balance_key(addr), +// ); +// merge_system_contract_entry( +// fork_db, +// ACCOUNT_CODE_STORAGE_ADDRESS.to_address(), +// foundry_zksync_core::get_account_code_key(addr), +// ); +// merge_system_contract_entry( +// fork_db, +// NONCE_HOLDER_ADDRESS.to_address(), +// foundry_zksync_core::get_nonce_key(addr), +// ); + +// if let Some(acc) = active.accounts.get(&addr) { +// merge_system_contract_entry( +// fork_db, +// KNOWN_CODES_STORAGE_ADDRESS.to_address(), +// U256::from_be_slice(&acc.info.code_hash.0[..]), +// ); +// } +// } + +// /// Clones the account data from the `active_journaled_state` into the `fork_journaled_state` for +// /// zksync storage. +// fn merge_zk_journaled_state_data( +// addr: Address, +// active_journaled_state: &JournaledState, +// fork_journaled_state: &mut JournaledState, +// zk_state: &ZkMergeState, +// ) { +// let merge_system_contract_entry = +// |fork_journaled_state: &mut JournaledState, system_contract: Address, slot: U256| { +// if let Some(acc) = active_journaled_state.state.get(&system_contract) { +// // prepare only the specified slot in account storage +// let mut new_acc = acc.clone(); +// new_acc.storage = Default::default(); +// if let Some(value) = acc.storage.get(&slot).cloned() { +// new_acc.storage.insert(slot, value); +// } + +// match fork_journaled_state.state.entry(system_contract) { +// Entry::Vacant(vacant) => { +// vacant.insert(new_acc); +// } +// Entry::Occupied(mut occupied) => { +// let fork_account = occupied.get_mut(); +// fork_account.storage.extend(new_acc.storage); +// } +// } +// } +// }; + +// merge_system_contract_entry( +// fork_journaled_state, +// L2_BASE_TOKEN_ADDRESS.to_address(), +// foundry_zksync_core::get_balance_key(addr), +// ); +// merge_system_contract_entry( +// fork_journaled_state, +// ACCOUNT_CODE_STORAGE_ADDRESS.to_address(), +// foundry_zksync_core::get_account_code_key(addr), +// ); +// merge_system_contract_entry( +// fork_journaled_state, +// NONCE_HOLDER_ADDRESS.to_address(), +// foundry_zksync_core::get_nonce_key(addr), +// ); + +// if let Some(acc) = active_journaled_state.state.get(&addr) { +// merge_system_contract_entry( +// fork_journaled_state, +// KNOWN_CODES_STORAGE_ADDRESS.to_address(), +// U256::from_be_slice(&acc.info.code_hash.0[..]), +// ); +// } + +// // merge immutable storage. +// let immutable_simulator_addr = IMMUTABLE_SIMULATOR_STORAGE_ADDRESS.to_address(); +// if let Some(immutable_storage_keys) = zk_state.persistent_immutable_keys.get(&addr) { +// for slot_key in immutable_storage_keys { +// merge_system_contract_entry(fork_journaled_state, immutable_simulator_addr, +// *slot_key); } +// } +// } /// Returns true of the address is a contract fn is_contract_in_state(journaled_state: &JournaledState, acc: Address) -> bool { @@ -2229,7 +2260,8 @@ fn commit_transaction( let fork = fork.clone(); let journaled_state = journaled_state.clone(); let depth = journaled_state.depth; - let mut db = Backend::new_with_fork(fork_id, fork, journaled_state); + let mut db = + Backend::new_with_fork(fork_id, fork, journaled_state, EvmBackendStrategy::new()); let mut evm = crate::utils::new_evm_with_inspector(&mut db as _, env, inspector); // Adjust inner EVM depth to ensure that inspectors receive accurate data. @@ -2312,7 +2344,7 @@ mod tests { evm_opts, }; - let backend = Backend::spawn(Some(fork)); + let backend = Backend::spawn(Some(fork), EvmBackendStrategy::new()); // some rng contract from etherscan let address: Address = "63091244180ae240c87d1f528f5f269134cb07b3".parse().unwrap(); diff --git a/crates/evm/core/src/backend/strategy.rs b/crates/evm/core/src/backend/strategy.rs new file mode 100644 index 000000000..2c924d74f --- /dev/null +++ b/crates/evm/core/src/backend/strategy.rs @@ -0,0 +1,192 @@ +use std::sync::{Arc, Mutex}; + +use crate::InspectorExt; + +use super::{Backend, BackendInner, Fork, ForkDB, ForkType, FoundryEvmInMemoryDB}; +use alloy_primitives::Address; +use eyre::Context; +use revm::{ + db::CacheDB, + primitives::{EnvWithHandlerCfg, ResultAndState}, + DatabaseRef, JournaledState, +}; +use serde::{Deserialize, Serialize}; + +pub struct BackendStrategyForkInfo<'a> { + pub active_fork: Option<&'a Fork>, + pub active_type: ForkType, + pub target_type: ForkType, +} + +pub trait BackendStrategy: std::fmt::Debug + Send + Sync + Default + Clone + Serialize + for<'a> Deserialize<'a> + 'static +where + Self: Sized, +{ + fn new() -> Arc> { + Arc::new(Mutex::new(Self::default())) + } + + fn name(&self) -> &'static str; + + /// When creating or switching forks, we update the AccountInfo of the contract + fn update_fork_db( + &self, + fork_info: BackendStrategyForkInfo<'_>, + mem_db: &FoundryEvmInMemoryDB, + backend_inner: &BackendInner, + active_journaled_state: &mut JournaledState, + target_fork: &mut Fork, + ); + + /// Executes the configured test call of the `env` without committing state changes. + /// + /// Note: in case there are any cheatcodes executed that modify the environment, this will + /// update the given `env` with the new values. + #[instrument(name = "inspect", level = "debug", skip_all)] + fn inspect( + &mut self, + backend: &mut Backend, + env: &mut EnvWithHandlerCfg, + inspector: &mut I, + _extra: Option>, + ) -> eyre::Result { + backend.initialize(env); + let mut evm = crate::utils::new_evm_with_inspector(backend, env.clone(), inspector); + + let res = evm.transact().wrap_err("backend: failed while inspecting")?; + + env.env = evm.context.evm.inner.env; + + Ok(res) + } +} + +// struct _ObjectSafe(dyn BackendStrategy); + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct EvmBackendStrategy; + +impl BackendStrategy for EvmBackendStrategy { + fn name(&self) -> &'static str { + "evm" + } + + fn update_fork_db( + &self, + fork_info: BackendStrategyForkInfo<'_>, + mem_db: &FoundryEvmInMemoryDB, + backend_inner: &BackendInner, + active_journaled_state: &mut JournaledState, + target_fork: &mut Fork, + ) { + self.update_fork_db_contracts( + fork_info, + mem_db, + backend_inner, + active_journaled_state, + target_fork, + ) + } +} + +impl EvmBackendStrategy { + /// Merges the state of all `accounts` from the currently active db into the given `fork` + pub(crate) fn update_fork_db_contracts( + &self, + fork_info: BackendStrategyForkInfo<'_>, + mem_db: &FoundryEvmInMemoryDB, + backend_inner: &BackendInner, + active_journaled_state: &mut JournaledState, + target_fork: &mut Fork, + ) { + let accounts = backend_inner.persistent_accounts.iter().copied(); + if let Some(db) = fork_info.active_fork.map(|f| &f.db) { + EvmBackendMergeStrategy::merge_account_data( + accounts, + db, + active_journaled_state, + target_fork, + ) + } else { + EvmBackendMergeStrategy::merge_account_data( + accounts, + mem_db, + active_journaled_state, + target_fork, + ) + } + } +} +pub struct EvmBackendMergeStrategy; +impl EvmBackendMergeStrategy { + /// Clones the data of the given `accounts` from the `active` database into the `fork_db` + /// This includes the data held in storage (`CacheDB`) and kept in the `JournaledState`. + pub fn merge_account_data( + accounts: impl IntoIterator, + active: &CacheDB, + active_journaled_state: &mut JournaledState, + target_fork: &mut Fork, + ) { + for addr in accounts.into_iter() { + merge_db_account_data(addr, active, &mut target_fork.db); + merge_journaled_state_data( + addr, + active_journaled_state, + &mut target_fork.journaled_state, + ); + } + + // need to mock empty journal entries in case the current checkpoint is higher than the + // existing journal entries + while active_journaled_state.journal.len() > target_fork.journaled_state.journal.len() { + target_fork.journaled_state.journal.push(Default::default()); + } + + *active_journaled_state = target_fork.journaled_state.clone(); + } +} + +/// Clones the account data from the `active_journaled_state` into the `fork_journaled_state` +pub fn merge_journaled_state_data( + addr: Address, + active_journaled_state: &JournaledState, + fork_journaled_state: &mut JournaledState, +) { + if let Some(mut acc) = active_journaled_state.state.get(&addr).cloned() { + trace!(?addr, "updating journaled_state account data"); + if let Some(fork_account) = fork_journaled_state.state.get_mut(&addr) { + // This will merge the fork's tracked storage with active storage and update values + fork_account.storage.extend(std::mem::take(&mut acc.storage)); + // swap them so we can insert the account as whole in the next step + std::mem::swap(&mut fork_account.storage, &mut acc.storage); + } + fork_journaled_state.state.insert(addr, acc); + } +} + +/// Clones the account data from the `active` db into the `ForkDB` +pub fn merge_db_account_data( + addr: Address, + active: &CacheDB, + fork_db: &mut ForkDB, +) { + let mut acc = if let Some(acc) = active.accounts.get(&addr).cloned() { + acc + } else { + // Account does not exist + return; + }; + + if let Some(code) = active.contracts.get(&acc.info.code_hash).cloned() { + fork_db.contracts.insert(acc.info.code_hash, code); + } + + if let Some(fork_account) = fork_db.accounts.get_mut(&addr) { + // This will merge the fork's tracked storage with active storage and update values + fork_account.storage.extend(std::mem::take(&mut acc.storage)); + // swap them so we can insert the account as whole in the next step + std::mem::swap(&mut fork_account.storage, &mut acc.storage); + } + + fork_db.accounts.insert(addr, acc); +} diff --git a/crates/evm/evm/Cargo.toml b/crates/evm/evm/Cargo.toml index ca0287b65..15f0b1f88 100644 --- a/crates/evm/evm/Cargo.toml +++ b/crates/evm/evm/Cargo.toml @@ -25,7 +25,7 @@ foundry-evm-traces.workspace = true foundry-zksync-core.workspace = true foundry-zksync-inspectors.workspace = true -alloy-dyn-abi = { workspace = true, features = [ "arbitrary", "eip712" ] } +alloy-dyn-abi = { workspace = true, features = ["arbitrary", "eip712"] } alloy-json-abi.workspace = true alloy-primitives = { workspace = true, features = [ "serde", @@ -53,3 +53,4 @@ thiserror.workspace = true tracing.workspace = true indicatif = "0.17" serde.workspace = true +serde_json.workspace = true diff --git a/crates/evm/evm/src/executors/builder.rs b/crates/evm/evm/src/executors/builder.rs index 3e6b3a1a8..bbfa7720f 100644 --- a/crates/evm/evm/src/executors/builder.rs +++ b/crates/evm/evm/src/executors/builder.rs @@ -1,5 +1,5 @@ use crate::{executors::Executor, inspectors::InspectorStackBuilder}; -use foundry_evm_core::backend::Backend; +use foundry_evm_core::backend::{strategy::BackendStrategy, Backend}; use revm::primitives::{Env, EnvWithHandlerCfg, SpecId}; /// The builder that allows to configure an evm [`Executor`] which a stack of optional @@ -83,7 +83,7 @@ impl ExecutorBuilder { /// Builds the executor as configured. #[inline] - pub fn build(self, env: Env, db: Backend) -> Executor { + pub fn build(self, env: Env, db: Backend) -> Executor { let Self { mut stack, gas_limit, spec_id, legacy_assertions, use_zk } = self; if stack.block.is_none() { stack.block = Some(env.block.clone()); diff --git a/crates/evm/evm/src/executors/fuzz/mod.rs b/crates/evm/evm/src/executors/fuzz/mod.rs index 8c8d7ff69..282359763 100644 --- a/crates/evm/evm/src/executors/fuzz/mod.rs +++ b/crates/evm/evm/src/executors/fuzz/mod.rs @@ -6,8 +6,7 @@ use eyre::Result; use foundry_common::evm::Breakpoints; use foundry_config::FuzzConfig; use foundry_evm_core::{ - constants::MAGIC_ASSUME, - decode::{RevertDecoder, SkipReason}, + backend::strategy::BackendStrategy, constants::MAGIC_ASSUME, decode::{RevertDecoder, SkipReason} }; use foundry_evm_coverage::HitMaps; use foundry_evm_fuzz::{ @@ -50,9 +49,9 @@ pub struct FuzzTestData { /// After instantiation, calling `fuzz` will proceed to hammer the deployed smart contract with /// inputs, until it finds a counterexample. The provided [`TestRunner`] contains all the /// configuration which can be overridden via [environment variables](proptest::test_runner::Config) -pub struct FuzzedExecutor { +pub struct FuzzedExecutor { /// The EVM executor - executor: Executor, + executor: Executor, /// The fuzzer runner: TestRunner, /// The account that calls tests @@ -61,10 +60,10 @@ pub struct FuzzedExecutor { config: FuzzConfig, } -impl FuzzedExecutor { +impl FuzzedExecutor where B: BackendStrategy { /// Instantiates a fuzzed executor given a testrunner pub fn new( - executor: Executor, + executor: Executor, runner: TestRunner, sender: Address, config: FuzzConfig, diff --git a/crates/evm/evm/src/executors/invariant/mod.rs b/crates/evm/evm/src/executors/invariant/mod.rs index b4a46d775..69c1f3a30 100644 --- a/crates/evm/evm/src/executors/invariant/mod.rs +++ b/crates/evm/evm/src/executors/invariant/mod.rs @@ -8,9 +8,7 @@ use eyre::{eyre, ContextCompat, Result}; use foundry_common::contracts::{ContractsByAddress, ContractsByArtifact}; use foundry_config::InvariantConfig; use foundry_evm_core::{ - abi::HARDHAT_CONSOLE_ADDRESS, - constants::{CALLER, CHEATCODE_ADDRESS, DEFAULT_CREATE2_DEPLOYER, MAGIC_ASSUME}, - precompiles::PRECOMPILES, + abi::HARDHAT_CONSOLE_ADDRESS, backend::strategy::BackendStrategy, constants::{CALLER, CHEATCODE_ADDRESS, DEFAULT_CREATE2_DEPLOYER, MAGIC_ASSUME}, precompiles::PRECOMPILES }; use foundry_evm_fuzz::{ invariant::{ @@ -229,7 +227,7 @@ impl InvariantTest { /// End invariant test run by collecting results, cleaning collected artifacts and reverting /// created fuzz state. - pub fn end_run(&self, run: InvariantTestRun, gas_samples: usize) { + pub fn end_run(&self, run: InvariantTestRun, gas_samples: usize) { // We clear all the targeted contracts created during this run. self.targeted_contracts.clear_created_contracts(run.created_contracts); @@ -247,11 +245,11 @@ impl InvariantTest { } /// Contains data for an invariant test run. -pub struct InvariantTestRun { +pub struct InvariantTestRun { // Invariant run call sequence. pub inputs: Vec, // Current invariant run executor. - pub executor: Executor, + pub executor: Executor, // Invariant run stat reports (eg. gas usage). pub fuzz_runs: Vec, // Contracts created during current invariant run. @@ -264,9 +262,9 @@ pub struct InvariantTestRun { pub assume_rejects_counter: u32, } -impl InvariantTestRun { +impl InvariantTestRun { /// Instantiates an invariant test run. - pub fn new(first_input: BasicTxDetails, executor: Executor, depth: usize) -> Self { + pub fn new(first_input: BasicTxDetails, executor: Executor, depth: usize) -> Self { Self { inputs: vec![first_input], executor, @@ -285,8 +283,8 @@ impl InvariantTestRun { /// contracts with inputs, until it finds a counterexample sequence. The provided [`TestRunner`] /// contains all the configuration which can be overridden via [environment /// variables](proptest::test_runner::Config) -pub struct InvariantExecutor<'a> { - pub executor: Executor, +pub struct InvariantExecutor<'a, B> { + pub executor: Executor, /// Proptest runner. runner: TestRunner, /// The invariant configuration @@ -300,10 +298,10 @@ pub struct InvariantExecutor<'a> { artifact_filters: ArtifactFilters, } -impl<'a> InvariantExecutor<'a> { +impl<'a, B> InvariantExecutor<'a, B> where B: BackendStrategy { /// Instantiates a fuzzed executor EVM given a testrunner pub fn new( - executor: Executor, + executor: Executor, runner: TestRunner, config: InvariantConfig, setup_contracts: &'a ContractsByAddress, @@ -882,8 +880,8 @@ fn collect_data( /// Calls the `afterInvariant()` function on a contract. /// Returns call result and if call succeeded. /// The state after the call is not persisted. -pub(crate) fn call_after_invariant_function( - executor: &Executor, +pub(crate) fn call_after_invariant_function( + executor: &Executor, to: Address, ) -> std::result::Result<(RawCallResult, bool), EvmError> { let calldata = Bytes::from_static(&IInvariantTest::afterInvariantCall::SELECTOR); @@ -893,8 +891,8 @@ pub(crate) fn call_after_invariant_function( } /// Calls the invariant function and returns call result and if succeeded. -pub(crate) fn call_invariant_function( - executor: &Executor, +pub(crate) fn call_invariant_function( + executor: &Executor, address: Address, calldata: Bytes, ) -> Result<(RawCallResult, bool)> { diff --git a/crates/evm/evm/src/executors/invariant/replay.rs b/crates/evm/evm/src/executors/invariant/replay.rs index 969f1587f..d675092b7 100644 --- a/crates/evm/evm/src/executors/invariant/replay.rs +++ b/crates/evm/evm/src/executors/invariant/replay.rs @@ -7,6 +7,7 @@ use alloy_dyn_abi::JsonAbiExt; use alloy_primitives::{map::HashMap, Log}; use eyre::Result; use foundry_common::{ContractsByAddress, ContractsByArtifact}; +use foundry_evm_core::backend::strategy::BackendStrategy; use foundry_evm_coverage::HitMaps; use foundry_evm_fuzz::{ invariant::{BasicTxDetails, InvariantContract}, @@ -22,9 +23,9 @@ use std::sync::Arc; /// Replays a call sequence for collecting logs and traces. /// Returns counterexample to be used when the call sequence is a failed scenario. #[allow(clippy::too_many_arguments)] -pub fn replay_run( +pub fn replay_run( invariant_contract: &InvariantContract<'_>, - mut executor: Executor, + mut executor: Executor, known_contracts: &ContractsByArtifact, mut ided_contracts: ContractsByAddress, logs: &mut Vec, @@ -105,10 +106,10 @@ pub fn replay_run( /// Replays the error case, shrinks the failing sequence and collects all necessary traces. #[allow(clippy::too_many_arguments)] -pub fn replay_error( +pub fn replay_error( failed_case: &FailedInvariantCaseData, invariant_contract: &InvariantContract<'_>, - mut executor: Executor, + mut executor: Executor, known_contracts: &ContractsByArtifact, ided_contracts: ContractsByAddress, logs: &mut Vec, @@ -149,7 +150,7 @@ pub fn replay_error( } /// Sets up the calls generated by the internal fuzzer, if they exist. -fn set_up_inner_replay(executor: &mut Executor, inner_sequence: &[Option]) { +fn set_up_inner_replay(executor: &mut Executor, inner_sequence: &[Option]) { if let Some(fuzzer) = &mut executor.inspector_mut().fuzzer { if let Some(call_generator) = &mut fuzzer.call_generator { call_generator.last_sequence = Arc::new(RwLock::new(inner_sequence.to_owned())); diff --git a/crates/evm/evm/src/executors/invariant/result.rs b/crates/evm/evm/src/executors/invariant/result.rs index 8920a1209..c725d62fe 100644 --- a/crates/evm/evm/src/executors/invariant/result.rs +++ b/crates/evm/evm/src/executors/invariant/result.rs @@ -6,7 +6,7 @@ use crate::executors::{Executor, RawCallResult}; use alloy_dyn_abi::JsonAbiExt; use eyre::Result; use foundry_config::InvariantConfig; -use foundry_evm_core::utils::StateChangeset; +use foundry_evm_core::{backend::strategy::BackendStrategy, utils::StateChangeset}; use foundry_evm_coverage::HitMaps; use foundry_evm_fuzz::{ invariant::{BasicTxDetails, FuzzRunIdentifiedContracts, InvariantContract}, @@ -51,11 +51,11 @@ impl RichInvariantResults { /// Given the executor state, asserts that no invariant has been broken. Otherwise, it fills the /// external `invariant_failures.failed_invariant` map and returns a generic error. /// Either returns the call result if successful, or nothing if there was an error. -pub(crate) fn assert_invariants( +pub(crate) fn assert_invariants( invariant_contract: &InvariantContract<'_>, invariant_config: &InvariantConfig, targeted_contracts: &FuzzRunIdentifiedContracts, - executor: &Executor, + executor: &Executor, calldata: &[BasicTxDetails], invariant_failures: &mut InvariantFailures, ) -> Result> { @@ -93,10 +93,10 @@ pub(crate) fn assert_invariants( /// Returns if invariant test can continue and last successful call result of the invariant test /// function (if it can continue). -pub(crate) fn can_continue( +pub(crate) fn can_continue( invariant_contract: &InvariantContract<'_>, invariant_test: &InvariantTest, - invariant_run: &mut InvariantTestRun, + invariant_run: &mut InvariantTestRun, invariant_config: &InvariantConfig, call_result: RawCallResult, state_changeset: &StateChangeset, @@ -160,10 +160,10 @@ pub(crate) fn can_continue( /// Given the executor state, asserts conditions within `afterInvariant` function. /// If call fails then the invariant test is considered failed. -pub(crate) fn assert_after_invariant( +pub(crate) fn assert_after_invariant( invariant_contract: &InvariantContract<'_>, invariant_test: &InvariantTest, - invariant_run: &InvariantTestRun, + invariant_run: &InvariantTestRun, invariant_config: &InvariantConfig, ) -> Result { let (call_result, success) = diff --git a/crates/evm/evm/src/executors/invariant/shrink.rs b/crates/evm/evm/src/executors/invariant/shrink.rs index c468c58ee..aed5785f9 100644 --- a/crates/evm/evm/src/executors/invariant/shrink.rs +++ b/crates/evm/evm/src/executors/invariant/shrink.rs @@ -5,7 +5,7 @@ use crate::executors::{ Executor, }; use alloy_primitives::{Address, Bytes, U256}; -use foundry_evm_core::constants::MAGIC_ASSUME; +use foundry_evm_core::{backend::strategy::BackendStrategy, constants::MAGIC_ASSUME}; use foundry_evm_fuzz::invariant::BasicTxDetails; use indicatif::ProgressBar; use proptest::bits::{BitSetLike, VarBitSet}; @@ -85,10 +85,10 @@ impl CallSequenceShrinker { /// /// The shrunk call sequence always respect the order failure is reproduced as it is tested /// top-down. -pub(crate) fn shrink_sequence( +pub(crate) fn shrink_sequence( failed_case: &FailedInvariantCaseData, calls: &[BasicTxDetails], - executor: &Executor, + executor: &Executor, call_after_invariant: bool, progress: Option<&ProgressBar>, ) -> eyre::Result> { @@ -143,8 +143,8 @@ pub(crate) fn shrink_sequence( /// persisted failures. /// Returns the result of invariant check (and afterInvariant call if needed) and if sequence was /// entirely applied. -pub fn check_sequence( - mut executor: Executor, +pub fn check_sequence( + mut executor: Executor, calls: &[BasicTxDetails], sequence: Vec, test_address: Address, diff --git a/crates/evm/evm/src/executors/mod.rs b/crates/evm/evm/src/executors/mod.rs index 4e276d823..9043f5b94 100644 --- a/crates/evm/evm/src/executors/mod.rs +++ b/crates/evm/evm/src/executors/mod.rs @@ -17,7 +17,10 @@ use alloy_primitives::{ }; use alloy_sol_types::{sol, SolCall}; use foundry_evm_core::{ - backend::{Backend, BackendError, BackendResult, CowBackend, DatabaseExt, GLOBAL_FAIL_SLOT}, + backend::{ + strategy::BackendStrategy, Backend, BackendError, BackendResult, CowBackend, DatabaseExt, + GLOBAL_FAIL_SLOT, + }, constants::{ CALLER, CHEATCODE_ADDRESS, CHEATCODE_CONTRACT_HASH, DEFAULT_CREATE2_DEPLOYER, DEFAULT_CREATE2_DEPLOYER_CODE, DEFAULT_CREATE2_DEPLOYER_DEPLOYER, @@ -73,13 +76,13 @@ sol! { /// deployment /// - `setup`: a special case of `transact`, used to set up the environment for a test #[derive(Clone, Debug)] -pub struct Executor { +pub struct Executor { /// The underlying `revm::Database` that contains the EVM storage. // Note: We do not store an EVM here, since we are really // only interested in the database. REVM's `EVM` is a thin // wrapper around spawning a new EVM on every call anyway, // so the performance difference should be negligible. - pub backend: Backend, + pub backend: Backend, /// The EVM environment. pub env: EnvWithHandlerCfg, /// The Revm inspector stack. @@ -99,7 +102,10 @@ pub struct Executor { pub use_zk: bool, } -impl Executor { +impl Executor +where + B: BackendStrategy, +{ /// Creates a new `ExecutorBuilder`. #[inline] pub fn builder() -> ExecutorBuilder { @@ -109,7 +115,7 @@ impl Executor { /// Creates a new `Executor` with the given arguments. #[inline] pub fn new( - mut backend: Backend, + mut backend: Backend, env: EnvWithHandlerCfg, inspector: InspectorStack, gas_limit: u64, @@ -140,18 +146,18 @@ impl Executor { } } - fn clone_with_backend(&self, backend: Backend) -> Self { + fn clone_with_backend(&self, backend: Backend) -> Self { let env = EnvWithHandlerCfg::new_with_spec_id(Box::new(self.env().clone()), self.spec_id()); Self::new(backend, env, self.inspector().clone(), self.gas_limit, self.legacy_assertions) } /// Returns a reference to the EVM backend. - pub fn backend(&self) -> &Backend { + pub fn backend(&self) -> &Backend { &self.backend } /// Returns a mutable reference to the EVM backend. - pub fn backend_mut(&mut self) -> &mut Backend { + pub fn backend_mut(&mut self) -> &mut Backend { &mut self.backend } @@ -452,23 +458,10 @@ impl Executor { pub fn transact_with_env(&mut self, mut env: EnvWithHandlerCfg) -> eyre::Result { let mut inspector = self.inspector.clone(); let backend = &mut self.backend; - let result_and_state = match self.zk_tx.take() { - None => backend.inspect(&mut env, &mut inspector)?, - Some(zk_tx) => { - // apply fork-related env instead of cheatcode handler - // since it won't be run inside zkvm - env.block = self.env.block.clone(); - env.tx.gas_price = self.env.tx.gas_price; - backend.inspect_ref_zk( - &mut env, - // this will persist the added factory deps, - // no need to commit them later - &mut self.zk_persisted_factory_deps, - Some(zk_tx.factory_deps), - zk_tx.paymaster_data, - )? - } - }; + let strategy = backend.strategy.clone(); // to take a mutable borrow + let extra = self.zk_tx.take().map(|zk_tx| serde_json::to_vec(&zk_tx).unwrap()); + let result_and_state = + strategy.lock().unwrap().inspect(backend, &mut env, &mut inspector, extra)?; let mut result = convert_executed_result( env, inspector, diff --git a/crates/evm/evm/src/executors/trace.rs b/crates/evm/evm/src/executors/trace.rs index 69c68442b..6d5f0f19f 100644 --- a/crates/evm/evm/src/executors/trace.rs +++ b/crates/evm/evm/src/executors/trace.rs @@ -1,17 +1,27 @@ use crate::executors::{Executor, ExecutorBuilder}; use foundry_compilers::artifacts::EvmVersion; use foundry_config::{utils::evm_spec_id, Chain, Config}; -use foundry_evm_core::{backend::Backend, fork::CreateFork, opts::EvmOpts}; +use foundry_evm_core::{ + backend::{strategy::BackendStrategy, Backend}, + fork::CreateFork, + opts::EvmOpts, +}; use foundry_evm_traces::{InternalTraceMode, TraceMode}; use revm::primitives::{Env, SpecId}; -use std::ops::{Deref, DerefMut}; +use std::{ + ops::{Deref, DerefMut}, + sync::{Arc, Mutex}, +}; /// A default executor with tracing enabled -pub struct TracingExecutor { - executor: Executor, +pub struct TracingExecutor { + executor: Executor, } -impl TracingExecutor { +impl TracingExecutor +where + B: BackendStrategy, +{ pub fn new( env: revm::primitives::Env, fork: Option, @@ -19,8 +29,9 @@ impl TracingExecutor { debug: bool, decode_internal: bool, alphanet: bool, + strategy: Arc>, ) -> Self { - let db = Backend::spawn(fork); + let db = Backend::spawn(fork, strategy); let trace_mode = TraceMode::Call.with_debug(debug).with_decode_internal(if decode_internal { InternalTraceMode::Full @@ -58,15 +69,15 @@ impl TracingExecutor { } } -impl Deref for TracingExecutor { - type Target = Executor; +impl Deref for TracingExecutor { + type Target = Executor; fn deref(&self) -> &Self::Target { &self.executor } } -impl DerefMut for TracingExecutor { +impl DerefMut for TracingExecutor { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.executor } diff --git a/crates/forge/Cargo.toml b/crates/forge/Cargo.toml index cd30adeb6..f54ff1fc2 100644 --- a/crates/forge/Cargo.toml +++ b/crates/forge/Cargo.toml @@ -38,6 +38,8 @@ foundry-linking.workspace = true foundry-zksync-core.workspace = true foundry-zksync-compiler.workspace = true forge-script-sequence.workspace = true +foundry-strategy-core.workspace = true +foundry-strategy-zksync.workspace = true ethers-contract-abigen = { workspace = true, features = ["providers"] } diff --git a/crates/forge/bin/cmd/coverage.rs b/crates/forge/bin/cmd/coverage.rs index 9cc8a69e6..f5da0e3c9 100644 --- a/crates/forge/bin/cmd/coverage.rs +++ b/crates/forge/bin/cmd/coverage.rs @@ -3,15 +3,12 @@ use alloy_primitives::{map::HashMap, Address, Bytes, U256}; use clap::{Parser, ValueEnum, ValueHint}; use eyre::{Context, Result}; use forge::{ - coverage::{ + backend::strategy::{BackendStrategy, EvmBackendStrategy}, coverage::{ analysis::{SourceAnalysis, SourceAnalyzer, SourceFile, SourceFiles}, anchors::find_anchors, BytecodeReporter, ContractId, CoverageReport, CoverageReporter, DebugReporter, ItemAnchor, LcovReporter, SummaryReporter, - }, - opts::EvmOpts, - utils::IcPcMap, - MultiContractRunnerBuilder, TestOptions, + }, opts::EvmOpts, utils::IcPcMap, MultiContractRunnerBuilder, TestOptions }; use foundry_cli::utils::{LoadConfig, STATIC_FUZZ_SEED}; use foundry_common::{compile::ProjectCompiler, fs}; @@ -240,7 +237,7 @@ impl CoverageArgs { ..Default::default() }) .set_coverage(true) - .build(&root, output.clone(), None, env, evm_opts, DualCompiledContracts::default())?; + .build(&root, output.clone(), None, env, evm_opts, DualCompiledContracts::default(), EvmBackendStrategy::new())?; let known_contracts = runner.known_contracts.clone(); diff --git a/crates/forge/bin/cmd/test/mod.rs b/crates/forge/bin/cmd/test/mod.rs index 61ba99ca7..46b7df782 100644 --- a/crates/forge/bin/cmd/test/mod.rs +++ b/crates/forge/bin/cmd/test/mod.rs @@ -4,9 +4,11 @@ use chrono::Utc; use clap::{Parser, ValueHint}; use eyre::{Context, OptionExt, Result}; use forge::{ + backend::strategy::{BackendStrategy, EvmBackendStrategy}, decode::decode_console_logs, gas_report::{GasReport, GasReportKind}, multi_runner::matches_contract, + opts::EvmOpts, result::{SuiteResult, TestOutcome, TestStatus}, traces::{ debug::{ContractSources, DebugTraceIdentifier}, @@ -38,6 +40,7 @@ use foundry_config::{ }; use foundry_debugger::Debugger; use foundry_evm::traces::identifier::TraceIdentifiers; +use foundry_strategy_zksync::ZkBackendStrategy; use foundry_zksync_compiler::DualCompiledContracts; use regex::Regex; use std::{ @@ -269,10 +272,21 @@ impl TestArgs { /// configured filter will be executed /// /// Returns the test results for all matching tests. - pub async fn execute_tests(mut self) -> Result { + pub async fn execute_tests(self) -> Result { // Merge all configs. - let (mut config, mut evm_opts) = self.load_config_and_evm_opts_emit_warnings()?; + let (config, evm_opts) = self.load_config_and_evm_opts_emit_warnings()?; + if config.zksync.should_compile() { + self.execute_tests_inner::(config, evm_opts).await + } else { + self.execute_tests_inner::(config, evm_opts).await + } + } + async fn execute_tests_inner( + mut self, + mut config: Config, + mut evm_opts: EvmOpts, + ) -> Result { // Set number of max threads to execute tests. // If not specified then the number of threads determined by rayon will be used. if let Some(test_threads) = config.threads { @@ -392,6 +406,7 @@ impl TestArgs { env, evm_opts, dual_compiled_contracts.unwrap_or_default(), + B::new(), )?; let mut maybe_override_mt = |flag, maybe_regex: Option<&Option>| { @@ -491,9 +506,9 @@ impl TestArgs { } /// Run all tests that matches the filter predicate from a test runner - pub async fn run_tests( + pub async fn run_tests( &self, - mut runner: MultiContractRunner, + mut runner: MultiContractRunner, config: Arc, verbosity: u8, filter: &ProjectPathsAwareFilter, @@ -928,7 +943,7 @@ impl Provider for TestArgs { } /// Lists all matching tests -fn list(runner: MultiContractRunner, filter: &ProjectPathsAwareFilter) -> Result { +fn list(runner: MultiContractRunner, filter: &ProjectPathsAwareFilter) -> Result { let results = runner.list(filter); if shell::is_json() { diff --git a/crates/forge/src/multi_runner.rs b/crates/forge/src/multi_runner.rs index 2356d9b99..2162d18fd 100644 --- a/crates/forge/src/multi_runner.rs +++ b/crates/forge/src/multi_runner.rs @@ -16,7 +16,7 @@ use foundry_compilers::{ }; use foundry_config::Config; use foundry_evm::{ - backend::Backend, + backend::{strategy::BackendStrategy, Backend}, decode::RevertDecoder, executors::ExecutorBuilder, fork::CreateFork, @@ -35,7 +35,7 @@ use std::{ collections::BTreeMap, fmt::Debug, path::Path, - sync::{mpsc, Arc}, + sync::{mpsc, Arc, Mutex}, time::Instant, }; @@ -49,7 +49,7 @@ pub type DeployableContracts = BTreeMap; /// 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 { +pub struct MultiContractRunner { /// Mapping of contract name to JsonAbi, creation bytecode and library bytecode which /// needs to be deployed & linked against pub contracts: DeployableContracts, @@ -88,10 +88,10 @@ pub struct MultiContractRunner { /// Dual compiled contracts pub dual_compiled_contracts: DualCompiledContracts, /// Use zk runner. - pub use_zk: bool, + pub strategy: Arc>, } -impl MultiContractRunner { +impl MultiContractRunner where B: BackendStrategy { /// Returns an iterator over all contracts that match the filter. pub fn matching_contracts<'a: 'b, 'b>( &'a self, @@ -181,8 +181,7 @@ impl MultiContractRunner { trace!("running all tests"); // The DB backend that serves all the data. - let mut db = Backend::spawn(self.fork.take()); - db.is_zk = self.use_zk; + let db = Backend::spawn(self.fork.take(), self.strategy.clone()); let find_timer = Instant::now(); let contracts = self.matching_contracts(filter).collect::>(); @@ -240,7 +239,7 @@ impl MultiContractRunner { &self, artifact_id: &ArtifactId, contract: &TestContract, - db: Backend, + db: Backend, filter: &dyn TestFilter, tokio_handle: &tokio::runtime::Handle, progress: Option<&TestsProgress>, @@ -255,7 +254,7 @@ impl MultiContractRunner { Some(artifact_id.name.clone()), Some(artifact_id.version.clone()), self.dual_compiled_contracts.clone(), - self.use_zk, + self.strategy.lock().unwrap().name() == "zk", // use_zk ); let trace_mode = TraceMode::default() @@ -272,7 +271,7 @@ impl MultiContractRunner { .enable_isolation(self.isolation) .alphanet(self.alphanet) }) - .use_zk_vm(self.use_zk) + .use_zk_vm(self.strategy.lock().unwrap().name() == "zk") // use_zk .spec(self.evm_spec) .gas_limit(self.evm_opts.gas_limit()) .legacy_assertions(self.config.legacy_assertions) @@ -406,7 +405,7 @@ impl MultiContractRunnerBuilder { /// Given an EVM, proceeds to return a runner which is able to execute all tests /// against that evm - pub fn build( + pub fn build( self, root: &Path, output: ProjectCompileOutput, @@ -414,7 +413,8 @@ impl MultiContractRunnerBuilder { env: revm::primitives::Env, evm_opts: EvmOpts, dual_compiled_contracts: DualCompiledContracts, - ) -> Result { + strategy: Arc>, + ) -> Result> { let use_zk = zk_output.is_some(); let mut known_contracts = ContractsByArtifact::default(); let output = output.with_stripped_file_prefixes(root); @@ -516,7 +516,7 @@ impl MultiContractRunnerBuilder { libs_to_deploy, libraries, dual_compiled_contracts, - use_zk, + strategy, }) } } diff --git a/crates/forge/src/runner.rs b/crates/forge/src/runner.rs index 474b2c72b..f723e2a6e 100644 --- a/crates/forge/src/runner.rs +++ b/crates/forge/src/runner.rs @@ -17,21 +17,17 @@ use foundry_common::{ }; use foundry_config::{FuzzConfig, InvariantConfig}; use foundry_evm::{ - constants::CALLER, - decode::RevertDecoder, - executors::{ + backend::strategy::BackendStrategy, constants::CALLER, decode::RevertDecoder, executors::{ fuzz::FuzzedExecutor, invariant::{ check_sequence, replay_error, replay_run, InvariantExecutor, InvariantFuzzError, }, CallResult, EvmError, ExecutionErr, Executor, ITest, RawCallResult, - }, - fuzz::{ + }, fuzz::{ fixture_name, invariant::{CallDetails, InvariantContract}, CounterExample, FuzzFixtures, - }, - traces::{load_contracts, TraceKind, TraceMode}, + }, traces::{load_contracts, TraceKind, TraceMode} }; use proptest::test_runner::TestRunner; use rayon::prelude::*; @@ -46,7 +42,7 @@ pub const LIBRARY_DEPLOYER: Address = address!("1F95D37F27EA0dEA9C252FC09D5A6eaA /// A type that executes all tests of a contract #[derive(Clone, Debug)] -pub struct ContractRunner<'a> { +pub struct ContractRunner<'a, B> { /// The name of the contract. pub name: &'a str, /// The data of the contract. @@ -54,7 +50,7 @@ pub struct ContractRunner<'a> { /// The libraries that need to be deployed before the contract. pub libs_to_deploy: &'a Vec, /// The executor used by the runner. - pub executor: Executor, + pub executor: Executor, /// Revert decoder. Contains all known errors. pub revert_decoder: &'a RevertDecoder, /// The initial balance of the test contract. @@ -71,7 +67,7 @@ pub struct ContractRunner<'a> { pub span: tracing::Span, } -impl ContractRunner<'_> { +impl ContractRunner<'_, B> where B: BackendStrategy { /// Deploys the test contract inside the runner from the sending account, and optionally runs /// the `setUp` function on the test contract. pub fn setup(&mut self, call_setup: bool) -> TestSetup { @@ -703,7 +699,7 @@ impl ContractRunner<'_> { &self, func: &Function, setup: TestSetup, - ) -> Result<(Cow<'_, Executor>, TestResult, Address), TestResult> { + ) -> Result<(Cow<'_, Executor>, TestResult, Address), TestResult> { let address = setup.address; let mut executor = Cow::Borrowed(&self.executor); let mut test_result = TestResult::new(setup); diff --git a/crates/forge/tests/it/config.rs b/crates/forge/tests/it/config.rs index 9cabd998a..ca39700e8 100644 --- a/crates/forge/tests/it/config.rs +++ b/crates/forge/tests/it/config.rs @@ -1,8 +1,7 @@ //! Test config. use forge::{ - result::{SuiteResult, TestStatus}, - MultiContractRunner, + backend::strategy::BackendStrategy, result::{SuiteResult, TestStatus}, MultiContractRunner }; use foundry_evm::{ decode::decode_console_logs, @@ -15,18 +14,18 @@ use itertools::Itertools; use std::collections::BTreeMap; /// How to execute a test run. -pub struct TestConfig { - pub runner: MultiContractRunner, +pub struct TestConfig { + pub runner: MultiContractRunner, pub should_fail: bool, pub filter: Filter, } -impl TestConfig { - pub fn new(runner: MultiContractRunner) -> Self { +impl TestConfig where B: BackendStrategy { + pub fn new(runner: MultiContractRunner) -> Self { Self::with_filter(runner, Filter::matches_all()) } - pub fn with_filter(runner: MultiContractRunner, filter: Filter) -> Self { + pub fn with_filter(runner: MultiContractRunner, filter: Filter) -> Self { init_tracing(); Self { runner, should_fail: false, filter } } diff --git a/crates/forge/tests/it/repros.rs b/crates/forge/tests/it/repros.rs index c2154b6aa..84aa6c60a 100644 --- a/crates/forge/tests/it/repros.rs +++ b/crates/forge/tests/it/repros.rs @@ -10,8 +10,7 @@ use alloy_dyn_abi::{DecodedEvent, DynSolValue, EventExt}; use alloy_json_abi::Event; use alloy_primitives::{address, b256, Address, U256}; use forge::{ - decode::decode_console_logs, - result::{TestKind, TestStatus}, + backend::strategy::EvmBackendStrategy, decode::decode_console_logs, result::{TestKind, TestStatus} }; use foundry_config::{fs_permissions::PathPermission, Config, FsPermissions}; use foundry_evm::traces::{CallKind, CallTraceDecoder, DecodedCallData, TraceKind}; @@ -64,7 +63,7 @@ async fn repro_config( should_fail: bool, sender: Option
, test_data: &ForgeTestData, -) -> TestConfig { +) -> TestConfig { foundry_test_utils::init_tracing(); let filter = Filter::path(&format!(".*repros/Issue{issue}.t.sol")); diff --git a/crates/forge/tests/it/test_helpers.rs b/crates/forge/tests/it/test_helpers.rs index e11f0a160..b42d105bb 100644 --- a/crates/forge/tests/it/test_helpers.rs +++ b/crates/forge/tests/it/test_helpers.rs @@ -3,8 +3,7 @@ use alloy_chains::NamedChain; use alloy_primitives::U256; use forge::{ - revm::primitives::SpecId, MultiContractRunner, MultiContractRunnerBuilder, TestOptions, - TestOptionsBuilder, + backend::strategy::{BackendStrategy, EvmBackendStrategy}, revm::primitives::SpecId, MultiContractRunner, MultiContractRunnerBuilder, TestOptions, TestOptionsBuilder }; use foundry_compilers::{ artifacts::{EvmVersion, Libraries, Settings}, @@ -24,6 +23,7 @@ use foundry_evm::{ constants::CALLER, opts::{Env, EvmOpts}, }; +use foundry_strategy_zksync::ZkBackendStrategy; use foundry_test_utils::{ fd_lock, init_tracing, rpc::next_rpc_endpoint, util::OutputExt, TestCommand, ZkSyncNode, }; @@ -282,7 +282,7 @@ impl ForgeTestData { } /// Builds a non-tracing runner - pub fn runner(&self) -> MultiContractRunner { + pub fn runner(&self) -> MultiContractRunner { let mut config = self.config.clone(); config.fs_permissions = FsPermissions::new(vec![PathPermission::read_write(manifest_root())]); @@ -291,7 +291,7 @@ impl ForgeTestData { /// Builds a non-tracing zksync runner /// TODO: This needs to be implemented as currently it is a copy of the original function - pub fn runner_zksync(&self) -> MultiContractRunner { + pub fn runner_zksync(&self) -> MultiContractRunner { let mut zk_config = self.zk_test_data.zk_config.clone(); zk_config.fs_permissions = FsPermissions::new(vec![PathPermission::read_write(manifest_root())]); @@ -299,7 +299,7 @@ impl ForgeTestData { } /// Builds a non-tracing runner - pub fn runner_with_config(&self, mut config: Config) -> MultiContractRunner { + pub fn runner_with_config(&self, mut config: Config) -> MultiContractRunner { config.rpc_endpoints = rpc_endpoints(); config.allow_paths.push(manifest_root().to_path_buf()); @@ -324,13 +324,16 @@ impl ForgeTestData { .enable_isolation(opts.isolate) .sender(sender) .with_test_options(self.test_opts.clone()) - .build(root, output, None, env, opts, Default::default()) + .build(root, output, None, env, opts, Default::default(), EvmBackendStrategy::new()) .unwrap() } /// Builds a non-tracing runner with zksync /// TODO: This needs to be added as currently it is a copy of the original function - pub fn runner_with_zksync_config(&self, mut zk_config: Config) -> MultiContractRunner { + pub fn runner_with_zksync_config( + &self, + mut zk_config: Config, + ) -> MultiContractRunner { zk_config.rpc_endpoints = rpc_endpoints_zk(); zk_config.allow_paths.push(manifest_root().to_path_buf()); @@ -358,12 +361,20 @@ impl ForgeTestData { .enable_isolation(opts.isolate) .sender(sender) .with_test_options(test_opts) - .build(root, output, Some(zk_output), env, opts, dual_compiled_contracts) + .build( + root, + output, + Some(zk_output), + env, + opts, + dual_compiled_contracts, + ZkBackendStrategy::new(), + ) .unwrap() } /// Builds a tracing runner - pub fn tracing_runner(&self) -> MultiContractRunner { + pub fn tracing_runner(&self) -> MultiContractRunner { let mut opts = self.evm_opts.clone(); opts.verbosity = 5; self.base_runner() @@ -374,12 +385,13 @@ impl ForgeTestData { opts.local_evm_env(), opts, Default::default(), + EvmBackendStrategy::new() ) .unwrap() } /// Builds a runner that runs against forked state - pub async fn forked_runner(&self, rpc: &str) -> MultiContractRunner { + pub async fn forked_runner(&self, rpc: &str) -> MultiContractRunner { let mut opts = self.evm_opts.clone(); opts.env.chain_id = None; // clear chain id so the correct one gets fetched from the RPC @@ -390,7 +402,7 @@ impl ForgeTestData { self.base_runner() .with_fork(fork) - .build(self.project.root(), self.output.clone(), None, env, opts, Default::default()) + .build(self.project.root(), self.output.clone(), None, env, opts, Default::default(), EvmBackendStrategy::new()) .unwrap() } } diff --git a/crates/forge/tests/it/zk/repros.rs b/crates/forge/tests/it/zk/repros.rs index 3221ef683..80021d484 100644 --- a/crates/forge/tests/it/zk/repros.rs +++ b/crates/forge/tests/it/zk/repros.rs @@ -9,6 +9,7 @@ use crate::{ }; use alloy_primitives::Address; use foundry_config::{fs_permissions::PathPermission, FsPermissions}; +use foundry_strategy_zksync::ZkBackendStrategy; use foundry_test_utils::Filter; // zk-specific repros configuration @@ -17,7 +18,7 @@ async fn repro_config( should_fail: bool, sender: Option
, test_data: &ForgeTestData, -) -> TestConfig { +) -> TestConfig { foundry_test_utils::init_tracing(); let filter = Filter::path(&format!(".*repros/Issue{issue}.t.sol")); diff --git a/crates/script/src/broadcast.rs b/crates/script/src/broadcast.rs index 38e279910..3e55e574d 100644 --- a/crates/script/src/broadcast.rs +++ b/crates/script/src/broadcast.rs @@ -200,15 +200,15 @@ impl SendTransactionsKind { /// State after we have bundled all /// [`TransactionWithMetadata`](forge_script_sequence::TransactionWithMetadata) objects into a /// single [`ScriptSequenceKind`] object containing one or more script sequences. -pub struct BundledState { +pub struct BundledState { pub args: ScriptArgs, - pub script_config: ScriptConfig, + pub script_config: ScriptConfig, pub script_wallets: Wallets, pub build_data: LinkedBuildData, pub sequence: ScriptSequenceKind, } -impl BundledState { +impl BundledState { pub async fn wait_for_pending(mut self) -> Result { let progress = ScriptProgress::default(); let progress_ref = &progress; @@ -243,7 +243,7 @@ impl BundledState { } /// Broadcasts transactions from all sequences. - pub async fn broadcast(mut self) -> Result { + pub async fn broadcast(mut self) -> Result> { let required_addresses = self .sequence .sequences() diff --git a/crates/script/src/build.rs b/crates/script/src/build.rs index 3bb0da6a7..35f3ec1b7 100644 --- a/crates/script/src/build.rs +++ b/crates/script/src/build.rs @@ -22,7 +22,10 @@ use foundry_compilers::{ zksync::compile::output::ProjectCompileOutput as ZkProjectCompileOutput, ArtifactId, ProjectCompileOutput, }; -use foundry_evm::{constants::DEFAULT_CREATE2_DEPLOYER, traces::debug::ContractSources}; +use foundry_evm::{ + backend::strategy::BackendStrategy, constants::DEFAULT_CREATE2_DEPLOYER, + traces::debug::ContractSources, +}; use foundry_linking::Linker; use foundry_zksync_compiler::DualCompiledContracts; use std::{collections::BTreeMap, path::PathBuf, str::FromStr, sync::Arc}; @@ -48,7 +51,7 @@ impl BuildData { /// Links contracts. Uses CREATE2 linking when possible, otherwise falls back to /// default linking with sender nonce and address. - pub async fn link(self, script_config: &ScriptConfig) -> Result { + pub async fn link(self, script_config: &ScriptConfig) -> Result { let can_use_create2 = if let Some(fork_url) = &script_config.evm_opts.fork_url { let provider = try_get_http_provider(fork_url)?; let deployer_code = provider.get_code_at(DEFAULT_CREATE2_DEPLOYER).await?; @@ -191,16 +194,16 @@ impl LinkedBuildData { } /// First state basically containing only inputs of the user. -pub struct PreprocessedState { +pub struct PreprocessedState { pub args: ScriptArgs, - pub script_config: ScriptConfig, + pub script_config: ScriptConfig, pub script_wallets: Wallets, } -impl PreprocessedState { +impl PreprocessedState { /// Parses user input and compiles the contracts depending on script target. /// After compilation, finds exact [ArtifactId] of the target contract. - pub fn compile(self) -> Result { + pub fn compile(self) -> Result> { let Self { args, script_config, script_wallets } = self; let project = script_config.config.project()?; @@ -305,16 +308,19 @@ impl PreprocessedState { } /// State after we have determined and compiled target contract to be executed. -pub struct CompiledState { +pub struct CompiledState { pub args: ScriptArgs, - pub script_config: ScriptConfig, + pub script_config: ScriptConfig, pub script_wallets: Wallets, pub build_data: BuildData, } -impl CompiledState { +impl CompiledState +where + B: BackendStrategy, +{ /// Uses provided sender address to compute library addresses and link contracts with them. - pub async fn link(self) -> Result { + pub async fn link(self) -> Result> { let Self { args, script_config, script_wallets, build_data } = self; let build_data = build_data.link(&script_config).await?; @@ -323,7 +329,7 @@ impl CompiledState { } /// Tries loading the resumed state from the cache files, skipping simulation stage. - pub async fn resume(self) -> Result { + pub async fn resume(self) -> Result> { let chain = if self.args.multi { None } else { diff --git a/crates/script/src/execute.rs b/crates/script/src/execute.rs index 8f6e86f41..0d2442b88 100644 --- a/crates/script/src/execute.rs +++ b/crates/script/src/execute.rs @@ -24,6 +24,7 @@ use foundry_common::{ use foundry_config::{Config, NamedChain}; use foundry_debugger::Debugger; use foundry_evm::{ + backend::strategy::BackendStrategy, decode::decode_console_logs, inspectors::cheatcodes::BroadcastableTransactions, traces::{ @@ -39,9 +40,9 @@ use yansi::Paint; /// State after linking, contains the linked build data along with library addresses and optional /// array of libraries that need to be predeployed. -pub struct LinkedState { +pub struct LinkedState { pub args: ScriptArgs, - pub script_config: ScriptConfig, + pub script_config: ScriptConfig, pub script_wallets: Wallets, pub build_data: LinkedBuildData, } @@ -59,10 +60,10 @@ pub struct ExecutionData { pub abi: JsonAbi, } -impl LinkedState { +impl LinkedState { /// Given linked and compiled artifacts, prepares data we need for execution. /// This includes the function to call and the calldata to pass to it. - pub async fn prepare_execution(self) -> Result { + pub async fn prepare_execution(self) -> Result> { let Self { args, script_config, script_wallets, build_data } = self; let target_contract = build_data.get_target_contract()?; @@ -91,19 +92,22 @@ impl LinkedState { /// Same as [LinkedState], but also contains [ExecutionData]. #[derive(Debug)] -pub struct PreExecutionState { +pub struct PreExecutionState { pub args: ScriptArgs, - pub script_config: ScriptConfig, + pub script_config: ScriptConfig, pub script_wallets: Wallets, pub build_data: LinkedBuildData, pub execution_data: ExecutionData, } -impl PreExecutionState { +impl PreExecutionState +where + B: BackendStrategy, +{ /// Executes the script and returns the state after execution. /// Might require executing script twice in cases when we determine sender from execution. #[async_recursion] - pub async fn execute(mut self) -> Result { + pub async fn execute(mut self) -> Result> { let mut runner = self .script_config .get_runner_with_cheatcodes( @@ -112,6 +116,7 @@ impl PreExecutionState { self.args.debug, self.build_data.build_data.target.clone(), self.build_data.build_data.dual_compiled_contracts.clone().unwrap_or_default(), + B::new(), ) .await?; let result = self.execute_with_runner(&mut runner).await?; @@ -143,7 +148,7 @@ impl PreExecutionState { } /// Executes the script using the provided runner and returns the [ScriptResult]. - pub async fn execute_with_runner(&self, runner: &mut ScriptRunner) -> Result { + pub async fn execute_with_runner(&self, runner: &mut ScriptRunner) -> Result { let (address, mut setup_result) = runner.setup( &self.build_data.predeploy_libraries, self.execution_data.bytecode.clone(), @@ -274,18 +279,18 @@ pub struct ExecutionArtifacts { } /// State after the script has been executed. -pub struct ExecutedState { +pub struct ExecutedState { pub args: ScriptArgs, - pub script_config: ScriptConfig, + pub script_config: ScriptConfig, pub script_wallets: Wallets, pub build_data: LinkedBuildData, pub execution_data: ExecutionData, pub execution_result: ScriptResult, } -impl ExecutedState { +impl ExecutedState { /// Collects the data we need for simulation and various post-execution tasks. - pub async fn prepare_simulation(self) -> Result { + pub async fn prepare_simulation(self) -> Result> { let returns = self.get_returns()?; let decoder = self.build_trace_decoder(&self.build_data.known_contracts).await?; @@ -389,7 +394,7 @@ impl ExecutedState { } } -impl PreSimulationState { +impl PreSimulationState { pub fn show_json(&self) -> Result<()> { let result = &self.execution_result; diff --git a/crates/script/src/lib.rs b/crates/script/src/lib.rs index 46b387e20..5c264f160 100644 --- a/crates/script/src/lib.rs +++ b/crates/script/src/lib.rs @@ -42,7 +42,7 @@ use foundry_config::{ Config, }; use foundry_evm::{ - backend::Backend, + backend::{strategy::{BackendStrategy, EvmBackendStrategy}, Backend}, constants::DEFAULT_CREATE2_DEPLOYER, executors::ExecutorBuilder, inspectors::{ @@ -55,7 +55,7 @@ use foundry_evm::{ use foundry_wallets::MultiWalletOpts; use foundry_zksync_compiler::DualCompiledContracts; use serde::Serialize; -use std::path::PathBuf; +use std::{path::PathBuf, sync::{Arc, Mutex}}; mod broadcast; mod build; @@ -214,7 +214,7 @@ pub struct ScriptArgs { } impl ScriptArgs { - pub async fn preprocess(self) -> Result { + pub async fn preprocess(self) -> Result> { let script_wallets = Wallets::new(self.wallets.get_multi_wallet().await?, self.evm_opts.sender); @@ -224,7 +224,7 @@ impl ScriptArgs { evm_opts.sender = sender; } - let script_config = ScriptConfig::new(config, evm_opts).await?; + let script_config = ScriptConfig::::new(config, evm_opts).await?; Ok(PreprocessedState { args: self, script_config, script_wallets }) } @@ -233,7 +233,7 @@ impl ScriptArgs { pub async fn run_script(self) -> Result<()> { trace!(target: "script", "executing script command"); - let compiled = self.preprocess().await?.compile()?; + let compiled = self.preprocess::().await?.compile()?; // Move from `CompiledState` to `BundledState` either by resuming or executing and // simulating script. @@ -528,15 +528,15 @@ struct JsonResult<'a> { } #[derive(Clone, Debug)] -pub struct ScriptConfig { +pub struct ScriptConfig { pub config: Config, pub evm_opts: EvmOpts, pub sender_nonce: u64, /// Maps a rpc url to a backend - pub backends: HashMap, + pub backends: HashMap>, } -impl ScriptConfig { +impl ScriptConfig where B: BackendStrategy { pub async fn new(config: Config, evm_opts: EvmOpts) -> Result { let sender_nonce = if let Some(fork_url) = evm_opts.fork_url.as_ref() { next_nonce(evm_opts.sender, fork_url).await? @@ -558,8 +558,8 @@ impl ScriptConfig { Ok(()) } - async fn get_runner(&mut self) -> Result { - self._get_runner(None, false).await + async fn get_runner(&mut self, strategy: Arc>,) -> Result> { + self._get_runner(None, false, strategy).await } async fn get_runner_with_cheatcodes( @@ -569,10 +569,12 @@ impl ScriptConfig { debug: bool, target: ArtifactId, dual_compiled_contracts: DualCompiledContracts, - ) -> Result { + strategy: Arc>, + ) -> Result> { self._get_runner( Some((known_contracts, script_wallets, target, dual_compiled_contracts)), debug, + strategy, ) .await } @@ -581,7 +583,8 @@ impl ScriptConfig { &mut self, cheats_data: Option<(ContractsByArtifact, Wallets, ArtifactId, DualCompiledContracts)>, debug: bool, - ) -> Result { + strategy: Arc>, + ) -> Result> { trace!("preparing script runner"); let env = self.evm_opts.evm_env().await?; @@ -590,7 +593,7 @@ impl ScriptConfig { Some(db) => db.clone(), None => { let fork = self.evm_opts.get_fork(&self.config, env.clone()); - let backend = Backend::spawn(fork); + let backend = Backend::spawn(fork, strategy); self.backends.insert(fork_url.clone(), backend.clone()); backend } @@ -599,7 +602,7 @@ impl ScriptConfig { // It's only really `None`, when we don't pass any `--fork-url`. And if so, there is // no need to cache it, since there won't be any onchain simulation that we'd need // to cache the backend for. - Backend::spawn(None) + Backend::spawn(None, strategy) }; // We need to enable tracing to decode contract names: local or external. diff --git a/crates/script/src/runner.rs b/crates/script/src/runner.rs index 99bafee63..aeedb0bfb 100644 --- a/crates/script/src/runner.rs +++ b/crates/script/src/runner.rs @@ -6,11 +6,7 @@ use eyre::Result; use foundry_cheatcodes::BroadcastableTransaction; use foundry_config::Config; use foundry_evm::{ - constants::{CALLER, DEFAULT_CREATE2_DEPLOYER}, - executors::{DeployResult, EvmError, ExecutionErr, Executor, RawCallResult}, - opts::EvmOpts, - revm::interpreter::{return_ok, InstructionResult}, - traces::{TraceKind, Traces}, + backend::strategy::BackendStrategy, constants::{CALLER, DEFAULT_CREATE2_DEPLOYER}, executors::{DeployResult, EvmError, ExecutionErr, Executor, RawCallResult}, opts::EvmOpts, revm::interpreter::{return_ok, InstructionResult}, traces::{TraceKind, Traces} }; use foundry_zksync_core::ZkTransactionMetadata; use std::collections::VecDeque; @@ -18,13 +14,13 @@ use yansi::Paint; /// Drives script execution #[derive(Debug)] -pub struct ScriptRunner { - pub executor: Executor, +pub struct ScriptRunner { + pub executor: Executor, pub evm_opts: EvmOpts, } -impl ScriptRunner { - pub fn new(executor: Executor, evm_opts: EvmOpts) -> Self { +impl ScriptRunner where B: BackendStrategy { + pub fn new(executor: Executor, evm_opts: EvmOpts) -> Self { Self { executor, evm_opts } } diff --git a/crates/script/src/simulate.rs b/crates/script/src/simulate.rs index 478f766fa..4fd575c5a 100644 --- a/crates/script/src/simulate.rs +++ b/crates/script/src/simulate.rs @@ -17,12 +17,15 @@ use forge_script_sequence::{ScriptSequence, TransactionWithMetadata}; use foundry_cheatcodes::Wallets; use foundry_cli::utils::{has_different_gas_calc, now}; use foundry_common::{get_contract_name, ContractData}; -use foundry_evm::traces::{decode_trace_arena, render_trace_arena}; +use foundry_evm::{ + backend::strategy::BackendStrategy, + traces::{decode_trace_arena, render_trace_arena}, +}; use futures::future::{join_all, try_join_all}; use parking_lot::RwLock; use std::{ collections::{BTreeMap, VecDeque}, - sync::Arc, + sync::{Arc, Mutex}, }; /// Same as [ExecutedState](crate::execute::ExecutedState), but also contains [ExecutionArtifacts] @@ -30,9 +33,9 @@ use std::{ /// /// Can be either converted directly to [BundledState] or driven to it through /// [FilledTransactionsState]. -pub struct PreSimulationState { +pub struct PreSimulationState { pub args: ScriptArgs, - pub script_config: ScriptConfig, + pub script_config: ScriptConfig, pub script_wallets: Wallets, pub build_data: LinkedBuildData, pub execution_data: ExecutionData, @@ -40,13 +43,16 @@ pub struct PreSimulationState { pub execution_artifacts: ExecutionArtifacts, } -impl PreSimulationState { +impl PreSimulationState +where + B: BackendStrategy, +{ /// If simulation is enabled, simulates transactions against fork and fills gas estimation and /// metadata. Otherwise, metadata (e.g. additional contracts, created contract names) is /// left empty. /// /// Both modes will panic if any of the transactions have None for the `rpc` field. - pub async fn fill_metadata(self) -> Result { + pub async fn fill_metadata(self) -> Result> { let address_to_abi = self.build_address_to_abi_map(); let mut transactions = self @@ -100,7 +106,7 @@ impl PreSimulationState { trace!(target: "script", "executing onchain simulation"); let runners = Arc::new( - self.build_runners() + self.build_runners(B::new()) .await? .into_iter() .map(|(rpc, runner)| (rpc, Arc::new(RwLock::new(runner)))) @@ -219,18 +225,24 @@ impl PreSimulationState { } /// Build [ScriptRunner] forking given RPC for each RPC used in the script. - async fn build_runners(&self) -> Result> { + async fn build_runners( + &self, + strategy: Arc>, + ) -> Result)>> { let rpcs = self.execution_artifacts.rpc_data.total_rpcs.clone(); let n = rpcs.len(); let s = if n != 1 { "s" } else { "" }; sh_println!("\n## Setting up {n} EVM{s}.")?; - let futs = rpcs.into_iter().map(|rpc| async move { - let mut script_config = self.script_config.clone(); - script_config.evm_opts.fork_url = Some(rpc.clone()); - let runner = script_config.get_runner().await?; - Ok((rpc.clone(), runner)) + let futs = rpcs.into_iter().map(|rpc| { + let strategy = strategy.clone(); + async move { + let mut script_config = self.script_config.clone(); + script_config.evm_opts.fork_url = Some(rpc.clone()); + let runner = script_config.get_runner(strategy).await?; + Ok((rpc.clone(), runner)) + } }); try_join_all(futs).await } @@ -239,22 +251,22 @@ impl PreSimulationState { /// At this point we have converted transactions collected during script execution to /// [TransactionWithMetadata] objects which contain additional metadata needed for broadcasting and /// verification. -pub struct FilledTransactionsState { +pub struct FilledTransactionsState { pub args: ScriptArgs, - pub script_config: ScriptConfig, + pub script_config: ScriptConfig, pub script_wallets: Wallets, pub build_data: LinkedBuildData, pub execution_artifacts: ExecutionArtifacts, pub transactions: VecDeque, } -impl FilledTransactionsState { +impl FilledTransactionsState { /// Bundles all transactions of the [`TransactionWithMetadata`] type in a list of /// [`ScriptSequence`]. List length will be higher than 1, if we're dealing with a multi /// chain deployment. /// /// Each transaction will be added with the correct transaction type and gas estimation. - pub async fn bundle(self) -> Result { + pub async fn bundle(self) -> Result> { let is_multi_deployment = self.execution_artifacts.rpc_data.total_rpcs.len() > 1; if is_multi_deployment && !self.build_data.libraries.is_empty() { diff --git a/crates/script/src/verify.rs b/crates/script/src/verify.rs index 68b28a1ce..625e32e76 100644 --- a/crates/script/src/verify.rs +++ b/crates/script/src/verify.rs @@ -18,14 +18,14 @@ use yansi::Paint; /// State after we have broadcasted the script. /// It is assumed that at this point [BroadcastedState::sequence] contains receipts for all /// broadcasted transactions. -pub struct BroadcastedState { +pub struct BroadcastedState { pub args: ScriptArgs, - pub script_config: ScriptConfig, + pub script_config: ScriptConfig, pub build_data: LinkedBuildData, pub sequence: ScriptSequenceKind, } -impl BroadcastedState { +impl BroadcastedState { pub async fn verify(self) -> Result<()> { let Self { args, script_config, build_data, mut sequence, .. } = self; diff --git a/crates/strategy/core/Cargo.toml b/crates/strategy/core/Cargo.toml new file mode 100644 index 000000000..8098bfab8 --- /dev/null +++ b/crates/strategy/core/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "foundry-strategy-core" + +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[lints] +workspace = true + +[dependencies] +foundry-evm-core.workspace = true diff --git a/crates/strategy/core/src/lib.rs b/crates/strategy/core/src/lib.rs new file mode 100644 index 000000000..4bc6f9a7d --- /dev/null +++ b/crates/strategy/core/src/lib.rs @@ -0,0 +1,26 @@ +use std::sync::{Arc, Mutex}; + +use foundry_evm_core::backend::strategy::{BackendStrategy, EvmBackendStrategy}; + +pub trait RunnerStrategy: Send + Sync { + fn name(&self) -> &'static str; + fn backend_strategy(&self) -> Arc>; +} + +pub struct EvmRunnerStrategy { + pub backend: Arc>, +} +impl Default for EvmRunnerStrategy { + fn default() -> Self { + Self { backend: Arc::new(Mutex::new(EvmBackendStrategy)) } + } +} +impl RunnerStrategy for EvmRunnerStrategy { + fn name(&self) -> &'static str { + "evm" + } + + fn backend_strategy(&self) -> Arc> { + self.backend.clone() + } +} \ No newline at end of file diff --git a/crates/strategy/zksync/Cargo.toml b/crates/strategy/zksync/Cargo.toml new file mode 100644 index 000000000..ce7c9d9ad --- /dev/null +++ b/crates/strategy/zksync/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "foundry-strategy-zksync" + +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[lints] +workspace = true + +[dependencies] +foundry-common.workspace = true +foundry-compilers.workspace = true +foundry-evm-traces.workspace = true +foundry-evm-core.workspace = true +foundry-strategy-core.workspace = true +foundry-zksync-core.workspace = true +revm-inspectors.workspace = true + +alloy-primitives.workspace = true + +eyre.workspace = true +revm.workspace = true +tracing.workspace = true +serde.workspace = true +serde_json.workspace = true diff --git a/crates/strategy/zksync/src/lib.rs b/crates/strategy/zksync/src/lib.rs new file mode 100644 index 000000000..30209ec35 --- /dev/null +++ b/crates/strategy/zksync/src/lib.rs @@ -0,0 +1,318 @@ +use std::{ + collections::hash_map::Entry, + sync::{Arc, Mutex}, +}; + +use alloy_primitives::{Address, U256}; +use foundry_evm_core::{ + backend::{ + strategy::{ + merge_db_account_data, merge_journaled_state_data, BackendStrategy, + BackendStrategyForkInfo, + }, + Backend, BackendInner, Fork, ForkDB, FoundryEvmInMemoryDB, + }, + InspectorExt, +}; +use foundry_strategy_core::RunnerStrategy; +use foundry_zksync_core::{ + convert::ConvertH160, PaymasterParams, ACCOUNT_CODE_STORAGE_ADDRESS, H256, IMMUTABLE_SIMULATOR_STORAGE_ADDRESS, KNOWN_CODES_STORAGE_ADDRESS, L2_BASE_TOKEN_ADDRESS, NONCE_HOLDER_ADDRESS +}; +use revm::{ + db::CacheDB, + primitives::{EnvWithHandlerCfg, HashMap, HashSet, ResultAndState}, + DatabaseRef, JournaledState, +}; +use serde::{Deserialize, Serialize}; +use tracing::trace; + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct ZkBackendStrategy { + persisted_factory_deps: HashMap>, + persistent_immutable_keys: HashMap>, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +struct ZkBackendInspectData { + #[serde(skip_serializing_if = "Option::is_none")] + pub factory_deps: Option>>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub paymaster_data: Option, +} + +impl BackendStrategy for ZkBackendStrategy { + fn name(&self) -> &'static str { + "zk" + } + + /// When creating or switching forks, we update the AccountInfo of the contract. + fn update_fork_db( + &self, + fork_info: BackendStrategyForkInfo<'_>, + mem_db: &FoundryEvmInMemoryDB, + backend_inner: &BackendInner, + active_journaled_state: &mut JournaledState, + target_fork: &mut Fork, + ) { + self.update_fork_db_contracts( + fork_info, + mem_db, + backend_inner, + active_journaled_state, + target_fork, + ) + } + + fn inspect( + &mut self, + backend: &mut Backend, + env: &mut EnvWithHandlerCfg, + _inspector: &mut I, + extra: Option>, + ) -> eyre::Result { + backend.initialize(env); + + let zk_extra: ZkBackendInspectData = serde_json::from_slice(&extra.unwrap()).unwrap(); + foundry_zksync_core::vm::transact( + Some(&mut self.persisted_factory_deps), + zk_extra.factory_deps, + zk_extra.paymaster_data, + env, + backend, + ) + } +} + +impl ZkBackendStrategy { + /// Merges the state of all `accounts` from the currently active db into the given `fork` + pub(crate) fn update_fork_db_contracts( + &self, + fork_info: BackendStrategyForkInfo<'_>, + mem_db: &FoundryEvmInMemoryDB, + backend_inner: &BackendInner, + active_journaled_state: &mut JournaledState, + target_fork: &mut Fork, + ) { + let _require_zk_storage_merge = + fork_info.active_type.is_zk() && fork_info.target_type.is_zk(); + + // Ignore EVM interoperatability and import everything + // if !require_zk_storage_merge { + // return; + // } + + let accounts = backend_inner.persistent_accounts.iter().copied(); + let zk_state = &ZkMergeState { persistent_immutable_keys: &self.persistent_immutable_keys }; + if let Some(db) = fork_info.active_fork.map(|f| &f.db) { + ZkBackendMergeStrategy::merge_account_data( + accounts, + db, + active_journaled_state, + target_fork, + zk_state, + ) + } else { + ZkBackendMergeStrategy::merge_account_data( + accounts, + mem_db, + active_journaled_state, + target_fork, + zk_state, + ) + } + } +} + +pub(crate) struct ZkBackendMergeStrategy; + +/// Defines the zksync specific state to help during merge. +pub(crate) struct ZkMergeState<'a> { + persistent_immutable_keys: &'a HashMap>, +} + +impl ZkBackendMergeStrategy { + /// Clones the data of the given `accounts` from the `active` database into the `fork_db` + /// This includes the data held in storage (`CacheDB`) and kept in the `JournaledState`. + pub fn merge_account_data( + accounts: impl IntoIterator, + active: &CacheDB, + active_journaled_state: &mut JournaledState, + target_fork: &mut Fork, + zk_state: &ZkMergeState<'_>, + ) { + for addr in accounts.into_iter() { + merge_db_account_data(addr, active, &mut target_fork.db); + merge_zk_account_data(addr, active, &mut target_fork.db, zk_state); + merge_journaled_state_data( + addr, + active_journaled_state, + &mut target_fork.journaled_state, + ); + merge_zk_journaled_state_data( + addr, + active_journaled_state, + &mut target_fork.journaled_state, + zk_state, + ); + } + + // need to mock empty journal entries in case the current checkpoint is higher than the + // existing journal entries + while active_journaled_state.journal.len() > target_fork.journaled_state.journal.len() { + target_fork.journaled_state.journal.push(Default::default()); + } + + *active_journaled_state = target_fork.journaled_state.clone(); + } +} + +/// Clones the zk account data from the `active` db into the `ForkDB` +fn merge_zk_account_data( + addr: Address, + active: &CacheDB, + fork_db: &mut ForkDB, + _zk_state: &ZkMergeState<'_>, +) { + let merge_system_contract_entry = + |fork_db: &mut ForkDB, system_contract: Address, slot: U256| { + let Some(acc) = active.accounts.get(&system_contract) else { return }; + + // port contract cache over + if let Some(code) = active.contracts.get(&acc.info.code_hash) { + trace!("merging contract cache"); + fork_db.contracts.insert(acc.info.code_hash, code.clone()); + } + + // prepare only the specified slot in account storage + let mut new_acc = acc.clone(); + new_acc.storage = Default::default(); + if let Some(value) = acc.storage.get(&slot) { + new_acc.storage.insert(slot, *value); + } + + // port account storage over + match fork_db.accounts.entry(system_contract) { + Entry::Vacant(vacant) => { + trace!("target account not present - inserting from active"); + // if the fork_db doesn't have the target account + // insert the entire thing + vacant.insert(new_acc); + } + Entry::Occupied(mut occupied) => { + trace!("target account present - merging storage slots"); + // if the fork_db does have the system, + // extend the existing storage (overriding) + let fork_account = occupied.get_mut(); + fork_account.storage.extend(&new_acc.storage); + } + } + }; + + merge_system_contract_entry( + fork_db, + L2_BASE_TOKEN_ADDRESS.to_address(), + foundry_zksync_core::get_balance_key(addr), + ); + merge_system_contract_entry( + fork_db, + ACCOUNT_CODE_STORAGE_ADDRESS.to_address(), + foundry_zksync_core::get_account_code_key(addr), + ); + merge_system_contract_entry( + fork_db, + NONCE_HOLDER_ADDRESS.to_address(), + foundry_zksync_core::get_nonce_key(addr), + ); + + if let Some(acc) = active.accounts.get(&addr) { + merge_system_contract_entry( + fork_db, + KNOWN_CODES_STORAGE_ADDRESS.to_address(), + U256::from_be_slice(&acc.info.code_hash.0[..]), + ); + } +} + +/// Clones the account data from the `active_journaled_state` into the `fork_journaled_state` for +/// zksync storage. +fn merge_zk_journaled_state_data( + addr: Address, + active_journaled_state: &JournaledState, + fork_journaled_state: &mut JournaledState, + zk_state: &ZkMergeState<'_>, +) { + let merge_system_contract_entry = + |fork_journaled_state: &mut JournaledState, system_contract: Address, slot: U256| { + if let Some(acc) = active_journaled_state.state.get(&system_contract) { + // prepare only the specified slot in account storage + let mut new_acc = acc.clone(); + new_acc.storage = Default::default(); + if let Some(value) = acc.storage.get(&slot).cloned() { + new_acc.storage.insert(slot, value); + } + + match fork_journaled_state.state.entry(system_contract) { + Entry::Vacant(vacant) => { + vacant.insert(new_acc); + } + Entry::Occupied(mut occupied) => { + let fork_account = occupied.get_mut(); + fork_account.storage.extend(new_acc.storage); + } + } + } + }; + + merge_system_contract_entry( + fork_journaled_state, + L2_BASE_TOKEN_ADDRESS.to_address(), + foundry_zksync_core::get_balance_key(addr), + ); + merge_system_contract_entry( + fork_journaled_state, + ACCOUNT_CODE_STORAGE_ADDRESS.to_address(), + foundry_zksync_core::get_account_code_key(addr), + ); + merge_system_contract_entry( + fork_journaled_state, + NONCE_HOLDER_ADDRESS.to_address(), + foundry_zksync_core::get_nonce_key(addr), + ); + + if let Some(acc) = active_journaled_state.state.get(&addr) { + merge_system_contract_entry( + fork_journaled_state, + KNOWN_CODES_STORAGE_ADDRESS.to_address(), + U256::from_be_slice(&acc.info.code_hash.0[..]), + ); + } + + // merge immutable storage. + let immutable_simulator_addr = IMMUTABLE_SIMULATOR_STORAGE_ADDRESS.to_address(); + if let Some(immutable_storage_keys) = zk_state.persistent_immutable_keys.get(&addr) { + for slot_key in immutable_storage_keys { + merge_system_contract_entry(fork_journaled_state, immutable_simulator_addr, *slot_key); + } + } +} + +pub struct ZkRunnerStrategy { + pub backend: Arc>, +} +impl Default for ZkRunnerStrategy { + fn default() -> Self { + Self { + backend: Arc::new(Mutex::new(ZkBackendStrategy::default())), + } + } +} +impl RunnerStrategy for ZkRunnerStrategy { + fn name(&self) -> &'static str { + "zk" + } + + fn backend_strategy(&self) -> Arc> { + self.backend.clone() + } +} diff --git a/crates/verify/src/bytecode.rs b/crates/verify/src/bytecode.rs index a4f368a96..c03b7b6b9 100644 --- a/crates/verify/src/bytecode.rs +++ b/crates/verify/src/bytecode.rs @@ -19,7 +19,11 @@ use foundry_cli::{ use foundry_common::shell; use foundry_compilers::{artifacts::EvmVersion, info::ContractInfo}; use foundry_config::{figment, impl_figment_convert, Config}; -use foundry_evm::{constants::DEFAULT_CREATE2_DEPLOYER, utils::configure_tx_env}; +use foundry_evm::{ + backend::strategy::{BackendStrategy, EvmBackendStrategy}, + constants::DEFAULT_CREATE2_DEPLOYER, + utils::configure_tx_env, +}; use revm_primitives::AccountInfo; use std::path::PathBuf; use yansi::Paint; @@ -233,6 +237,7 @@ impl VerifyBytecodeArgs { gen_blk_num, etherscan_metadata.evm_version()?.unwrap_or(EvmVersion::default()), evm_opts, + ::new(), ) .await?; @@ -420,6 +425,7 @@ impl VerifyBytecodeArgs { simulation_block - 1, // env.fork_block_number etherscan_metadata.evm_version()?.unwrap_or(EvmVersion::default()), evm_opts, + ::new(), ) .await?; env.block.number = U256::from(simulation_block); diff --git a/crates/verify/src/utils.rs b/crates/verify/src/utils.rs index e3065aca0..0482f5544 100644 --- a/crates/verify/src/utils.rs +++ b/crates/verify/src/utils.rs @@ -1,3 +1,5 @@ +use std::sync::{Arc, Mutex}; + use crate::{bytecode::VerifyBytecodeArgs, types::VerificationType}; use alloy_dyn_abi::DynSolValue; use alloy_primitives::{Address, Bytes, U256}; @@ -12,7 +14,7 @@ use foundry_block_explorers::{ use foundry_common::{abi::encode_args, compile::ProjectCompiler, provider::RetryProvider, shell}; use foundry_compilers::artifacts::{BytecodeHash, CompactContractBytecode, EvmVersion}; use foundry_config::Config; -use foundry_evm::{constants::DEFAULT_CREATE2_DEPLOYER, executors::TracingExecutor, opts::EvmOpts}; +use foundry_evm::{backend::strategy::BackendStrategy, constants::DEFAULT_CREATE2_DEPLOYER, executors::TracingExecutor, opts::EvmOpts}; use reqwest::Url; use revm_primitives::{ db::Database, @@ -321,17 +323,18 @@ pub fn check_args_len( Ok(()) } -pub async fn get_tracing_executor( +pub async fn get_tracing_executor( fork_config: &mut Config, fork_blk_num: u64, evm_version: EvmVersion, evm_opts: EvmOpts, -) -> Result<(Env, TracingExecutor)> { + strategy: Arc>, +) -> Result<(Env, TracingExecutor)> { fork_config.fork_block_number = Some(fork_blk_num); fork_config.evm_version = evm_version; let (env, fork, _chain, is_alphanet) = - TracingExecutor::get_fork_material(fork_config, evm_opts).await?; + TracingExecutor::::get_fork_material(fork_config, evm_opts).await?; let executor = TracingExecutor::new( env.clone(), @@ -340,6 +343,7 @@ pub async fn get_tracing_executor( false, false, is_alphanet, + strategy, ); Ok((env, executor)) @@ -354,8 +358,8 @@ pub fn configure_env_block(env: &mut Env, block: &AnyNetworkBlock) { env.block.gas_limit = U256::from(block.header.gas_limit); } -pub fn deploy_contract( - executor: &mut TracingExecutor, +pub fn deploy_contract( + executor: &mut TracingExecutor, env: &Env, spec_id: SpecId, transaction: &Transaction, @@ -383,8 +387,8 @@ pub fn deploy_contract( } } -pub async fn get_runtime_codes( - executor: &mut TracingExecutor, +pub async fn get_runtime_codes( + executor: &mut TracingExecutor, provider: &RetryProvider, address: Address, fork_address: Address, From a4f5995531b8f448c58490190c26594744ee7344 Mon Sep 17 00:00:00 2001 From: Nisheeth Barthwal Date: Wed, 4 Dec 2024 14:25:22 +0100 Subject: [PATCH 2/3] fix test build --- crates/cast/bin/cmd/call.rs | 17 +++- crates/cast/bin/cmd/run.rs | 5 +- crates/chisel/bin/main.rs | 29 +++--- crates/chisel/src/cmd.rs | 2 +- crates/chisel/src/dispatcher.rs | 11 ++- crates/chisel/src/session.rs | 5 +- crates/chisel/src/session_source.rs | 10 ++- crates/chisel/tests/cache.rs | 88 ++++++++++--------- crates/evm/core/src/backend/mod.rs | 9 +- crates/evm/core/src/backend/strategy.rs | 5 +- crates/evm/evm/src/executors/fuzz/mod.rs | 9 +- crates/evm/evm/src/executors/invariant/mod.rs | 10 ++- .../evm/evm/src/executors/invariant/replay.rs | 5 +- crates/forge/bin/cmd/coverage.rs | 18 +++- crates/forge/bin/cmd/test/mod.rs | 5 +- crates/forge/src/multi_runner.rs | 6 +- crates/forge/src/runner.rs | 16 +++- crates/forge/tests/it/config.rs | 9 +- crates/forge/tests/it/repros.rs | 4 +- crates/forge/tests/it/test_helpers.rs | 21 ++++- crates/script/src/lib.rs | 17 +++- crates/script/src/runner.rs | 12 ++- crates/strategy/core/src/lib.rs | 2 +- crates/strategy/zksync/src/lib.rs | 10 +-- crates/verify/src/utils.rs | 5 +- 25 files changed, 231 insertions(+), 99 deletions(-) diff --git a/crates/cast/bin/cmd/call.rs b/crates/cast/bin/cmd/call.rs index 275fb813a..fa72d5494 100644 --- a/crates/cast/bin/cmd/call.rs +++ b/crates/cast/bin/cmd/call.rs @@ -1,7 +1,11 @@ use crate::tx::{CastTxBuilder, SenderKind}; use alloy_primitives::{TxKind, U256}; use alloy_rpc_types::{BlockId, BlockNumberOrTag}; -use cast::{backend::strategy::{BackendStrategy, EvmBackendStrategy}, traces::TraceKind, Cast}; +use cast::{ + backend::strategy::{BackendStrategy, EvmBackendStrategy}, + traces::TraceKind, + Cast, +}; use clap::Parser; use eyre::Result; use foundry_cli::{ @@ -177,8 +181,15 @@ impl CallArgs { env.cfg.disable_block_gas_limit = true; env.block.gas_limit = U256::MAX; - let mut executor = - TracingExecutor::new(env, fork, evm_version, debug, decode_internal, alphanet, EvmBackendStrategy::new()); + let mut executor = TracingExecutor::new( + env, + fork, + evm_version, + debug, + decode_internal, + alphanet, + EvmBackendStrategy::new(), + ); let value = tx.value.unwrap_or_default(); let input = tx.inner.input.into_input().unwrap_or_default(); diff --git a/crates/cast/bin/cmd/run.rs b/crates/cast/bin/cmd/run.rs index a83f328de..bf202bff4 100644 --- a/crates/cast/bin/cmd/run.rs +++ b/crates/cast/bin/cmd/run.rs @@ -1,7 +1,10 @@ use alloy_primitives::U256; use alloy_provider::Provider; use alloy_rpc_types::BlockTransactions; -use cast::{backend::strategy::{BackendStrategy, EvmBackendStrategy}, revm::primitives::EnvWithHandlerCfg}; +use cast::{ + backend::strategy::{BackendStrategy, EvmBackendStrategy}, + revm::primitives::EnvWithHandlerCfg, +}; use clap::Parser; use eyre::{Result, WrapErr}; use foundry_cli::{ diff --git a/crates/chisel/bin/main.rs b/crates/chisel/bin/main.rs index 46ab06cd5..c1472673d 100644 --- a/crates/chisel/bin/main.rs +++ b/crates/chisel/bin/main.rs @@ -133,15 +133,16 @@ async fn main_args(args: Chisel) -> eyre::Result<()> { let (config, evm_opts) = args.load_config_and_evm_opts()?; // Create a new cli dispatcher - let mut dispatcher = ChiselDispatcher::::new(chisel::session_source::SessionSourceConfig { - // Enable traces if any level of verbosity was passed - traces: config.verbosity > 0, - foundry_config: config, - no_vm: args.no_vm, - evm_opts, - backend: None, - calldata: None, - })?; + let mut dispatcher = + ChiselDispatcher::::new(chisel::session_source::SessionSourceConfig { + // Enable traces if any level of verbosity was passed + traces: config.verbosity > 0, + foundry_config: config, + no_vm: args.no_vm, + evm_opts, + backend: None, + calldata: None, + })?; // Execute prelude Solidity source files evaluate_prelude(&mut dispatcher, args.prelude).await?; @@ -266,7 +267,10 @@ impl Provider for Chisel { } /// Evaluate a single Solidity line. -async fn dispatch_repl_line(dispatcher: &mut ChiselDispatcher, line: &str) -> eyre::Result { +async fn dispatch_repl_line( + dispatcher: &mut ChiselDispatcher, + line: &str, +) -> eyre::Result { let r = dispatcher.dispatch(line).await; match &r { DispatchResult::Success(msg) | DispatchResult::CommandSuccess(msg) => { @@ -315,7 +319,10 @@ async fn evaluate_prelude( } /// Loads a single Solidity file into the prelude. -async fn load_prelude_file(dispatcher: &mut ChiselDispatcher, file: PathBuf) -> eyre::Result<()> { +async fn load_prelude_file( + dispatcher: &mut ChiselDispatcher, + file: PathBuf, +) -> eyre::Result<()> { let prelude = fs::read_to_string(file) .wrap_err("Could not load source file. Are you sure this path is correct?")?; dispatch_repl_line(dispatcher, &prelude).await?; diff --git a/crates/chisel/src/cmd.rs b/crates/chisel/src/cmd.rs index 4f5d56ed3..255b257fa 100644 --- a/crates/chisel/src/cmd.rs +++ b/crates/chisel/src/cmd.rs @@ -4,8 +4,8 @@ //! can be executed within the REPL. use crate::prelude::ChiselDispatcher; -use std::{error::Error, str::FromStr}; use foundry_evm::backend::strategy::EvmBackendStrategy; +use std::{error::Error, str::FromStr}; use strum::EnumIter; /// Builtin chisel command variants diff --git a/crates/chisel/src/dispatcher.rs b/crates/chisel/src/dispatcher.rs index e8d339f7e..74c6d1bdf 100644 --- a/crates/chisel/src/dispatcher.rs +++ b/crates/chisel/src/dispatcher.rs @@ -15,11 +15,13 @@ use alloy_primitives::{hex, Address}; use forge_fmt::FormatterConfig; use foundry_config::{Config, RpcEndpoint}; use foundry_evm::{ - backend::strategy::BackendStrategy, decode::decode_console_logs, traces::{ + backend::strategy::BackendStrategy, + decode::decode_console_logs, + traces::{ decode_trace_arena, identifier::{SignaturesIdentifier, TraceIdentifiers}, render_trace_arena, CallTraceDecoder, CallTraceDecoderBuilder, TraceKind, - } + }, }; use regex::Regex; use reqwest::Url; @@ -133,7 +135,10 @@ pub fn format_source(source: &str, config: FormatterConfig) -> eyre::Result ChiselDispatcher where B: BackendStrategy { +impl ChiselDispatcher +where + B: BackendStrategy, +{ /// Associated public function to create a new Dispatcher instance pub fn new(config: SessionSourceConfig) -> eyre::Result { ChiselSession::new(config).map(|session| Self { session }) diff --git a/crates/chisel/src/session.rs b/crates/chisel/src/session.rs index bb0b5ab9e..624bff2dd 100644 --- a/crates/chisel/src/session.rs +++ b/crates/chisel/src/session.rs @@ -20,7 +20,10 @@ pub struct ChiselSession { } // ChiselSession Common Associated Functions -impl ChiselSession where B: BackendStrategy { +impl ChiselSession +where + B: BackendStrategy, +{ /// Create a new `ChiselSession` with a specified `solc` version and configuration. /// /// ### Takes diff --git a/crates/chisel/src/session_source.rs b/crates/chisel/src/session_source.rs index d740554f5..f09645a08 100644 --- a/crates/chisel/src/session_source.rs +++ b/crates/chisel/src/session_source.rs @@ -12,7 +12,10 @@ use foundry_compilers::{ compilers::solc::Solc, }; use foundry_config::{Config, SolcReq}; -use foundry_evm::{backend::{strategy::BackendStrategy, Backend}, opts::EvmOpts}; +use foundry_evm::{ + backend::{strategy::BackendStrategy, Backend}, + opts::EvmOpts, +}; use semver::Version; use serde::{Deserialize, Serialize}; use solang_parser::{diagnostics::Diagnostic, pt}; @@ -155,7 +158,10 @@ pub struct SessionSource { pub config: SessionSourceConfig, } -impl SessionSource where B: BackendStrategy { +impl SessionSource +where + B: BackendStrategy, +{ /// Creates a new source given a solidity compiler version /// /// # Panics diff --git a/crates/chisel/tests/cache.rs b/crates/chisel/tests/cache.rs index 0e1f86845..ecea7be07 100644 --- a/crates/chisel/tests/cache.rs +++ b/crates/chisel/tests/cache.rs @@ -41,11 +41,12 @@ fn test_write_session() { let foundry_config = Config { evm_version: EvmVersion::London, ..Default::default() }; // Create a new session - let mut env = ChiselSession::::new(chisel::session_source::SessionSourceConfig { - foundry_config, - ..Default::default() - }) - .unwrap_or_else(|e| panic!("Failed to create ChiselSession!, {e}")); + let mut env = + ChiselSession::::new(chisel::session_source::SessionSourceConfig { + foundry_config, + ..Default::default() + }) + .unwrap_or_else(|e| panic!("Failed to create ChiselSession!, {e}")); // Write the session let cached_session_name = env.write().unwrap(); @@ -69,11 +70,12 @@ fn test_write_session_with_name() { let foundry_config = Config { evm_version: EvmVersion::London, ..Default::default() }; // Create a new session - let mut env = ChiselSession::::new(chisel::session_source::SessionSourceConfig { - foundry_config, - ..Default::default() - }) - .unwrap_or_else(|e| panic!("Failed to create ChiselSession! {e}")); + let mut env = + ChiselSession::::new(chisel::session_source::SessionSourceConfig { + foundry_config, + ..Default::default() + }) + .unwrap_or_else(|e| panic!("Failed to create ChiselSession! {e}")); env.id = Some(String::from("test")); // Write the session @@ -93,11 +95,12 @@ fn test_clear_cache() { let foundry_config = Config { evm_version: EvmVersion::London, ..Default::default() }; ChiselSession::::create_cache_dir().unwrap(); - let mut env = ChiselSession::::new(chisel::session_source::SessionSourceConfig { - foundry_config, - ..Default::default() - }) - .unwrap_or_else(|_| panic!("Failed to create ChiselSession!")); + let mut env = + ChiselSession::::new(chisel::session_source::SessionSourceConfig { + foundry_config, + ..Default::default() + }) + .unwrap_or_else(|_| panic!("Failed to create ChiselSession!")); env.write().unwrap(); // Clear the cache @@ -119,11 +122,12 @@ fn test_list_sessions() { let foundry_config = Config { evm_version: EvmVersion::London, ..Default::default() }; // Create a new session - let mut env = ChiselSession::::new(chisel::session_source::SessionSourceConfig { - foundry_config, - ..Default::default() - }) - .unwrap_or_else(|e| panic!("Failed to create ChiselSession! {e}")); + let mut env = + ChiselSession::::new(chisel::session_source::SessionSourceConfig { + foundry_config, + ..Default::default() + }) + .unwrap_or_else(|e| panic!("Failed to create ChiselSession! {e}")); env.write().unwrap(); @@ -146,11 +150,12 @@ fn test_load_cache() { let foundry_config = Config { evm_version: EvmVersion::London, ..Default::default() }; // Create a new session - let mut env = ChiselSession::::new(chisel::session_source::SessionSourceConfig { - foundry_config, - ..Default::default() - }) - .unwrap_or_else(|e| panic!("Failed to create ChiselSession! {e}")); + let mut env = + ChiselSession::::new(chisel::session_source::SessionSourceConfig { + foundry_config, + ..Default::default() + }) + .unwrap_or_else(|e| panic!("Failed to create ChiselSession! {e}")); env.write().unwrap(); // Load the session @@ -174,11 +179,12 @@ fn test_write_same_session_multiple_times() { let foundry_config = Config { evm_version: EvmVersion::London, ..Default::default() }; // Create a new session - let mut env = ChiselSession::::new(chisel::session_source::SessionSourceConfig { - foundry_config, - ..Default::default() - }) - .unwrap_or_else(|e| panic!("Failed to create ChiselSession! {e}")); + let mut env = + ChiselSession::::new(chisel::session_source::SessionSourceConfig { + foundry_config, + ..Default::default() + }) + .unwrap_or_else(|e| panic!("Failed to create ChiselSession! {e}")); env.write().unwrap(); env.write().unwrap(); env.write().unwrap(); @@ -197,21 +203,23 @@ fn test_load_latest_cache() { let foundry_config = Config { evm_version: EvmVersion::London, ..Default::default() }; // Create sessions - let mut env = ChiselSession::::new(chisel::session_source::SessionSourceConfig { - foundry_config: foundry_config.clone(), - ..Default::default() - }) - .unwrap_or_else(|e| panic!("Failed to create ChiselSession! {e}")); + let mut env = + ChiselSession::::new(chisel::session_source::SessionSourceConfig { + foundry_config: foundry_config.clone(), + ..Default::default() + }) + .unwrap_or_else(|e| panic!("Failed to create ChiselSession! {e}")); env.write().unwrap(); let wait_time = std::time::Duration::from_millis(100); std::thread::sleep(wait_time); - let mut env2 = ChiselSession::::new(chisel::session_source::SessionSourceConfig { - foundry_config, - ..Default::default() - }) - .unwrap_or_else(|e| panic!("Failed to create ChiselSession! {e}")); + let mut env2 = + ChiselSession::::new(chisel::session_source::SessionSourceConfig { + foundry_config, + ..Default::default() + }) + .unwrap_or_else(|e| panic!("Failed to create ChiselSession! {e}")); env2.write().unwrap(); // Load the latest session diff --git a/crates/evm/core/src/backend/mod.rs b/crates/evm/core/src/backend/mod.rs index e9f9ef9ec..8453a19b3 100644 --- a/crates/evm/core/src/backend/mod.rs +++ b/crates/evm/core/src/backend/mod.rs @@ -2313,7 +2313,14 @@ fn apply_state_changeset( #[cfg(test)] #[allow(clippy::needless_return)] mod tests { - use crate::{backend::Backend, fork::CreateFork, opts::EvmOpts}; + use crate::{ + backend::{ + strategy::{BackendStrategy, EvmBackendStrategy}, + Backend, + }, + fork::CreateFork, + opts::EvmOpts, + }; use alloy_primitives::{Address, U256}; use alloy_provider::Provider; use foundry_common::provider::get_http_provider; diff --git a/crates/evm/core/src/backend/strategy.rs b/crates/evm/core/src/backend/strategy.rs index 2c924d74f..21a98f39a 100644 --- a/crates/evm/core/src/backend/strategy.rs +++ b/crates/evm/core/src/backend/strategy.rs @@ -18,14 +18,15 @@ pub struct BackendStrategyForkInfo<'a> { pub target_type: ForkType, } -pub trait BackendStrategy: std::fmt::Debug + Send + Sync + Default + Clone + Serialize + for<'a> Deserialize<'a> + 'static +pub trait BackendStrategy: + std::fmt::Debug + Send + Sync + Default + Clone + Serialize + for<'a> Deserialize<'a> + 'static where Self: Sized, { fn new() -> Arc> { Arc::new(Mutex::new(Self::default())) } - + fn name(&self) -> &'static str; /// When creating or switching forks, we update the AccountInfo of the contract diff --git a/crates/evm/evm/src/executors/fuzz/mod.rs b/crates/evm/evm/src/executors/fuzz/mod.rs index 282359763..a22444330 100644 --- a/crates/evm/evm/src/executors/fuzz/mod.rs +++ b/crates/evm/evm/src/executors/fuzz/mod.rs @@ -6,7 +6,9 @@ use eyre::Result; use foundry_common::evm::Breakpoints; use foundry_config::FuzzConfig; use foundry_evm_core::{ - backend::strategy::BackendStrategy, constants::MAGIC_ASSUME, decode::{RevertDecoder, SkipReason} + backend::strategy::BackendStrategy, + constants::MAGIC_ASSUME, + decode::{RevertDecoder, SkipReason}, }; use foundry_evm_coverage::HitMaps; use foundry_evm_fuzz::{ @@ -60,7 +62,10 @@ pub struct FuzzedExecutor { config: FuzzConfig, } -impl FuzzedExecutor where B: BackendStrategy { +impl FuzzedExecutor +where + B: BackendStrategy, +{ /// Instantiates a fuzzed executor given a testrunner pub fn new( executor: Executor, diff --git a/crates/evm/evm/src/executors/invariant/mod.rs b/crates/evm/evm/src/executors/invariant/mod.rs index 69c1f3a30..6b84b2fb8 100644 --- a/crates/evm/evm/src/executors/invariant/mod.rs +++ b/crates/evm/evm/src/executors/invariant/mod.rs @@ -8,7 +8,10 @@ use eyre::{eyre, ContextCompat, Result}; use foundry_common::contracts::{ContractsByAddress, ContractsByArtifact}; use foundry_config::InvariantConfig; use foundry_evm_core::{ - abi::HARDHAT_CONSOLE_ADDRESS, backend::strategy::BackendStrategy, constants::{CALLER, CHEATCODE_ADDRESS, DEFAULT_CREATE2_DEPLOYER, MAGIC_ASSUME}, precompiles::PRECOMPILES + abi::HARDHAT_CONSOLE_ADDRESS, + backend::strategy::BackendStrategy, + constants::{CALLER, CHEATCODE_ADDRESS, DEFAULT_CREATE2_DEPLOYER, MAGIC_ASSUME}, + precompiles::PRECOMPILES, }; use foundry_evm_fuzz::{ invariant::{ @@ -298,7 +301,10 @@ pub struct InvariantExecutor<'a, B> { artifact_filters: ArtifactFilters, } -impl<'a, B> InvariantExecutor<'a, B> where B: BackendStrategy { +impl<'a, B> InvariantExecutor<'a, B> +where + B: BackendStrategy, +{ /// Instantiates a fuzzed executor EVM given a testrunner pub fn new( executor: Executor, diff --git a/crates/evm/evm/src/executors/invariant/replay.rs b/crates/evm/evm/src/executors/invariant/replay.rs index d675092b7..91f1f65cb 100644 --- a/crates/evm/evm/src/executors/invariant/replay.rs +++ b/crates/evm/evm/src/executors/invariant/replay.rs @@ -150,7 +150,10 @@ pub fn replay_error( } /// Sets up the calls generated by the internal fuzzer, if they exist. -fn set_up_inner_replay(executor: &mut Executor, inner_sequence: &[Option]) { +fn set_up_inner_replay( + executor: &mut Executor, + inner_sequence: &[Option], +) { if let Some(fuzzer) = &mut executor.inspector_mut().fuzzer { if let Some(call_generator) = &mut fuzzer.call_generator { call_generator.last_sequence = Arc::new(RwLock::new(inner_sequence.to_owned())); diff --git a/crates/forge/bin/cmd/coverage.rs b/crates/forge/bin/cmd/coverage.rs index f5da0e3c9..e048724c2 100644 --- a/crates/forge/bin/cmd/coverage.rs +++ b/crates/forge/bin/cmd/coverage.rs @@ -3,12 +3,16 @@ use alloy_primitives::{map::HashMap, Address, Bytes, U256}; use clap::{Parser, ValueEnum, ValueHint}; use eyre::{Context, Result}; use forge::{ - backend::strategy::{BackendStrategy, EvmBackendStrategy}, coverage::{ + backend::strategy::{BackendStrategy, EvmBackendStrategy}, + coverage::{ analysis::{SourceAnalysis, SourceAnalyzer, SourceFile, SourceFiles}, anchors::find_anchors, BytecodeReporter, ContractId, CoverageReport, CoverageReporter, DebugReporter, ItemAnchor, LcovReporter, SummaryReporter, - }, opts::EvmOpts, utils::IcPcMap, MultiContractRunnerBuilder, TestOptions + }, + opts::EvmOpts, + utils::IcPcMap, + MultiContractRunnerBuilder, TestOptions, }; use foundry_cli::utils::{LoadConfig, STATIC_FUZZ_SEED}; use foundry_common::{compile::ProjectCompiler, fs}; @@ -237,7 +241,15 @@ impl CoverageArgs { ..Default::default() }) .set_coverage(true) - .build(&root, output.clone(), None, env, evm_opts, DualCompiledContracts::default(), EvmBackendStrategy::new())?; + .build( + &root, + output.clone(), + None, + env, + evm_opts, + DualCompiledContracts::default(), + EvmBackendStrategy::new(), + )?; let known_contracts = runner.known_contracts.clone(); diff --git a/crates/forge/bin/cmd/test/mod.rs b/crates/forge/bin/cmd/test/mod.rs index 46b7df782..a54777576 100644 --- a/crates/forge/bin/cmd/test/mod.rs +++ b/crates/forge/bin/cmd/test/mod.rs @@ -943,7 +943,10 @@ impl Provider for TestArgs { } /// Lists all matching tests -fn list(runner: MultiContractRunner, filter: &ProjectPathsAwareFilter) -> Result { +fn list( + runner: MultiContractRunner, + filter: &ProjectPathsAwareFilter, +) -> Result { let results = runner.list(filter); if shell::is_json() { diff --git a/crates/forge/src/multi_runner.rs b/crates/forge/src/multi_runner.rs index 2162d18fd..c8843d876 100644 --- a/crates/forge/src/multi_runner.rs +++ b/crates/forge/src/multi_runner.rs @@ -91,7 +91,10 @@ pub struct MultiContractRunner { pub strategy: Arc>, } -impl MultiContractRunner where B: BackendStrategy { +impl MultiContractRunner +where + B: BackendStrategy, +{ /// Returns an iterator over all contracts that match the filter. pub fn matching_contracts<'a: 'b, 'b>( &'a self, @@ -405,6 +408,7 @@ impl MultiContractRunnerBuilder { /// Given an EVM, proceeds to return a runner which is able to execute all tests /// against that evm + #[allow(clippy::too_many_arguments)] pub fn build( self, root: &Path, diff --git a/crates/forge/src/runner.rs b/crates/forge/src/runner.rs index f723e2a6e..39c2b9267 100644 --- a/crates/forge/src/runner.rs +++ b/crates/forge/src/runner.rs @@ -17,17 +17,22 @@ use foundry_common::{ }; use foundry_config::{FuzzConfig, InvariantConfig}; use foundry_evm::{ - backend::strategy::BackendStrategy, constants::CALLER, decode::RevertDecoder, executors::{ + backend::strategy::BackendStrategy, + constants::CALLER, + decode::RevertDecoder, + executors::{ fuzz::FuzzedExecutor, invariant::{ check_sequence, replay_error, replay_run, InvariantExecutor, InvariantFuzzError, }, CallResult, EvmError, ExecutionErr, Executor, ITest, RawCallResult, - }, fuzz::{ + }, + fuzz::{ fixture_name, invariant::{CallDetails, InvariantContract}, CounterExample, FuzzFixtures, - }, traces::{load_contracts, TraceKind, TraceMode} + }, + traces::{load_contracts, TraceKind, TraceMode}, }; use proptest::test_runner::TestRunner; use rayon::prelude::*; @@ -67,7 +72,10 @@ pub struct ContractRunner<'a, B> { pub span: tracing::Span, } -impl ContractRunner<'_, B> where B: BackendStrategy { +impl ContractRunner<'_, B> +where + B: BackendStrategy, +{ /// Deploys the test contract inside the runner from the sending account, and optionally runs /// the `setUp` function on the test contract. pub fn setup(&mut self, call_setup: bool) -> TestSetup { diff --git a/crates/forge/tests/it/config.rs b/crates/forge/tests/it/config.rs index ca39700e8..7c00c5e27 100644 --- a/crates/forge/tests/it/config.rs +++ b/crates/forge/tests/it/config.rs @@ -1,7 +1,9 @@ //! Test config. use forge::{ - backend::strategy::BackendStrategy, result::{SuiteResult, TestStatus}, MultiContractRunner + backend::strategy::BackendStrategy, + result::{SuiteResult, TestStatus}, + MultiContractRunner, }; use foundry_evm::{ decode::decode_console_logs, @@ -20,7 +22,10 @@ pub struct TestConfig { pub filter: Filter, } -impl TestConfig where B: BackendStrategy { +impl TestConfig +where + B: BackendStrategy, +{ pub fn new(runner: MultiContractRunner) -> Self { Self::with_filter(runner, Filter::matches_all()) } diff --git a/crates/forge/tests/it/repros.rs b/crates/forge/tests/it/repros.rs index 84aa6c60a..e72b6d020 100644 --- a/crates/forge/tests/it/repros.rs +++ b/crates/forge/tests/it/repros.rs @@ -10,7 +10,9 @@ use alloy_dyn_abi::{DecodedEvent, DynSolValue, EventExt}; use alloy_json_abi::Event; use alloy_primitives::{address, b256, Address, U256}; use forge::{ - backend::strategy::EvmBackendStrategy, decode::decode_console_logs, result::{TestKind, TestStatus} + backend::strategy::EvmBackendStrategy, + decode::decode_console_logs, + result::{TestKind, TestStatus}, }; use foundry_config::{fs_permissions::PathPermission, Config, FsPermissions}; use foundry_evm::traces::{CallKind, CallTraceDecoder, DecodedCallData, TraceKind}; diff --git a/crates/forge/tests/it/test_helpers.rs b/crates/forge/tests/it/test_helpers.rs index b42d105bb..b52104157 100644 --- a/crates/forge/tests/it/test_helpers.rs +++ b/crates/forge/tests/it/test_helpers.rs @@ -3,7 +3,9 @@ use alloy_chains::NamedChain; use alloy_primitives::U256; use forge::{ - backend::strategy::{BackendStrategy, EvmBackendStrategy}, revm::primitives::SpecId, MultiContractRunner, MultiContractRunnerBuilder, TestOptions, TestOptionsBuilder + backend::strategy::{BackendStrategy, EvmBackendStrategy}, + revm::primitives::SpecId, + MultiContractRunner, MultiContractRunnerBuilder, TestOptions, TestOptionsBuilder, }; use foundry_compilers::{ artifacts::{EvmVersion, Libraries, Settings}, @@ -299,7 +301,10 @@ impl ForgeTestData { } /// Builds a non-tracing runner - pub fn runner_with_config(&self, mut config: Config) -> MultiContractRunner { + pub fn runner_with_config( + &self, + mut config: Config, + ) -> MultiContractRunner { config.rpc_endpoints = rpc_endpoints(); config.allow_paths.push(manifest_root().to_path_buf()); @@ -385,7 +390,7 @@ impl ForgeTestData { opts.local_evm_env(), opts, Default::default(), - EvmBackendStrategy::new() + EvmBackendStrategy::new(), ) .unwrap() } @@ -402,7 +407,15 @@ impl ForgeTestData { self.base_runner() .with_fork(fork) - .build(self.project.root(), self.output.clone(), None, env, opts, Default::default(), EvmBackendStrategy::new()) + .build( + self.project.root(), + self.output.clone(), + None, + env, + opts, + Default::default(), + EvmBackendStrategy::new(), + ) .unwrap() } } diff --git a/crates/script/src/lib.rs b/crates/script/src/lib.rs index 5c264f160..e969b5848 100644 --- a/crates/script/src/lib.rs +++ b/crates/script/src/lib.rs @@ -42,7 +42,10 @@ use foundry_config::{ Config, }; use foundry_evm::{ - backend::{strategy::{BackendStrategy, EvmBackendStrategy}, Backend}, + backend::{ + strategy::{BackendStrategy, EvmBackendStrategy}, + Backend, + }, constants::DEFAULT_CREATE2_DEPLOYER, executors::ExecutorBuilder, inspectors::{ @@ -55,7 +58,10 @@ use foundry_evm::{ use foundry_wallets::MultiWalletOpts; use foundry_zksync_compiler::DualCompiledContracts; use serde::Serialize; -use std::{path::PathBuf, sync::{Arc, Mutex}}; +use std::{ + path::PathBuf, + sync::{Arc, Mutex}, +}; mod broadcast; mod build; @@ -536,7 +542,10 @@ pub struct ScriptConfig { pub backends: HashMap>, } -impl ScriptConfig where B: BackendStrategy { +impl ScriptConfig +where + B: BackendStrategy, +{ pub async fn new(config: Config, evm_opts: EvmOpts) -> Result { let sender_nonce = if let Some(fork_url) = evm_opts.fork_url.as_ref() { next_nonce(evm_opts.sender, fork_url).await? @@ -558,7 +567,7 @@ impl ScriptConfig where B: BackendStrategy { Ok(()) } - async fn get_runner(&mut self, strategy: Arc>,) -> Result> { + async fn get_runner(&mut self, strategy: Arc>) -> Result> { self._get_runner(None, false, strategy).await } diff --git a/crates/script/src/runner.rs b/crates/script/src/runner.rs index aeedb0bfb..03b31ca27 100644 --- a/crates/script/src/runner.rs +++ b/crates/script/src/runner.rs @@ -6,7 +6,12 @@ use eyre::Result; use foundry_cheatcodes::BroadcastableTransaction; use foundry_config::Config; use foundry_evm::{ - backend::strategy::BackendStrategy, constants::{CALLER, DEFAULT_CREATE2_DEPLOYER}, executors::{DeployResult, EvmError, ExecutionErr, Executor, RawCallResult}, opts::EvmOpts, revm::interpreter::{return_ok, InstructionResult}, traces::{TraceKind, Traces} + backend::strategy::BackendStrategy, + constants::{CALLER, DEFAULT_CREATE2_DEPLOYER}, + executors::{DeployResult, EvmError, ExecutionErr, Executor, RawCallResult}, + opts::EvmOpts, + revm::interpreter::{return_ok, InstructionResult}, + traces::{TraceKind, Traces}, }; use foundry_zksync_core::ZkTransactionMetadata; use std::collections::VecDeque; @@ -19,7 +24,10 @@ pub struct ScriptRunner { pub evm_opts: EvmOpts, } -impl ScriptRunner where B: BackendStrategy { +impl ScriptRunner +where + B: BackendStrategy, +{ pub fn new(executor: Executor, evm_opts: EvmOpts) -> Self { Self { executor, evm_opts } } diff --git a/crates/strategy/core/src/lib.rs b/crates/strategy/core/src/lib.rs index 4bc6f9a7d..b8bbb73cd 100644 --- a/crates/strategy/core/src/lib.rs +++ b/crates/strategy/core/src/lib.rs @@ -23,4 +23,4 @@ impl RunnerStrategy for EvmRunnerStrategy { fn backend_strategy(&self) -> Arc> { self.backend.clone() } -} \ No newline at end of file +} diff --git a/crates/strategy/zksync/src/lib.rs b/crates/strategy/zksync/src/lib.rs index 30209ec35..d7a83ae33 100644 --- a/crates/strategy/zksync/src/lib.rs +++ b/crates/strategy/zksync/src/lib.rs @@ -16,7 +16,9 @@ use foundry_evm_core::{ }; use foundry_strategy_core::RunnerStrategy; use foundry_zksync_core::{ - convert::ConvertH160, PaymasterParams, ACCOUNT_CODE_STORAGE_ADDRESS, H256, IMMUTABLE_SIMULATOR_STORAGE_ADDRESS, KNOWN_CODES_STORAGE_ADDRESS, L2_BASE_TOKEN_ADDRESS, NONCE_HOLDER_ADDRESS + convert::ConvertH160, PaymasterParams, ACCOUNT_CODE_STORAGE_ADDRESS, H256, + IMMUTABLE_SIMULATOR_STORAGE_ADDRESS, KNOWN_CODES_STORAGE_ADDRESS, L2_BASE_TOKEN_ADDRESS, + NONCE_HOLDER_ADDRESS, }; use revm::{ db::CacheDB, @@ -96,7 +98,7 @@ impl ZkBackendStrategy { ) { let _require_zk_storage_merge = fork_info.active_type.is_zk() && fork_info.target_type.is_zk(); - + // Ignore EVM interoperatability and import everything // if !require_zk_storage_merge { // return; @@ -302,9 +304,7 @@ pub struct ZkRunnerStrategy { } impl Default for ZkRunnerStrategy { fn default() -> Self { - Self { - backend: Arc::new(Mutex::new(ZkBackendStrategy::default())), - } + Self { backend: Arc::new(Mutex::new(ZkBackendStrategy::default())) } } } impl RunnerStrategy for ZkRunnerStrategy { diff --git a/crates/verify/src/utils.rs b/crates/verify/src/utils.rs index 0482f5544..cc881f0e8 100644 --- a/crates/verify/src/utils.rs +++ b/crates/verify/src/utils.rs @@ -14,7 +14,10 @@ use foundry_block_explorers::{ use foundry_common::{abi::encode_args, compile::ProjectCompiler, provider::RetryProvider, shell}; use foundry_compilers::artifacts::{BytecodeHash, CompactContractBytecode, EvmVersion}; use foundry_config::Config; -use foundry_evm::{backend::strategy::BackendStrategy, constants::DEFAULT_CREATE2_DEPLOYER, executors::TracingExecutor, opts::EvmOpts}; +use foundry_evm::{ + backend::strategy::BackendStrategy, constants::DEFAULT_CREATE2_DEPLOYER, + executors::TracingExecutor, opts::EvmOpts, +}; use reqwest::Url; use revm_primitives::{ db::Database, From d4e3a517c06114aa91f677ec05b327e10da1079a Mon Sep 17 00:00:00 2001 From: Nisheeth Barthwal Date: Mon, 9 Dec 2024 14:04:05 +0100 Subject: [PATCH 3/3] abstract more --- Cargo.lock | 4 + crates/cheatcodes/src/config.rs | 24 +-- crates/cheatcodes/src/inspector.rs | 18 +-- crates/evm/core/Cargo.toml | 1 + crates/evm/core/src/backend/cow.rs | 28 ++++ crates/evm/core/src/backend/mod.rs | 57 +++---- crates/evm/core/src/backend/strategy.rs | 77 ++++++++- crates/evm/evm/Cargo.toml | 1 + crates/evm/evm/src/executors/mod.rs | 106 +++++++++---- crates/forge/bin/cmd/test/mod.rs | 3 + crates/forge/src/multi_runner.rs | 16 +- crates/strategy/zksync/Cargo.toml | 2 + crates/strategy/zksync/src/lib.rs | 203 ++++++++++++++++++++++-- crates/zksync/core/src/vm/runner.rs | 2 +- 14 files changed, 441 insertions(+), 101 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ea7b4e756..95c4c43f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5093,6 +5093,7 @@ dependencies = [ "foundry-evm-coverage", "foundry-evm-fuzz", "foundry-evm-traces", + "foundry-strategy-zksync", "foundry-zksync-core", "foundry-zksync-inspectors", "indicatif", @@ -5140,6 +5141,7 @@ dependencies = [ "foundry-evm-abi", "foundry-fork-db", "foundry-test-utils", + "foundry-zksync-compiler", "foundry-zksync-core", "futures 0.3.31", "itertools 0.13.0", @@ -5278,12 +5280,14 @@ name = "foundry-strategy-zksync" version = "0.0.2" dependencies = [ "alloy-primitives", + "alloy-sol-types", "eyre", "foundry-common", "foundry-compilers", "foundry-evm-core", "foundry-evm-traces", "foundry-strategy-core", + "foundry-zksync-compiler", "foundry-zksync-core", "revm", "revm-inspectors", diff --git a/crates/cheatcodes/src/config.rs b/crates/cheatcodes/src/config.rs index 6c1446bda..e0c73ac0c 100644 --- a/crates/cheatcodes/src/config.rs +++ b/crates/cheatcodes/src/config.rs @@ -19,7 +19,7 @@ use std::{ /// /// This is essentially a subset of various `Config` settings `Cheatcodes` needs to know. #[derive(Clone, Debug)] -pub struct CheatsConfig { +pub struct CheatsConfig { /// Whether the FFI cheatcode is enabled. pub ffi: bool, /// Use the create 2 factory in all cases including tests and non-broadcasting scripts. @@ -56,15 +56,15 @@ pub struct CheatsConfig { pub running_version: Option, /// ZKSolc -> Solc Contract codes pub dual_compiled_contracts: DualCompiledContracts, - /// Use ZK-VM on startup - pub use_zk: bool, /// Whether to enable legacy (non-reverting) assertions. pub assertions_revert: bool, /// Optional seed for the RNG algorithm. pub seed: Option, + /// Execution strategy. + pub strategy: S, } -impl CheatsConfig { +impl CheatsConfig { /// Extracts the necessary settings from the Config pub fn new( config: &Config, @@ -73,7 +73,7 @@ impl CheatsConfig { running_contract: Option, running_version: Option, dual_compiled_contracts: DualCompiledContracts, - use_zk: bool, + strategy: S, ) -> Self { let mut allowed_paths = vec![config.root.0.clone()]; allowed_paths.extend(config.libs.clone()); @@ -104,7 +104,7 @@ impl CheatsConfig { running_contract, running_version, dual_compiled_contracts, - use_zk, + strategy, assertions_revert: config.assertions_revert, seed: config.fuzz.seed, } @@ -216,7 +216,10 @@ impl CheatsConfig { } } -impl Default for CheatsConfig { +impl Default for CheatsConfig +where + S: Default, +{ fn default() -> Self { Self { ffi: false, @@ -236,7 +239,7 @@ impl Default for CheatsConfig { running_contract: Default::default(), running_version: Default::default(), dual_compiled_contracts: Default::default(), - use_zk: false, + strategy: Default::default(), assertions_revert: true, seed: None, } @@ -247,8 +250,9 @@ impl Default for CheatsConfig { mod tests { use super::*; use foundry_config::fs_permissions::PathPermission; + use foundry_evm_core::backend::strategy::EvmBackendStrategy; - fn config(root: &str, fs_permissions: FsPermissions) -> CheatsConfig { + fn config(root: &str, fs_permissions: FsPermissions) -> CheatsConfig { CheatsConfig::new( &Config { root: PathBuf::from(root).into(), fs_permissions, ..Default::default() }, Default::default(), @@ -256,7 +260,7 @@ mod tests { None, None, Default::default(), - false, + EvmBackendStrategy, ) } diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index 4d4f937b4..c682e2def 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -32,7 +32,7 @@ use foundry_cheatcodes_common::{ use foundry_common::{evm::Breakpoints, TransactionMaybeSigned, SELECTOR_LEN}; use foundry_evm_core::{ abi::{Vm::stopExpectSafeMemoryCall, HARDHAT_CONSOLE_ADDRESS}, - backend::{DatabaseError, DatabaseExt, LocalForkId, RevertDiagnostic}, + backend::{strategy::CheatcodeInspectorStrategy, DatabaseError, DatabaseExt, LocalForkId, RevertDiagnostic}, constants::{ CHEATCODE_ADDRESS, CHEATCODE_CONTRACT_HASH, DEFAULT_CREATE2_DEPLOYER, DEFAULT_CREATE2_DEPLOYER_CODE, MAGIC_ASSUME, @@ -96,7 +96,7 @@ pub type InnerEcx<'a, 'b, 'c> = &'a mut InnerEvmContext<&'b mut (dyn DatabaseExt pub trait CheatcodesExecutor { /// Core trait method accepting mutable reference to [Cheatcodes] and returning /// [revm::Inspector]. - fn get_inspector<'a>(&'a mut self, cheats: &'a mut Cheatcodes) -> Box; + fn get_inspector<'a, S>(&'a mut self, cheats: &'a mut Cheatcodes) -> Box; /// Obtains [revm::Evm] instance and executes the given CREATE frame. fn exec_create( @@ -152,7 +152,7 @@ pub trait CheatcodesExecutor { None } - fn trace_zksync(&mut self, ccx_state: &mut Cheatcodes, ecx: Ecx, call_traces: Vec) { + fn trace_zksync(&mut self, ccx_state: &mut Cheatcodes, ecx: Ecx, call_traces: Vec) { let mut inspector = self.get_inspector(ccx_state); // We recreate the EvmContext here to satisfy the lifetime parameters as 'static, with @@ -234,7 +234,7 @@ where struct TransparentCheatcodesExecutor; impl CheatcodesExecutor for TransparentCheatcodesExecutor { - fn get_inspector<'a>(&'a mut self, cheats: &'a mut Cheatcodes) -> Box { + fn get_inspector<'a, S>(&'a mut self, cheats: &'a mut Cheatcodes) -> Box { Box::new(cheats) } } @@ -495,7 +495,7 @@ impl ZkStartupMigration { /// cheatcode address: by default, the caller, test contract and newly deployed contracts are /// allowed to execute cheatcodes #[derive(Clone, Debug)] -pub struct Cheatcodes { +pub struct Cheatcodes { /// The block environment /// /// Used in the cheatcode handler to overwrite the block environment separately from the @@ -561,7 +561,7 @@ pub struct Cheatcodes { pub broadcastable_transactions: BroadcastableTransactions, /// Additional, user configurable context this Inspector has access to when inspecting a call - pub config: Arc, + pub config: CheatsConfig, /// Test-scoped context holding data that needs to be reset every test run pub context: Context, @@ -649,15 +649,15 @@ pub struct Cheatcodes { // This is not derived because calling this in `fn new` with `..Default::default()` creates a second // `CheatsConfig` which is unused, and inside it `ProjectPathsConfig` is relatively expensive to // create. -impl Default for Cheatcodes { +impl Default for Cheatcodes where S: CheatcodeInspectorStrategy { fn default() -> Self { Self::new(Arc::default()) } } -impl Cheatcodes { +impl Cheatcodes where S: CheatcodeInspectorStrategy { /// Creates a new `Cheatcodes` with the given settings. - pub fn new(config: Arc) -> Self { + pub fn new(config: Arc>) -> Self { let mut dual_compiled_contracts = config.dual_compiled_contracts.clone(); // We add the empty bytecode manually so it is correctly translated in zk mode. diff --git a/crates/evm/core/Cargo.toml b/crates/evm/core/Cargo.toml index fe46a1f4f..b5444e9da 100644 --- a/crates/evm/core/Cargo.toml +++ b/crates/evm/core/Cargo.toml @@ -18,6 +18,7 @@ foundry-cheatcodes-spec.workspace = true foundry-common.workspace = true foundry-config.workspace = true foundry-zksync-core.workspace = true +foundry-zksync-compiler.workspace = true foundry-evm-abi.workspace = true alloy-dyn-abi = { workspace = true, features = ["arbitrary", "eip712"] } diff --git a/crates/evm/core/src/backend/cow.rs b/crates/evm/core/src/backend/cow.rs index dda22e888..673b2310c 100644 --- a/crates/evm/core/src/backend/cow.rs +++ b/crates/evm/core/src/backend/cow.rs @@ -108,6 +108,29 @@ where Ok(res) } + /// Executes the configured transaction of the `env` without committing state changes + /// + /// Note: in case there are any cheatcodes executed that modify the environment, this will + /// update the given `env` with the new values. + #[instrument(name = "inspect", level = "debug", skip_all)] + pub fn inspect_with_extra( + &mut self, + env: &mut EnvWithHandlerCfg, + inspector: &mut I, + ) -> eyre::Result { + // this is a new call to inspect with a new env, so even if we've cloned the backend + // already, we reset the initialized state + self.is_initialized = false; + self.spec_id = env.handler_cfg.spec_id; + let mut evm = crate::utils::new_evm_with_inspector(self, env.clone(), inspector); + + let res = evm.transact().wrap_err("backend: failed while inspecting")?; + + env.env = evm.context.evm.inner.env; + + Ok(res) + } + pub fn new_borrowed(backend: &'a Backend) -> Self { Self { backend: Cow::Borrowed(backend), is_initialized: false, spec_id: SpecId::LATEST } } @@ -146,6 +169,11 @@ impl DatabaseExt for CowBackend<'_, S> where S: BackendStrategy, { + fn initialize(&mut self, env: &EnvWithHandlerCfg) { + self.backend.to_mut().initialize(&env); + self.is_initialized = true; + } + fn get_fork_info(&mut self, id: LocalForkId) -> eyre::Result { self.backend.to_mut().get_fork_info(id) } diff --git a/crates/evm/core/src/backend/mod.rs b/crates/evm/core/src/backend/mod.rs index 8453a19b3..36621a7d0 100644 --- a/crates/evm/core/src/backend/mod.rs +++ b/crates/evm/core/src/backend/mod.rs @@ -88,6 +88,9 @@ pub struct ForkInfo { /// An extension trait that allows us to easily extend the `revm::Inspector` capabilities #[auto_impl::auto_impl(&mut)] pub trait DatabaseExt: Database + DatabaseCommit { + /// Initialize any settings that must be tracked while switching evms. + fn initialize(&mut self, env: &EnvWithHandlerCfg); + /// Creates a new state snapshot at the current point of execution. /// /// A state snapshot is associated with a new unique id that's created for the snapshot. @@ -807,14 +810,6 @@ where logs } - /// Initializes settings we need to keep track of. - /// - /// We need to track these mainly to prevent issues when switching between different evms - pub fn initialize(&mut self, env: &EnvWithHandlerCfg) { - self.set_caller(env.tx.caller); - self.set_spec_id(env.handler_cfg.spec_id); - } - /// Returns the `EnvWithHandlerCfg` with the current `spec_id` set. fn env_with_handler_cfg(&self, env: Env) -> EnvWithHandlerCfg { EnvWithHandlerCfg::new_with_spec_id(Box::new(env), self.inner.spec_id) @@ -845,24 +840,24 @@ where // Ok(res) } - /// Executes the configured test call of the `env` without committing state changes - pub fn inspect_ref_zk( - &mut self, - env: &mut EnvWithHandlerCfg, - persisted_factory_deps: &mut HashMap>, - factory_deps: Option>>, - paymaster_data: Option, - ) -> eyre::Result { - self.initialize(env); - - foundry_zksync_core::vm::transact( - Some(persisted_factory_deps), - factory_deps, - paymaster_data, - env, - self, - ) - } + // /// Executes the configured test call of the `env` without committing state changes + // pub fn inspect_ref_zk( + // &mut self, + // env: &mut EnvWithHandlerCfg, + // persisted_factory_deps: &mut HashMap>, + // factory_deps: Option>>, + // paymaster_data: Option, + // ) -> eyre::Result { + // self.initialize(env); + + // foundry_zksync_core::vm::transact( + // Some(persisted_factory_deps), + // factory_deps, + // paymaster_data, + // env, + // self, + // ) + // } /// Returns true if the address is a precompile pub fn is_existing_precompile(&self, addr: &Address) -> bool { @@ -1003,6 +998,15 @@ impl DatabaseExt for Backend where S: BackendStrategy, { + /// Initializes settings we need to keep track of. + /// + /// We need to track these mainly to prevent issues when switching between different evms + fn initialize(&mut self, env: &EnvWithHandlerCfg) { + self.set_caller(env.tx.caller); + self.set_spec_id(env.handler_cfg.spec_id); + } + + fn get_fork_info(&mut self, id: LocalForkId) -> eyre::Result { let fork_id = self.ensure_fork_id(id).cloned()?; let fork_env = self @@ -1256,7 +1260,6 @@ where caller_account.into() }); - // TODO(zk): add zk_state by using it inside strategy self.strategy.lock().unwrap().update_fork_db( BackendStrategyForkInfo { active_fork: self.active_fork(), diff --git a/crates/evm/core/src/backend/strategy.rs b/crates/evm/core/src/backend/strategy.rs index 21a98f39a..4dff4f36e 100644 --- a/crates/evm/core/src/backend/strategy.rs +++ b/crates/evm/core/src/backend/strategy.rs @@ -1,10 +1,14 @@ -use std::sync::{Arc, Mutex}; +use std::{ + fmt::Debug, + sync::{Arc, Mutex}, +}; use crate::InspectorExt; -use super::{Backend, BackendInner, Fork, ForkDB, ForkType, FoundryEvmInMemoryDB}; +use super::{BackendInner, DatabaseExt, Fork, ForkDB, ForkType, FoundryEvmInMemoryDB}; use alloy_primitives::Address; use eyre::Context; +use foundry_zksync_compiler::DualCompiledContracts; use revm::{ db::CacheDB, primitives::{EnvWithHandlerCfg, ResultAndState}, @@ -18,8 +22,65 @@ pub struct BackendStrategyForkInfo<'a> { pub target_type: ForkType, } +pub trait GlobalStrategy: Debug + Send + Sync + Default + Clone { + type Backend: BackendStrategy; + type Executor: ExecutorStrategy; + type CheatcodeInspector: CheatcodeInspectorStrategy; + + fn backend_strategy() -> Arc> { + Self::Backend::new() + } + + fn executor_strategy() -> Arc> { + Self::Executor::new() + } + + fn cheatcode_strategy() -> Self::CheatcodeInspector { + Self::CheatcodeInspector::new() + } +} + +pub trait CheatcodeInspectorStrategy: Debug + Send + Sync + Default + Clone { + fn new() -> Self { + Self::default() + } + + fn initialize(&mut self, dual_compiled_contracts: DualCompiledContracts); +} + +pub trait ExecutorStrategy: Debug + Send + Sync + Default + Clone { + fn new() -> Arc> { + Arc::new(Mutex::new(Self::default())) + } +} + +#[derive(Debug, Default, Clone)] +pub struct EvmStrategy; + +impl GlobalStrategy for EvmStrategy { + type Backend = EvmBackendStrategy; + type Executor = EvmExecutor; + type CheatcodeInspector = EvmCheatcodeInspector; +} + +#[derive(Debug, Default, Clone)] +pub struct EvmExecutor; +impl ExecutorStrategy for EvmExecutor { + fn new() -> Arc> { + Arc::new(Mutex::new(Self::default())) + } +} + +#[derive(Debug, Default, Clone)] +pub struct EvmCheatcodeInspector; +impl CheatcodeInspectorStrategy for EvmCheatcodeInspector { + fn initialize(&mut self, _dual_compiled_contracts: DualCompiledContracts) { + // do nothing + } +} + pub trait BackendStrategy: - std::fmt::Debug + Send + Sync + Default + Clone + Serialize + for<'a> Deserialize<'a> + 'static + Debug + Send + Sync + Default + Clone + Serialize + for<'a> Deserialize<'a> + 'static where Self: Sized, { @@ -44,15 +105,15 @@ where /// Note: in case there are any cheatcodes executed that modify the environment, this will /// update the given `env` with the new values. #[instrument(name = "inspect", level = "debug", skip_all)] - fn inspect( + fn inspect<'i, 'db, I: InspectorExt>( &mut self, - backend: &mut Backend, + db: &'db mut dyn DatabaseExt, env: &mut EnvWithHandlerCfg, - inspector: &mut I, + inspector: &'i mut I, _extra: Option>, ) -> eyre::Result { - backend.initialize(env); - let mut evm = crate::utils::new_evm_with_inspector(backend, env.clone(), inspector); + db.initialize(env); + let mut evm = crate::utils::new_evm_with_inspector(db, env.clone(), inspector); let res = evm.transact().wrap_err("backend: failed while inspecting")?; diff --git a/crates/evm/evm/Cargo.toml b/crates/evm/evm/Cargo.toml index 15f0b1f88..cc0126566 100644 --- a/crates/evm/evm/Cargo.toml +++ b/crates/evm/evm/Cargo.toml @@ -24,6 +24,7 @@ foundry-evm-fuzz.workspace = true foundry-evm-traces.workspace = true foundry-zksync-core.workspace = true foundry-zksync-inspectors.workspace = true +foundry-strategy-zksync.workspace = true alloy-dyn-abi = { workspace = true, features = ["arbitrary", "eip712"] } alloy-json-abi.workspace = true diff --git a/crates/evm/evm/src/executors/mod.rs b/crates/evm/evm/src/executors/mod.rs index 9043f5b94..977c24e04 100644 --- a/crates/evm/evm/src/executors/mod.rs +++ b/crates/evm/evm/src/executors/mod.rs @@ -18,7 +18,7 @@ use alloy_primitives::{ use alloy_sol_types::{sol, SolCall}; use foundry_evm_core::{ backend::{ - strategy::BackendStrategy, Backend, BackendError, BackendResult, CowBackend, DatabaseExt, + strategy::{BackendStrategy, GlobalStrategy, ExecutorStrategy}, Backend, BackendError, BackendResult, CowBackend, DatabaseExt, GLOBAL_FAIL_SLOT, }, constants::{ @@ -30,6 +30,7 @@ use foundry_evm_core::{ }; use foundry_evm_coverage::HitMaps; use foundry_evm_traces::{SparsedTraceArena, TraceMode}; +use foundry_strategy_zksync::ZkBackendInspectData; use foundry_zksync_core::ZkTransactionMetadata; use revm::{ db::{DatabaseCommit, DatabaseRef}, @@ -40,7 +41,7 @@ use revm::{ }, Database, }; -use std::borrow::Cow; +use std::{borrow::Cow, sync::{Arc, Mutex}}; mod builder; pub use builder::ExecutorBuilder; @@ -76,13 +77,13 @@ sol! { /// deployment /// - `setup`: a special case of `transact`, used to set up the environment for a test #[derive(Clone, Debug)] -pub struct Executor { +pub struct Executor { /// The underlying `revm::Database` that contains the EVM storage. // Note: We do not store an EVM here, since we are really // only interested in the database. REVM's `EVM` is a thin // wrapper around spawning a new EVM on every call anyway, // so the performance difference should be negligible. - pub backend: Backend, + pub backend: Backend, /// The EVM environment. pub env: EnvWithHandlerCfg, /// The Revm inspector stack. @@ -99,12 +100,13 @@ pub struct Executor { // simulate persisted factory deps zk_persisted_factory_deps: HashMap>, - pub use_zk: bool, + strategy: Arc>, + // pub use_zk: bool, } -impl Executor +impl Executor where - B: BackendStrategy, + G: GlobalStrategy, { /// Creates a new `ExecutorBuilder`. #[inline] @@ -115,7 +117,7 @@ where /// Creates a new `Executor` with the given arguments. #[inline] pub fn new( - mut backend: Backend, + mut backend: Backend, env: EnvWithHandlerCfg, inspector: InspectorStack, gas_limit: u64, @@ -142,22 +144,22 @@ where legacy_assertions, zk_tx: None, zk_persisted_factory_deps: Default::default(), - use_zk: false, + strategy: G::Executor::new(), } } - fn clone_with_backend(&self, backend: Backend) -> Self { + fn clone_with_backend(&self, backend: Backend) -> Self { let env = EnvWithHandlerCfg::new_with_spec_id(Box::new(self.env().clone()), self.spec_id()); Self::new(backend, env, self.inspector().clone(), self.gas_limit, self.legacy_assertions) } /// Returns a reference to the EVM backend. - pub fn backend(&self) -> &Backend { + pub fn backend(&self) -> &Backend { &self.backend } /// Returns a mutable reference to the EVM backend. - pub fn backend_mut(&mut self) -> &mut Backend { + pub fn backend_mut(&mut self) -> &mut Backend { &mut self.backend } @@ -435,21 +437,38 @@ where pub fn call_with_env(&self, mut env: EnvWithHandlerCfg) -> eyre::Result { let mut inspector = self.inspector().clone(); let mut backend = CowBackend::new_borrowed(self.backend()); - let result = match &self.zk_tx { - None => backend.inspect(&mut env, &mut inspector)?, - Some(zk_tx) => { - // apply fork-related env instead of cheatcode handler - // since it won't be run inside zkvm - env.block = self.env.block.clone(); - env.tx.gas_price = self.env.tx.gas_price; - backend.inspect_ref_zk( - &mut env, - &mut self.zk_persisted_factory_deps.clone(), - Some(zk_tx.factory_deps.clone()), - zk_tx.paymaster_data.clone(), - )? + // let result = match &self.zk_tx { + // None => backend.inspect(&mut env, &mut inspector)?, + // Some(zk_tx) => { + // // apply fork-related env instead of cheatcode handler + // // since it won't be run inside zkvm + // env.block = self.env.block.clone(); + // env.tx.gas_price = self.env.tx.gas_price; + // backend.inspect_ref_zk( + // &mut env, + // &mut self.zk_persisted_factory_deps.clone(), + // Some(zk_tx.factory_deps.clone()), + // zk_tx.paymaster_data.clone(), + // )? + // } + // }; + let strategy = backend.backend.strategy.clone(); // clone to take a mutable borrow + let extra = match &self.zk_tx { + Some(ZkTransactionMetadata { factory_deps, paymaster_data }) => { + serde_json::to_vec(&ZkBackendInspectData { + use_evm: false, + factory_deps: Some(factory_deps.clone()), + paymaster_data: paymaster_data.clone(), + }) + .unwrap() + } + None => { + serde_json::to_vec(&ZkBackendInspectData { use_evm: true, ..Default::default() }) + .unwrap() } }; + let result = + strategy.lock().unwrap().inspect(&mut backend, &mut env, &mut inspector, Some(extra))?; convert_executed_result(env, inspector, result, backend.has_state_snapshot_failure()) } @@ -458,10 +477,41 @@ where pub fn transact_with_env(&mut self, mut env: EnvWithHandlerCfg) -> eyre::Result { let mut inspector = self.inspector.clone(); let backend = &mut self.backend; - let strategy = backend.strategy.clone(); // to take a mutable borrow - let extra = self.zk_tx.take().map(|zk_tx| serde_json::to_vec(&zk_tx).unwrap()); + println!("TRANSACT HAS ZK? {}", self.zk_tx.is_some()); + // let result_and_state = match self.zk_tx.take() { + // None => backend.inspect(&mut env, &mut inspector)?, + // Some(zk_tx) => { + // // apply fork-related env instead of cheatcode handler + // // since it won't be run inside zkvm + // env.block = self.env.block.clone(); + // env.tx.gas_price = self.env.tx.gas_price; + // backend.inspect_ref_zk( + // &mut env, + // // this will persist the added factory deps, + // // no need to commit them later + // &mut self.zk_persisted_factory_deps, + // Some(zk_tx.factory_deps), + // zk_tx.paymaster_data, + // )? + // } + // }; + let strategy = backend.strategy.clone(); // clone to take a mutable borrow + let extra = match &self.zk_tx { + Some(ZkTransactionMetadata { factory_deps, paymaster_data }) => { + serde_json::to_vec(&ZkBackendInspectData { + use_evm: false, + factory_deps: Some(factory_deps.clone()), + paymaster_data: paymaster_data.clone(), + }) + .unwrap() + } + None => { + serde_json::to_vec(&ZkBackendInspectData { use_evm: true, ..Default::default() }) + .unwrap() + } + }; let result_and_state = - strategy.lock().unwrap().inspect(backend, &mut env, &mut inspector, extra)?; + strategy.lock().unwrap().inspect(backend, &mut env, &mut inspector, Some(extra))?; let mut result = convert_executed_result( env, inspector, diff --git a/crates/forge/bin/cmd/test/mod.rs b/crates/forge/bin/cmd/test/mod.rs index a54777576..a913daf3e 100644 --- a/crates/forge/bin/cmd/test/mod.rs +++ b/crates/forge/bin/cmd/test/mod.rs @@ -276,8 +276,10 @@ impl TestArgs { // Merge all configs. let (config, evm_opts) = self.load_config_and_evm_opts_emit_warnings()?; if config.zksync.should_compile() { + info!("executing with zksync strategy"); self.execute_tests_inner::(config, evm_opts).await } else { + info!("executing with evm strategy"); self.execute_tests_inner::(config, evm_opts).await } } @@ -323,6 +325,7 @@ impl TestArgs { let output = compiler.compile(&project)?; + // TODO(zk-new) only compile if zk strategy let (zk_output, dual_compiled_contracts) = if config.zksync.should_compile() { let zk_project = foundry_zksync_compiler::config_create_project(&config, config.cache, false)?; diff --git a/crates/forge/src/multi_runner.rs b/crates/forge/src/multi_runner.rs index c8843d876..d0f268c56 100644 --- a/crates/forge/src/multi_runner.rs +++ b/crates/forge/src/multi_runner.rs @@ -16,7 +16,10 @@ use foundry_compilers::{ }; use foundry_config::Config; use foundry_evm::{ - backend::{strategy::BackendStrategy, Backend}, + backend::{ + strategy::{BackendStrategy, GlobalStrategy}, + Backend, + }, decode::RevertDecoder, executors::ExecutorBuilder, fork::CreateFork, @@ -49,7 +52,7 @@ pub type DeployableContracts = BTreeMap; /// 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 { +pub struct MultiContractRunner { /// Mapping of contract name to JsonAbi, creation bytecode and library bytecode which /// needs to be deployed & linked against pub contracts: DeployableContracts, @@ -88,12 +91,12 @@ pub struct MultiContractRunner { /// Dual compiled contracts pub dual_compiled_contracts: DualCompiledContracts, /// Use zk runner. - pub strategy: Arc>, + pub strategy: G, } -impl MultiContractRunner +impl MultiContractRunner where - B: BackendStrategy, + G: GlobalStrategy, { /// Returns an iterator over all contracts that match the filter. pub fn matching_contracts<'a: 'b, 'b>( @@ -184,7 +187,7 @@ where trace!("running all tests"); // The DB backend that serves all the data. - let db = Backend::spawn(self.fork.take(), self.strategy.clone()); + let db = Backend::spawn(self.fork.take(), self.strategy.backend_strategy()); let find_timer = Instant::now(); let contracts = self.matching_contracts(filter).collect::>(); @@ -265,6 +268,7 @@ where .with_decode_internal(self.decode_internal) .with_verbosity(self.evm_opts.verbosity); + // TODO(zk-new) disable use_zk_vm let executor = ExecutorBuilder::new() .inspectors(|stack| { stack diff --git a/crates/strategy/zksync/Cargo.toml b/crates/strategy/zksync/Cargo.toml index ce7c9d9ad..b61c264f0 100644 --- a/crates/strategy/zksync/Cargo.toml +++ b/crates/strategy/zksync/Cargo.toml @@ -13,12 +13,14 @@ repository.workspace = true workspace = true [dependencies] +alloy-sol-types.workspace = true foundry-common.workspace = true foundry-compilers.workspace = true foundry-evm-traces.workspace = true foundry-evm-core.workspace = true foundry-strategy-core.workspace = true foundry-zksync-core.workspace = true +foundry-zksync-compiler.workspace = true revm-inspectors.workspace = true alloy-primitives.workspace = true diff --git a/crates/strategy/zksync/src/lib.rs b/crates/strategy/zksync/src/lib.rs index d7a83ae33..101dd6080 100644 --- a/crates/strategy/zksync/src/lib.rs +++ b/crates/strategy/zksync/src/lib.rs @@ -3,44 +3,214 @@ use std::{ sync::{Arc, Mutex}, }; -use alloy_primitives::{Address, U256}; +use alloy_primitives::{keccak256, Address, Bytes, B256, U256}; +use alloy_sol_types::SolValue; use foundry_evm_core::{ backend::{ strategy::{ merge_db_account_data, merge_journaled_state_data, BackendStrategy, - BackendStrategyForkInfo, + BackendStrategyForkInfo, CheatcodeInspectorStrategy, EvmBackendStrategy, + ExecutorStrategy, GlobalStrategy, }, - Backend, BackendInner, Fork, ForkDB, FoundryEvmInMemoryDB, + BackendInner, DatabaseExt, Fork, ForkDB, FoundryEvmInMemoryDB, }, + constants::{CHEATCODE_ADDRESS, CHEATCODE_CONTRACT_HASH}, InspectorExt, }; use foundry_strategy_core::RunnerStrategy; +use foundry_zksync_compiler::{DualCompiledContract, DualCompiledContracts}; use foundry_zksync_core::{ - convert::ConvertH160, PaymasterParams, ACCOUNT_CODE_STORAGE_ADDRESS, H256, + convert::ConvertH160, PaymasterParams, ZkPaymasterData, ACCOUNT_CODE_STORAGE_ADDRESS, H256, IMMUTABLE_SIMULATOR_STORAGE_ADDRESS, KNOWN_CODES_STORAGE_ADDRESS, L2_BASE_TOKEN_ADDRESS, NONCE_HOLDER_ADDRESS, }; use revm::{ db::CacheDB, - primitives::{EnvWithHandlerCfg, HashMap, HashSet, ResultAndState}, + primitives::{Bytecode, EnvWithHandlerCfg, HashMap, HashSet, ResultAndState}, DatabaseRef, JournaledState, }; use serde::{Deserialize, Serialize}; use tracing::trace; +#[derive(Debug, Default, Clone)] +pub struct ZksyncStrategy; + +impl GlobalStrategy for ZksyncStrategy { + type Backend = ZkBackendStrategy; + type Executor = ZkExecutor; + type CheatcodeInspector = ZkCheatcodeInspector; +} + +#[derive(Debug, Default, Clone)] +pub struct ZkExecutor; +impl ExecutorStrategy for ZkExecutor { + fn new() -> Arc> { + Arc::new(Mutex::new(Self::default())) + } +} + +#[derive(Debug, Default, Clone)] +pub struct ZkCheatcodeInspector { + /// When in zkEVM context, execute the next CALL or CREATE in the EVM instead. + pub skip_zk_vm: bool, + + /// Any contracts that were deployed in `skip_zk_vm` step. + /// This makes it easier to dispatch calls to any of these addresses in zkEVM context, directly + /// to EVM. Alternatively, we'd need to add `vm.zkVmSkip()` to these calls manually. + pub skip_zk_vm_addresses: HashSet
, + + /// Records the next create address for `skip_zk_vm_addresses`. + pub record_next_create_address: bool, + + /// Paymaster params + pub paymaster_params: Option, + + /// Dual compiled contracts + pub dual_compiled_contracts: DualCompiledContracts, + + /// The migration status of the database to zkEVM storage, `None` if we start in EVM context. + pub zk_startup_migration: ZkStartupMigration, + + /// Factory deps stored through `zkUseFactoryDep`. These factory deps are used in the next + /// CREATE or CALL, and cleared after. + pub zk_use_factory_deps: Vec, + + /// The list of factory_deps seen so far during a test or script execution. + /// Ideally these would be persisted in the storage, but since modifying [revm::JournaledState] + /// would be a significant refactor, we maintain the factory_dep part in the [Cheatcodes]. + /// This can be done as each test runs with its own [Cheatcodes] instance, thereby + /// providing the necessary level of isolation. + pub persisted_factory_deps: HashMap>, + + /// Nonce update persistence behavior in zkEVM for the tx caller. + pub zk_persist_nonce_update: ZkPersistNonceUpdate, +} + +/// Allows overriding nonce update behavior for the tx caller in the zkEVM. +/// +/// Since each CREATE or CALL is executed as a separate transaction within zkEVM, we currently skip +/// persisting nonce updates as it erroneously increments the tx nonce. However, under certain +/// situations, e.g. deploying contracts, transacts, etc. the nonce updates must be persisted. +#[derive(Default, Debug, Clone)] +pub enum ZkPersistNonceUpdate { + /// Never update the nonce. This is currently the default behavior. + #[default] + Never, + /// Override the default behavior, and persist nonce update for tx caller for the next + /// zkEVM execution _only_. + PersistNext, +} + +impl ZkPersistNonceUpdate { + /// Persist nonce update for the tx caller for next execution. + pub fn persist_next(&mut self) { + *self = Self::PersistNext; + } + + /// Retrieve if a nonce update must be persisted, or not. Resets the state to default. + pub fn check(&mut self) -> bool { + let persist_nonce_update = match self { + Self::Never => false, + Self::PersistNext => true, + }; + *self = Default::default(); + + persist_nonce_update + } +} + +impl CheatcodeInspectorStrategy for ZkCheatcodeInspector { + fn initialize(&mut self, mut dual_compiled_contracts: DualCompiledContracts) { + // We add the empty bytecode manually so it is correctly translated in zk mode. + // This is used in many places in foundry, e.g. in cheatcode contract's account code. + let empty_bytes = Bytes::from_static(&[0]); + let zk_bytecode_hash = foundry_zksync_core::hash_bytecode(&foundry_zksync_core::EMPTY_CODE); + let zk_deployed_bytecode = foundry_zksync_core::EMPTY_CODE.to_vec(); + + dual_compiled_contracts.push(DualCompiledContract { + name: String::from("EmptyEVMBytecode"), + zk_bytecode_hash, + zk_deployed_bytecode: zk_deployed_bytecode.clone(), + zk_factory_deps: Default::default(), + evm_bytecode_hash: B256::from_slice(&keccak256(&empty_bytes)[..]), + evm_deployed_bytecode: Bytecode::new_raw(empty_bytes.clone()).bytecode().to_vec(), + evm_bytecode: Bytecode::new_raw(empty_bytes).bytecode().to_vec(), + }); + + let cheatcodes_bytecode = { + let mut bytecode = CHEATCODE_ADDRESS.abi_encode_packed(); + bytecode.append(&mut [0; 12].to_vec()); + Bytes::from(bytecode) + }; + dual_compiled_contracts.push(DualCompiledContract { + name: String::from("CheatcodeBytecode"), + // we put a different bytecode hash here so when importing back to EVM + // we avoid collision with EmptyEVMBytecode for the cheatcodes + zk_bytecode_hash: foundry_zksync_core::hash_bytecode(CHEATCODE_CONTRACT_HASH.as_ref()), + zk_deployed_bytecode: cheatcodes_bytecode.to_vec(), + zk_factory_deps: Default::default(), + evm_bytecode_hash: CHEATCODE_CONTRACT_HASH, + evm_deployed_bytecode: cheatcodes_bytecode.to_vec(), + evm_bytecode: cheatcodes_bytecode.to_vec(), + }); + + let mut persisted_factory_deps = HashMap::new(); + persisted_factory_deps.insert(zk_bytecode_hash, zk_deployed_bytecode); + + self.zk_startup_migration = ZkStartupMigration::Defer; + } +} + +/// Setting for migrating the database to zkEVM storage when starting in ZKsync mode. +/// The migration is performed on the DB via the inspector so must only be performed once. +#[derive(Debug, Default, Clone)] +pub enum ZkStartupMigration { + /// Defer database migration to a later execution point. + /// + /// This is required as we need to wait for some baseline deployments + /// to occur before the test/script execution is performed. + #[default] + Defer, + /// Allow database migration. + Allow, + /// Database migration has already been performed. + Done, +} + +impl ZkStartupMigration { + /// Check if startup migration is allowed. Migration is disallowed if it's to be deferred or has + /// already been performed. + pub fn is_allowed(&self) -> bool { + matches!(self, Self::Allow) + } + + /// Allow migrating the the DB to zkEVM storage. + pub fn allow(&mut self) { + *self = Self::Allow + } + + /// Mark the migration as completed. It must not be performed again. + pub fn done(&mut self) { + *self = Self::Done + } +} + #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct ZkBackendStrategy { + evm: EvmBackendStrategy, persisted_factory_deps: HashMap>, persistent_immutable_keys: HashMap>, } #[derive(Debug, Default, Clone, Serialize, Deserialize)] -struct ZkBackendInspectData { +pub struct ZkBackendInspectData { #[serde(skip_serializing_if = "Option::is_none")] pub factory_deps: Option>>, #[serde(skip_serializing_if = "Option::is_none")] pub paymaster_data: Option, + + pub use_evm: bool, } impl BackendStrategy for ZkBackendStrategy { @@ -66,22 +236,31 @@ impl BackendStrategy for ZkBackendStrategy { ) } - fn inspect( + fn inspect<'i, 'db, I: InspectorExt>( &mut self, - backend: &mut Backend, + db: &'db mut dyn DatabaseExt, env: &mut EnvWithHandlerCfg, - _inspector: &mut I, + inspector: &'i mut I, extra: Option>, ) -> eyre::Result { - backend.initialize(env); + let zk_extra = extra + .as_ref() + .map(|bytes| { + serde_json::from_slice::<'_, ZkBackendInspectData>(&bytes).unwrap_or_default() + }) + .unwrap_or_default(); + + if zk_extra.use_evm { + return self.evm.inspect(db, env, inspector, extra); + } - let zk_extra: ZkBackendInspectData = serde_json::from_slice(&extra.unwrap()).unwrap(); + db.initialize(env); foundry_zksync_core::vm::transact( Some(&mut self.persisted_factory_deps), zk_extra.factory_deps, zk_extra.paymaster_data, env, - backend, + db, ) } } diff --git a/crates/zksync/core/src/vm/runner.rs b/crates/zksync/core/src/vm/runner.rs index 01af26d4d..3e532e45c 100644 --- a/crates/zksync/core/src/vm/runner.rs +++ b/crates/zksync/core/src/vm/runner.rs @@ -33,7 +33,7 @@ pub fn transact<'a, DB>( db: &'a mut DB, ) -> eyre::Result where - DB: Database, + DB: Database + ?Sized, ::Error: Debug, { info!(calldata = ?env.tx.data, fdeps = factory_deps.as_ref().map(|deps| deps.iter().map(|dep| dep.len()).join(",")).unwrap_or_default(), "zk transact");