diff --git a/Cargo.lock b/Cargo.lock index 872764abb..12112ef55 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2561,6 +2561,7 @@ dependencies = [ "foundry-compilers", "foundry-config", "foundry-evm", + "foundry-strategy-zksync", "regex", "reqwest 0.12.9", "revm", @@ -4665,6 +4666,7 @@ dependencies = [ "foundry-debugger", "foundry-evm", "foundry-linking", + "foundry-strategy-zksync", "foundry-wallets", "foundry-zksync-compiler", "foundry-zksync-core", @@ -4806,7 +4808,6 @@ dependencies = [ "foundry-evm-core", "foundry-evm-traces", "foundry-wallets", - "foundry-zksync-compiler", "foundry-zksync-core", "foundry-zksync-inspectors", "itertools 0.13.0", @@ -4826,7 +4827,6 @@ dependencies = [ "tracing", "vergen", "walkdir", - "zksync_types", ] [[package]] @@ -4871,6 +4871,7 @@ dependencies = [ "foundry-config", "foundry-debugger", "foundry-evm", + "foundry-strategy-zksync", "foundry-wallets", "futures 0.3.31", "indicatif", @@ -5150,6 +5151,7 @@ dependencies = [ "alloy-dyn-abi", "alloy-json-abi", "alloy-primitives", + "alloy-serde", "alloy-sol-types", "eyre", "foundry-cheatcodes", @@ -5160,6 +5162,7 @@ dependencies = [ "foundry-evm-coverage", "foundry-evm-fuzz", "foundry-evm-traces", + "foundry-zksync-compiler", "foundry-zksync-core", "foundry-zksync-inspectors", "indicatif", @@ -5335,6 +5338,33 @@ dependencies = [ "syn 2.0.89", ] +[[package]] +name = "foundry-strategy-zksync" +version = "0.0.2" +dependencies = [ + "alloy-json-abi", + "alloy-primitives", + "alloy-rpc-types", + "alloy-sol-types", + "alloy-zksync", + "eyre", + "foundry-cheatcodes", + "foundry-common", + "foundry-config", + "foundry-evm", + "foundry-evm-core", + "foundry-zksync-compiler", + "foundry-zksync-core", + "itertools 0.13.0", + "revm", + "semver 1.0.23", + "serde", + "serde_json", + "tokio", + "tracing", + "zksync_types", +] + [[package]] name = "foundry-test-utils" version = "0.0.2" diff --git a/Cargo.toml b/Cargo.toml index 23cb3b0e7..ae9d8abfb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ members = [ "crates/script-sequence/", "crates/macros/", "crates/test-utils/", + "crates/strategy/zksync/", ] resolver = "2" @@ -173,6 +174,7 @@ 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-zksync = { path = "crates/strategy/zksync" } # solc & compilation utilities # foundry-block-explorers = { version = "0.9.0", default-features = false } diff --git a/crates/cast/bin/cmd/call.rs b/crates/cast/bin/cmd/call.rs index aefc5f1c0..d2ad6e270 100644 --- a/crates/cast/bin/cmd/call.rs +++ b/crates/cast/bin/cmd/call.rs @@ -112,6 +112,7 @@ impl CallArgs { let figment = Into::::into(&self.eth).merge(&self); let evm_opts = figment.extract::()?; let mut config = Config::try_from(figment)?.sanitized(); + let strategy = utils::get_executor_strategy(&config); let Self { to, @@ -177,8 +178,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); + let mut executor = TracingExecutor::new( + env, + fork, + evm_version, + debug, + decode_internal, + alphanet, + strategy, + ); 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 79083fa8d..ebb201282 100644 --- a/crates/cast/bin/cmd/run.rs +++ b/crates/cast/bin/cmd/run.rs @@ -8,7 +8,7 @@ use clap::Parser; use eyre::{Result, WrapErr}; use foundry_cli::{ opts::{EtherscanOpts, RpcOpts}, - utils::{handle_traces, init_progress, TraceResult}, + utils::{self, handle_traces, init_progress, TraceResult}, }; use foundry_common::{is_known_system_sender, shell, SYSTEM_TRANSACTION_TYPE}; use foundry_compilers::artifacts::EvmVersion; @@ -99,6 +99,7 @@ impl RunArgs { let figment = Into::::into(&self.rpc).merge(&self); let evm_opts = figment.extract::()?; let mut config = Config::try_from(figment)?.sanitized(); + let strategy = utils::get_executor_strategy(&config); let compute_units_per_second = if self.no_rate_limit { Some(u64::MAX) } else { self.compute_units_per_second }; @@ -166,6 +167,7 @@ impl RunArgs { self.debug, self.decode_internal, alphanet, + strategy, ); let mut env = EnvWithHandlerCfg::new_with_spec_id(Box::new(env.clone()), executor.spec_id()); diff --git a/crates/cheatcodes/Cargo.toml b/crates/cheatcodes/Cargo.toml index e817e1598..8c58f5462 100644 --- a/crates/cheatcodes/Cargo.toml +++ b/crates/cheatcodes/Cargo.toml @@ -32,11 +32,8 @@ foundry-evm-traces.workspace = true foundry-wallets.workspace = true forge-script-sequence.workspace = true foundry-zksync-core.workspace = true -foundry-zksync-compiler.workspace = true foundry-zksync-inspectors.workspace = true -zksync_types.workspace = true - alloy-dyn-abi.workspace = true alloy-json-abi.workspace = true alloy-primitives.workspace = true diff --git a/crates/cheatcodes/src/config.rs b/crates/cheatcodes/src/config.rs index e1185014c..00a393dde 100644 --- a/crates/cheatcodes/src/config.rs +++ b/crates/cheatcodes/src/config.rs @@ -1,5 +1,8 @@ use super::Result; -use crate::Vm::Rpc; +use crate::{ + strategy::{CheatcodeInspectorStrategy, EvmCheatcodeInspectorStrategy}, + Vm::Rpc, +}; use alloy_primitives::{map::AddressHashMap, U256}; use foundry_common::{fs::normalize_path, ContractsByArtifact}; use foundry_compilers::{utils::canonicalize, ProjectPathsConfig}; @@ -8,8 +11,6 @@ use foundry_config::{ ResolvedRpcEndpoints, }; use foundry_evm_core::opts::EvmOpts; -use foundry_zksync_compiler::DualCompiledContracts; -use foundry_zksync_core::vm::ZkEnv; use semver::Version; use std::{ path::{Path, PathBuf}, @@ -55,16 +56,12 @@ pub struct CheatsConfig { pub running_contract: Option, /// Version of the script/test contract which is currently running. pub running_version: Option, - /// ZKSolc -> Solc Contract codes - pub dual_compiled_contracts: DualCompiledContracts, - /// Use ZK-VM on startup - pub use_zk: bool, + /// The behavior strategy. + pub strategy: Box, /// Whether to enable legacy (non-reverting) assertions. pub assertions_revert: bool, /// Optional seed for the RNG algorithm. pub seed: Option, - /// Era Vm environment - pub zk_env: Option, } impl CheatsConfig { @@ -76,9 +73,7 @@ impl CheatsConfig { available_artifacts: Option, running_contract: Option, running_version: Option, - dual_compiled_contracts: DualCompiledContracts, - use_zk: bool, - zk_env: Option, + strategy: Box, ) -> Self { let mut allowed_paths = vec![config.root.0.clone()]; allowed_paths.extend(config.libs.clone()); @@ -108,11 +103,9 @@ impl CheatsConfig { available_artifacts, running_contract, running_version, - dual_compiled_contracts, - use_zk, + strategy, assertions_revert: config.assertions_revert, seed: config.fuzz.seed, - zk_env, } } @@ -241,11 +234,9 @@ impl Default for CheatsConfig { available_artifacts: Default::default(), running_contract: Default::default(), running_version: Default::default(), - dual_compiled_contracts: Default::default(), - use_zk: false, + strategy: Box::new(EvmCheatcodeInspectorStrategy::default()), assertions_revert: true, seed: None, - zk_env: Default::default(), } } } @@ -262,9 +253,7 @@ mod tests { None, None, None, - Default::default(), - false, - None, + Box::new(EvmCheatcodeInspectorStrategy::default()), ) } diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index bda24a598..0b3b574ca 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -7,7 +7,7 @@ use crate::{ }; use alloy_consensus::TxEnvelope; use alloy_genesis::{Genesis, GenesisAccount}; -use alloy_primitives::{Address, Bytes, B256, U256}; +use alloy_primitives::{Address, B256, U256}; use alloy_rlp::Decodable; use alloy_sol_types::SolValue; use foundry_common::fs::{read_json_file, write_json_file}; @@ -18,7 +18,7 @@ use foundry_evm_core::{ }; use foundry_evm_traces::StackSnapshotType; use rand::Rng; -use revm::primitives::{Account, Bytecode, SpecId, KECCAK_EMPTY}; +use revm::primitives::{Account, SpecId}; use std::{collections::BTreeMap, path::Path}; mod record_debug_step; use record_debug_step::{convert_call_trace_to_debug_step, flatten_call_trace}; @@ -64,12 +64,7 @@ impl Cheatcode for getNonce_0Call { fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { account } = self; - if ccx.state.use_zk_vm { - let nonce = foundry_zksync_core::cheatcodes::get_nonce(*account, ccx.ecx); - return Ok(nonce.abi_encode()); - } - - get_nonce(ccx, account) + ccx.with_strategy(|strategy, ccx| strategy.cheatcode_get_nonce(ccx, *account)) } } @@ -355,13 +350,7 @@ impl Cheatcode for getBlobhashesCall { impl Cheatcode for rollCall { fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { newHeight } = self; - if ccx.state.use_zk_vm { - foundry_zksync_core::cheatcodes::roll(*newHeight, ccx.ecx); - return Ok(Default::default()) - } - - ccx.ecx.env.block.number = *newHeight; - Ok(Default::default()) + ccx.with_strategy(|strategy, ccx| strategy.cheatcode_roll(ccx, *newHeight)) } } @@ -383,13 +372,7 @@ impl Cheatcode for txGasPriceCall { impl Cheatcode for warpCall { fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { newTimestamp } = self; - if ccx.state.use_zk_vm { - foundry_zksync_core::cheatcodes::warp(*newTimestamp, ccx.ecx); - return Ok(Default::default()) - } - ccx.ecx.env.block.timestamp = *newTimestamp; - - Ok(Default::default()) + ccx.with_strategy(|strategy, ccx| strategy.cheatcode_warp(ccx, *newTimestamp)) } } @@ -423,52 +406,23 @@ impl Cheatcode for getBlobBaseFeeCall { impl Cheatcode for dealCall { fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { account: address, newBalance: new_balance } = *self; - let old_balance = if ccx.state.use_zk_vm { - foundry_zksync_core::cheatcodes::deal(address, new_balance, ccx.ecx) - } else { - let account = journaled_account(ccx.ecx, address)?; - std::mem::replace(&mut account.info.balance, new_balance) - }; - let record = DealRecord { address, old_balance, new_balance }; - ccx.state.eth_deals.push(record); - Ok(Default::default()) + + ccx.with_strategy(|strategy, ccx| strategy.cheatcode_deal(ccx, address, new_balance)) } } impl Cheatcode for etchCall { fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { target, newRuntimeBytecode } = self; - if ccx.state.use_zk_vm { - foundry_zksync_core::cheatcodes::etch(*target, newRuntimeBytecode, ccx.ecx); - - return Ok(Default::default()); - } - ensure_not_precompile!(target, ccx); - ccx.ecx.load_account(*target)?; - let bytecode = Bytecode::new_raw(Bytes::copy_from_slice(newRuntimeBytecode)); - ccx.ecx.journaled_state.set_code(*target, bytecode); - Ok(Default::default()) + ccx.with_strategy(|strategy, ccx| strategy.cheatcode_etch(ccx, *target, newRuntimeBytecode)) } } impl Cheatcode for resetNonceCall { fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { account } = self; - if ccx.state.use_zk_vm { - foundry_zksync_core::cheatcodes::set_nonce(*account, U256::ZERO, ccx.ecx); - return Ok(Default::default()); - } - - let account = journaled_account(ccx.ecx, *account)?; - // Per EIP-161, EOA nonces start at 0, but contract nonces - // start at 1. Comparing by code_hash instead of code - // to avoid hitting the case where account's code is None. - let empty = account.info.code_hash == KECCAK_EMPTY; - let nonce = if empty { 0 } else { 1 }; - account.info.nonce = nonce; - debug!(target: "cheatcodes", nonce, "reset"); - Ok(Default::default()) + ccx.with_strategy(|strategy, ccx| strategy.cheatcode_reset_nonce(ccx, *account)) } } @@ -476,21 +430,7 @@ impl Cheatcode for setNonceCall { fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { account, newNonce } = *self; - if ccx.state.use_zk_vm { - foundry_zksync_core::cheatcodes::set_nonce(account, U256::from(newNonce), ccx.ecx); - return Ok(Default::default()); - } - - let account = journaled_account(ccx.ecx, account)?; - // nonce must increment only - let current = account.info.nonce; - ensure!( - newNonce >= current, - "new nonce ({newNonce}) must be strictly equal to or higher than the \ - account's current nonce ({current})" - ); - account.info.nonce = newNonce; - Ok(Default::default()) + ccx.with_strategy(|strategy, ccx| strategy.cheatcode_set_nonce(ccx, account, newNonce)) } } @@ -498,14 +438,9 @@ impl Cheatcode for setNonceUnsafeCall { fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { account, newNonce } = *self; - if ccx.state.use_zk_vm { - foundry_zksync_core::cheatcodes::set_nonce(account, U256::from(newNonce), ccx.ecx); - return Ok(Default::default()); - } - - let account = journaled_account(ccx.ecx, account)?; - account.info.nonce = newNonce; - Ok(Default::default()) + ccx.with_strategy(|strategy, ccx| { + strategy.cheatcode_set_nonce_unsafe(ccx, account, newNonce) + }) } } @@ -718,7 +653,6 @@ impl Cheatcode for broadcastRawTransactionCall { ccx.state.broadcastable_transactions.push_back(BroadcastableTransaction { rpc: ccx.db.active_fork_url(), transaction: tx.try_into()?, - zk_tx: None, }); } @@ -1021,10 +955,7 @@ fn read_callers(state: &Cheatcodes, default_sender: &Address) -> Result { } /// Ensures the `Account` is loaded and touched. -pub(super) fn journaled_account<'a>( - ecx: InnerEcx<'a, '_, '_>, - addr: Address, -) -> Result<&'a mut Account> { +pub fn journaled_account<'a>(ecx: InnerEcx<'a, '_, '_>, addr: Address) -> Result<&'a mut Account> { ecx.load_account(addr)?; ecx.journaled_state.touch(&addr); Ok(ecx.journaled_state.state.get_mut(&addr).expect("account is loaded")) diff --git a/crates/cheatcodes/src/evm/fork.rs b/crates/cheatcodes/src/evm/fork.rs index cf80782e5..17bb9bacf 100644 --- a/crates/cheatcodes/src/evm/fork.rs +++ b/crates/cheatcodes/src/evm/fork.rs @@ -125,7 +125,7 @@ impl Cheatcode for selectForkCall { persist_caller(ccx); check_broadcast(ccx.state)?; - ccx.state.select_fork_vm(ccx.ecx, *forkId); + ccx.with_strategy(|strategy, ccx| strategy.zksync_select_fork_vm(ccx.ecx, *forkId)); ccx.ecx.db.select_fork(*forkId, &mut ccx.ecx.env, &mut ccx.ecx.journaled_state)?; Ok(Default::default()) @@ -281,7 +281,7 @@ fn create_select_fork(ccx: &mut CheatsCtxt, url_or_alias: &str, block: Option Result { let Self { callee, data, returnData } = self; - let _ = make_acc_non_empty(callee, ccx.ecx)?; - - if ccx.state.use_zk_vm { - foundry_zksync_core::cheatcodes::set_mocked_account(*callee, ccx.ecx, ccx.caller); - } - - mock_call(ccx.state, callee, data, None, returnData, InstructionResult::Return); - Ok(Default::default()) + ccx.with_strategy(|strategy, ccx| { + strategy.cheatcode_mock_call(ccx, *callee, data, returnData) + }) } } @@ -90,14 +85,9 @@ impl Cheatcode for mockCalls_1Call { impl Cheatcode for mockCallRevert_0Call { fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { callee, data, revertData } = self; - let _ = make_acc_non_empty(callee, ccx.ecx)?; - - if ccx.state.use_zk_vm { - foundry_zksync_core::cheatcodes::set_mocked_account(*callee, ccx.ecx, ccx.caller); - } - - mock_call(ccx.state, callee, data, None, revertData, InstructionResult::Revert); - Ok(Default::default()) + ccx.with_strategy(|strategy, ccx| { + strategy.cheatcode_mock_call_revert(ccx, *callee, data, revertData) + }) } } @@ -154,7 +144,7 @@ impl Cheatcode for mockFunctionCall { } } -fn mock_call( +pub fn mock_call( state: &mut Cheatcodes, callee: &Address, cdata: &Bytes, @@ -184,7 +174,7 @@ fn mock_calls( // Etches a single byte onto the account if it is empty to circumvent the `extcodesize` // check Solidity might perform. -fn make_acc_non_empty(callee: &Address, ecx: InnerEcx) -> Result { +pub fn make_acc_non_empty(callee: &Address, ecx: InnerEcx) -> Result { let acc = ecx.load_account(*callee)?; let empty_bytecode = acc.info.code.as_ref().map_or(true, Bytecode::is_empty); diff --git a/crates/cheatcodes/src/fs.rs b/crates/cheatcodes/src/fs.rs index 6325b71be..c70133a76 100644 --- a/crates/cheatcodes/src/fs.rs +++ b/crates/cheatcodes/src/fs.rs @@ -12,7 +12,6 @@ use dialoguer::{Input, Password}; use forge_script_sequence::{BroadcastReader, TransactionWithMetadata}; use foundry_common::fs; use foundry_config::fs_permissions::FsAccessKind; -use foundry_zksync_compiler::ContractType; use revm::interpreter::CreateInputs; use revm_inspectors::tracing::types::CallKind; use semver::Version; @@ -284,7 +283,7 @@ impl Cheatcode for getArtifactPathByDeployedCodeCall { impl Cheatcode for getCodeCall { fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { artifactPath: path } = self; - Ok(get_artifact_code(state, path, false)?.abi_encode()) + state.with_strategy(|strategy, state| strategy.get_artifact_code(state, path, false)) } } @@ -350,7 +349,7 @@ impl Cheatcode for deployCode_1Call { /// - `path/to/contract.sol:0.8.23` /// - `ContractName` /// - `ContractName:0.8.23` -pub(crate) fn get_artifact_code(state: &Cheatcodes, path: &str, deployed: bool) -> Result { +pub fn get_artifact_code(state: &Cheatcodes, path: &str, deployed: bool) -> Result { let path = if path.ends_with(".json") { PathBuf::from(path) } else { @@ -416,27 +415,18 @@ pub(crate) fn get_artifact_code(state: &Cheatcodes, path: &str, deployed: bool) [] => Err(fmt_err!("no matching artifact found")), [artifact] => Ok(artifact), filtered => { - // If we find more than one artifact, we need to filter by contract type - // depending on whether we are using the zkvm or evm - filtered - .iter() - .find(|(id, _)| { - let contract_type = state - .config - .dual_compiled_contracts - .get_contract_type_by_artifact(id); - match contract_type { - Some(ContractType::ZK) => state.use_zk_vm, - Some(ContractType::EVM) => !state.use_zk_vm, - None => false, - } - }) - .or_else(|| { - // If we know the current script/test contract solc version, try to - // filter by it - state.config.running_version.as_ref().and_then(|version| { - filtered.iter().find(|(id, _)| id.version == *version) - }) + // If we know the current script/test contract solc version, try to filter + // by it + state + .config + .running_version + .as_ref() + .and_then(|version| { + let filtered = filtered + .iter() + .filter(|(id, _)| id.version == *version) + .collect::>(); + (filtered.len() == 1).then(|| filtered[0]) }) .ok_or_else(|| fmt_err!("multiple matching artifacts found")) } diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index daa73a39c..af90c7a71 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -2,13 +2,12 @@ use crate::{ evm::{ - journaled_account, mapping::{self, MappingSlots}, prank::Prank, DealRecord, GasRecord, }, - inspector::utils::CommonCreateInput, script::{Broadcast, Wallets}, + strategy::CheatcodeInspectorStrategy, test::{ assume::AssumeNoRevert, expect::{self, ExpectedEmit, ExpectedRevert, ExpectedRevertKind}, @@ -18,11 +17,10 @@ use crate::{ Vm::{self, AccountAccess}, }; use alloy_primitives::{ - hex, keccak256, + hex, map::{AddressHashMap, HashMap}, Address, Bytes, Log, TxKind, B256, U256, }; -use alloy_rpc_types::request::{TransactionInput, TransactionRequest}; use alloy_sol_types::{SolCall, SolInterface, SolValue}; use foundry_cheatcodes_common::{ expect::{ExpectedCallData, ExpectedCallTracker, ExpectedCallType}, @@ -32,24 +30,14 @@ 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}, - constants::{ - CHEATCODE_ADDRESS, CHEATCODE_CONTRACT_HASH, DEFAULT_CREATE2_DEPLOYER, - DEFAULT_CREATE2_DEPLOYER_CODE, MAGIC_ASSUME, - }, - decode::decode_console_log, + backend::{DatabaseError, DatabaseExt, RevertDiagnostic}, + constants::{CHEATCODE_ADDRESS, MAGIC_ASSUME}, utils::new_evm_with_existing_context, InspectorExt, }; use foundry_evm_traces::TracingInspectorConfig; use foundry_wallets::multi_wallet::MultiWallet; -use foundry_zksync_compiler::{DualCompiledContract, DualCompiledContracts}; -use foundry_zksync_core::{ - convert::{ConvertAddress, ConvertH160, ConvertH256, ConvertRU256, ConvertU256}, - get_account_code_key, get_balance_key, get_nonce_key, - vm::ZkEnv, - Call, ZkPaymasterData, ZkTransactionMetadata, DEFAULT_CREATE2_DEPLOYER_ZKSYNC, -}; +use foundry_zksync_core::Call; use foundry_zksync_inspectors::TraceCollector; use itertools::Itertools; use proptest::test_runner::{RngAlgorithm, TestRng, TestRunner}; @@ -61,34 +49,27 @@ use revm::{ InterpreterResult, }, primitives::{ - AccountInfo, BlockEnv, Bytecode, CreateScheme, EVMError, Env, EvmStorageSlot, - ExecutionResult, HashMap as rHashMap, Output, SignedAuthorization, SpecId, EOF_MAGIC_BYTES, - KECCAK_EMPTY, + BlockEnv, CreateScheme, EVMError, EvmStorageSlot, SignedAuthorization, SpecId, + EOF_MAGIC_BYTES, }, EvmContext, InnerEvmContext, Inspector, }; use serde_json::Value; use std::{ - collections::{BTreeMap, HashSet, VecDeque}, + collections::{BTreeMap, VecDeque}, fs::File, io::BufReader, ops::Range, path::PathBuf, sync::Arc, }; -use zksync_types::{ - block::{pack_block_info, unpack_block_info}, - transaction_request::PaymasterParams, - utils::{decompose_full_nonce, nonces_to_full_nonce}, - ACCOUNT_CODE_STORAGE_ADDRESS, CONTRACT_DEPLOYER_ADDRESS, CURRENT_VIRTUAL_BLOCK_INFO_POSITION, - H256, KNOWN_CODES_STORAGE_ADDRESS, L2_BASE_TOKEN_ADDRESS, NONCE_HOLDER_ADDRESS, - SYSTEM_CONTEXT_ADDRESS, -}; mod utils; +pub use utils::CommonCreateInput; pub type Ecx<'a, 'b, 'c> = &'a mut EvmContext<&'b mut (dyn DatabaseExt + 'c)>; pub type InnerEcx<'a, 'b, 'c> = &'a mut InnerEvmContext<&'b mut (dyn DatabaseExt + 'c)>; +pub type Strategy<'a> = &'a mut dyn CheatcodeInspectorStrategy; /// Helper trait for obtaining complete [revm::Inspector] instance from mutable reference to /// [Cheatcodes]. @@ -279,8 +260,6 @@ pub struct BroadcastableTransaction { pub rpc: Option, /// The transaction to broadcast. pub transaction: TransactionMaybeSigned, - /// ZK-VM factory deps - pub zk_tx: Option, } #[derive(Clone, Debug, Copy)] @@ -412,73 +391,6 @@ impl ArbitraryStorage { /// List of transactions that can be broadcasted. pub type BroadcastableTransactions = VecDeque; -/// 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 - } -} - -/// 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 - } -} - /// An EVM inspector that handles calls to various cheatcodes, each with their own behavior. /// /// Cheatcodes can be called by contracts during execution to modify the VM environment, such as @@ -496,7 +408,7 @@ impl ZkStartupMigration { /// contract deployed on the live network is able to execute cheatcodes by simply calling the /// cheatcode address: by default, the caller, test contract and newly deployed contracts are /// allowed to execute cheatcodes -#[derive(Clone, Debug)] +#[derive(Debug)] pub struct Cheatcodes { /// The block environment /// @@ -615,44 +527,50 @@ pub struct Cheatcodes { /// Unlocked wallets used in scripts and testing of scripts. pub wallets: Option, - /// Use ZK-VM to execute CALLs and CREATEs. - pub use_zk_vm: bool, - - /// 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: Option, - - /// 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, + /// The behavior strategy. + pub strategy: Option>, +} - pub zk_env: ZkEnv, +impl Clone for Cheatcodes { + fn clone(&self) -> Self { + Self { + block: self.block.clone(), + active_delegation: self.active_delegation.clone(), + gas_price: self.gas_price, + labels: self.labels.clone(), + prank: self.prank.clone(), + expected_revert: self.expected_revert.clone(), + assume_no_revert: self.assume_no_revert.clone(), + fork_revert_diagnostic: self.fork_revert_diagnostic.clone(), + accesses: self.accesses.clone(), + recorded_account_diffs_stack: self.recorded_account_diffs_stack.clone(), + record_debug_steps_info: self.record_debug_steps_info, + recorded_logs: self.recorded_logs.clone(), + mocked_calls: self.mocked_calls.clone(), + mocked_functions: self.mocked_functions.clone(), + expected_calls: self.expected_calls.clone(), + expected_emits: self.expected_emits.clone(), + allowed_mem_writes: self.allowed_mem_writes.clone(), + broadcast: self.broadcast.clone(), + broadcastable_transactions: self.broadcastable_transactions.clone(), + config: self.config.clone(), + context: self.context.clone(), + fs_commit: self.fs_commit, + serialized_jsons: self.serialized_jsons.clone(), + eth_deals: self.eth_deals.clone(), + gas_metering: self.gas_metering.clone(), + gas_snapshots: self.gas_snapshots.clone(), + mapping_slots: self.mapping_slots.clone(), + pc: self.pc, + breakpoints: self.breakpoints.clone(), + test_runner: self.test_runner.clone(), + ignored_traces: self.ignored_traces.clone(), + arbitrary_storage: self.arbitrary_storage.clone(), + deprecated: self.deprecated.clone(), + wallets: self.wallets.clone(), + strategy: self.strategy.as_ref().map(|s| s.new_cloned()), + } + } } // This is not derived because calling this in `fn new` with `..Default::default()` creates a second @@ -667,50 +585,10 @@ impl Default for Cheatcodes { impl Cheatcodes { /// Creates a new `Cheatcodes` with the given settings. 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. - // 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); - - let zk_startup_migration = config.use_zk.then_some(ZkStartupMigration::Defer); - let zk_env = config.zk_env.clone().unwrap_or_default(); - Self { fs_commit: true, labels: config.labels.clone(), + strategy: Some(config.strategy.clone()), config, block: Default::default(), active_delegation: Default::default(), @@ -743,18 +621,6 @@ impl Cheatcodes { arbitrary_storage: Default::default(), deprecated: Default::default(), wallets: Default::default(), - dual_compiled_contracts, - zk_startup_migration, - use_zk_vm: Default::default(), - skip_zk_vm: Default::default(), - skip_zk_vm_addresses: Default::default(), - record_next_create_address: Default::default(), - //TODO(zk): use initialized above - persisted_factory_deps: Default::default(), - paymaster_params: None, - zk_use_factory_deps: Default::default(), - zk_persist_nonce_update: Default::default(), - zk_env, } } @@ -843,198 +709,6 @@ impl Cheatcodes { } } } - /// Selects the appropriate VM for the fork. Options: EVM, ZK-VM. - /// CALL and CREATE are handled by the selected VM. - /// - /// Additionally: - /// * Translates block information - /// * Translates all persisted addresses - pub fn select_fork_vm(&mut self, data: InnerEcx, fork_id: LocalForkId) { - let fork_info = data.db.get_fork_info(fork_id).expect("failed getting fork info"); - if fork_info.fork_type.is_evm() { - self.select_evm(data) - } else { - self.select_zk_vm(data, Some(&fork_info.fork_env)) - } - } - - /// Switch to EVM and translate block info, balances, nonces and deployed codes for persistent - /// accounts - pub fn select_evm(&mut self, data: InnerEcx) { - if !self.use_zk_vm { - tracing::info!("already in EVM"); - return - } - - tracing::info!("switching to EVM"); - self.use_zk_vm = false; - - let system_account = SYSTEM_CONTEXT_ADDRESS.to_address(); - journaled_account(data, system_account).expect("failed to load account"); - let balance_account = L2_BASE_TOKEN_ADDRESS.to_address(); - journaled_account(data, balance_account).expect("failed to load account"); - let nonce_account = NONCE_HOLDER_ADDRESS.to_address(); - journaled_account(data, nonce_account).expect("failed to load account"); - let account_code_account = ACCOUNT_CODE_STORAGE_ADDRESS.to_address(); - journaled_account(data, account_code_account).expect("failed to load account"); - - // TODO we might need to store the deployment nonce under the contract storage - // to not lose it across VMs. - - let block_info_key = CURRENT_VIRTUAL_BLOCK_INFO_POSITION.to_ru256(); - let block_info = data.sload(system_account, block_info_key).unwrap_or_default(); - let (block_number, block_timestamp) = unpack_block_info(block_info.to_u256()); - data.env.block.number = U256::from(block_number); - data.env.block.timestamp = U256::from(block_timestamp); - - let test_contract = data.db.get_test_contract_address(); - for address in data.db.persistent_accounts().into_iter().chain([data.env.tx.caller]) { - info!(?address, "importing to evm state"); - - let balance_key = get_balance_key(address); - let nonce_key = get_nonce_key(address); - - let balance = data.sload(balance_account, balance_key).unwrap_or_default().data; - let full_nonce = data.sload(nonce_account, nonce_key).unwrap_or_default(); - let (tx_nonce, _deployment_nonce) = decompose_full_nonce(full_nonce.to_u256()); - let nonce = tx_nonce.as_u64(); - - let account_code_key = get_account_code_key(address); - let (code_hash, code) = data - .sload(account_code_account, account_code_key) - .ok() - .and_then(|zk_bytecode_hash| { - self.dual_compiled_contracts - .find_by_zk_bytecode_hash(zk_bytecode_hash.to_h256()) - .map(|contract| { - ( - contract.evm_bytecode_hash, - Some(Bytecode::new_raw(Bytes::from( - contract.evm_deployed_bytecode.clone(), - ))), - ) - }) - }) - .unwrap_or_else(|| (KECCAK_EMPTY, None)); - - let account = journaled_account(data, address).expect("failed to load account"); - let _ = std::mem::replace(&mut account.info.balance, balance); - let _ = std::mem::replace(&mut account.info.nonce, nonce); - - if test_contract.map(|addr| addr == address).unwrap_or_default() { - tracing::trace!(?address, "ignoring code translation for test contract"); - } else { - account.info.code_hash = code_hash; - account.info.code.clone_from(&code); - } - } - } - - /// Switch to ZK-VM and translate block info, balances, nonces and deployed codes for persistent - /// accounts - pub fn select_zk_vm(&mut self, data: InnerEcx, new_env: Option<&Env>) { - if self.use_zk_vm { - tracing::info!("already in ZK-VM"); - return - } - - tracing::info!("switching to ZK-VM"); - self.use_zk_vm = true; - - let env = new_env.unwrap_or(data.env.as_ref()); - - let mut system_storage: rHashMap = Default::default(); - let block_info_key = CURRENT_VIRTUAL_BLOCK_INFO_POSITION.to_ru256(); - let block_info = - pack_block_info(env.block.number.as_limbs()[0], env.block.timestamp.as_limbs()[0]); - system_storage.insert(block_info_key, EvmStorageSlot::new(block_info.to_ru256())); - - let mut l2_eth_storage: rHashMap = Default::default(); - let mut nonce_storage: rHashMap = Default::default(); - let mut account_code_storage: rHashMap = Default::default(); - let mut known_codes_storage: rHashMap = Default::default(); - let mut deployed_codes: HashMap = Default::default(); - - let test_contract = data.db.get_test_contract_address(); - for address in data.db.persistent_accounts().into_iter().chain([data.env.tx.caller]) { - info!(?address, "importing to zk state"); - - let account = journaled_account(data, address).expect("failed to load account"); - let info = &account.info; - - let balance_key = get_balance_key(address); - l2_eth_storage.insert(balance_key, EvmStorageSlot::new(info.balance)); - - // TODO we need to find a proper way to handle deploy nonces instead of replicating - let full_nonce = nonces_to_full_nonce(info.nonce.into(), info.nonce.into()); - - let nonce_key = get_nonce_key(address); - nonce_storage.insert(nonce_key, EvmStorageSlot::new(full_nonce.to_ru256())); - - if test_contract.map(|test_address| address == test_address).unwrap_or_default() { - // avoid migrating test contract code - tracing::trace!(?address, "ignoring code translation for test contract"); - continue; - } - - if let Some(contract) = self.dual_compiled_contracts.iter().find(|contract| { - info.code_hash != KECCAK_EMPTY && info.code_hash == contract.evm_bytecode_hash - }) { - account_code_storage.insert( - get_account_code_key(address), - EvmStorageSlot::new(contract.zk_bytecode_hash.to_ru256()), - ); - known_codes_storage - .insert(contract.zk_bytecode_hash.to_ru256(), EvmStorageSlot::new(U256::ZERO)); - - let code_hash = B256::from_slice(contract.zk_bytecode_hash.as_bytes()); - deployed_codes.insert( - address, - AccountInfo { - balance: info.balance, - nonce: info.nonce, - code_hash, - code: Some(Bytecode::new_raw(Bytes::from( - contract.zk_deployed_bytecode.clone(), - ))), - }, - ); - } else { - tracing::debug!(code_hash = ?info.code_hash, ?address, "no zk contract found") - } - } - - let system_addr = SYSTEM_CONTEXT_ADDRESS.to_address(); - let system_account = journaled_account(data, system_addr).expect("failed to load account"); - system_account.storage.extend(system_storage.clone()); - - let balance_addr = L2_BASE_TOKEN_ADDRESS.to_address(); - let balance_account = - journaled_account(data, balance_addr).expect("failed to load account"); - balance_account.storage.extend(l2_eth_storage.clone()); - - let nonce_addr = NONCE_HOLDER_ADDRESS.to_address(); - let nonce_account = journaled_account(data, nonce_addr).expect("failed to load account"); - nonce_account.storage.extend(nonce_storage.clone()); - - let account_code_addr = ACCOUNT_CODE_STORAGE_ADDRESS.to_address(); - let account_code_account = - journaled_account(data, account_code_addr).expect("failed to load account"); - account_code_account.storage.extend(account_code_storage.clone()); - - let known_codes_addr = KNOWN_CODES_STORAGE_ADDRESS.to_address(); - let known_codes_account = - journaled_account(data, known_codes_addr).expect("failed to load account"); - known_codes_account.storage.extend(known_codes_storage.clone()); - - for (address, info) in deployed_codes { - let account = journaled_account(data, address).expect("failed to load account"); - let _ = std::mem::replace(&mut account.info.balance, info.balance); - let _ = std::mem::replace(&mut account.info.nonce, info.nonce); - account.info.code_hash = info.code_hash; - account.info.code.clone_from(&info.code); - } - } // common create functionality for both legacy and EOF. fn create_common( @@ -1088,99 +762,14 @@ impl Cheatcodes { if ecx_inner.journaled_state.depth() == broadcast.depth { input.set_caller(broadcast.new_origin); - let is_fixed_gas_limit = check_if_fixed_gas_limit(ecx_inner, input.gas_limit()); - - let mut to = None; - let mut nonce: u64 = - ecx_inner.journaled_state.state()[&broadcast.new_origin].info.nonce; - //drop the mutable borrow of account - let mut call_init_code = input.init_code(); - let mut zk_tx = if self.use_zk_vm { - to = Some(TxKind::Call(CONTRACT_DEPLOYER_ADDRESS.to_address())); - nonce = foundry_zksync_core::nonce(broadcast.new_origin, ecx_inner) as u64; - let init_code = input.init_code(); - let find_contract = self - .dual_compiled_contracts - .find_bytecode(&init_code.0) - .unwrap_or_else(|| panic!("failed finding contract for {init_code:?}")); - - let constructor_args = find_contract.constructor_args(); - let contract = find_contract.contract(); - - let factory_deps = - self.dual_compiled_contracts.fetch_all_factory_deps(contract); - - let create_input = foundry_zksync_core::encode_create_params( - &input.scheme().unwrap_or(CreateScheme::Create), - contract.zk_bytecode_hash, - constructor_args.to_vec(), - ); - call_init_code = Bytes::from(create_input); - Some(factory_deps) - } else { - None - }; - let rpc = ecx_inner.db.active_fork_url(); - let paymaster_params = - self.paymaster_params.clone().map(|paymaster_data| PaymasterParams { - paymaster: paymaster_data.address.to_h160(), - paymaster_input: paymaster_data.input.to_vec(), - }); - if let Some(mut factory_deps) = zk_tx { - let injected_factory_deps = self.zk_use_factory_deps.iter().map(|contract| { - crate::fs::get_artifact_code(self, contract, false) - .inspect(|_| info!(contract, "pushing factory dep")) - .unwrap_or_else(|_| { - panic!("failed to get bytecode for factory deps contract {contract}") - }) - .to_vec() - }).collect_vec(); - factory_deps.extend(injected_factory_deps); - let mut batched = - foundry_zksync_core::vm::batch_factory_dependencies(factory_deps); - debug!(batches = batched.len(), "splitting factory deps for broadcast"); - // the last batch is the final one that does the deployment - zk_tx = batched.pop(); - - for factory_deps in batched { - self.broadcastable_transactions.push_back(BroadcastableTransaction { - rpc: rpc.clone(), - transaction: TransactionRequest { - from: Some(broadcast.new_origin), - to: Some(TxKind::Call(Address::ZERO)), - value: Some(input.value()), - nonce: Some(nonce), - ..Default::default() - } - .into(), - zk_tx: Some(ZkTransactionMetadata { - factory_deps, - paymaster_data: paymaster_params.clone(), - }), - }); - - //update nonce for each tx - nonce += 1; - } - } - - self.broadcastable_transactions.push_back(BroadcastableTransaction { - rpc, - transaction: TransactionRequest { - from: Some(broadcast.new_origin), - to, - value: Some(input.value()), - input: TransactionInput::new(call_init_code), - nonce: Some(nonce), - gas: if is_fixed_gas_limit { Some(input.gas_limit()) } else { None }, - ..Default::default() - } - .into(), - zk_tx: zk_tx.map(|factory_deps| { - ZkTransactionMetadata::new(factory_deps, paymaster_params) - }), - }); + self.strategy.as_mut().unwrap().record_broadcastable_create_transactions( + self.config.clone(), + &input, + ecx_inner, + broadcast, + &mut self.broadcastable_transactions, + ); input.log_debug(self, &input.scheme().unwrap_or(CreateScheme::Create)); } @@ -1212,222 +801,15 @@ impl Cheatcodes { }]); } - if self.use_zk_vm { - if let Some(result) = self.try_create_in_zk(ecx, input, executor) { - return Some(result); - } + if let Some(result) = self.with_strategy(|strategy, cheatcodes| { + strategy.zksync_try_create(cheatcodes, ecx, &input, executor) + }) { + return Some(result); } None } - /// Try handling the `CREATE` within zkEVM. - /// If `Some` is returned then the result must be returned immediately, else the call must be - /// handled in EVM. - fn try_create_in_zk( - &mut self, - ecx: Ecx, - input: Input, - executor: &mut impl CheatcodesExecutor, - ) -> Option - where - Input: CommonCreateInput, - { - if self.skip_zk_vm { - self.skip_zk_vm = false; // handled the skip, reset flag - self.record_next_create_address = true; - info!("running create in EVM, instead of zkEVM (skipped)"); - return None - } - - if let Some(CreateScheme::Create) = input.scheme() { - let caller = input.caller(); - let nonce = ecx - .inner - .journaled_state - .load_account(input.caller(), &mut ecx.inner.db) - .expect("to load caller account") - .info - .nonce; - let address = caller.create(nonce); - if ecx.db.get_test_contract_address().map(|addr| address == addr).unwrap_or_default() { - info!("running create in EVM, instead of zkEVM (Test Contract) {:#?}", address); - return None - } - } - - let init_code = input.init_code(); - if init_code.0 == DEFAULT_CREATE2_DEPLOYER_CODE { - info!("running create in EVM, instead of zkEVM (DEFAULT_CREATE2_DEPLOYER_CODE)"); - return None - } - - info!("running create in zkEVM"); - - let find_contract = self - .dual_compiled_contracts - .find_bytecode(&init_code.0) - .unwrap_or_else(|| panic!("failed finding contract for {init_code:?}")); - - let constructor_args = find_contract.constructor_args(); - let contract = find_contract.contract(); - - let zk_create_input = foundry_zksync_core::encode_create_params( - &input.scheme().unwrap_or(CreateScheme::Create), - contract.zk_bytecode_hash, - constructor_args.to_vec(), - ); - - let mut factory_deps = self.dual_compiled_contracts.fetch_all_factory_deps(contract); - let injected_factory_deps = self - .zk_use_factory_deps - .iter() - .flat_map(|contract| { - let artifact_code = crate::fs::get_artifact_code(self, contract, false) - .inspect(|_| info!(contract, "pushing factory dep")) - .unwrap_or_else(|_| { - panic!( - "failed to get bytecode for injected factory deps contract {contract}" - ) - }) - .to_vec(); - let res = self.dual_compiled_contracts.find_bytecode(&artifact_code).unwrap(); - self.dual_compiled_contracts.fetch_all_factory_deps(res.contract()) - }) - .collect_vec(); - factory_deps.extend(injected_factory_deps); - - // NOTE(zk): Clear injected factory deps so that they are not sent on further transactions - self.zk_use_factory_deps.clear(); - tracing::debug!(contract = contract.name, "using dual compiled contract"); - - let zk_persist_nonce_update = self.zk_persist_nonce_update.check(); - let ccx = foundry_zksync_core::vm::CheatcodeTracerContext { - mocked_calls: self.mocked_calls.clone(), - expected_calls: Some(&mut self.expected_calls), - accesses: self.accesses.as_mut(), - persisted_factory_deps: Some(&mut self.persisted_factory_deps), - paymaster_data: self.paymaster_params.take(), - persist_nonce_update: self.broadcast.is_some() || zk_persist_nonce_update, - zk_env: self.zk_env.clone(), - }; - - let zk_create = foundry_zksync_core::vm::ZkCreateInputs { - value: input.value().to_u256(), - msg_sender: input.caller(), - create_input: zk_create_input, - factory_deps, - }; - - let mut gas = Gas::new(input.gas_limit()); - match foundry_zksync_core::vm::create::<_, DatabaseError>(zk_create, ecx, ccx) { - Ok(result) => { - if let Some(recorded_logs) = &mut self.recorded_logs { - recorded_logs.extend(result.logs.clone().into_iter().map(|log| Vm::Log { - topics: log.data.topics().to_vec(), - data: log.data.data.clone(), - emitter: log.address, - })); - } - - // append console logs from zkEVM to the current executor's LogTracer - result.logs.iter().filter_map(decode_console_log).for_each(|decoded_log| { - executor.console_log( - &mut CheatsCtxt { - state: self, - ecx: &mut ecx.inner, - precompiles: &mut ecx.precompiles, - gas_limit: input.gas_limit(), - caller: input.caller(), - }, - decoded_log, - ); - }); - - // append traces - executor.trace_zksync(self, ecx, result.call_traces); - - // for each log in cloned logs call handle_expect_emit - if !self.expected_emits.is_empty() { - for log in result.logs { - expect::handle_expect_emit(self, &log, &mut Default::default()); - } - } - - // record immutable variables - if result.execution_result.is_success() { - for (addr, imm_values) in result.recorded_immutables { - let addr = addr.to_address(); - let keys = imm_values - .into_keys() - .map(|slot_index| { - foundry_zksync_core::get_immutable_slot_key(addr, slot_index) - .to_ru256() - }) - .collect::>(); - ecx.db.save_zk_immutable_storage(addr, keys); - } - } - - match result.execution_result { - ExecutionResult::Success { output, gas_used, .. } => { - let _ = gas.record_cost(gas_used); - match output { - Output::Create(bytes, address) => Some(CreateOutcome { - result: InterpreterResult { - result: InstructionResult::Return, - output: bytes, - gas, - }, - address, - }), - _ => Some(CreateOutcome { - result: InterpreterResult { - result: InstructionResult::Revert, - output: Bytes::new(), - gas, - }, - address: None, - }), - } - } - ExecutionResult::Revert { output, gas_used, .. } => { - let _ = gas.record_cost(gas_used); - Some(CreateOutcome { - result: InterpreterResult { - result: InstructionResult::Revert, - output, - gas, - }, - address: None, - }) - } - ExecutionResult::Halt { .. } => Some(CreateOutcome { - result: InterpreterResult { - result: InstructionResult::Revert, - output: Bytes::from_iter(String::from("zk vm halted").as_bytes()), - gas, - }, - address: None, - }), - } - } - Err(err) => { - error!("error inspecting zkEVM: {err:?}"); - Some(CreateOutcome { - result: InterpreterResult { - result: InstructionResult::Revert, - output: Bytes::from_iter( - format!("error inspecting zkEVM: {err:?}").as_bytes(), - ), - gas, - }, - address: None, - }) - } - } - } - // common create_end functionality for both legacy and EOF. fn create_end_common(&mut self, ecx: Ecx, mut outcome: CreateOutcome) -> CreateOutcome where { @@ -1535,12 +917,10 @@ where { } } - if self.record_next_create_address { - self.record_next_create_address = false; - if let Some(address) = outcome.address { - self.skip_zk_vm_addresses.insert(address); - } - } + self.strategy + .as_mut() + .expect("failed acquiring strategy") + .zksync_record_create_address(&outcome); outcome } @@ -1583,10 +963,10 @@ where { let prev = account.info.nonce; let nonce = prev.saturating_sub(1); account.info.nonce = nonce; - if self.use_zk_vm { - // NOTE(zk): We sync with the nonce changes to ensure that the nonce matches - foundry_zksync_core::cheatcodes::set_nonce(sender, U256::from(nonce), ecx); - } + self.strategy + .as_mut() + .expect("failed acquiring strategy") + .zksync_sync_nonce(sender, nonce, ecx); trace!(target: "cheatcodes", %sender, nonce, prev, "corrected nonce"); } @@ -1620,31 +1000,10 @@ where { return None; } - let mut factory_deps = Vec::new(); - - if call.target_address == DEFAULT_CREATE2_DEPLOYER && self.use_zk_vm { - call.target_address = DEFAULT_CREATE2_DEPLOYER_ZKSYNC; - call.bytecode_address = DEFAULT_CREATE2_DEPLOYER_ZKSYNC; - - let (salt, init_code) = call.input.split_at(32); - let find_contract = self - .dual_compiled_contracts - .find_bytecode(init_code) - .unwrap_or_else(|| panic!("failed finding contract for {init_code:?}")); - - let constructor_args = find_contract.constructor_args(); - let contract = find_contract.contract(); - - factory_deps = self.dual_compiled_contracts.fetch_all_factory_deps(contract); - - let create_input = foundry_zksync_core::encode_create_params( - &CreateScheme::Create2 { salt: U256::from_be_slice(salt) }, - contract.zk_bytecode_hash, - constructor_args.to_vec(), - ); - - call.input = create_input.into(); - } + self.strategy + .as_mut() + .expect("failed acquiring strategy") + .zksync_set_deployer_call_input(call); // Handle expected calls @@ -1778,79 +1137,21 @@ where { }) } - let is_fixed_gas_limit = check_if_fixed_gas_limit(ecx_inner, call.gas_limit); - - let account = - ecx_inner.journaled_state.state().get_mut(&broadcast.new_origin).unwrap(); - - let nonce = if self.use_zk_vm { - foundry_zksync_core::nonce(broadcast.new_origin, ecx_inner) as u64 - } else { - account.info.nonce - }; + self.strategy + .as_mut() + .expect("failed acquiring strategy") + .record_broadcastable_call_transactions( + self.config.clone(), + call, + ecx_inner, + broadcast, + &mut self.broadcastable_transactions, + &mut self.active_delegation, + ); let account = ecx_inner.journaled_state.state().get_mut(&broadcast.new_origin).unwrap(); - let zk_tx = if self.use_zk_vm { - let injected_factory_deps = self.zk_use_factory_deps.iter().flat_map(|contract| { - let artifact_code = crate::fs::get_artifact_code(self, contract, false) - .inspect(|_| info!(contract, "pushing factory dep")) - .unwrap_or_else(|_| { - panic!("failed to get bytecode for factory deps contract {contract}") - }) - .to_vec(); - let res = self.dual_compiled_contracts.find_bytecode(&artifact_code).unwrap(); - self.dual_compiled_contracts.fetch_all_factory_deps(res.contract()) - }).collect_vec(); - factory_deps.extend(injected_factory_deps.clone()); - - let paymaster_params = - self.paymaster_params.clone().map(|paymaster_data| PaymasterParams { - paymaster: paymaster_data.address.to_h160(), - paymaster_input: paymaster_data.input.to_vec(), - }); - // We shouldn't need factory_deps for CALLs - if call.target_address == DEFAULT_CREATE2_DEPLOYER_ZKSYNC { - Some(ZkTransactionMetadata { - factory_deps: factory_deps.clone(), - paymaster_data: paymaster_params, - }) - } else { - Some(ZkTransactionMetadata { - // For this case we use only the injected factory deps - factory_deps: injected_factory_deps, - paymaster_data: paymaster_params, - }) - } - } else { - None - }; - - let mut tx_req = TransactionRequest { - from: Some(broadcast.new_origin), - to: Some(TxKind::from(Some(call.target_address))), - value: call.transfer_value(), - input: TransactionInput::new(call.input.clone()), - nonce: Some(nonce), - chain_id: Some(ecx_inner.env.cfg.chain_id), - gas: if is_fixed_gas_limit { Some(call.gas_limit) } else { None }, - ..Default::default() - }; - - if let Some(auth_list) = self.active_delegation.take() { - tx_req.authorization_list = Some(vec![auth_list]); - } else { - tx_req.authorization_list = None; - } - - self.broadcastable_transactions.push_back(BroadcastableTransaction { - rpc: ecx_inner.db.active_fork_url(), - transaction: tx_req.into(), - zk_tx, - }); - debug!(target: "cheatcodes", tx=?self.broadcastable_transactions.back().unwrap(), "broadcastable call"); - // Explicitly increment nonce if calls are not isolated. if !self.config.evm_opts.isolate { let prev = account.info.nonce; @@ -1919,161 +1220,15 @@ where { }]); } - if self.use_zk_vm { - if let Some(result) = self.try_call_in_zk(factory_deps, ecx, call, executor) { - return Some(result); - } + if let Some(result) = self.with_strategy(|strategy, cheatcodes| { + strategy.zksync_try_call(cheatcodes, ecx, call, executor) + }) { + return Some(result); } None } - /// Try handling the `CALL` within zkEVM. - /// If `Some` is returned then the result must be returned immediately, else the call must be - /// handled in EVM. - fn try_call_in_zk( - &mut self, - factory_deps: Vec>, - ecx: Ecx, - call: &mut CallInputs, - executor: &mut impl CheatcodesExecutor, - ) -> Option { - // also skip if the target was created during a zkEVM skip - self.skip_zk_vm = - self.skip_zk_vm || self.skip_zk_vm_addresses.contains(&call.target_address); - if self.skip_zk_vm { - self.skip_zk_vm = false; // handled the skip, reset flag - info!("running create in EVM, instead of zkEVM (skipped) {:#?}", call); - return None; - } - - if ecx - .db - .get_test_contract_address() - .map(|addr| call.bytecode_address == addr) - .unwrap_or_default() - { - info!( - "running call in EVM, instead of zkEVM (Test Contract) {:#?}", - call.bytecode_address - ); - return None - } - - info!("running call in zkEVM {:#?}", call); - let zk_persist_nonce_update = self.zk_persist_nonce_update.check(); - - // NOTE(zk): Clear injected factory deps here even though it's actually used in broadcast. - // To be consistent with where we clear factory deps in try_create_in_zk. - self.zk_use_factory_deps.clear(); - - let ccx = foundry_zksync_core::vm::CheatcodeTracerContext { - mocked_calls: self.mocked_calls.clone(), - expected_calls: Some(&mut self.expected_calls), - accesses: self.accesses.as_mut(), - persisted_factory_deps: Some(&mut self.persisted_factory_deps), - paymaster_data: self.paymaster_params.take(), - persist_nonce_update: self.broadcast.is_some() || zk_persist_nonce_update, - zk_env: self.zk_env.clone(), - }; - - let mut gas = Gas::new(call.gas_limit); - match foundry_zksync_core::vm::call::<_, DatabaseError>(call, factory_deps, ecx, ccx) { - Ok(result) => { - // append console logs from zkEVM to the current executor's LogTracer - result.logs.iter().filter_map(decode_console_log).for_each(|decoded_log| { - executor.console_log( - &mut CheatsCtxt { - state: self, - ecx: &mut ecx.inner, - precompiles: &mut ecx.precompiles, - gas_limit: call.gas_limit, - caller: call.caller, - }, - decoded_log, - ); - }); - - // skip log processing for static calls - if !call.is_static { - if let Some(recorded_logs) = &mut self.recorded_logs { - recorded_logs.extend(result.logs.clone().into_iter().map(|log| Vm::Log { - topics: log.data.topics().to_vec(), - data: log.data.data.clone(), - emitter: log.address, - })); - } - - // append traces - executor.trace_zksync(self, ecx, result.call_traces); - - // for each log in cloned logs call handle_expect_emit - if !self.expected_emits.is_empty() { - for log in result.logs { - expect::handle_expect_emit(self, &log, &mut Default::default()); - } - } - } - - match result.execution_result { - ExecutionResult::Success { output, gas_used, .. } => { - let _ = gas.record_cost(gas_used); - match output { - Output::Call(bytes) => Some(CallOutcome { - result: InterpreterResult { - result: InstructionResult::Return, - output: bytes, - gas, - }, - memory_offset: call.return_memory_offset.clone(), - }), - _ => Some(CallOutcome { - result: InterpreterResult { - result: InstructionResult::Revert, - output: Bytes::new(), - gas, - }, - memory_offset: call.return_memory_offset.clone(), - }), - } - } - ExecutionResult::Revert { output, gas_used, .. } => { - let _ = gas.record_cost(gas_used); - Some(CallOutcome { - result: InterpreterResult { - result: InstructionResult::Revert, - output, - gas, - }, - memory_offset: call.return_memory_offset.clone(), - }) - } - ExecutionResult::Halt { .. } => Some(CallOutcome { - result: InterpreterResult { - result: InstructionResult::Revert, - output: Bytes::from_iter(String::from("zk vm halted").as_bytes()), - gas, - }, - memory_offset: call.return_memory_offset.clone(), - }), - } - } - Err(err) => { - error!("error inspecting zkEVM: {err:?}"); - Some(CallOutcome { - result: InterpreterResult { - result: InstructionResult::Revert, - output: Bytes::from_iter( - format!("error inspecting zkEVM: {err:?}").as_bytes(), - ), - gas, - }, - memory_offset: call.return_memory_offset.clone(), - }) - } - } - } - pub fn rng(&mut self) -> &mut impl Rng { self.test_runner().rng() } @@ -2109,6 +1264,17 @@ where { None => false, } } + + pub fn with_strategy(&mut self, mut f: F) -> R + where + F: FnMut(Strategy, &mut Self) -> R, + { + let mut strategy = self.strategy.take(); + let result = f(strategy.as_mut().expect("failed acquiring strategy").as_mut(), self); + self.strategy = strategy; + + result + } } impl Inspector<&mut dyn DatabaseExt> for Cheatcodes { @@ -2123,23 +1289,15 @@ impl Inspector<&mut dyn DatabaseExt> for Cheatcodes { ecx.env.tx.gas_price = gas_price; } - let migration_allowed = self - .zk_startup_migration - .as_ref() - .map(|migration| migration.is_allowed()) - .unwrap_or(false); - if migration_allowed && !self.use_zk_vm { - self.select_zk_vm(ecx, None); - if let Some(zk_startup_migration) = &mut self.zk_startup_migration { - zk_startup_migration.done(); - } - debug!("startup zkEVM storage migration completed"); - } - // Record gas for current frame. if self.gas_metering.paused { self.gas_metering.paused_frames.push(interpreter.gas); } + + self.strategy + .as_mut() + .expect("failed acquiring strategy") + .post_initialize_interp(interpreter, ecx); } #[inline] @@ -2184,33 +1342,9 @@ impl Inspector<&mut dyn DatabaseExt> for Cheatcodes { #[inline] fn step_end(&mut self, interpreter: &mut Interpreter, ecx: Ecx) { - // override address(x).balance retrieval to make it consistent between EraVM and EVM - if self.use_zk_vm { - let address = match interpreter.current_opcode() { - op::SELFBALANCE => interpreter.contract().target_address, - op::BALANCE => { - if interpreter.stack.is_empty() { - interpreter.instruction_result = InstructionResult::StackUnderflow; - return; - } - - Address::from_word(B256::from(unsafe { interpreter.stack.pop_unsafe() })) - } - _ => return, - }; - - // Safety: Length is checked above. - let balance = foundry_zksync_core::balance(address, ecx); - - // Skip the current BALANCE instruction since we've already handled it - match interpreter.stack.push(balance) { - Ok(_) => unsafe { - interpreter.instruction_pointer = interpreter.instruction_pointer.add(1); - }, - Err(e) => { - interpreter.instruction_result = e; - } - } + if self.strategy.as_mut().expect("failed acquiring strategy").pre_step_end(interpreter, ecx) + { + return; } if self.gas_metering.paused { @@ -3054,7 +2188,7 @@ fn disallowed_mem_write( // Determines if the gas limit on a given call was manually set in the script and should therefore // not be overwritten by later estimations -fn check_if_fixed_gas_limit(ecx: InnerEcx, call_gas_limit: u64) -> bool { +pub fn check_if_fixed_gas_limit(ecx: InnerEcx, call_gas_limit: u64) -> bool { // If the gas limit was not set in the source code it is set to the estimated gas left at the // time of the call, which should be rather close to configured gas limit. // TODO: Find a way to reliably make this determination. diff --git a/crates/cheatcodes/src/inspector/utils.rs b/crates/cheatcodes/src/inspector/utils.rs index a0d7820aa..451b5cddd 100644 --- a/crates/cheatcodes/src/inspector/utils.rs +++ b/crates/cheatcodes/src/inspector/utils.rs @@ -4,7 +4,7 @@ use alloy_primitives::{Address, Bytes, U256}; use revm::interpreter::{CreateInputs, CreateScheme, EOFCreateInputs, EOFCreateKind}; /// Common behaviour of legacy and EOF create inputs. -pub(crate) trait CommonCreateInput { +pub trait CommonCreateInput { fn caller(&self) -> Address; fn gas_limit(&self) -> u64; fn value(&self) -> U256; diff --git a/crates/cheatcodes/src/lib.rs b/crates/cheatcodes/src/lib.rs index 9d7442bef..56b6cfb43 100644 --- a/crates/cheatcodes/src/lib.rs +++ b/crates/cheatcodes/src/lib.rs @@ -17,6 +17,7 @@ extern crate tracing; use alloy_primitives::Address; use foundry_evm_core::backend::DatabaseExt; +use inspector::Strategy; use revm::{ContextPrecompiles, InnerEvmContext}; use spec::Status; @@ -41,24 +42,33 @@ mod env; pub use env::set_execution_context; mod evm; +pub use evm::{ + journaled_account, + mock::{make_acc_non_empty, mock_call}, + DealRecord, +}; mod fs; mod inspector; +pub use inspector::{check_if_fixed_gas_limit, CommonCreateInput, Ecx, InnerEcx}; mod json; mod script; -pub use script::{Wallets, WalletsInner}; +pub use script::{Broadcast, Wallets, WalletsInner}; mod string; mod test; +pub use test::expect::{handle_expect_emit, handle_expect_revert}; mod toml; mod utils; +pub mod strategy; + /// Cheatcode implementation. pub(crate) trait Cheatcode: CheatcodeDef + DynCheatcode { /// Applies this cheatcode to the given state. @@ -133,15 +143,15 @@ impl dyn DynCheatcode { /// The cheatcode context, used in `Cheatcode`. pub struct CheatsCtxt<'cheats, 'evm, 'db, 'db2> { /// The cheatcodes inspector state. - pub(crate) state: &'cheats mut Cheatcodes, + pub state: &'cheats mut Cheatcodes, /// The EVM data. - pub(crate) ecx: &'evm mut InnerEvmContext<&'db mut (dyn DatabaseExt + 'db2)>, + pub ecx: &'evm mut InnerEvmContext<&'db mut (dyn DatabaseExt + 'db2)>, /// The precompiles context. - pub(crate) precompiles: &'evm mut ContextPrecompiles<&'db mut (dyn DatabaseExt + 'db2)>, + pub precompiles: &'evm mut ContextPrecompiles<&'db mut (dyn DatabaseExt + 'db2)>, /// The original `msg.sender`. - pub(crate) caller: Address, + pub caller: Address, /// Gas limit of the current cheatcode call. - pub(crate) gas_limit: u64, + pub gas_limit: u64, } impl<'db, 'db2> std::ops::Deref for CheatsCtxt<'_, '_, 'db, 'db2> { @@ -165,4 +175,15 @@ impl CheatsCtxt<'_, '_, '_, '_> { pub(crate) fn is_precompile(&self, address: &Address) -> bool { self.precompiles.contains(address) } + + pub(crate) fn with_strategy(&mut self, mut f: F) -> R + where + F: FnMut(Strategy, &mut Self) -> R, + { + let mut strategy = self.state.strategy.take(); + let result = f(strategy.as_mut().expect("failed acquiring strategy").as_mut(), self); + self.state.strategy = strategy; + + result + } } diff --git a/crates/cheatcodes/src/strategy.rs b/crates/cheatcodes/src/strategy.rs new file mode 100644 index 000000000..91154b660 --- /dev/null +++ b/crates/cheatcodes/src/strategy.rs @@ -0,0 +1,338 @@ +use std::{fmt::Debug, sync::Arc}; + +use alloy_primitives::{Address, Bytes, FixedBytes, TxKind, U256}; +use alloy_rpc_types::{TransactionInput, TransactionRequest}; +use alloy_sol_types::SolValue; +use foundry_evm_core::backend::LocalForkId; +use revm::{ + interpreter::{CallInputs, CallOutcome, CreateOutcome, InstructionResult, Interpreter}, + primitives::{Bytecode, SignedAuthorization, KECCAK_EMPTY}, +}; + +use crate::{ + evm::{self, journaled_account, mock::make_acc_non_empty, DealRecord}, + inspector::{check_if_fixed_gas_limit, CommonCreateInput, Ecx, InnerEcx}, + mock_call, + script::Broadcast, + BroadcastableTransaction, BroadcastableTransactions, Cheatcodes, CheatcodesExecutor, + CheatsConfig, CheatsCtxt, Result, +}; + +pub trait CheatcodeInspectorStrategy: Debug + Send + Sync + CheatcodeInspectorStrategyExt { + fn name(&self) -> &'static str; + + fn new_cloned(&self) -> Box; + + /// Get nonce. + fn get_nonce(&mut self, ccx: &mut CheatsCtxt, address: Address) -> Result { + let account = ccx.ecx.journaled_state.load_account(address, &mut ccx.ecx.db)?; + Ok(account.info.nonce) + } + + /// Called when the main test or script contract is deployed. + fn base_contract_deployed(&mut self) {} + + /// Cheatcode: roll. + fn cheatcode_roll(&mut self, ccx: &mut CheatsCtxt, new_height: U256) -> Result { + ccx.ecx.env.block.number = new_height; + Ok(Default::default()) + } + + /// Cheatcode: warp. + fn cheatcode_warp(&mut self, ccx: &mut CheatsCtxt, new_timestamp: U256) -> Result { + ccx.ecx.env.block.timestamp = new_timestamp; + Ok(Default::default()) + } + + /// Cheatcode: deal. + fn cheatcode_deal( + &mut self, + ccx: &mut CheatsCtxt, + address: Address, + new_balance: U256, + ) -> Result { + let account = journaled_account(ccx.ecx, address)?; + let old_balance = std::mem::replace(&mut account.info.balance, new_balance); + let record = DealRecord { address, old_balance, new_balance }; + ccx.state.eth_deals.push(record); + Ok(Default::default()) + } + + /// Cheatcode: etch. + fn cheatcode_etch( + &mut self, + ccx: &mut CheatsCtxt, + target: Address, + new_runtime_bytecode: &Bytes, + ) -> Result { + ensure_not_precompile!(&target, ccx); + ccx.ecx.load_account(target)?; + let bytecode = Bytecode::new_raw(Bytes::copy_from_slice(new_runtime_bytecode)); + ccx.ecx.journaled_state.set_code(target, bytecode); + Ok(Default::default()) + } + + /// Cheatcode: getNonce. + fn cheatcode_get_nonce(&mut self, ccx: &mut CheatsCtxt, address: Address) -> Result { + evm::get_nonce(ccx, &address) + } + + /// Cheatcode: resetNonce. + fn cheatcode_reset_nonce(&mut self, ccx: &mut CheatsCtxt, account: Address) -> Result { + let account = journaled_account(ccx.ecx, account)?; + // Per EIP-161, EOA nonces start at 0, but contract nonces + // start at 1. Comparing by code_hash instead of code + // to avoid hitting the case where account's code is None. + let empty = account.info.code_hash == KECCAK_EMPTY; + let nonce = if empty { 0 } else { 1 }; + account.info.nonce = nonce; + debug!(target: "cheatcodes", nonce, "reset"); + Ok(Default::default()) + } + + /// Cheatcode: setNonce. + fn cheatcode_set_nonce( + &mut self, + ccx: &mut CheatsCtxt, + account: Address, + new_nonce: u64, + ) -> Result { + let account = journaled_account(ccx.ecx, account)?; + // nonce must increment only + let current = account.info.nonce; + ensure!( + new_nonce >= current, + "new nonce ({new_nonce}) must be strictly equal to or higher than the \ + account's current nonce ({current})" + ); + account.info.nonce = new_nonce; + Ok(Default::default()) + } + + /// Cheatcode: setNonceUnsafe. + fn cheatcode_set_nonce_unsafe( + &mut self, + ccx: &mut CheatsCtxt, + account: Address, + new_nonce: u64, + ) -> Result { + let account = journaled_account(ccx.ecx, account)?; + account.info.nonce = new_nonce; + Ok(Default::default()) + } + + /// Mocks a call to return with a value. + fn cheatcode_mock_call( + &mut self, + ccx: &mut CheatsCtxt, + callee: Address, + data: &Bytes, + return_data: &Bytes, + ) -> Result { + let _ = make_acc_non_empty(&callee, ccx.ecx)?; + mock_call(ccx.state, &callee, data, None, return_data, InstructionResult::Return); + Ok(Default::default()) + } + + /// Mocks a call to revert with a value. + fn cheatcode_mock_call_revert( + &mut self, + ccx: &mut CheatsCtxt, + callee: Address, + data: &Bytes, + revert_data: &Bytes, + ) -> Result { + let _ = make_acc_non_empty(&callee, ccx.ecx)?; + mock_call(ccx.state, &callee, data, None, revert_data, InstructionResult::Revert); + Ok(Default::default()) + } + + /// Retrieve artifact code. + fn get_artifact_code(&self, state: &Cheatcodes, path: &str, deployed: bool) -> Result { + Ok(crate::fs::get_artifact_code(state, path, deployed)?.abi_encode()) + } + + /// Record broadcastable transaction during CREATE. + fn record_broadcastable_create_transactions( + &mut self, + config: Arc, + input: &dyn CommonCreateInput, + ecx_inner: InnerEcx, + broadcast: &Broadcast, + broadcastable_transactions: &mut BroadcastableTransactions, + ); + + /// Record broadcastable transaction during CALL. + fn record_broadcastable_call_transactions( + &mut self, + config: Arc, + input: &CallInputs, + ecx_inner: InnerEcx, + broadcast: &Broadcast, + broadcastable_transactions: &mut BroadcastableTransactions, + active_delegation: &mut Option, + ); + + fn post_initialize_interp(&mut self, _interpreter: &mut Interpreter, _ecx: Ecx) {} + + /// Used to override opcode behaviors. Returns true if handled. + fn pre_step_end(&mut self, _interpreter: &mut Interpreter, _ecx: Ecx) -> bool { + false + } +} + +/// We define this in our fork +pub trait CheatcodeInspectorStrategyExt { + fn zksync_cheatcode_skip_zkvm(&mut self) -> Result { + Ok(Default::default()) + } + + fn zksync_cheatcode_set_paymaster( + &mut self, + _paymaster_address: Address, + _paymaster_input: &Bytes, + ) -> Result { + Ok(Default::default()) + } + + fn zksync_cheatcode_use_factory_deps(&mut self, _name: String) -> Result { + Ok(Default::default()) + } + + #[allow(clippy::too_many_arguments)] + fn zksync_cheatcode_register_contract( + &mut self, + _name: String, + _zk_bytecode_hash: FixedBytes<32>, + _zk_deployed_bytecode: Vec, + _zk_factory_deps: Vec>, + _evm_bytecode_hash: FixedBytes<32>, + _evm_deployed_bytecode: Vec, + _evm_bytecode: Vec, + ) -> Result { + Ok(Default::default()) + } + + fn zksync_cheatcode_select_zk_vm(&mut self, _data: InnerEcx, _enable: bool) {} + + fn zksync_record_create_address(&mut self, _outcome: &CreateOutcome) {} + + fn zksync_sync_nonce(&mut self, _sender: Address, _nonce: u64, _ecx: Ecx) {} + + fn zksync_set_deployer_call_input(&mut self, _call: &mut CallInputs) {} + + fn zksync_try_create( + &mut self, + _state: &mut Cheatcodes, + _ecx: Ecx<'_, '_, '_>, + _input: &dyn CommonCreateInput, + _executor: &mut dyn CheatcodesExecutor, + ) -> Option { + None + } + + fn zksync_try_call( + &mut self, + _state: &mut Cheatcodes, + _ecx: Ecx, + _input: &CallInputs, + _executor: &mut dyn CheatcodesExecutor, + ) -> Option { + None + } + + fn zksync_select_fork_vm(&mut self, _data: InnerEcx, _fork_id: LocalForkId) {} +} + +#[derive(Debug, Default, Clone)] +pub struct EvmCheatcodeInspectorStrategy {} + +impl CheatcodeInspectorStrategy for EvmCheatcodeInspectorStrategy { + fn name(&self) -> &'static str { + "evm" + } + + fn new_cloned(&self) -> Box { + Box::new(self.clone()) + } + + fn record_broadcastable_create_transactions( + &mut self, + _config: Arc, + input: &dyn CommonCreateInput, + ecx_inner: InnerEcx, + broadcast: &Broadcast, + broadcastable_transactions: &mut BroadcastableTransactions, + ) { + let is_fixed_gas_limit = check_if_fixed_gas_limit(ecx_inner, input.gas_limit()); + + let to = None; + let nonce: u64 = ecx_inner.journaled_state.state()[&broadcast.new_origin].info.nonce; + //drop the mutable borrow of account + let call_init_code = input.init_code(); + let rpc = ecx_inner.db.active_fork_url(); + + broadcastable_transactions.push_back(BroadcastableTransaction { + rpc, + transaction: TransactionRequest { + from: Some(broadcast.new_origin), + to, + value: Some(input.value()), + input: TransactionInput::new(call_init_code), + nonce: Some(nonce), + gas: if is_fixed_gas_limit { Some(input.gas_limit()) } else { None }, + ..Default::default() + } + .into(), + }); + } + + fn record_broadcastable_call_transactions( + &mut self, + _config: Arc, + call: &CallInputs, + ecx_inner: InnerEcx, + broadcast: &Broadcast, + broadcastable_transactions: &mut BroadcastableTransactions, + active_delegation: &mut Option, + ) { + let is_fixed_gas_limit = check_if_fixed_gas_limit(ecx_inner, call.gas_limit); + + let account = ecx_inner.journaled_state.state().get_mut(&broadcast.new_origin).unwrap(); + let nonce = account.info.nonce; + + let mut tx_req = TransactionRequest { + from: Some(broadcast.new_origin), + to: Some(TxKind::from(Some(call.target_address))), + value: call.transfer_value(), + input: TransactionInput::new(call.input.clone()), + nonce: Some(nonce), + chain_id: Some(ecx_inner.env.cfg.chain_id), + gas: if is_fixed_gas_limit { Some(call.gas_limit) } else { None }, + ..Default::default() + }; + + if let Some(auth_list) = active_delegation.take() { + tx_req.authorization_list = Some(vec![auth_list]); + } else { + tx_req.authorization_list = None; + } + + broadcastable_transactions.push_back(BroadcastableTransaction { + rpc: ecx_inner.db.active_fork_url(), + transaction: tx_req.into(), + }); + debug!(target: "cheatcodes", tx=?broadcastable_transactions.back().unwrap(), "broadcastable call"); + } +} + +impl CheatcodeInspectorStrategyExt for EvmCheatcodeInspectorStrategy {} + +impl Clone for Box { + fn clone(&self) -> Self { + self.new_cloned() + } +} + +struct _ObjectSafe0(dyn CheatcodeInspectorStrategy); +struct _ObjectSafe1(dyn CheatcodeInspectorStrategyExt); diff --git a/crates/cheatcodes/src/test.rs b/crates/cheatcodes/src/test.rs index c4b5f219c..de3f1eb72 100644 --- a/crates/cheatcodes/src/test.rs +++ b/crates/cheatcodes/src/test.rs @@ -5,8 +5,6 @@ use alloy_primitives::Address; use alloy_sol_types::SolValue; use chrono::DateTime; use foundry_evm_core::constants::MAGIC_SKIP; -use foundry_zksync_compiler::DualCompiledContract; -use foundry_zksync_core::ZkPaymasterData; use std::env; pub(crate) mod assert; @@ -17,11 +15,7 @@ impl Cheatcode for zkVmCall { fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { enable } = *self; - if enable { - ccx.state.select_zk_vm(ccx.ecx, None); - } else { - ccx.state.select_evm(ccx.ecx); - } + ccx.with_strategy(|strategy, ccx| strategy.zksync_cheatcode_select_zk_vm(ccx.ecx, enable)); Ok(Default::default()) } @@ -29,27 +23,23 @@ impl Cheatcode for zkVmCall { impl Cheatcode for zkVmSkipCall { fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { - ccx.state.skip_zk_vm = ccx.state.use_zk_vm; - - Ok(Default::default()) + ccx.with_strategy(|strategy, _ccx| strategy.zksync_cheatcode_skip_zkvm()) } } impl Cheatcode for zkUsePaymasterCall { fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { paymaster_address, paymaster_input } = self; - ccx.state.paymaster_params = - Some(ZkPaymasterData { address: *paymaster_address, input: paymaster_input.clone() }); - Ok(Default::default()) + ccx.with_strategy(|strategy, _ccx| { + strategy.zksync_cheatcode_set_paymaster(*paymaster_address, paymaster_input) + }) } } impl Cheatcode for zkUseFactoryDepCall { fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { name } = self; - info!("Adding factory dependency: {:?}", name); - ccx.state.zk_use_factory_deps.push(name.clone()); - Ok(Default::default()) + ccx.with_strategy(|strategy, _ccx| strategy.zksync_cheatcode_use_factory_deps(name.clone())) } } @@ -64,28 +54,17 @@ impl Cheatcode for zkRegisterContractCall { zkDeployedBytecode, } = self; - let new_contract = DualCompiledContract { - name: name.clone(), - zk_bytecode_hash: zkBytecodeHash.0.into(), - zk_deployed_bytecode: zkDeployedBytecode.to_vec(), - //TODO: add argument to cheatcode - zk_factory_deps: vec![], - evm_bytecode_hash: *evmBytecodeHash, - evm_deployed_bytecode: evmDeployedBytecode.to_vec(), - evm_bytecode: evmBytecode.to_vec(), - }; - - if let Some(existing) = ccx.state.dual_compiled_contracts.iter().find(|contract| { - contract.evm_bytecode_hash == new_contract.evm_bytecode_hash && - contract.zk_bytecode_hash == new_contract.zk_bytecode_hash - }) { - warn!(name = existing.name, "contract already exists with the given bytecode hashes"); - return Ok(Default::default()) - } - - ccx.state.dual_compiled_contracts.push(new_contract); - - Ok(Default::default()) + ccx.with_strategy(|strategy, _ccx| { + strategy.zksync_cheatcode_register_contract( + name.clone(), + zkBytecodeHash.0.into(), + zkDeployedBytecode.to_vec(), + vec![], //TODO: add argument to cheatcode + *evmBytecodeHash, + evmDeployedBytecode.to_vec(), + evmBytecode.to_vec(), + ) + }) } } diff --git a/crates/cheatcodes/src/test/expect.rs b/crates/cheatcodes/src/test/expect.rs index 94afe820d..213cc3448 100644 --- a/crates/cheatcodes/src/test/expect.rs +++ b/crates/cheatcodes/src/test/expect.rs @@ -519,7 +519,7 @@ fn expect_emit( Ok(Default::default()) } -pub(crate) fn handle_expect_emit( +pub fn handle_expect_emit( state: &mut Cheatcodes, log: &alloy_primitives::Log, interpreter: &mut Interpreter, @@ -644,7 +644,7 @@ fn expect_revert( Ok(Default::default()) } -pub(crate) fn handle_expect_revert( +pub fn handle_expect_revert( is_cheatcode: bool, is_create: bool, expected_revert: &ExpectedRevert, diff --git a/crates/chisel/Cargo.toml b/crates/chisel/Cargo.toml index 65b3c9748..d10a2d270 100644 --- a/crates/chisel/Cargo.toml +++ b/crates/chisel/Cargo.toml @@ -36,6 +36,7 @@ foundry-common.workspace = true foundry-compilers = { workspace = true, features = ["project-util", "full"] } foundry-config.workspace = true foundry-evm.workspace = true +foundry-strategy-zksync.workspace = true alloy-dyn-abi = { workspace = true, features = ["arbitrary"] } alloy-primitives = { workspace = true, features = [ diff --git a/crates/chisel/src/executor.rs b/crates/chisel/src/executor.rs index 87d69b9ba..7a72c111e 100644 --- a/crates/chisel/src/executor.rs +++ b/crates/chisel/src/executor.rs @@ -10,6 +10,7 @@ use alloy_json_abi::EventParam; use alloy_primitives::{hex, Address, U256}; use core::fmt::Debug; use eyre::{Result, WrapErr}; +use foundry_cli::utils; use foundry_compilers::Artifact; use foundry_evm::{ backend::Backend, decode::decode_console_logs, executors::ExecutorBuilder, @@ -315,12 +316,14 @@ impl SessionSource { let env = self.config.evm_opts.evm_env().await.expect("Could not instantiate fork environment"); + let strategy = utils::get_executor_strategy(&self.config.foundry_config); + // Create an in-memory backend let backend = match self.config.backend.take() { 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, strategy.new_backend_strategy()); self.config.backend = Some(backend.clone()); backend } @@ -336,9 +339,7 @@ impl SessionSource { None, None, Some(self.solc.version.clone()), - Default::default(), - false, - None, + strategy.new_cheatcode_inspector_strategy(), ) .into(), ) @@ -346,7 +347,7 @@ impl SessionSource { .gas_limit(self.config.evm_opts.gas_limit()) .spec(self.config.foundry_config.evm_spec_id()) .legacy_assertions(self.config.foundry_config.legacy_assertions) - .build(env, backend); + .build(env, backend, strategy); // Create a [ChiselRunner] with a default balance of [U256::MAX] and // the sender [Address::zero]. diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index ee1fc4c09..facdda6a4 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -19,6 +19,7 @@ foundry-config.workspace = true foundry-debugger.workspace = true foundry-evm.workspace = true foundry-wallets.workspace = true +foundry-strategy-zksync.workspace = true foundry-compilers = { workspace = true, features = ["full"] } diff --git a/crates/cli/src/utils/mod.rs b/crates/cli/src/utils/mod.rs index a66923de9..48cf75cce 100644 --- a/crates/cli/src/utils/mod.rs +++ b/crates/cli/src/utils/mod.rs @@ -9,6 +9,8 @@ use foundry_common::{ shell, }; use foundry_config::{Chain, Config}; +use foundry_evm::executors::strategy::{EvmExecutorStrategy, ExecutorStrategy}; +use foundry_strategy_zksync::ZksyncExecutorStrategy; use serde::de::DeserializeOwned; use std::{ ffi::OsStr, @@ -91,6 +93,16 @@ pub fn get_provider(config: &Config) -> Result { get_provider_builder(config)?.build() } +pub fn get_executor_strategy(config: &Config) -> Box { + if config.zksync.should_compile() { + info!("using zksync strategy"); + Box::new(ZksyncExecutorStrategy::default()) + } else { + info!("using evm strategy"); + Box::new(EvmExecutorStrategy::default()) + } +} + /// Returns a [RetryProvider] instantiated using [Config]'s /// RPC for ZKsync pub fn get_provider_zksync(config: &Config) -> Result> { diff --git a/crates/config/src/zksync.rs b/crates/config/src/zksync.rs index 925612b4c..67e1d0565 100644 --- a/crates/config/src/zksync.rs +++ b/crates/config/src/zksync.rs @@ -70,8 +70,8 @@ pub struct ZkSyncConfig { impl Default for ZkSyncConfig { fn default() -> Self { Self { - compile: Default::default(), - startup: true, + compile: false, + startup: false, zksolc: Default::default(), solc_path: Default::default(), bytecode_hash: Default::default(), diff --git a/crates/evm/core/src/backend/cow.rs b/crates/evm/core/src/backend/cow.rs index 4b7798ec7..f95b9087e 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, @@ -9,23 +9,15 @@ use crate::{ InspectorExt, }; use alloy_genesis::GenesisAccount; -use alloy_primitives::{map::HashMap, Address, B256, U256}; +use alloy_primitives::{Address, B256, U256}; use alloy_rpc_types::TransactionRequest; -use eyre::WrapErr; use foundry_fork_db::DatabaseError; -use foundry_zksync_core::{vm::ZkEnv, PaymasterParams}; use revm::{ db::DatabaseRef, - primitives::{ - Account, AccountInfo, Bytecode, Env, EnvWithHandlerCfg, HashMap as Map, ResultAndState, - SpecId, - }, + primitives::{Account, AccountInfo, Bytecode, Env, EnvWithHandlerCfg, HashMap as Map, SpecId}, Database, DatabaseCommit, JournaledState, }; -use std::{ - borrow::Cow, - collections::{BTreeMap, HashSet}, -}; +use std::{borrow::Cow, collections::BTreeMap}; /// A wrapper around `Backend` that ensures only `revm::DatabaseRef` functions are called. /// @@ -50,9 +42,9 @@ pub struct CowBackend<'a> { /// No calls on the `CowBackend` will ever persistently modify the `backend`'s state. pub backend: Cow<'a, Backend>, /// Keeps track of whether the backed is already initialized - is_initialized: bool, + pub is_initialized: bool, /// The [SpecId] of the current backend. - spec_id: SpecId, + pub spec_id: SpecId, } impl<'a> CowBackend<'a> { @@ -61,52 +53,6 @@ impl<'a> CowBackend<'a> { Self { backend: Cow::Borrowed(backend), is_initialized: false, spec_id: SpecId::LATEST } } - /// Executes the configured zk transaction of the `env` without committing state changes - pub fn inspect_ref_zk( - &mut self, - env: &mut Env, - zk_env: &ZkEnv, - persisted_factory_deps: &mut HashMap>, - factory_deps: Option>>, - paymaster_data: Option, - ) -> 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; - - foundry_zksync_core::vm::transact( - Some(persisted_factory_deps), - factory_deps, - paymaster_data, - env, - zk_env, - self, - ) - } - - /// 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( - &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,8 +92,8 @@ impl DatabaseExt for CowBackend<'_> { self.backend.to_mut().get_fork_info(id) } - fn save_zk_immutable_storage(&mut self, addr: Address, keys: HashSet) { - self.backend.to_mut().save_zk_immutable_storage(addr, keys) + fn get_strategy(&mut self) -> &mut dyn BackendStrategy { + self.backend.to_mut().strategy.as_mut() } fn snapshot_state(&mut self, journaled_state: &JournaledState, env: &Env) -> U256 { diff --git a/crates/evm/core/src/backend/mod.rs b/crates/evm/core/src/backend/mod.rs index 2edd0ec86..fbd5bf0a2 100644 --- a/crates/evm/core/src/backend/mod.rs +++ b/crates/evm/core/src/backend/mod.rs @@ -15,11 +15,6 @@ use alloy_rpc_types::{BlockNumberOrTag, Transaction, TransactionRequest}; 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, vm::ZkEnv, PaymasterParams, ACCOUNT_CODE_STORAGE_ADDRESS, - IMMUTABLE_SIMULATOR_STORAGE_ADDRESS, KNOWN_CODES_STORAGE_ADDRESS, L2_BASE_TOKEN_ADDRESS, - NONCE_HOLDER_ADDRESS, -}; use itertools::Itertools; use revm::{ db::{CacheDB, DatabaseRef}, @@ -27,14 +22,15 @@ use revm::{ precompile::{PrecompileSpecId, Precompiles}, primitives::{ Account, AccountInfo, BlobExcessGasAndPrice, Bytecode, Env, EnvWithHandlerCfg, EvmState, - EvmStorageSlot, HashMap as Map, Log, ResultAndState, SpecId, KECCAK_EMPTY, + EvmStorageSlot, HashMap as Map, Log, SpecId, KECCAK_EMPTY, }, Database, DatabaseCommit, JournaledState, }; use std::{ - collections::{hash_map::Entry, BTreeMap, HashSet}, + collections::{BTreeMap, HashSet}, time::Instant, }; +use strategy::{BackendStrategy, BackendStrategyForkInfo}; mod diagnostic; pub use diagnostic::RevertDiagnostic; @@ -54,8 +50,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`. /// @@ -103,13 +101,8 @@ pub trait DatabaseExt: Database + DatabaseCommit { /// and the the fork environment. fn get_fork_info(&mut self, id: LocalForkId) -> eyre::Result; - /// Saves the storage keys for immutable variables per address. - /// - /// These are required during fork to help merge the persisted addresses, as they are stored - /// hashed so there is currently no way to retrieve all the address associated storage keys. - /// We store all the storage keys here, even if the addresses are not marked persistent as - /// they can be marked at a later stage as well. - fn save_zk_immutable_storage(&mut self, addr: Address, keys: HashSet); + /// Retrieve the strategy. + fn get_strategy(&mut self) -> &mut dyn BackendStrategy; /// Reverts the snapshot if it exists /// @@ -463,9 +456,12 @@ struct _ObjectSafe(dyn DatabaseExt); /// **Note:** State snapshots work across fork-swaps, e.g. if fork `A` is currently active, then a /// snapshot is created before fork `B` is selected, then fork `A` will be the active fork again /// after reverting the snapshot. -#[derive(Clone, Debug)] +#[derive(Debug)] #[must_use] pub struct Backend { + /// The behavior strategy. + pub strategy: Box, + /// The access point for managing forks forks: MultiFork, // The default in memory db @@ -495,15 +491,20 @@ 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. - zk_recorded_immutable_keys: HashMap>, +} + +impl Clone for Backend { + fn clone(&self) -> Self { + Self { + strategy: self.strategy.new_cloned(), + forks: self.forks.clone(), + mem_db: self.mem_db.clone(), + fork_init_journaled_state: self.fork_init_journaled_state.clone(), + active_fork_ids: self.active_fork_ids, + inner: self.inner.clone(), + fork_url_type: self.fork_url_type.clone(), + } + } } impl Backend { @@ -511,8 +512,8 @@ impl Backend { /// /// 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: Box) -> Self { + Self::new(MultiFork::spawn(), fork, strategy) } /// Creates a new instance of `Backend` @@ -521,7 +522,11 @@ 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: Box, + ) -> Self { trace!(target: "backend", forking_mode=?fork.is_some(), "creating executor backend"); // Note: this will take of registering the `fork` let inner = BackendInner { @@ -536,8 +541,7 @@ impl Backend { active_fork_ids: None, inner, fork_url_type: Default::default(), - is_zk: false, - zk_recorded_immutable_keys: Default::default(), + strategy, }; if let Some(fork) = fork { @@ -560,8 +564,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: Box, + ) -> 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); @@ -577,8 +586,7 @@ impl Backend { active_fork_ids: None, inner: Default::default(), fork_url_type: Default::default(), - is_zk: false, - zk_recorded_immutable_keys: Default::default(), + strategy: self.strategy.new_cloned(), } } @@ -680,42 +688,6 @@ 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, - ) - } - } - /// Returns the memory db used if not in forking mode pub fn mem_db(&self) -> &FoundryEvmInMemoryDB { &self.mem_db @@ -806,7 +778,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); } @@ -816,47 +788,6 @@ impl Backend { EnvWithHandlerCfg::new_with_spec_id(Box::new(env), self.inner.spec_id) } - /// 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)] - pub fn inspect( - &mut self, - env: &mut EnvWithHandlerCfg, - inspector: &mut I, - ) -> 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")?; - - env.env = evm.context.evm.inner.env; - - Ok(res) - } - - /// Executes the configured test call of the `env` without committing state changes - pub fn inspect_ref_zk( - &mut self, - env: &mut EnvWithHandlerCfg, - zk_env: &ZkEnv, - 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, - zk_env, - self, - ) - } - /// Returns true if the address is a precompile pub fn is_existing_precompile(&self, addr: &Address) -> bool { self.inner.precompiles().contains(addr) @@ -983,6 +914,7 @@ impl Backend { &fork_id, &persistent_accounts, &mut NoOpInspector, + self.strategy.as_mut(), )?; } @@ -1006,11 +938,8 @@ impl DatabaseExt for Backend { Ok(ForkInfo { fork_type, fork_env }) } - fn save_zk_immutable_storage(&mut self, addr: Address, keys: HashSet) { - self.zk_recorded_immutable_keys - .entry(addr) - .and_modify(|entry| entry.extend(&keys)) - .or_insert(keys); + fn get_strategy(&mut self) -> &mut dyn BackendStrategy { + self.strategy.as_mut() } fn snapshot_state(&mut self, journaled_state: &JournaledState, env: &Env) -> U256 { @@ -1159,23 +1088,19 @@ 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 fork_env = self .forks @@ -1244,7 +1169,17 @@ impl DatabaseExt for Backend { caller_account.into() }); - self.update_fork_db(active_journaled_state, &mut fork, zk_state); + self.strategy.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); @@ -1271,7 +1206,7 @@ impl DatabaseExt for Backend { let (fork_id, backend, fork_env) = self.forks.roll_fork(self.inner.ensure_fork_id(id).cloned()?, block_number)?; // this will update the local mapping - self.inner.roll_fork(id, fork_id, backend)?; + self.inner.roll_fork(id, fork_id, backend, self.strategy.as_mut())?; if let Some((active_id, active_idx)) = self.active_fork_ids { // the currently active fork is the targeted fork of this call @@ -1293,7 +1228,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); + self.strategy.merge_journaled_state_data( + addr, + journaled_state, + &mut active.journaled_state, + ); } // Ensure all previously loaded accounts are present in the journaled state to @@ -1306,7 +1245,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( + self.strategy.merge_journaled_state_data( *addr, journaled_state, &mut active.journaled_state, @@ -1387,6 +1326,7 @@ impl DatabaseExt for Backend { &fork_id, &persistent_accounts, inspector, + self.strategy.as_mut(), ) } @@ -1692,8 +1632,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 { @@ -1868,6 +1808,7 @@ impl BackendInner { id: LocalForkId, new_fork_id: ForkId, backend: SharedBackend, + strategy: &mut dyn BackendStrategy, ) -> eyre::Result { let fork_id = self.ensure_fork_id(id)?; let idx = self.ensure_fork_index(fork_id)?; @@ -1876,7 +1817,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; } @@ -1962,228 +1903,6 @@ 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); - } - } -} - /// Returns true of the address is a contract fn is_contract_in_state(journaled_state: &JournaledState, acc: Address) -> bool { journaled_state @@ -2209,6 +1928,7 @@ fn update_env_block(env: &mut Env, block: &AnyRpcBlock) { /// Executes the given transaction and commits state changes to the database _and_ the journaled /// state, with an inspector. +#[allow(clippy::too_many_arguments)] fn commit_transaction( tx: &Transaction, mut env: EnvWithHandlerCfg, @@ -2217,6 +1937,7 @@ fn commit_transaction( fork_id: &ForkId, persistent_accounts: &HashSet
, inspector: &mut dyn InspectorExt, + strategy: &mut dyn BackendStrategy, ) -> eyre::Result<()> { // TODO: Remove after https://github.com/foundry-rs/foundry/pull/9131 // if the tx has the blob_versioned_hashes field, we assume it's a Cancun block @@ -2231,7 +1952,7 @@ 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, strategy.new_cloned()); 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. @@ -2283,7 +2004,11 @@ fn apply_state_changeset( #[cfg(test)] #[allow(clippy::needless_return)] mod tests { - use crate::{backend::Backend, fork::CreateFork, opts::EvmOpts}; + use crate::{ + backend::{strategy::EvmBackendStrategy, Backend}, + fork::CreateFork, + opts::EvmOpts, + }; use alloy_primitives::{Address, U256}; use alloy_provider::Provider; use foundry_common::provider::get_http_provider; @@ -2314,7 +2039,7 @@ mod tests { evm_opts, }; - let backend = Backend::spawn(Some(fork)); + let backend = Backend::spawn(Some(fork), Box::new(EvmBackendStrategy)); // 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..18ebe9e6b --- /dev/null +++ b/crates/evm/core/src/backend/strategy.rs @@ -0,0 +1,201 @@ +use std::fmt::Debug; + +use super::{BackendInner, Fork, ForkDB, ForkType, FoundryEvmInMemoryDB}; +use alloy_primitives::{Address, U256}; +use revm::{db::CacheDB, primitives::HashSet, 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: Debug + Send + Sync + BackendStrategyExt { + fn name(&self) -> &'static str; + + fn new_cloned(&self) -> Box; + + /// 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, + ); + + /// Clones the account data from the `active_journaled_state` into the `fork_journaled_state` + fn merge_journaled_state_data( + &self, + addr: Address, + active_journaled_state: &JournaledState, + fork_journaled_state: &mut JournaledState, + ); + + fn merge_db_account_data(&self, addr: Address, active: &ForkDB, fork_db: &mut ForkDB); +} + +pub trait BackendStrategyExt { + /// Saves the storage keys for immutable variables per address. + /// + /// These are required during fork to help merge the persisted addresses, as they are stored + /// hashed so there is currently no way to retrieve all the address associated storage keys. + /// We store all the storage keys here, even if the addresses are not marked persistent as + /// they can be marked at a later stage as well. + fn zksync_save_immutable_storage(&mut self, _addr: Address, _keys: HashSet) {} +} + +struct _ObjectSafe(dyn BackendStrategy); + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct EvmBackendStrategy; + +impl BackendStrategy for EvmBackendStrategy { + fn name(&self) -> &'static str { + "evm" + } + + fn new_cloned(&self) -> Box { + Box::new(self.clone()) + } + + 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 merge_journaled_state_data( + &self, + addr: Address, + active_journaled_state: &JournaledState, + fork_journaled_state: &mut JournaledState, + ) { + EvmBackendMergeStrategy::merge_journaled_state_data( + addr, + active_journaled_state, + fork_journaled_state, + ); + } + + fn merge_db_account_data(&self, addr: Address, active: &ForkDB, fork_db: &mut ForkDB) { + EvmBackendMergeStrategy::merge_db_account_data(addr, active, fork_db); + } +} + +impl BackendStrategyExt for EvmBackendStrategy {} + +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() { + Self::merge_db_account_data(addr, active, &mut target_fork.db); + Self::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..66defeb07 100644 --- a/crates/evm/evm/Cargo.toml +++ b/crates/evm/evm/Cargo.toml @@ -23,10 +23,12 @@ foundry-evm-coverage.workspace = true foundry-evm-fuzz.workspace = true foundry-evm-traces.workspace = true foundry-zksync-core.workspace = true +foundry-zksync-compiler.workspace = true foundry-zksync-inspectors.workspace = true alloy-dyn-abi = { workspace = true, features = [ "arbitrary", "eip712" ] } alloy-json-abi.workspace = true +alloy-serde.workspace = true alloy-primitives = { workspace = true, features = [ "serde", "getrandom", diff --git a/crates/evm/evm/src/executors/builder.rs b/crates/evm/evm/src/executors/builder.rs index c7ad6c180..40fc2fa31 100644 --- a/crates/evm/evm/src/executors/builder.rs +++ b/crates/evm/evm/src/executors/builder.rs @@ -1,8 +1,9 @@ use crate::{executors::Executor, inspectors::InspectorStackBuilder}; use foundry_evm_core::backend::Backend; -use foundry_zksync_core::vm::ZkEnv; use revm::primitives::{Env, EnvWithHandlerCfg, SpecId}; +use super::strategy::ExecutorStrategy; + /// The builder that allows to configure an evm [`Executor`] which a stack of optional /// [`revm::Inspector`]s, such as [`Cheatcodes`]. /// @@ -21,9 +22,6 @@ pub struct ExecutorBuilder { spec_id: SpecId, legacy_assertions: bool, - - use_zk: bool, - zk_env: ZkEnv, } impl Default for ExecutorBuilder { @@ -34,8 +32,6 @@ impl Default for ExecutorBuilder { gas_limit: None, spec_id: SpecId::LATEST, legacy_assertions: false, - use_zk: false, - zk_env: Default::default(), } } } @@ -78,24 +74,10 @@ impl ExecutorBuilder { self } - /// Sets the EVM spec to use - #[inline] - pub fn use_zk_vm(mut self, enable: bool) -> Self { - self.use_zk = enable; - self - } - - /// Sets zk_env - #[inline] - pub fn zk_env(mut self, zk_env: ZkEnv) -> Self { - self.zk_env = zk_env; - self - } - /// Builds the executor as configured. #[inline] - pub fn build(self, env: Env, db: Backend) -> Executor { - let Self { mut stack, gas_limit, spec_id, legacy_assertions, use_zk, zk_env } = self; + pub fn build(self, env: Env, db: Backend, strategy: Box) -> Executor { + let Self { mut stack, gas_limit, spec_id, legacy_assertions } = self; if stack.block.is_none() { stack.block = Some(env.block.clone()); } @@ -104,9 +86,6 @@ impl ExecutorBuilder { } let gas_limit = gas_limit.unwrap_or_else(|| env.block.gas_limit.saturating_to()); let env = EnvWithHandlerCfg::new_with_spec_id(Box::new(env), spec_id); - let mut exec = Executor::new(db, env, stack.build(), gas_limit, legacy_assertions); - exec.use_zk = use_zk; - exec.zk_env = zk_env; - exec + Executor::new(db, env, stack.build(), gas_limit, legacy_assertions, strategy) } } diff --git a/crates/evm/evm/src/executors/mod.rs b/crates/evm/evm/src/executors/mod.rs index f483076ef..1e856aa28 100644 --- a/crates/evm/evm/src/executors/mod.rs +++ b/crates/evm/evm/src/executors/mod.rs @@ -15,6 +15,7 @@ use alloy_primitives::{ map::{AddressHashMap, HashMap}, Address, Bytes, Log, U256, }; +use alloy_serde::OtherFields; use alloy_sol_types::{sol, SolCall}; use foundry_evm_core::{ backend::{Backend, BackendError, BackendResult, CowBackend, DatabaseExt, GLOBAL_FAIL_SLOT}, @@ -27,7 +28,6 @@ use foundry_evm_core::{ }; use foundry_evm_coverage::HitMaps; use foundry_evm_traces::{SparsedTraceArena, TraceMode}; -use foundry_zksync_core::{vm::ZkEnv, ZkTransactionMetadata}; use revm::{ db::{DatabaseCommit, DatabaseRef}, interpreter::{return_ok, InstructionResult}, @@ -35,9 +35,9 @@ use revm::{ AuthorizationList, BlockEnv, Bytecode, Env, EnvWithHandlerCfg, ExecutionResult, Output, ResultAndState, SignedAuthorization, SpecId, TxEnv, TxKind, }, - Database, }; use std::borrow::Cow; +use strategy::ExecutorStrategy; mod builder; pub use builder::ExecutorBuilder; @@ -51,6 +51,8 @@ pub use invariant::InvariantExecutor; mod trace; pub use trace::TracingExecutor; +pub mod strategy; + sol! { interface ITest { function setUp() external; @@ -72,7 +74,7 @@ sol! { /// - `deploy`: a special case of `transact`, specialized for persisting the state of a contract /// deployment /// - `setup`: a special case of `transact`, used to set up the environment for a test -#[derive(Clone, Debug)] +#[derive(Debug)] pub struct Executor { /// The underlying `revm::Database` that contains the EVM storage. // Note: We do not store an EVM here, since we are really @@ -91,13 +93,20 @@ pub struct Executor { /// Whether `failed()` should be called on the test contract to determine if the test failed. legacy_assertions: bool, - /// Sets up the next transaction to be executed as a ZK transaction. - zk_tx: Option, - // simulate persisted factory deps - zk_persisted_factory_deps: HashMap>, + strategy: Option>, +} - pub use_zk: bool, - pub zk_env: ZkEnv, +impl Clone for Executor { + fn clone(&self) -> Self { + Self { + backend: self.backend.clone(), + env: self.env.clone(), + inspector: self.inspector.clone(), + gas_limit: self.gas_limit, + legacy_assertions: self.legacy_assertions, + strategy: self.strategy.as_ref().map(|s| s.new_cloned()), + } + } } impl Executor { @@ -115,6 +124,7 @@ impl Executor { inspector: InspectorStack, gas_limit: u64, legacy_assertions: bool, + strategy: Box, ) -> Self { // Need to create a non-empty contract on the cheatcodes address so `extcodesize` checks // do not fail. @@ -129,22 +139,19 @@ impl Executor { }, ); - Self { - backend, - env, - inspector, - gas_limit, - legacy_assertions, - zk_tx: None, - zk_persisted_factory_deps: Default::default(), - use_zk: false, - zk_env: Default::default(), - } + Self { backend, env, inspector, gas_limit, legacy_assertions, strategy: Some(strategy) } } 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) + Self::new( + backend, + env, + self.inspector().clone(), + self.gas_limit, + self.legacy_assertions, + self.strategy.as_ref().map(|s| s.new_cloned()).expect("failed acquiring strategy"), + ) } /// Returns a reference to the EVM backend. @@ -207,18 +214,21 @@ impl Executor { Ok(()) } + pub fn with_strategy(&mut self, mut f: F) -> R + where + F: FnMut(&mut dyn ExecutorStrategy, &mut Self) -> R, + { + let mut strategy = self.strategy.take(); + let result = f(strategy.as_mut().expect("failed acquiring strategy").as_mut(), self); + self.strategy = strategy; + + result + } + /// Set the balance of an account. pub fn set_balance(&mut self, address: Address, amount: U256) -> BackendResult<()> { - trace!(?address, ?amount, "setting account balance ZK={}", self.use_zk); - let mut account = self.backend().basic_ref(address)?.unwrap_or_default(); - account.balance = amount; - self.backend_mut().insert_account_info(address, account); - - if self.use_zk { - let (address, slot) = foundry_zksync_core::state::get_balance_storage(address); - self.backend.insert_account_storage(address, slot, amount)?; - } - Ok(()) + trace!(?address, ?amount, "setting account balance"); + self.with_strategy(|strategy, executor| strategy.set_balance(executor, address, amount)) } /// Gets the balance of an account @@ -228,19 +238,7 @@ impl Executor { /// Set the nonce of an account. pub fn set_nonce(&mut self, address: Address, nonce: u64) -> BackendResult<()> { - let mut account = self.backend().basic_ref(address)?.unwrap_or_default(); - account.nonce = nonce; - self.backend_mut().insert_account_info(address, account); - if self.use_zk { - let (address, slot) = foundry_zksync_core::state::get_nonce_storage(address); - // fetch the full nonce to preserve account's deployment nonce - let full_nonce = self.backend.storage(address, slot)?; - let full_nonce = foundry_zksync_core::state::parse_full_nonce(full_nonce); - let new_full_nonce = - foundry_zksync_core::state::new_full_nonce(nonce, full_nonce.deploy_nonce); - self.backend.insert_account_storage(address, slot, new_full_nonce)?; - } - Ok(()) + self.with_strategy(|strategy, executor| strategy.set_nonce(executor, address, nonce)) } /// Returns the nonce of an account. @@ -271,6 +269,14 @@ impl Executor { self } + #[inline] + pub fn set_transaction_other_fields(&mut self, other_fields: OtherFields) { + self.strategy + .as_mut() + .expect("failed acquiring strategy") + .set_inspect_context(other_fields); + } + /// Deploys a contract and commits the new state to the underlying database. /// /// Executes a CREATE transaction with the contract `code` and persistent database state @@ -446,57 +452,47 @@ impl Executor { 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, - &self.zk_env, - &mut self.zk_persisted_factory_deps.clone(), - Some(zk_tx.factory_deps.clone()), - zk_tx.paymaster_data.clone(), - )? - } - }; - convert_executed_result(env, inspector, result, backend.has_state_snapshot_failure()) + // 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 + backend.is_initialized = false; + backend.spec_id = env.spec_id(); + + let result = self + .strategy + .as_ref() + .expect("failed acquiring strategy") + .new_cloned() + .call_inspect(&mut backend, &mut env, &mut inspector)?; + + convert_executed_result( + env.clone(), + inspector, + result, + backend.has_state_snapshot_failure(), + ) } /// Execute the transaction configured in `env.tx`. #[instrument(name = "transact", level = "debug", skip_all)] 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, - &self.zk_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 mut result = convert_executed_result( - env, - inspector, - result_and_state, - backend.has_state_snapshot_failure(), - )?; + self.with_strategy(|strategy, executor| { + let mut inspector = executor.inspector.clone(); + let backend = &mut executor.backend; + backend.initialize(&env); + + let result_and_state = + strategy.transact_inspect(backend, &mut env, &executor.env, &mut inspector)?; + + let mut result = convert_executed_result( + env.clone(), + inspector, + result_and_state, + backend.has_state_snapshot_failure(), + )?; - self.commit(&mut result); - Ok(result) + executor.commit(&mut result); + Ok(result) + }) } /// Commit the changeset to the database and adjust `self.inspector_config` values according to @@ -662,10 +658,6 @@ impl Executor { } } - pub fn setup_zk_tx(&mut self, zk_tx: ZkTransactionMetadata) { - self.zk_tx = Some(zk_tx); - } - /// Creates the environment to use when executing a transaction in a test context /// /// If using a backend with cheatcodes, `tx.gas_price` and `block.number` will be overwritten by diff --git a/crates/evm/evm/src/executors/strategy.rs b/crates/evm/evm/src/executors/strategy.rs new file mode 100644 index 000000000..06919d8ae --- /dev/null +++ b/crates/evm/evm/src/executors/strategy.rs @@ -0,0 +1,166 @@ +use std::fmt::Debug; + +use alloy_primitives::{Address, U256}; +use alloy_serde::OtherFields; +use eyre::{Context, Result}; +use foundry_cheatcodes::strategy::{CheatcodeInspectorStrategy, EvmCheatcodeInspectorStrategy}; +use foundry_evm_core::{ + backend::{ + strategy::{BackendStrategy, EvmBackendStrategy}, + BackendResult, DatabaseExt, + }, + InspectorExt, +}; +use foundry_zksync_compiler::DualCompiledContracts; +use revm::{ + primitives::{Env, EnvWithHandlerCfg, ResultAndState}, + DatabaseRef, +}; + +use super::Executor; + +pub trait ExecutorStrategy: Debug + Send + Sync + ExecutorStrategyExt { + fn name(&self) -> &'static str; + + fn new_cloned(&self) -> Box; + + fn set_balance( + &mut self, + executor: &mut Executor, + address: Address, + amount: U256, + ) -> BackendResult<()>; + + fn set_nonce( + &mut self, + executor: &mut Executor, + address: Address, + nonce: u64, + ) -> BackendResult<()>; + + fn set_inspect_context(&mut self, other_fields: OtherFields); + + fn call_inspect( + &self, + db: &mut dyn DatabaseExt, + env: &mut EnvWithHandlerCfg, + inspector: &mut dyn InspectorExt, + ) -> eyre::Result; + + fn transact_inspect( + &mut self, + db: &mut dyn DatabaseExt, + env: &mut EnvWithHandlerCfg, + _executor_env: &EnvWithHandlerCfg, + inspector: &mut dyn InspectorExt, + ) -> eyre::Result; + + fn new_backend_strategy(&self) -> Box; + fn new_cheatcode_inspector_strategy(&self) -> Box; + + // TODO perhaps need to create fresh strategies as well +} + +pub trait ExecutorStrategyExt { + fn zksync_set_dual_compiled_contracts( + &mut self, + _dual_compiled_contracts: DualCompiledContracts, + ) { + } + + fn zksync_set_fork_env(&mut self, _fork_url: &str, _env: &Env) -> Result<()> { + Ok(()) + } +} + +#[derive(Debug, Default, Clone)] +pub struct EvmExecutorStrategy {} + +impl ExecutorStrategy for EvmExecutorStrategy { + fn name(&self) -> &'static str { + "evm" + } + + fn new_cloned(&self) -> Box { + Box::new(self.clone()) + } + + fn set_inspect_context(&mut self, _other_fields: OtherFields) {} + + /// 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. + fn call_inspect( + &self, + db: &mut dyn DatabaseExt, + env: &mut EnvWithHandlerCfg, + inspector: &mut dyn InspectorExt, + ) -> eyre::Result { + let mut evm = crate::utils::new_evm_with_inspector(db, env.clone(), inspector); + + let res = evm.transact().wrap_err("backend: failed while inspecting")?; + + env.env = evm.context.evm.inner.env; + + Ok(res) + } + + /// Executes the configured test call of the `env` without committing state changes. + /// Modifications to the state are however allowed. + /// + /// Note: in case there are any cheatcodes executed that modify the environment, this will + /// update the given `env` with the new values. + fn transact_inspect( + &mut self, + db: &mut dyn DatabaseExt, + env: &mut EnvWithHandlerCfg, + _executor_env: &EnvWithHandlerCfg, + inspector: &mut dyn InspectorExt, + ) -> eyre::Result { + let mut evm = crate::utils::new_evm_with_inspector(db, env.clone(), inspector); + + let res = evm.transact().wrap_err("backend: failed while inspecting")?; + + env.env = evm.context.evm.inner.env; + + Ok(res) + } + + fn set_balance( + &mut self, + executor: &mut Executor, + address: Address, + amount: U256, + ) -> BackendResult<()> { + trace!(?address, ?amount, "setting account balance"); + let mut account = executor.backend().basic_ref(address)?.unwrap_or_default(); + account.balance = amount; + executor.backend_mut().insert_account_info(address, account); + + Ok(()) + } + + fn set_nonce( + &mut self, + executor: &mut Executor, + address: Address, + nonce: u64, + ) -> BackendResult<()> { + let mut account = executor.backend().basic_ref(address)?.unwrap_or_default(); + account.nonce = nonce; + executor.backend_mut().insert_account_info(address, account); + + Ok(()) + } + + fn new_backend_strategy(&self) -> Box { + Box::new(EvmBackendStrategy) + } + + fn new_cheatcode_inspector_strategy(&self) -> Box { + Box::new(EvmCheatcodeInspectorStrategy::default()) + } +} + +impl ExecutorStrategyExt for EvmExecutorStrategy {} diff --git a/crates/evm/evm/src/executors/trace.rs b/crates/evm/evm/src/executors/trace.rs index 69c68442b..ee93d0920 100644 --- a/crates/evm/evm/src/executors/trace.rs +++ b/crates/evm/evm/src/executors/trace.rs @@ -6,6 +6,8 @@ use foundry_evm_traces::{InternalTraceMode, TraceMode}; use revm::primitives::{Env, SpecId}; use std::ops::{Deref, DerefMut}; +use super::strategy::ExecutorStrategy; + /// A default executor with tracing enabled pub struct TracingExecutor { executor: Executor, @@ -19,8 +21,9 @@ impl TracingExecutor { debug: bool, decode_internal: bool, alphanet: bool, + strategy: Box, ) -> Self { - let db = Backend::spawn(fork); + let db = Backend::spawn(fork, strategy.new_backend_strategy()); let trace_mode = TraceMode::Call.with_debug(debug).with_decode_internal(if decode_internal { InternalTraceMode::Full @@ -33,7 +36,7 @@ impl TracingExecutor { executor: ExecutorBuilder::new() .inspectors(|stack| stack.trace_mode(trace_mode).alphanet(alphanet)) .spec(evm_spec_id(&version.unwrap_or_default(), alphanet)) - .build(env, db), + .build(env, db, strategy), } } diff --git a/crates/forge/bin/cmd/coverage.rs b/crates/forge/bin/cmd/coverage.rs index c4b6451dc..e7ea405a3 100644 --- a/crates/forge/bin/cmd/coverage.rs +++ b/crates/forge/bin/cmd/coverage.rs @@ -13,14 +13,13 @@ use forge::{ utils::IcPcMap, MultiContractRunnerBuilder, TestOptions, }; -use foundry_cli::utils::{LoadConfig, STATIC_FUZZ_SEED}; +use foundry_cli::utils::{self, LoadConfig, STATIC_FUZZ_SEED}; use foundry_common::{compile::ProjectCompiler, fs}; use foundry_compilers::{ artifacts::{sourcemap::SourceMap, CompactBytecode, CompactDeployedBytecode, SolcLanguage}, Artifact, ArtifactId, Project, ProjectCompileOutput, }; use foundry_config::{Config, SolcReq}; -use foundry_zksync_compiler::DualCompiledContracts; use rayon::prelude::*; use semver::Version; use std::{ @@ -223,6 +222,7 @@ impl CoverageArgs { ) -> Result<()> { let root = project.paths.root; let verbosity = evm_opts.verbosity; + let strategy = utils::get_executor_strategy(&config); // Build the contract runner let env = evm_opts.evm_env().await?; @@ -237,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, strategy)?; 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 d1d510bc4..384504bef 100644 --- a/crates/forge/bin/cmd/test/mod.rs +++ b/crates/forge/bin/cmd/test/mod.rs @@ -271,6 +271,7 @@ impl TestArgs { pub async fn execute_tests(mut self) -> Result { // Merge all configs. let (mut config, mut evm_opts) = self.load_config_and_evm_opts_emit_warnings()?; + let mut strategy = utils::get_executor_strategy(&config); // Explicitly enable isolation for gas reports for more correct gas accounting. if self.gas_report { @@ -367,6 +368,8 @@ impl TestArgs { // Prepare the test builder. let config = Arc::new(config); + strategy.zksync_set_dual_compiled_contracts(dual_compiled_contracts.unwrap_or_default()); + let runner = MultiContractRunnerBuilder::new(config.clone()) .set_debug(should_debug) .set_decode_internal(decode_internal) @@ -377,14 +380,7 @@ impl TestArgs { .with_test_options(test_options.clone()) .enable_isolation(evm_opts.isolate) .alphanet(evm_opts.alphanet) - .build( - project_root, - output.clone(), - zk_output, - env, - evm_opts, - dual_compiled_contracts.unwrap_or_default(), - )?; + .build(project_root, output.clone(), zk_output, env, evm_opts, strategy)?; let mut maybe_override_mt = |flag, maybe_regex: Option<&Option>| { if let Some(Some(regex)) = maybe_regex { diff --git a/crates/forge/cache/fuzz/failures b/crates/forge/cache/fuzz/failures new file mode 100644 index 000000000..b711f7730 --- /dev/null +++ b/crates/forge/cache/fuzz/failures @@ -0,0 +1,9 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc dbe270c3d1130d36d86d57df1c3ce779c2fcd8c13708e7d4573e7d47a4e437fe # shrinks to 0x7297e06f0000000000000000000000000000000000000000000000000000000000040c8d +cc 76930098b76c5e1cb12851857e63d1f1db4beeb22872c2c58057e20f9f6c2b27 # shrinks to 0x7297e06f00000000000000000000000000000000000000000000000000000000000000a0 +cc 17831930cdb03728f3d6e08676b25c54152a36184f3c1cfad441af2b3511fc12 # shrinks to 0x7297e06f000000000000000000000000000000000000000000642d5f2141b08300c99c29 diff --git a/crates/forge/src/multi_runner.rs b/crates/forge/src/multi_runner.rs index c6a43c1c5..f2f901590 100644 --- a/crates/forge/src/multi_runner.rs +++ b/crates/forge/src/multi_runner.rs @@ -18,7 +18,7 @@ use foundry_config::Config; use foundry_evm::{ backend::Backend, decode::RevertDecoder, - executors::ExecutorBuilder, + executors::{strategy::ExecutorStrategy, ExecutorBuilder}, fork::CreateFork, inspectors::CheatsConfig, opts::EvmOpts, @@ -26,7 +26,6 @@ use foundry_evm::{ traces::{InternalTraceMode, TraceMode}, }; use foundry_linking::{LinkOutput, Linker}; -use foundry_zksync_compiler::DualCompiledContracts; use rayon::prelude::*; use revm::primitives::SpecId; @@ -85,10 +84,8 @@ pub struct MultiContractRunner { pub libs_to_deploy: Vec, /// Library addresses used to link contracts. pub libraries: Libraries, - /// Dual compiled contracts - pub dual_compiled_contracts: DualCompiledContracts, - /// Use zk runner. - pub use_zk: bool, + /// Execution strategy. + pub strategy: Box, } impl MultiContractRunner { @@ -181,8 +178,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.new_backend_strategy()); let find_timer = Instant::now(); let contracts = self.matching_contracts(filter).collect::>(); @@ -254,9 +250,7 @@ impl MultiContractRunner { Some(self.known_contracts.clone()), Some(artifact_id.name.clone()), Some(artifact_id.version.clone()), - self.dual_compiled_contracts.clone(), - self.use_zk, - None, + self.strategy.new_cheatcode_inspector_strategy(), ); let trace_mode = TraceMode::default() @@ -273,11 +267,10 @@ impl MultiContractRunner { .enable_isolation(self.isolation) .alphanet(self.alphanet) }) - .use_zk_vm(self.use_zk) .spec(self.evm_spec) .gas_limit(self.evm_opts.gas_limit()) .legacy_assertions(self.config.legacy_assertions) - .build(self.env.clone(), db); + .build(self.env.clone(), db, self.strategy.new_cloned()); if !enabled!(tracing::Level::TRACE) { span_name = get_contract_name(&identifier); @@ -414,9 +407,8 @@ impl MultiContractRunnerBuilder { zk_output: Option, env: revm::primitives::Env, evm_opts: EvmOpts, - dual_compiled_contracts: DualCompiledContracts, + strategy: Box, ) -> Result { - let use_zk = zk_output.is_some(); let mut known_contracts = ContractsByArtifact::default(); let output = output.with_stripped_file_prefixes(root); let linker = Linker::new(root, output.artifact_ids().collect()); @@ -458,7 +450,7 @@ impl MultiContractRunnerBuilder { } } - if !use_zk { + if zk_output.is_none() { known_contracts = ContractsByArtifact::new(linked_contracts); } else if let Some(zk_output) = zk_output { let zk_contracts = zk_output.with_stripped_file_prefixes(root).into_artifacts(); @@ -516,8 +508,7 @@ impl MultiContractRunnerBuilder { known_contracts, 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 094ce9e67..067440c83 100644 --- a/crates/forge/src/runner.rs +++ b/crates/forge/src/runner.rs @@ -150,12 +150,12 @@ impl ContractRunner<'_> { // to simulate EVM behavior where only the tx that deploys the test contract increments the // nonce. if let Some(cheatcodes) = &mut self.executor.inspector.cheatcodes { - if let Some(zk_startup_migration) = &mut cheatcodes.zk_startup_migration { - debug!("test contract deployed, allowing startup storage migration"); - zk_startup_migration.allow(); - } - debug!("test contract deployed, allowing persisting next nonce update"); - cheatcodes.zk_persist_nonce_update.persist_next(); + debug!("test contract deployed"); + cheatcodes + .strategy + .as_mut() + .expect("failed acquiring strategy") + .base_contract_deployed(); } // Optionally call the `setUp` function diff --git a/crates/forge/tests/cli/zk_script.rs b/crates/forge/tests/cli/zk_script.rs index 2c3c37ddf..c4153043d 100644 --- a/crates/forge/tests/cli/zk_script.rs +++ b/crates/forge/tests/cli/zk_script.rs @@ -1,6 +1,7 @@ //! Contains tests related to `forge script` with zksync. use foundry_test_utils::util::OutputExt; +use foundry_zksync_core::ZkTransactionMetadata; forgetest_async!(test_zk_can_execute_script_with_arguments, |prj, cmd| { #[derive(serde::Deserialize, Debug)] @@ -12,15 +13,13 @@ forgetest_async!(test_zk_can_execute_script_with_arguments, |prj, cmd| { #[derive(serde::Deserialize, Debug)] #[allow(dead_code)] struct ZkTransaction { - zk: Zk, + transaction: ZkTransactionInner, } #[derive(serde::Deserialize, Debug)] - #[serde(rename_all = "camelCase")] #[allow(dead_code)] - struct Zk { - #[serde(default)] - factory_deps: Vec>, + struct ZkTransactionInner { + zksync: ZkTransactionMetadata, } let node = foundry_test_utils::ZkSyncNode::start(); diff --git a/crates/forge/tests/it/test_helpers.rs b/crates/forge/tests/it/test_helpers.rs index fe06d785f..cd80f6d52 100644 --- a/crates/forge/tests/it/test_helpers.rs +++ b/crates/forge/tests/it/test_helpers.rs @@ -3,9 +3,10 @@ use alloy_chains::NamedChain; use alloy_primitives::U256; use forge::{ - revm::primitives::SpecId, MultiContractRunner, MultiContractRunnerBuilder, TestOptions, - TestOptionsBuilder, + executors::strategy::EvmExecutorStrategy, revm::primitives::SpecId, MultiContractRunner, + MultiContractRunnerBuilder, TestOptions, TestOptionsBuilder, }; +use foundry_cli::utils; use foundry_compilers::{ artifacts::{EvmVersion, Libraries, Settings}, utils::RuntimeOrHandle, @@ -210,6 +211,7 @@ impl ForgeTestProfile { zk_config.cache_path = self.root().join("zk").join("cache"); zk_config.evm_version = EvmVersion::London; + zk_config.zksync.compile = true; zk_config.zksync.startup = true; zk_config.zksync.fallback_oz = true; zk_config.zksync.optimizer_mode = '3'; @@ -322,13 +324,14 @@ impl ForgeTestData { let sender = config.sender; + let strategy = utils::get_executor_strategy(&config); let mut builder = self.base_runner(); builder.config = Arc::new(config); builder .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, strategy) .unwrap() } @@ -356,13 +359,15 @@ impl ForgeTestData { test_opts.fuzz.no_zksync_reserved_addresses = zk_config.fuzz.no_zksync_reserved_addresses; let sender = zk_config.sender; + let mut strategy = utils::get_executor_strategy(&zk_config); + strategy.zksync_set_dual_compiled_contracts(dual_compiled_contracts); let mut builder = self.base_runner(); builder.config = Arc::new(zk_config); builder .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, strategy) .unwrap() } @@ -377,7 +382,7 @@ impl ForgeTestData { None, opts.local_evm_env(), opts, - Default::default(), + Box::new(EvmExecutorStrategy::default()), ) .unwrap() } @@ -394,7 +399,14 @@ 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, + Box::new(EvmExecutorStrategy::default()), + ) .unwrap() } } diff --git a/crates/forge/tests/it/zk/contracts.rs b/crates/forge/tests/it/zk/contracts.rs index 891e5849f..758b1b709 100644 --- a/crates/forge/tests/it/zk/contracts.rs +++ b/crates/forge/tests/it/zk/contracts.rs @@ -23,7 +23,8 @@ async fn test_zk_contract_can_call_function() { #[tokio::test(flavor = "multi_thread")] async fn test_zk_contract_persisted_contracts_after_fork() { let runner = TEST_DATA_DEFAULT.runner_zksync(); - let filter = Filter::new("testZkContractsPersistedDeployedContractNoArgs|testZkContractsPersistedDeployedContractArgs", "ZkContractsTest", ".*"); + let filter = + Filter::new("testZkContractsPersistedDeployedContractNoArgs|testZkContractsPersistedDeployedContractArgs", "ZkContractsTest", ".*"); TestConfig::with_filter(runner, filter).evm_spec(SpecId::SHANGHAI).run().await; } diff --git a/crates/script-sequence/src/transaction.rs b/crates/script-sequence/src/transaction.rs index 04c33199e..7f72a4d30 100644 --- a/crates/script-sequence/src/transaction.rs +++ b/crates/script-sequence/src/transaction.rs @@ -1,6 +1,5 @@ use alloy_primitives::{Address, Bytes, B256}; use foundry_common::TransactionMaybeSigned; -use foundry_zksync_core::ZkTransactionMetadata; use revm_inspectors::tracing::types::CallKind; use serde::{Deserialize, Serialize}; @@ -32,8 +31,6 @@ pub struct TransactionWithMetadata { pub transaction: TransactionMaybeSigned, pub additional_contracts: Vec, pub is_fixed_gas_limit: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub zk: Option, } fn default_string() -> Option { @@ -61,7 +58,6 @@ impl TransactionWithMetadata { is_fixed_gas_limit: Default::default(), additional_contracts: Default::default(), rpc: Default::default(), - zk: Default::default(), } } diff --git a/crates/script/Cargo.toml b/crates/script/Cargo.toml index 96d00e09d..d30aad360 100644 --- a/crates/script/Cargo.toml +++ b/crates/script/Cargo.toml @@ -24,6 +24,7 @@ foundry-cheatcodes.workspace = true foundry-wallets.workspace = true foundry-linking.workspace = true forge-script-sequence.workspace = true +foundry-strategy-zksync.workspace = true foundry-zksync-core.workspace = true foundry-zksync-compiler.workspace = true diff --git a/crates/script/src/broadcast.rs b/crates/script/src/broadcast.rs index 0b958894e..3171baaba 100644 --- a/crates/script/src/broadcast.rs +++ b/crates/script/src/broadcast.rs @@ -29,7 +29,7 @@ use foundry_common::{ TransactionMaybeSigned, }; use foundry_config::Config; -use foundry_zksync_core::{convert::ConvertH160, ZkTransactionMetadata}; +use foundry_zksync_core::convert::ConvertH160; use futures::{future::join_all, StreamExt}; use itertools::Itertools; use std::{cmp::Ordering, sync::Arc}; @@ -66,12 +66,18 @@ pub async fn send_transaction( provider: Arc, zk_provider: Arc>, mut kind: SendTransactionKind<'_>, - zk: Option<&ZkTransactionMetadata>, sequential_broadcast: bool, is_fixed_gas_limit: bool, estimate_via_rpc: bool, estimate_multiplier: u64, ) -> Result { + let zk_tx_meta = + if let SendTransactionKind::Raw(tx, _) | SendTransactionKind::Unlocked(tx) = &mut kind { + foundry_strategy_zksync::get_zksync_transaction_metadata(&tx.other) + } else { + None + }; + if let SendTransactionKind::Raw(tx, _) | SendTransactionKind::Unlocked(tx) = &mut kind { if sequential_broadcast { let from = tx.from.expect("no sender"); @@ -102,7 +108,7 @@ pub async fn send_transaction( // gas to be re-estimated right before broadcasting. if !is_fixed_gas_limit && estimate_via_rpc { // We skip estimating gas for zk transactions as the fee is estimated manually later. - if zk.is_none() { + if zk_tx_meta.is_none() { estimate_gas(tx, &provider, estimate_multiplier).await?; } } @@ -118,14 +124,16 @@ pub async fn send_transaction( SendTransactionKind::Raw(tx, signer) => { debug!("sending transaction: {:?}", tx); - if let Some(zk) = zk { + if let Some(zk_tx_meta) = zk_tx_meta { let mut inner = tx.inner.clone(); inner.transaction_type = Some(TxType::Eip712 as u8); let mut zk_tx: ZkTransactionRequest = inner.into(); - if !zk.factory_deps.is_empty() { - zk_tx.set_factory_deps(zk.factory_deps.iter().map(Bytes::from_iter).collect()); + if !zk_tx_meta.factory_deps.is_empty() { + zk_tx.set_factory_deps( + zk_tx_meta.factory_deps.iter().map(Bytes::from_iter).collect(), + ); } - if let Some(paymaster_data) = &zk.paymaster_data { + if let Some(paymaster_data) = &zk_tx_meta.paymaster_data { zk_tx.set_paymaster_params( alloy_zksync::network::unsigned_tx::eip712::PaymasterParams { paymaster: paymaster_data.paymaster.to_address(), @@ -336,7 +344,6 @@ impl BundledState { .skip(already_broadcasted) .map(|tx_with_metadata| { let is_fixed_gas_limit = tx_with_metadata.is_fixed_gas_limit; - let zk = tx_with_metadata.zk.clone(); let kind = match tx_with_metadata.tx().clone() { TransactionMaybeSigned::Signed { tx, .. } => { @@ -367,7 +374,7 @@ impl BundledState { } }; - Ok((kind, zk, is_fixed_gas_limit)) + Ok((kind, is_fixed_gas_limit)) }) .collect::>>()?; @@ -396,12 +403,11 @@ impl BundledState { batch_number * batch_size, batch_number * batch_size + std::cmp::min(batch_size, batch.len()) - 1 )); - for (kind, zk, is_fixed_gas_limit) in batch { + for (kind, is_fixed_gas_limit) in batch { let fut = send_transaction( provider.clone(), zk_provider.clone(), kind.clone(), - zk.as_ref(), sequential_broadcast, *is_fixed_gas_limit, estimate_via_rpc, diff --git a/crates/script/src/lib.rs b/crates/script/src/lib.rs index ea6a0545b..c56058bc4 100644 --- a/crates/script/src/lib.rs +++ b/crates/script/src/lib.rs @@ -19,7 +19,6 @@ use alloy_primitives::{ Address, Bytes, Log, TxKind, U256, }; use alloy_signer::Signer; -use alloy_zksync::provider::{zksync_provider, ZksyncProvider}; use broadcast::next_nonce; use build::PreprocessedState; use clap::{Parser, ValueHint}; @@ -29,7 +28,7 @@ use forge_script_sequence::{AdditionalContract, NestedValue}; use forge_verify::RetryArgs; use foundry_cli::{ opts::{CoreBuildArgs, GlobalOpts}, - utils::LoadConfig, + utils::{self, LoadConfig}, }; use foundry_common::{ abi::{encode_function_args, get_func}, @@ -58,7 +57,6 @@ use foundry_evm::{ }; use foundry_wallets::MultiWalletOpts; use foundry_zksync_compiler::DualCompiledContracts; -use foundry_zksync_core::vm::ZkEnv; use serde::Serialize; use std::path::PathBuf; @@ -593,13 +591,14 @@ impl ScriptConfig { ) -> Result { trace!("preparing script runner"); let env = self.evm_opts.evm_env().await?; + let mut strategy = utils::get_executor_strategy(&self.config); let db = if let Some(fork_url) = self.evm_opts.fork_url.as_ref() { match self.backends.get(fork_url) { 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.new_backend_strategy()); self.backends.insert(fork_url.clone(), backend.clone()); backend } @@ -608,7 +607,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.new_backend_strategy()) }; // We need to enable tracing to decode contract names: local or external. @@ -622,65 +621,34 @@ impl ScriptConfig { .gas_limit(self.evm_opts.gas_limit()) .legacy_assertions(self.config.legacy_assertions); - let use_zk = self.config.zksync.run_in_zk_mode(); - let mut maybe_zk_env = None; - if use_zk { - if let Some(fork_url) = &self.evm_opts.fork_url { - let provider = - zksync_provider().with_recommended_fillers().on_http(fork_url.parse()?); - // TODO(zk): switch to getFeeParams call when it is implemented for anvil-zksync - let maybe_details = - provider.get_block_details(env.block.number.try_into()?).await?; - if let Some(details) = maybe_details { - let zk_env = ZkEnv { - l1_gas_price: details - .l1_gas_price - .try_into() - .expect("failed to convert l1_gas_price to u64"), - fair_l2_gas_price: details - .l2_fair_gas_price - .try_into() - .expect("failed to convert fair_l2_gas_price to u64"), - fair_pubdata_price: details - .fair_pubdata_price - // TODO(zk): None as a value might mean L1Pegged model - // we need to find out if it will ever be relevant to - // us - .unwrap_or_default() - .try_into() - .expect("failed to convert fair_pubdata_price to u64"), - }; - builder = builder.zk_env(zk_env.clone()); - maybe_zk_env = Some(zk_env); - } - }; - } if let Some((known_contracts, script_wallets, target, dual_compiled_contracts)) = cheats_data { - builder = builder - .inspectors(|stack| { - stack - .cheatcodes( - CheatsConfig::new( - &self.config, - self.evm_opts.clone(), - Some(known_contracts), - Some(target.name), - Some(target.version), - dual_compiled_contracts, - use_zk, - maybe_zk_env, - ) - .into(), + strategy.zksync_set_dual_compiled_contracts(dual_compiled_contracts); + + if let Some(fork_url) = &self.evm_opts.fork_url { + strategy.zksync_set_fork_env(fork_url, &env)?; + } + + builder = builder.inspectors(|stack| { + stack + .cheatcodes( + CheatsConfig::new( + &self.config, + self.evm_opts.clone(), + Some(known_contracts), + Some(target.name), + Some(target.version), + strategy.new_cheatcode_inspector_strategy(), ) - .wallets(script_wallets) - .enable_isolation(self.evm_opts.isolate) - }) - .use_zk_vm(use_zk); + .into(), + ) + .wallets(script_wallets) + .enable_isolation(self.evm_opts.isolate) + }); } - let executor = builder.build(env, db); + let executor = builder.build(env, db, strategy); Ok(ScriptRunner::new(executor, self.evm_opts.clone())) } } diff --git a/crates/script/src/runner.rs b/crates/script/src/runner.rs index 34cc54e28..20ad42cf4 100644 --- a/crates/script/src/runner.rs +++ b/crates/script/src/runner.rs @@ -3,6 +3,7 @@ use crate::build::ScriptPredeployLibraries; use alloy_eips::eip7702::SignedAuthorization; use alloy_primitives::{Address, Bytes, TxKind, U256}; use alloy_rpc_types::TransactionRequest; +use alloy_serde::OtherFields; use eyre::Result; use foundry_cheatcodes::BroadcastableTransaction; use foundry_config::Config; @@ -13,7 +14,6 @@ use foundry_evm::{ revm::interpreter::{return_ok, InstructionResult}, traces::{TraceKind, Traces}, }; -use foundry_zksync_core::ZkTransactionMetadata; use std::collections::VecDeque; /// Drives script execution @@ -81,7 +81,6 @@ impl ScriptRunner { ..Default::default() } .into(), - zk_tx: None, }) }), ScriptPredeployLibraries::Create2(libraries, salt) => { @@ -117,7 +116,6 @@ impl ScriptRunner { ..Default::default() } .into(), - zk_tx: None, }); } @@ -175,12 +173,12 @@ impl ScriptRunner { // to simulate EVM behavior where only the tx that deploys the test contract increments the // nonce. if let Some(cheatcodes) = &mut self.executor.inspector.cheatcodes { - if let Some(zk_startup_migration) = &mut cheatcodes.zk_startup_migration { - debug!("script deployed, allowing startup storage migration"); - zk_startup_migration.allow(); - } - debug!("script deployed, allowing persisting next nonce update"); - cheatcodes.zk_persist_nonce_update.persist_next(); + debug!("script deployed"); + cheatcodes + .strategy + .as_mut() + .expect("failed acquiring strategy") + .base_contract_deployed(); } // Optionally call the `setUp` function @@ -260,11 +258,10 @@ impl ScriptRunner { calldata: Option, value: Option, authorization_list: Option>, - (use_zk, zk_tx): (bool, Option), + other_fields: Option, ) -> Result { - self.executor.use_zk = use_zk; - if let Some(zk_tx) = zk_tx { - self.executor.setup_zk_tx(zk_tx); + if let Some(other_fields) = other_fields { + self.executor.set_transaction_other_fields(other_fields); } if let Some(to) = to { diff --git a/crates/script/src/simulate.rs b/crates/script/src/simulate.rs index 2b6012b10..4293c8929 100644 --- a/crates/script/src/simulate.rs +++ b/crates/script/src/simulate.rs @@ -16,7 +16,7 @@ use eyre::{Context, Result}; use forge_script_sequence::{ScriptSequence, TransactionWithMetadata}; use foundry_cheatcodes::Wallets; use foundry_cli::utils::{has_different_gas_calc, now}; -use foundry_common::ContractData; +use foundry_common::{ContractData, TransactionMaybeSigned}; use foundry_evm::traces::{decode_trace_arena, render_trace_arena}; use futures::future::{join_all, try_join_all}; use parking_lot::RwLock; @@ -61,7 +61,7 @@ impl PreSimulationState { let nonce = tx.transaction.nonce().expect("all transactions should have a sender"); let to = tx.transaction.to(); - let mut builder = ScriptTransactionBuilder::new(tx.transaction, rpc, tx.zk_tx); + let mut builder = ScriptTransactionBuilder::new(tx.transaction, rpc); if let Some(TxKind::Call(_)) = to { builder.set_call(&address_to_abi, &self.execution_artifacts.decoder)?; @@ -114,9 +114,12 @@ impl PreSimulationState { .into_iter() .map(|mut transaction| async { let mut runner = runners.get(&transaction.rpc).expect("invalid rpc url").write(); - let zk_metadata = transaction.zk.clone(); let tx = transaction.tx_mut(); + let other_fields = match &tx { + TransactionMaybeSigned::Unsigned(tx) => Some(tx.other.clone()), + _ => None, + }; let to = if let Some(TxKind::Call(to)) = tx.to() { Some(to) } else { None }; let result = runner .simulate( @@ -126,7 +129,7 @@ impl PreSimulationState { tx.input().map(Bytes::copy_from_slice), tx.value(), tx.authorization_list(), - (self.script_config.config.zksync.run_in_zk_mode(), zk_metadata), + other_fields, ) .wrap_err("Internal EVM error during simulation")?; diff --git a/crates/script/src/transaction.rs b/crates/script/src/transaction.rs index 4bef28d57..ca6a62269 100644 --- a/crates/script/src/transaction.rs +++ b/crates/script/src/transaction.rs @@ -5,7 +5,6 @@ use eyre::Result; use forge_script_sequence::TransactionWithMetadata; use foundry_common::{fmt::format_token_raw, ContractData, TransactionMaybeSigned, SELECTOR_LEN}; use foundry_evm::{constants::DEFAULT_CREATE2_DEPLOYER, traces::CallTraceDecoder}; -use foundry_zksync_core::ZkTransactionMetadata; use itertools::Itertools; use revm_inspectors::tracing::types::CallKind; use std::collections::BTreeMap; @@ -16,16 +15,11 @@ pub struct ScriptTransactionBuilder { } impl ScriptTransactionBuilder { - pub fn new( - transaction: TransactionMaybeSigned, - rpc: String, - zk: Option, - ) -> Self { + pub fn new(transaction: TransactionMaybeSigned, rpc: String) -> Self { let mut transaction = TransactionWithMetadata::from_tx_request(transaction); transaction.rpc = rpc; // If tx.gas is already set that means it was specified in script transaction.is_fixed_gas_limit = transaction.tx().gas().is_some(); - transaction.zk = zk; Self { transaction } } diff --git a/crates/strategy/zksync/Cargo.toml b/crates/strategy/zksync/Cargo.toml new file mode 100644 index 000000000..22400a8c1 --- /dev/null +++ b/crates/strategy/zksync/Cargo.toml @@ -0,0 +1,39 @@ +[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] +alloy-sol-types.workspace = true +alloy-json-abi.workspace = true +alloy-zksync.workspace = true +foundry-common.workspace = true +foundry-config.workspace = true +foundry-evm.workspace = true +foundry-evm-core.workspace = true +foundry-cheatcodes.workspace = true +foundry-zksync-core.workspace = true +foundry-zksync-compiler.workspace = true + +alloy-primitives.workspace = true +alloy-rpc-types.workspace = true + +zksync_types.workspace = true + +eyre.workspace = true +revm.workspace = true +itertools.workspace = true +tracing.workspace = true +tokio.workspace = true +serde.workspace = true +serde_json.workspace = true +semver.workspace = true diff --git a/crates/strategy/zksync/src/backend.rs b/crates/strategy/zksync/src/backend.rs new file mode 100644 index 000000000..ee75740a7 --- /dev/null +++ b/crates/strategy/zksync/src/backend.rs @@ -0,0 +1,315 @@ +use std::collections::hash_map::Entry; + +use alloy_primitives::{map::HashMap, Address, U256}; +use foundry_evm::backend::strategy::BackendStrategyExt; +use foundry_evm_core::backend::{ + strategy::{ + BackendStrategy, BackendStrategyForkInfo, EvmBackendMergeStrategy, EvmBackendStrategy, + }, + BackendInner, Fork, ForkDB, FoundryEvmInMemoryDB, +}; +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 revm::{db::CacheDB, primitives::HashSet, DatabaseRef, JournaledState}; +use serde::{Deserialize, Serialize}; +use tracing::trace; + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct ZksyncBackendStrategy { + evm: EvmBackendStrategy, + /// Store storage keys per contract address for immutable variables. + persistent_immutable_keys: HashMap>, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +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 ZksyncBackendStrategy { + fn name(&self) -> &'static str { + "zk" + } + + fn new_cloned(&self) -> Box { + Box::new(self.clone()) + } + + /// 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 merge_journaled_state_data( + &self, + addr: Address, + active_journaled_state: &JournaledState, + fork_journaled_state: &mut JournaledState, + ) { + self.evm.merge_journaled_state_data(addr, active_journaled_state, fork_journaled_state); + let zk_state = + &ZksyncMergeState { persistent_immutable_keys: &self.persistent_immutable_keys }; + ZksyncBackendMerge::merge_zk_journaled_state_data( + addr, + active_journaled_state, + fork_journaled_state, + zk_state, + ); + } + + fn merge_db_account_data(&self, addr: Address, active: &ForkDB, fork_db: &mut ForkDB) { + self.evm.merge_db_account_data(addr, active, fork_db); + let zk_state = + &ZksyncMergeState { persistent_immutable_keys: &self.persistent_immutable_keys }; + ZksyncBackendMerge::merge_zk_account_data(addr, active, fork_db, zk_state); + } +} + +impl ZksyncBackendStrategy { + /// 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 = + &ZksyncMergeState { persistent_immutable_keys: &self.persistent_immutable_keys }; + if let Some(db) = fork_info.active_fork.map(|f| &f.db) { + ZksyncBackendMerge::merge_account_data( + accounts, + db, + active_journaled_state, + target_fork, + zk_state, + ) + } else { + ZksyncBackendMerge::merge_account_data( + accounts, + mem_db, + active_journaled_state, + target_fork, + zk_state, + ) + } + } +} + +impl BackendStrategyExt for ZksyncBackendStrategy { + fn zksync_save_immutable_storage(&mut self, addr: Address, keys: HashSet) { + self.persistent_immutable_keys + .entry(addr) + .and_modify(|entry| entry.extend(&keys)) + .or_insert(keys); + } +} + +pub(crate) struct ZksyncBackendMerge; + +/// Defines the zksync specific state to help during merge. +pub(crate) struct ZksyncMergeState<'a> { + persistent_immutable_keys: &'a HashMap>, +} + +impl ZksyncBackendMerge { + /// 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: &ZksyncMergeState<'_>, + ) { + for addr in accounts.into_iter() { + EvmBackendMergeStrategy::merge_db_account_data(addr, active, &mut target_fork.db); + Self::merge_zk_account_data(addr, active, &mut target_fork.db, zk_state); + EvmBackendMergeStrategy::merge_journaled_state_data( + addr, + active_journaled_state, + &mut target_fork.journaled_state, + ); + Self::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: &ZksyncMergeState<'_>, + ) { + 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: &ZksyncMergeState<'_>, + ) { + 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, + ); + } + } + } +} diff --git a/crates/strategy/zksync/src/cheatcode.rs b/crates/strategy/zksync/src/cheatcode.rs new file mode 100644 index 000000000..b29f8a741 --- /dev/null +++ b/crates/strategy/zksync/src/cheatcode.rs @@ -0,0 +1,1529 @@ +use std::{fs, path::PathBuf, sync::Arc}; + +use alloy_json_abi::ContractObject; +use alloy_primitives::{keccak256, map::HashMap, Address, Bytes, FixedBytes, TxKind, B256, U256}; +use alloy_rpc_types::{ + request::{TransactionInput, TransactionRequest}, + serde_helpers::WithOtherFields, +}; +use alloy_sol_types::SolValue; +use foundry_cheatcodes::{ + journaled_account, make_acc_non_empty, + strategy::{ + CheatcodeInspectorStrategy, CheatcodeInspectorStrategyExt, EvmCheatcodeInspectorStrategy, + }, + Broadcast, BroadcastableTransaction, BroadcastableTransactions, Cheatcodes, CheatcodesExecutor, + CheatsConfig, CheatsCtxt, CommonCreateInput, DealRecord, Ecx, Error, InnerEcx, Result, Vm, +}; +use foundry_common::TransactionMaybeSigned; +use foundry_config::fs_permissions::FsAccessKind; +use foundry_evm::{ + backend::{DatabaseError, LocalForkId}, + constants::{DEFAULT_CREATE2_DEPLOYER, DEFAULT_CREATE2_DEPLOYER_CODE}, +}; +use foundry_evm_core::{ + backend::DatabaseExt, + constants::{CHEATCODE_ADDRESS, CHEATCODE_CONTRACT_HASH}, +}; +use foundry_zksync_compiler::{ContractType, DualCompiledContract, DualCompiledContracts}; +use foundry_zksync_core::{ + convert::{ConvertAddress, ConvertH160, ConvertH256, ConvertRU256, ConvertU256}, + get_account_code_key, get_balance_key, get_nonce_key, + vm::ZkEnv, + PaymasterParams, ZkPaymasterData, ZkTransactionMetadata, ACCOUNT_CODE_STORAGE_ADDRESS, + CONTRACT_DEPLOYER_ADDRESS, DEFAULT_CREATE2_DEPLOYER_ZKSYNC, H256, KNOWN_CODES_STORAGE_ADDRESS, + L2_BASE_TOKEN_ADDRESS, NONCE_HOLDER_ADDRESS, +}; +use itertools::Itertools; +use revm::{ + interpreter::{ + opcode as op, CallInputs, CallOutcome, CreateOutcome, Gas, InstructionResult, Interpreter, + InterpreterResult, + }, + primitives::{ + AccountInfo, Bytecode, CreateScheme, Env, EvmStorageSlot, ExecutionResult, HashSet, Output, + SignedAuthorization, KECCAK_EMPTY, + }, +}; +use semver::Version; +use tracing::{debug, error, info, warn}; +use zksync_types::{ + block::{pack_block_info, unpack_block_info}, + utils::{decompose_full_nonce, nonces_to_full_nonce}, + CURRENT_VIRTUAL_BLOCK_INFO_POSITION, SYSTEM_CONTEXT_ADDRESS, +}; + +/// Key used to set transaction metadata in other fields. +pub const ZKSYNC_TRANSACTION_OTHER_FIELDS_KEY: &str = "zksync"; + +macro_rules! fmt_err { + ($msg:literal $(,)?) => { + Error::fmt(::std::format_args!($msg)) + }; + ($err:expr $(,)?) => { + >::from($err) + }; + ($fmt:expr, $($arg:tt)*) => { + Error::fmt(::std::format_args!($fmt, $($arg)*)) + }; +} + +macro_rules! bail { + ($msg:literal $(,)?) => { + return ::std::result::Result::Err(fmt_err!($msg)) + }; + ($err:expr $(,)?) => { + return ::std::result::Result::Err(fmt_err!($err)) + }; + ($fmt:expr, $($arg:tt)*) => { + return ::std::result::Result::Err(fmt_err!($fmt, $($arg)*)) + }; +} + +#[derive(Debug, Default, Clone)] +pub struct ZksyncCheatcodeInspectorStrategy { + evm: EvmCheatcodeInspectorStrategy, + + pub using_zk_vm: bool, + + /// 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, + + /// Stores the factory deps that were detected as part of CREATE2 deployer call. + /// Must be cleared every call. + pub set_deployer_call_input_factory_deps: Vec>, + + /// Era Vm environment + pub zk_env: ZkEnv, +} + +impl ZksyncCheatcodeInspectorStrategy { + pub fn new(dual_compiled_contracts: DualCompiledContracts, zk_env: ZkEnv) -> Self { + // 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(); + + let mut dual_compiled_contracts = dual_compiled_contracts; + 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 { + evm: EvmCheatcodeInspectorStrategy::default(), + using_zk_vm: false, // We need to migrate once on initialize_interp + skip_zk_vm: false, + skip_zk_vm_addresses: Default::default(), + record_next_create_address: Default::default(), + paymaster_params: Default::default(), + dual_compiled_contracts, + zk_startup_migration: ZkStartupMigration::Defer, + zk_use_factory_deps: Default::default(), + persisted_factory_deps: Default::default(), + zk_persist_nonce_update: Default::default(), + set_deployer_call_input_factory_deps: Default::default(), + zk_env, + } + } +} + +/// 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 ZksyncCheatcodeInspectorStrategy { + fn name(&self) -> &'static str { + "zk" + } + + fn new_cloned(&self) -> Box { + Box::new(self.clone()) + } + + fn get_nonce(&mut self, ccx: &mut CheatsCtxt<'_, '_, '_, '_>, address: Address) -> Result { + if !self.using_zk_vm { + return self.evm.get_nonce(ccx, address); + } + + let nonce = foundry_zksync_core::nonce(address, ccx.ecx) as u64; + Ok(nonce) + } + + fn base_contract_deployed(&mut self) { + debug!("allowing startup storage migration"); + self.zk_startup_migration.allow(); + debug!("allowing persisting next nonce update"); + self.zk_persist_nonce_update.persist_next(); + } + + fn cheatcode_get_nonce( + &mut self, + ccx: &mut CheatsCtxt<'_, '_, '_, '_>, + address: Address, + ) -> foundry_cheatcodes::Result { + if !self.using_zk_vm { + let nonce = self.evm.get_nonce(ccx, address)?; + return Ok(nonce.abi_encode()); + } + + let nonce = foundry_zksync_core::cheatcodes::get_nonce(address, ccx.ecx); + Ok(nonce.abi_encode()) + } + + fn cheatcode_roll(&mut self, ccx: &mut CheatsCtxt<'_, '_, '_, '_>, new_height: U256) -> Result { + if !self.using_zk_vm { + return self.evm.cheatcode_roll(ccx, new_height); + } + + ccx.ecx.env.block.number = new_height; + foundry_zksync_core::cheatcodes::roll(new_height, ccx.ecx); + Ok(Default::default()) + } + + fn cheatcode_warp( + &mut self, + ccx: &mut CheatsCtxt<'_, '_, '_, '_>, + new_timestamp: U256, + ) -> Result { + if !self.using_zk_vm { + return self.evm.cheatcode_warp(ccx, new_timestamp); + } + + ccx.ecx.env.block.number = new_timestamp; + foundry_zksync_core::cheatcodes::warp(new_timestamp, ccx.ecx); + Ok(Default::default()) + } + + fn cheatcode_deal( + &mut self, + ccx: &mut CheatsCtxt<'_, '_, '_, '_>, + address: Address, + new_balance: U256, + ) -> Result { + if !self.using_zk_vm { + return self.evm.cheatcode_deal(ccx, address, new_balance); + } + + let old_balance = foundry_zksync_core::cheatcodes::deal(address, new_balance, ccx.ecx); + let record = DealRecord { address, old_balance, new_balance }; + ccx.state.eth_deals.push(record); + Ok(Default::default()) + } + + fn cheatcode_etch( + &mut self, + ccx: &mut CheatsCtxt<'_, '_, '_, '_>, + target: Address, + new_runtime_bytecode: &Bytes, + ) -> Result { + if !self.using_zk_vm { + return self.evm.cheatcode_etch(ccx, target, new_runtime_bytecode); + } + + foundry_zksync_core::cheatcodes::etch(target, new_runtime_bytecode, ccx.ecx); + Ok(Default::default()) + } + + fn cheatcode_reset_nonce( + &mut self, + ccx: &mut CheatsCtxt<'_, '_, '_, '_>, + account: Address, + ) -> Result { + if !self.using_zk_vm { + return self.evm.cheatcode_reset_nonce(ccx, account); + } + + foundry_zksync_core::cheatcodes::set_nonce(account, U256::ZERO, ccx.ecx); + Ok(Default::default()) + } + + fn cheatcode_set_nonce( + &mut self, + ccx: &mut CheatsCtxt<'_, '_, '_, '_>, + account: Address, + new_nonce: u64, + ) -> Result { + if !self.using_zk_vm { + return self.evm.cheatcode_set_nonce(ccx, account, new_nonce); + } + + // nonce must increment only + let current = foundry_zksync_core::cheatcodes::get_nonce(account, ccx.ecx); + if U256::from(new_nonce) < current { + return Err(fmt_err!( + "new nonce ({new_nonce}) must be strictly equal to or higher than the \ + account's current nonce ({current})" + )); + } + + foundry_zksync_core::cheatcodes::set_nonce(account, U256::from(new_nonce), ccx.ecx); + Ok(Default::default()) + } + + fn cheatcode_set_nonce_unsafe( + &mut self, + ccx: &mut CheatsCtxt<'_, '_, '_, '_>, + account: Address, + new_nonce: u64, + ) -> Result { + if !self.using_zk_vm { + return self.evm.cheatcode_set_nonce_unsafe(ccx, account, new_nonce); + } + + foundry_zksync_core::cheatcodes::set_nonce(account, U256::from(new_nonce), ccx.ecx); + Ok(Default::default()) + } + + fn cheatcode_mock_call( + &mut self, + ccx: &mut CheatsCtxt<'_, '_, '_, '_>, + callee: Address, + data: &Bytes, + return_data: &Bytes, + ) -> Result { + if !self.using_zk_vm { + return self.evm.cheatcode_mock_call(ccx, callee, data, return_data); + } + + let _ = foundry_cheatcodes::make_acc_non_empty(&callee, ccx.ecx)?; + foundry_zksync_core::cheatcodes::set_mocked_account(callee, ccx.ecx, ccx.caller); + foundry_cheatcodes::mock_call( + ccx.state, + &callee, + data, + None, + return_data, + InstructionResult::Return, + ); + Ok(Default::default()) + } + + fn cheatcode_mock_call_revert( + &mut self, + ccx: &mut CheatsCtxt<'_, '_, '_, '_>, + callee: Address, + data: &Bytes, + revert_data: &Bytes, + ) -> Result { + if !self.using_zk_vm { + return self.evm.cheatcode_mock_call_revert(ccx, callee, data, revert_data); + } + + let _ = make_acc_non_empty(&callee, ccx.ecx)?; + foundry_zksync_core::cheatcodes::set_mocked_account(callee, ccx.ecx, ccx.caller); + // not calling + foundry_cheatcodes::mock_call( + ccx.state, + &callee, + data, + None, + revert_data, + InstructionResult::Revert, + ); + Ok(Default::default()) + } + + fn get_artifact_code(&self, state: &Cheatcodes, path: &str, deployed: bool) -> Result { + Ok(get_artifact_code( + &self.dual_compiled_contracts, + self.using_zk_vm, + &state.config, + path, + deployed, + )? + .abi_encode()) + } + + fn record_broadcastable_create_transactions( + &mut self, + config: Arc, + input: &dyn CommonCreateInput, + ecx_inner: InnerEcx<'_, '_, '_>, + broadcast: &Broadcast, + broadcastable_transactions: &mut BroadcastableTransactions, + ) { + if !self.using_zk_vm { + return self.evm.record_broadcastable_create_transactions( + config, + input, + ecx_inner, + broadcast, + broadcastable_transactions, + ); + } + + let is_fixed_gas_limit = + foundry_cheatcodes::check_if_fixed_gas_limit(ecx_inner, input.gas_limit()); + + let init_code = input.init_code(); + let to = Some(TxKind::Call(CONTRACT_DEPLOYER_ADDRESS.to_address())); + let mut nonce = foundry_zksync_core::nonce(broadcast.new_origin, ecx_inner) as u64; + let find_contract = self + .dual_compiled_contracts + .find_bytecode(&init_code.0) + .unwrap_or_else(|| panic!("failed finding contract for {init_code:?}")); + + let constructor_args = find_contract.constructor_args(); + let contract = find_contract.contract(); + + let factory_deps = self.dual_compiled_contracts.fetch_all_factory_deps(contract); + + let create_input = foundry_zksync_core::encode_create_params( + &input.scheme().unwrap_or(CreateScheme::Create), + contract.zk_bytecode_hash, + constructor_args.to_vec(), + ); + let call_init_code = Bytes::from(create_input); + + let mut zk_tx_factory_deps = factory_deps; + + let paymaster_params = + self.paymaster_params.clone().map(|paymaster_data| PaymasterParams { + paymaster: paymaster_data.address.to_h160(), + paymaster_input: paymaster_data.input.to_vec(), + }); + + let rpc = ecx_inner.db.active_fork_url(); + + let injected_factory_deps = self + .zk_use_factory_deps + .iter() + .map(|contract| { + get_artifact_code( + &self.dual_compiled_contracts, + self.using_zk_vm, + &config, + contract, + false, + ) + .inspect(|_| info!(contract, "pushing factory dep")) + .unwrap_or_else(|_| { + panic!("failed to get bytecode for factory deps contract {contract}") + }) + .to_vec() + }) + .collect_vec(); + zk_tx_factory_deps.extend(injected_factory_deps); + let mut batched = foundry_zksync_core::vm::batch_factory_dependencies(zk_tx_factory_deps); + debug!(batches = batched.len(), "splitting factory deps for broadcast"); + // the last batch is the final one that does the deployment + zk_tx_factory_deps = batched.pop().expect("must have at least 1 item"); + + for factory_deps in batched { + let mut tx = WithOtherFields::new(TransactionRequest { + from: Some(broadcast.new_origin), + to: Some(TxKind::Call(Address::ZERO)), + value: Some(input.value()), + nonce: Some(nonce), + ..Default::default() + }); + tx.other.insert( + ZKSYNC_TRANSACTION_OTHER_FIELDS_KEY.to_string(), + serde_json::to_value(ZkTransactionMetadata::new( + factory_deps, + paymaster_params.clone(), + )) + .expect("failed encoding json"), + ); + + broadcastable_transactions.push_back(BroadcastableTransaction { + rpc: rpc.clone(), + transaction: TransactionMaybeSigned::Unsigned(tx), + }); + + //update nonce for each tx + nonce += 1; + } + + let mut tx = WithOtherFields::new(TransactionRequest { + from: Some(broadcast.new_origin), + to, + value: Some(input.value()), + input: TransactionInput::new(call_init_code), + nonce: Some(nonce), + gas: if is_fixed_gas_limit { Some(input.gas_limit()) } else { None }, + ..Default::default() + }); + tx.other.insert( + ZKSYNC_TRANSACTION_OTHER_FIELDS_KEY.to_string(), + serde_json::to_value(ZkTransactionMetadata::new(zk_tx_factory_deps, paymaster_params)) + .expect("failed encoding json"), + ); + broadcastable_transactions.push_back(BroadcastableTransaction { + rpc, + transaction: TransactionMaybeSigned::Unsigned(tx), + }); + } + + fn record_broadcastable_call_transactions( + &mut self, + config: Arc, + call: &CallInputs, + ecx_inner: InnerEcx<'_, '_, '_>, + broadcast: &Broadcast, + broadcastable_transactions: &mut BroadcastableTransactions, + active_delegation: &mut Option, + ) { + if !self.using_zk_vm { + return self.evm.record_broadcastable_call_transactions( + config, + call, + ecx_inner, + broadcast, + broadcastable_transactions, + active_delegation, + ); + } + + let is_fixed_gas_limit = + foundry_cheatcodes::check_if_fixed_gas_limit(ecx_inner, call.gas_limit); + + let nonce = foundry_zksync_core::nonce(broadcast.new_origin, ecx_inner) as u64; + + let factory_deps = &mut self.set_deployer_call_input_factory_deps; + let injected_factory_deps = self + .zk_use_factory_deps + .iter() + .flat_map(|contract| { + let artifact_code = get_artifact_code( + &self.dual_compiled_contracts, + self.using_zk_vm, + &config, + contract, + false, + ) + .inspect(|_| info!(contract, "pushing factory dep")) + .unwrap_or_else(|_| { + panic!("failed to get bytecode for factory deps contract {contract}") + }) + .to_vec(); + let res = self.dual_compiled_contracts.find_bytecode(&artifact_code).unwrap(); + self.dual_compiled_contracts.fetch_all_factory_deps(res.contract()) + }) + .collect_vec(); + factory_deps.extend(injected_factory_deps.clone()); + + let paymaster_params = + self.paymaster_params.clone().map(|paymaster_data| PaymasterParams { + paymaster: paymaster_data.address.to_h160(), + paymaster_input: paymaster_data.input.to_vec(), + }); + let factory_deps = if call.target_address == DEFAULT_CREATE2_DEPLOYER_ZKSYNC { + // We shouldn't need factory_deps for CALLs + factory_deps.clone() + } else { + // For this case we use only the injected factory deps + injected_factory_deps + }; + let zk_tx = ZkTransactionMetadata::new(factory_deps, paymaster_params); + + let mut tx_req = TransactionRequest { + from: Some(broadcast.new_origin), + to: Some(TxKind::from(Some(call.target_address))), + value: call.transfer_value(), + input: TransactionInput::new(call.input.clone()), + nonce: Some(nonce), + chain_id: Some(ecx_inner.env.cfg.chain_id), + gas: if is_fixed_gas_limit { Some(call.gas_limit) } else { None }, + ..Default::default() + }; + + if let Some(auth_list) = active_delegation.take() { + tx_req.authorization_list = Some(vec![auth_list]); + } else { + tx_req.authorization_list = None; + } + let mut tx = WithOtherFields::new(tx_req); + + tx.other.insert( + ZKSYNC_TRANSACTION_OTHER_FIELDS_KEY.to_string(), + serde_json::to_value(zk_tx).expect("failed encoding json"), + ); + + broadcastable_transactions.push_back(BroadcastableTransaction { + rpc: ecx_inner.db.active_fork_url(), + transaction: TransactionMaybeSigned::Unsigned(tx), + }); + debug!(target: "cheatcodes", tx=?broadcastable_transactions.back().unwrap(), "broadcastable call"); + } + + fn post_initialize_interp(&mut self, _interpreter: &mut Interpreter, ecx: Ecx<'_, '_, '_>) { + if self.zk_startup_migration.is_allowed() && !self.using_zk_vm { + self.select_zk_vm(ecx, None); + self.zk_startup_migration.done(); + debug!("startup zkEVM storage migration completed"); + } + } + + /// Returns true if handled. + fn pre_step_end(&mut self, interpreter: &mut Interpreter, ecx: Ecx<'_, '_, '_>) -> bool { + // override address(x).balance retrieval to make it consistent between EraVM and EVM + if !self.using_zk_vm { + return false; + } + + let address = match interpreter.current_opcode() { + op::SELFBALANCE => interpreter.contract().target_address, + op::BALANCE => { + if interpreter.stack.is_empty() { + interpreter.instruction_result = InstructionResult::StackUnderflow; + return true; + } + + Address::from_word(B256::from(unsafe { interpreter.stack.pop_unsafe() })) + } + _ => return true, + }; + + // Safety: Length is checked above. + let balance = foundry_zksync_core::balance(address, ecx); + + // Skip the current BALANCE instruction since we've already handled it + match interpreter.stack.push(balance) { + Ok(_) => unsafe { + interpreter.instruction_pointer = interpreter.instruction_pointer.add(1); + }, + Err(e) => { + interpreter.instruction_result = e; + } + }; + + false + } +} + +impl CheatcodeInspectorStrategyExt for ZksyncCheatcodeInspectorStrategy { + fn zksync_cheatcode_skip_zkvm(&mut self) -> Result { + self.skip_zk_vm = true; + Ok(Default::default()) + } + + fn zksync_cheatcode_set_paymaster( + &mut self, + paymaster_address: Address, + paymaster_input: &Bytes, + ) -> Result { + self.paymaster_params = + Some(ZkPaymasterData { address: paymaster_address, input: paymaster_input.clone() }); + Ok(Default::default()) + } + + fn zksync_cheatcode_use_factory_deps(&mut self, name: String) -> foundry_cheatcodes::Result { + info!("Adding factory dependency: {:?}", name); + self.zk_use_factory_deps.push(name); + Ok(Default::default()) + } + + fn zksync_cheatcode_register_contract( + &mut self, + name: String, + zk_bytecode_hash: FixedBytes<32>, + zk_deployed_bytecode: Vec, + zk_factory_deps: Vec>, + evm_bytecode_hash: FixedBytes<32>, + evm_deployed_bytecode: Vec, + evm_bytecode: Vec, + ) -> Result { + let new_contract = DualCompiledContract { + name, + zk_bytecode_hash: H256(zk_bytecode_hash.0), + zk_deployed_bytecode, + zk_factory_deps, + evm_bytecode_hash, + evm_deployed_bytecode, + evm_bytecode, + }; + + if let Some(existing) = self.dual_compiled_contracts.iter().find(|contract| { + contract.evm_bytecode_hash == new_contract.evm_bytecode_hash && + contract.zk_bytecode_hash == new_contract.zk_bytecode_hash + }) { + warn!(name = existing.name, "contract already exists with the given bytecode hashes"); + return Ok(Default::default()) + } + + self.dual_compiled_contracts.push(new_contract); + + Ok(Default::default()) + } + + fn zksync_record_create_address(&mut self, outcome: &CreateOutcome) { + if self.record_next_create_address { + self.record_next_create_address = false; + if let Some(address) = outcome.address { + self.skip_zk_vm_addresses.insert(address); + } + } + } + + fn zksync_sync_nonce(&mut self, sender: Address, nonce: u64, ecx: Ecx<'_, '_, '_>) { + // NOTE(zk): We sync with the nonce changes to ensure that the nonce matches + foundry_zksync_core::cheatcodes::set_nonce(sender, U256::from(nonce), ecx); + } + + fn zksync_set_deployer_call_input(&mut self, call: &mut CallInputs) { + self.set_deployer_call_input_factory_deps.clear(); + if call.target_address == DEFAULT_CREATE2_DEPLOYER && self.using_zk_vm { + call.target_address = DEFAULT_CREATE2_DEPLOYER_ZKSYNC; + call.bytecode_address = DEFAULT_CREATE2_DEPLOYER_ZKSYNC; + + let (salt, init_code) = call.input.split_at(32); + let find_contract = self + .dual_compiled_contracts + .find_bytecode(init_code) + .unwrap_or_else(|| panic!("failed finding contract for {init_code:?}")); + + let constructor_args = find_contract.constructor_args(); + let contract = find_contract.contract(); + + // store these for broadcast reasons + self.set_deployer_call_input_factory_deps = + self.dual_compiled_contracts.fetch_all_factory_deps(contract); + + let create_input = foundry_zksync_core::encode_create_params( + &CreateScheme::Create2 { salt: U256::from_be_slice(salt) }, + contract.zk_bytecode_hash, + constructor_args.to_vec(), + ); + + call.input = create_input.into(); + } + } + + /// Try handling the `CREATE` within zkEVM. + /// If `Some` is returned then the result must be returned immediately, else the call must be + /// handled in EVM. + fn zksync_try_create( + &mut self, + state: &mut Cheatcodes, + ecx: Ecx<'_, '_, '_>, + input: &dyn CommonCreateInput, + executor: &mut dyn CheatcodesExecutor, + ) -> Option { + if !self.using_zk_vm { + return None; + } + + if self.skip_zk_vm { + self.skip_zk_vm = false; // handled the skip, reset flag + self.record_next_create_address = true; + info!("running create in EVM, instead of zkEVM (skipped)"); + return None + } + + if let Some(CreateScheme::Create) = input.scheme() { + let caller = input.caller(); + let nonce = ecx + .inner + .journaled_state + .load_account(input.caller(), &mut ecx.inner.db) + .expect("to load caller account") + .info + .nonce; + let address = caller.create(nonce); + if ecx.db.get_test_contract_address().map(|addr| address == addr).unwrap_or_default() { + info!("running create in EVM, instead of zkEVM (Test Contract) {:#?}", address); + return None + } + } + + let init_code = input.init_code(); + if init_code.0 == DEFAULT_CREATE2_DEPLOYER_CODE { + info!("running create in EVM, instead of zkEVM (DEFAULT_CREATE2_DEPLOYER_CODE)"); + return None + } + + info!("running create in zkEVM"); + + let find_contract = self + .dual_compiled_contracts + .find_bytecode(&init_code.0) + .unwrap_or_else(|| panic!("failed finding contract for {init_code:?}")); + + let constructor_args = find_contract.constructor_args(); + let contract = find_contract.contract(); + + let zk_create_input = foundry_zksync_core::encode_create_params( + &input.scheme().unwrap_or(CreateScheme::Create), + contract.zk_bytecode_hash, + constructor_args.to_vec(), + ); + + let mut factory_deps = self.dual_compiled_contracts.fetch_all_factory_deps(contract); + let injected_factory_deps = self + .zk_use_factory_deps + .iter() + .flat_map(|contract| { + let artifact_code = get_artifact_code( + &self.dual_compiled_contracts, + self.using_zk_vm, + &state.config, + contract, + false, + ) + .inspect(|_| info!(contract, "pushing factory dep")) + .unwrap_or_else(|_| { + panic!("failed to get bytecode for injected factory deps contract {contract}") + }) + .to_vec(); + let res = self.dual_compiled_contracts.find_bytecode(&artifact_code).unwrap(); + self.dual_compiled_contracts.fetch_all_factory_deps(res.contract()) + }) + .collect_vec(); + factory_deps.extend(injected_factory_deps); + + // NOTE(zk): Clear injected factory deps so that they are not sent on further transactions + self.zk_use_factory_deps.clear(); + tracing::debug!(contract = contract.name, "using dual compiled contract"); + + let zk_persist_nonce_update = self.zk_persist_nonce_update.check(); + let ccx = foundry_zksync_core::vm::CheatcodeTracerContext { + mocked_calls: state.mocked_calls.clone(), + expected_calls: Some(&mut state.expected_calls), + accesses: state.accesses.as_mut(), + persisted_factory_deps: Some(&mut self.persisted_factory_deps), + paymaster_data: self.paymaster_params.take(), + persist_nonce_update: state.broadcast.is_some() || zk_persist_nonce_update, + zk_env: self.zk_env.clone(), + }; + + let zk_create = foundry_zksync_core::vm::ZkCreateInputs { + value: input.value().to_u256(), + msg_sender: input.caller(), + create_input: zk_create_input, + factory_deps, + }; + + let mut gas = Gas::new(input.gas_limit()); + match foundry_zksync_core::vm::create::<_, DatabaseError>(zk_create, ecx, ccx) { + Ok(result) => { + if let Some(recorded_logs) = &mut state.recorded_logs { + recorded_logs.extend(result.logs.clone().into_iter().map(|log| Vm::Log { + topics: log.data.topics().to_vec(), + data: log.data.data.clone(), + emitter: log.address, + })); + } + + // append console logs from zkEVM to the current executor's LogTracer + result.logs.iter().filter_map(foundry_evm::decode::decode_console_log).for_each( + |decoded_log| { + executor.console_log( + &mut CheatsCtxt { + state, + ecx: &mut ecx.inner, + precompiles: &mut ecx.precompiles, + gas_limit: input.gas_limit(), + caller: input.caller(), + }, + decoded_log, + ); + }, + ); + + // append traces + executor.trace_zksync(state, ecx, result.call_traces); + + // for each log in cloned logs call handle_expect_emit + if !state.expected_emits.is_empty() { + for log in result.logs { + foundry_cheatcodes::handle_expect_emit( + state, + &log, + &mut Default::default(), + ); + } + } + + // record immutable variables + if result.execution_result.is_success() { + for (addr, imm_values) in result.recorded_immutables { + let addr = addr.to_address(); + let keys = imm_values + .into_keys() + .map(|slot_index| { + foundry_zksync_core::get_immutable_slot_key(addr, slot_index) + .to_ru256() + }) + .collect::>(); + ecx.db.get_strategy().zksync_save_immutable_storage(addr, keys); + } + } + + match result.execution_result { + ExecutionResult::Success { output, gas_used, .. } => { + let _ = gas.record_cost(gas_used); + match output { + Output::Create(bytes, address) => Some(CreateOutcome { + result: InterpreterResult { + result: InstructionResult::Return, + output: bytes, + gas, + }, + address, + }), + _ => Some(CreateOutcome { + result: InterpreterResult { + result: InstructionResult::Revert, + output: Bytes::new(), + gas, + }, + address: None, + }), + } + } + ExecutionResult::Revert { output, gas_used, .. } => { + let _ = gas.record_cost(gas_used); + Some(CreateOutcome { + result: InterpreterResult { + result: InstructionResult::Revert, + output, + gas, + }, + address: None, + }) + } + ExecutionResult::Halt { .. } => Some(CreateOutcome { + result: InterpreterResult { + result: InstructionResult::Revert, + output: Bytes::from_iter(String::from("zk vm halted").as_bytes()), + gas, + }, + address: None, + }), + } + } + Err(err) => { + error!("error inspecting zkEVM: {err:?}"); + Some(CreateOutcome { + result: InterpreterResult { + result: InstructionResult::Revert, + output: Bytes::from_iter( + format!("error inspecting zkEVM: {err:?}").as_bytes(), + ), + gas, + }, + address: None, + }) + } + } + } + + /// Try handling the `CALL` within zkEVM. + /// If `Some` is returned then the result must be returned immediately, else the call must be + /// handled in EVM. + fn zksync_try_call( + &mut self, + state: &mut Cheatcodes, + ecx: Ecx<'_, '_, '_>, + call: &CallInputs, + executor: &mut dyn CheatcodesExecutor, + ) -> Option { + // We need to clear them out for the next call. + let factory_deps = std::mem::take(&mut self.set_deployer_call_input_factory_deps); + + if !self.using_zk_vm { + return None; + } + + // also skip if the target was created during a zkEVM skip + self.skip_zk_vm = + self.skip_zk_vm || self.skip_zk_vm_addresses.contains(&call.target_address); + if self.skip_zk_vm { + self.skip_zk_vm = false; // handled the skip, reset flag + info!("running create in EVM, instead of zkEVM (skipped) {:#?}", call); + return None; + } + + if ecx + .db + .get_test_contract_address() + .map(|addr| call.bytecode_address == addr) + .unwrap_or_default() + { + info!( + "running call in EVM, instead of zkEVM (Test Contract) {:#?}", + call.bytecode_address + ); + return None + } + + info!("running call in zkEVM {:#?}", call); + let zk_persist_nonce_update = self.zk_persist_nonce_update.check(); + + // NOTE(zk): Clear injected factory deps here even though it's actually used in broadcast. + // To be consistent with where we clear factory deps in try_create_in_zk. + self.zk_use_factory_deps.clear(); + + let ccx = foundry_zksync_core::vm::CheatcodeTracerContext { + mocked_calls: state.mocked_calls.clone(), + expected_calls: Some(&mut state.expected_calls), + accesses: state.accesses.as_mut(), + persisted_factory_deps: Some(&mut self.persisted_factory_deps), + paymaster_data: self.paymaster_params.take(), + persist_nonce_update: state.broadcast.is_some() || zk_persist_nonce_update, + zk_env: self.zk_env.clone(), + }; + + let mut gas = Gas::new(call.gas_limit); + match foundry_zksync_core::vm::call::<_, DatabaseError>(call, factory_deps, ecx, ccx) { + Ok(result) => { + // append console logs from zkEVM to the current executor's LogTracer + result.logs.iter().filter_map(foundry_evm::decode::decode_console_log).for_each( + |decoded_log| { + executor.console_log( + &mut CheatsCtxt { + state, + ecx: &mut ecx.inner, + precompiles: &mut ecx.precompiles, + gas_limit: call.gas_limit, + caller: call.caller, + }, + decoded_log, + ); + }, + ); + + // skip log processing for static calls + if !call.is_static { + if let Some(recorded_logs) = &mut state.recorded_logs { + recorded_logs.extend(result.logs.clone().into_iter().map(|log| Vm::Log { + topics: log.data.topics().to_vec(), + data: log.data.data.clone(), + emitter: log.address, + })); + } + + // append traces + executor.trace_zksync(state, ecx, result.call_traces); + + // for each log in cloned logs call handle_expect_emit + if !state.expected_emits.is_empty() { + for log in result.logs { + foundry_cheatcodes::handle_expect_emit( + state, + &log, + &mut Default::default(), + ); + } + } + } + + match result.execution_result { + ExecutionResult::Success { output, gas_used, .. } => { + let _ = gas.record_cost(gas_used); + match output { + Output::Call(bytes) => Some(CallOutcome { + result: InterpreterResult { + result: InstructionResult::Return, + output: bytes, + gas, + }, + memory_offset: call.return_memory_offset.clone(), + }), + _ => Some(CallOutcome { + result: InterpreterResult { + result: InstructionResult::Revert, + output: Bytes::new(), + gas, + }, + memory_offset: call.return_memory_offset.clone(), + }), + } + } + ExecutionResult::Revert { output, gas_used, .. } => { + let _ = gas.record_cost(gas_used); + Some(CallOutcome { + result: InterpreterResult { + result: InstructionResult::Revert, + output, + gas, + }, + memory_offset: call.return_memory_offset.clone(), + }) + } + ExecutionResult::Halt { .. } => Some(CallOutcome { + result: InterpreterResult { + result: InstructionResult::Revert, + output: Bytes::from_iter(String::from("zk vm halted").as_bytes()), + gas, + }, + memory_offset: call.return_memory_offset.clone(), + }), + } + } + Err(err) => { + error!("error inspecting zkEVM: {err:?}"); + Some(CallOutcome { + result: InterpreterResult { + result: InstructionResult::Revert, + output: Bytes::from_iter( + format!("error inspecting zkEVM: {err:?}").as_bytes(), + ), + gas, + }, + memory_offset: call.return_memory_offset.clone(), + }) + } + } + } + + fn zksync_select_fork_vm(&mut self, data: InnerEcx<'_, '_, '_>, fork_id: LocalForkId) { + self.select_fork_vm(data, fork_id); + } + + fn zksync_cheatcode_select_zk_vm(&mut self, data: InnerEcx<'_, '_, '_>, enable: bool) { + if enable { + self.select_zk_vm(data, None) + } else { + self.select_evm(data); + } + } +} + +impl ZksyncCheatcodeInspectorStrategy { + /// Selects the appropriate VM for the fork. Options: EVM, ZK-VM. + /// CALL and CREATE are handled by the selected VM. + /// + /// Additionally: + /// * Translates block information + /// * Translates all persisted addresses + pub fn select_fork_vm(&mut self, data: InnerEcx<'_, '_, '_>, fork_id: LocalForkId) { + let fork_info = data.db.get_fork_info(fork_id).expect("failed getting fork info"); + if fork_info.fork_type.is_evm() { + self.select_evm(data) + } else { + self.select_zk_vm(data, Some(&fork_info.fork_env)) + } + } + + /// Switch to EVM and translate block info, balances, nonces and deployed codes for persistent + /// accounts + pub fn select_evm(&mut self, data: InnerEcx<'_, '_, '_>) { + if !self.using_zk_vm { + tracing::info!("already in EVM"); + return + } + + tracing::info!("switching to EVM"); + self.using_zk_vm = false; + + let system_account = SYSTEM_CONTEXT_ADDRESS.to_address(); + journaled_account(data, system_account).expect("failed to load account"); + let balance_account = L2_BASE_TOKEN_ADDRESS.to_address(); + journaled_account(data, balance_account).expect("failed to load account"); + let nonce_account = NONCE_HOLDER_ADDRESS.to_address(); + journaled_account(data, nonce_account).expect("failed to load account"); + let account_code_account = ACCOUNT_CODE_STORAGE_ADDRESS.to_address(); + journaled_account(data, account_code_account).expect("failed to load account"); + + // TODO we might need to store the deployment nonce under the contract storage + // to not lose it across VMs. + + let block_info_key = CURRENT_VIRTUAL_BLOCK_INFO_POSITION.to_ru256(); + let block_info = data.sload(system_account, block_info_key).unwrap_or_default(); + let (block_number, block_timestamp) = unpack_block_info(block_info.to_u256()); + data.env.block.number = U256::from(block_number); + data.env.block.timestamp = U256::from(block_timestamp); + + let test_contract = data.db.get_test_contract_address(); + for address in data.db.persistent_accounts().into_iter().chain([data.env.tx.caller]) { + info!(?address, "importing to evm state"); + + let balance_key = get_balance_key(address); + let nonce_key = get_nonce_key(address); + + let balance = data.sload(balance_account, balance_key).unwrap_or_default().data; + let full_nonce = data.sload(nonce_account, nonce_key).unwrap_or_default(); + let (tx_nonce, _deployment_nonce) = decompose_full_nonce(full_nonce.to_u256()); + let nonce = tx_nonce.as_u64(); + + let account_code_key = get_account_code_key(address); + let (code_hash, code) = data + .sload(account_code_account, account_code_key) + .ok() + .and_then(|zk_bytecode_hash| { + self.dual_compiled_contracts + .find_by_zk_bytecode_hash(zk_bytecode_hash.to_h256()) + .map(|contract| { + ( + contract.evm_bytecode_hash, + Some(Bytecode::new_raw(Bytes::from( + contract.evm_deployed_bytecode.clone(), + ))), + ) + }) + }) + .unwrap_or_else(|| (KECCAK_EMPTY, None)); + + let account = journaled_account(data, address).expect("failed to load account"); + let _ = std::mem::replace(&mut account.info.balance, balance); + let _ = std::mem::replace(&mut account.info.nonce, nonce); + + if test_contract.map(|addr| addr == address).unwrap_or_default() { + tracing::trace!(?address, "ignoring code translation for test contract"); + } else { + account.info.code_hash = code_hash; + account.info.code.clone_from(&code); + } + } + } + + /// Switch to ZK-VM and translate block info, balances, nonces and deployed codes for persistent + /// accounts + pub fn select_zk_vm(&mut self, data: InnerEcx<'_, '_, '_>, new_env: Option<&Env>) { + if self.using_zk_vm { + tracing::info!("already in ZK-VM"); + return + } + + tracing::info!("switching to ZK-VM"); + self.using_zk_vm = true; + + let env = new_env.unwrap_or(data.env.as_ref()); + + let mut system_storage: HashMap = Default::default(); + let block_info_key = CURRENT_VIRTUAL_BLOCK_INFO_POSITION.to_ru256(); + let block_info = + pack_block_info(env.block.number.as_limbs()[0], env.block.timestamp.as_limbs()[0]); + system_storage.insert(block_info_key, EvmStorageSlot::new(block_info.to_ru256())); + + let mut l2_eth_storage: HashMap = Default::default(); + let mut nonce_storage: HashMap = Default::default(); + let mut account_code_storage: HashMap = Default::default(); + let mut known_codes_storage: HashMap = Default::default(); + let mut deployed_codes: HashMap = Default::default(); + + let test_contract = data.db.get_test_contract_address(); + for address in data.db.persistent_accounts().into_iter().chain([data.env.tx.caller]) { + info!(?address, "importing to zk state"); + + let account = journaled_account(data, address).expect("failed to load account"); + let info = &account.info; + + let balance_key = get_balance_key(address); + l2_eth_storage.insert(balance_key, EvmStorageSlot::new(info.balance)); + + // TODO we need to find a proper way to handle deploy nonces instead of replicating + let full_nonce = nonces_to_full_nonce(info.nonce.into(), info.nonce.into()); + + let nonce_key = get_nonce_key(address); + nonce_storage.insert(nonce_key, EvmStorageSlot::new(full_nonce.to_ru256())); + + if test_contract.map(|test_address| address == test_address).unwrap_or_default() { + // avoid migrating test contract code + tracing::trace!(?address, "ignoring code translation for test contract"); + continue; + } + + if let Some(contract) = self.dual_compiled_contracts.iter().find(|contract| { + info.code_hash != KECCAK_EMPTY && info.code_hash == contract.evm_bytecode_hash + }) { + account_code_storage.insert( + get_account_code_key(address), + EvmStorageSlot::new(contract.zk_bytecode_hash.to_ru256()), + ); + known_codes_storage + .insert(contract.zk_bytecode_hash.to_ru256(), EvmStorageSlot::new(U256::ZERO)); + + let code_hash = B256::from_slice(contract.zk_bytecode_hash.as_bytes()); + deployed_codes.insert( + address, + AccountInfo { + balance: info.balance, + nonce: info.nonce, + code_hash, + code: Some(Bytecode::new_raw(Bytes::from( + contract.zk_deployed_bytecode.clone(), + ))), + }, + ); + } else { + tracing::debug!(code_hash = ?info.code_hash, ?address, "no zk contract found") + } + } + + let system_addr = SYSTEM_CONTEXT_ADDRESS.to_address(); + let system_account = journaled_account(data, system_addr).expect("failed to load account"); + system_account.storage.extend(system_storage.clone()); + + let balance_addr = L2_BASE_TOKEN_ADDRESS.to_address(); + let balance_account = + journaled_account(data, balance_addr).expect("failed to load account"); + balance_account.storage.extend(l2_eth_storage.clone()); + + let nonce_addr = NONCE_HOLDER_ADDRESS.to_address(); + let nonce_account = journaled_account(data, nonce_addr).expect("failed to load account"); + nonce_account.storage.extend(nonce_storage.clone()); + + let account_code_addr = ACCOUNT_CODE_STORAGE_ADDRESS.to_address(); + let account_code_account = + journaled_account(data, account_code_addr).expect("failed to load account"); + account_code_account.storage.extend(account_code_storage.clone()); + + let known_codes_addr = KNOWN_CODES_STORAGE_ADDRESS.to_address(); + let known_codes_account = + journaled_account(data, known_codes_addr).expect("failed to load account"); + known_codes_account.storage.extend(known_codes_storage.clone()); + + for (address, info) in deployed_codes { + let account = journaled_account(data, address).expect("failed to load account"); + let _ = std::mem::replace(&mut account.info.balance, info.balance); + let _ = std::mem::replace(&mut account.info.nonce, info.nonce); + account.info.code_hash = info.code_hash; + account.info.code.clone_from(&info.code); + } + } +} + +fn get_artifact_code( + dual_compiled_contracts: &DualCompiledContracts, + using_zk_vm: bool, + config: &Arc, + path: &str, + deployed: bool, +) -> Result { + let path = if path.ends_with(".json") { + PathBuf::from(path) + } else { + let mut parts = path.split(':'); + + let mut file = None; + let mut contract_name = None; + let mut version = None; + + let path_or_name = parts.next().unwrap(); + if path_or_name.contains('.') { + file = Some(PathBuf::from(path_or_name)); + if let Some(name_or_version) = parts.next() { + if name_or_version.contains('.') { + version = Some(name_or_version); + } else { + contract_name = Some(name_or_version); + version = parts.next(); + } + } + } else { + contract_name = Some(path_or_name); + version = parts.next(); + } + + let version = if let Some(version) = version { + Some(Version::parse(version).map_err(|e| fmt_err!("failed parsing version: {e}"))?) + } else { + None + }; + + // Use available artifacts list if present + if let Some(artifacts) = &config.available_artifacts { + let filtered = artifacts + .iter() + .filter(|(id, _)| { + // name might be in the form of "Counter.0.8.23" + let id_name = id.name.split('.').next().unwrap(); + + if let Some(path) = &file { + if !id.source.ends_with(path) { + return false; + } + } + if let Some(name) = contract_name { + if id_name != name { + return false; + } + } + if let Some(ref version) = version { + if id.version.minor != version.minor || + id.version.major != version.major || + id.version.patch != version.patch + { + return false; + } + } + true + }) + .collect::>(); + + let artifact = match &filtered[..] { + [] => Err(fmt_err!("no matching artifact found")), + [artifact] => Ok(artifact), + filtered => { + // If we find more than one artifact, we need to filter by contract type + // depending on whether we are using the zkvm or evm + filtered + .iter() + .find(|(id, _)| { + let contract_type = + dual_compiled_contracts.get_contract_type_by_artifact(id); + match contract_type { + Some(ContractType::ZK) => using_zk_vm, + Some(ContractType::EVM) => !using_zk_vm, + None => false, + } + }) + .or_else(|| { + // If we know the current script/test contract solc version, try to + // filter by it + config.running_version.as_ref().and_then(|version| { + filtered.iter().find(|(id, _)| id.version == *version) + }) + }) + .ok_or_else(|| fmt_err!("multiple matching artifacts found")) + } + }?; + + let maybe_bytecode = if deployed { + artifact.1.deployed_bytecode().cloned() + } else { + artifact.1.bytecode().cloned() + }; + + return maybe_bytecode + .ok_or_else(|| fmt_err!("no bytecode for contract; is it abstract or unlinked?")); + } else { + let path_in_artifacts = + match (file.map(|f| f.to_string_lossy().to_string()), contract_name) { + (Some(file), Some(contract_name)) => { + PathBuf::from(format!("{file}/{contract_name}.json")) + } + (None, Some(contract_name)) => { + PathBuf::from(format!("{contract_name}.sol/{contract_name}.json")) + } + (Some(file), None) => { + let name = file.replace(".sol", ""); + PathBuf::from(format!("{file}/{name}.json")) + } + _ => bail!("invalid artifact path"), + }; + + config.paths.artifacts.join(path_in_artifacts) + } + }; + + let path = config.ensure_path_allowed(path, FsAccessKind::Read)?; + let data = fs::read_to_string(path)?; + let artifact = serde_json::from_str::(&data)?; + let maybe_bytecode = if deployed { artifact.deployed_bytecode } else { artifact.bytecode }; + maybe_bytecode.ok_or_else(|| fmt_err!("no bytecode for contract; is it abstract or unlinked?")) +} + +/// 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 + } +} diff --git a/crates/strategy/zksync/src/executor.rs b/crates/strategy/zksync/src/executor.rs new file mode 100644 index 000000000..e96c4ff60 --- /dev/null +++ b/crates/strategy/zksync/src/executor.rs @@ -0,0 +1,194 @@ +use alloy_primitives::{Address, U256}; +use alloy_rpc_types::serde_helpers::OtherFields; +use alloy_zksync::provider::{zksync_provider, ZksyncProvider}; +use eyre::Result; +use foundry_cheatcodes::strategy::CheatcodeInspectorStrategy; + +use foundry_evm::{ + backend::{strategy::BackendStrategy, BackendResult, DatabaseExt}, + executors::{ + strategy::{EvmExecutorStrategy, ExecutorStrategy, ExecutorStrategyExt}, + Executor, + }, + InspectorExt, +}; +use foundry_zksync_compiler::DualCompiledContracts; +use foundry_zksync_core::{vm::ZkEnv, ZkTransactionMetadata}; +use revm::{ + primitives::{Env, EnvWithHandlerCfg, HashMap, ResultAndState}, + Database, +}; +use zksync_types::H256; + +use crate::{ + cheatcode::ZKSYNC_TRANSACTION_OTHER_FIELDS_KEY, ZksyncBackendStrategy, + ZksyncCheatcodeInspectorStrategy, +}; + +#[derive(Debug, Default, Clone)] +pub struct ZksyncExecutorStrategy { + evm: EvmExecutorStrategy, + inspect_context: Option, + persisted_factory_deps: HashMap>, + dual_compiled_contracts: DualCompiledContracts, + zk_env: ZkEnv, +} + +impl ExecutorStrategy for ZksyncExecutorStrategy { + fn name(&self) -> &'static str { + "zk" + } + + fn new_cloned(&self) -> Box { + Box::new(self.clone()) + } + + fn set_inspect_context(&mut self, other_fields: OtherFields) { + let maybe_context = get_zksync_transaction_metadata(&other_fields); + self.inspect_context = maybe_context; + } + + fn set_balance( + &mut self, + executor: &mut Executor, + address: Address, + amount: U256, + ) -> BackendResult<()> { + self.evm.set_balance(executor, address, amount)?; + + let (address, slot) = foundry_zksync_core::state::get_balance_storage(address); + executor.backend.insert_account_storage(address, slot, amount)?; + + Ok(()) + } + + fn set_nonce( + &mut self, + executor: &mut Executor, + address: Address, + nonce: u64, + ) -> BackendResult<()> { + self.evm.set_nonce(executor, address, nonce)?; + + let (address, slot) = foundry_zksync_core::state::get_nonce_storage(address); + // fetch the full nonce to preserve account's deployment nonce + let full_nonce = executor.backend.storage(address, slot)?; + let full_nonce = foundry_zksync_core::state::parse_full_nonce(full_nonce); + let new_full_nonce = + foundry_zksync_core::state::new_full_nonce(nonce, full_nonce.deploy_nonce); + executor.backend.insert_account_storage(address, slot, new_full_nonce)?; + + Ok(()) + } + + fn new_backend_strategy(&self) -> Box { + Box::new(ZksyncBackendStrategy::default()) + } + + fn new_cheatcode_inspector_strategy(&self) -> Box { + Box::new(ZksyncCheatcodeInspectorStrategy::new( + self.dual_compiled_contracts.clone(), + self.zk_env.clone(), + )) + } + + fn call_inspect( + &self, + db: &mut dyn DatabaseExt, + env: &mut EnvWithHandlerCfg, + inspector: &mut dyn InspectorExt, + ) -> eyre::Result { + match self.inspect_context.as_ref() { + None => self.evm.call_inspect(db, env, inspector), + Some(zk_tx) => foundry_zksync_core::vm::transact( + Some(&mut self.persisted_factory_deps.clone()), + Some(zk_tx.factory_deps.clone()), + zk_tx.paymaster_data.clone(), + env, + &self.zk_env, + db, + ), + } + } + + fn transact_inspect( + &mut self, + db: &mut dyn DatabaseExt, + env: &mut EnvWithHandlerCfg, + executor_env: &EnvWithHandlerCfg, + inspector: &mut dyn InspectorExt, + ) -> eyre::Result { + match self.inspect_context.take() { + None => self.evm.transact_inspect(db, env, executor_env, inspector), + Some(zk_tx) => { + // apply fork-related env instead of cheatcode handler + // since it won't be set by zkEVM + env.block = executor_env.block.clone(); + env.tx.gas_price = executor_env.tx.gas_price; + + foundry_zksync_core::vm::transact( + Some(&mut self.persisted_factory_deps), + Some(zk_tx.factory_deps), + zk_tx.paymaster_data, + env, + &self.zk_env, + db, + ) + } + } + } +} + +impl ExecutorStrategyExt for ZksyncExecutorStrategy { + fn zksync_set_dual_compiled_contracts( + &mut self, + dual_compiled_contracts: DualCompiledContracts, + ) { + self.dual_compiled_contracts = dual_compiled_contracts; + } + + fn zksync_set_fork_env(&mut self, fork_url: &str, env: &Env) -> Result<()> { + let provider = zksync_provider().with_recommended_fillers().on_http(fork_url.parse()?); + let block_number = env.block.number.try_into()?; + // TODO(zk): switch to getFeeParams call when it is implemented for anvil-zksync + let maybe_block_details = tokio::task::block_in_place(move || { + tokio::runtime::Handle::current().block_on(provider.get_block_details(block_number)) + }) + .ok() + .flatten(); + + if let Some(block_details) = maybe_block_details { + self.zk_env = ZkEnv { + l1_gas_price: block_details + .l1_gas_price + .try_into() + .expect("failed to convert l1_gas_price to u64"), + fair_l2_gas_price: block_details + .l2_fair_gas_price + .try_into() + .expect("failed to convert fair_l2_gas_price to u64"), + fair_pubdata_price: block_details + .fair_pubdata_price + // TODO(zk): None as a value might mean L1Pegged model + // we need to find out if it will ever be relevant to + // us + .unwrap_or_default() + .try_into() + .expect("failed to convert fair_pubdata_price to u64"), + }; + } + + Ok(()) + } +} + +/// Retrieve metadata for zksync tx +pub fn get_zksync_transaction_metadata( + other_fields: &OtherFields, +) -> Option { + other_fields + .get_deserialized::(ZKSYNC_TRANSACTION_OTHER_FIELDS_KEY) + .transpose() + .ok() + .flatten() +} diff --git a/crates/strategy/zksync/src/lib.rs b/crates/strategy/zksync/src/lib.rs new file mode 100644 index 000000000..0d7e2493b --- /dev/null +++ b/crates/strategy/zksync/src/lib.rs @@ -0,0 +1,14 @@ +//! # foundry-strategy-zksync +//! +//! Strategies for ZKsync network. + +#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +mod backend; +mod cheatcode; +mod executor; + +pub use backend::ZksyncBackendStrategy; +pub use cheatcode::ZksyncCheatcodeInspectorStrategy; +pub use executor::{get_zksync_transaction_metadata, ZksyncExecutorStrategy}; diff --git a/crates/test-utils/src/util.rs b/crates/test-utils/src/util.rs index 8d7f6cbb5..4e7d3c22b 100644 --- a/crates/test-utils/src/util.rs +++ b/crates/test-utils/src/util.rs @@ -881,6 +881,7 @@ impl TestCommand { #[track_caller] pub fn assert(&mut self) -> OutputAssert { let assert = OutputAssert::new(self.execute()); + if self.redact_output { return assert.with_assert(test_assert()); }; diff --git a/crates/verify/src/bytecode.rs b/crates/verify/src/bytecode.rs index d3079fae7..24ceaa547 100644 --- a/crates/verify/src/bytecode.rs +++ b/crates/verify/src/bytecode.rs @@ -123,6 +123,7 @@ impl VerifyBytecodeArgs { // Setup let config = self.load_config_emit_warnings(); let provider = utils::get_provider(&config)?; + let strategy = utils::get_executor_strategy(&config); // If chain is not set, we try to get it from the RPC. // If RPC is not set, the default chain is used. @@ -239,6 +240,7 @@ impl VerifyBytecodeArgs { gen_blk_num, etherscan_metadata.evm_version()?.unwrap_or(EvmVersion::default()), evm_opts, + strategy.new_cloned(), ) .await?; @@ -442,6 +444,7 @@ impl VerifyBytecodeArgs { simulation_block - 1, // env.fork_block_number etherscan_metadata.evm_version()?.unwrap_or(EvmVersion::default()), evm_opts, + strategy.new_cloned(), ) .await?; env.block.number = U256::from(simulation_block); diff --git a/crates/verify/src/utils.rs b/crates/verify/src/utils.rs index a14d6af6d..fbf6a2664 100644 --- a/crates/verify/src/utils.rs +++ b/crates/verify/src/utils.rs @@ -12,7 +12,11 @@ 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::{ + constants::DEFAULT_CREATE2_DEPLOYER, + executors::{strategy::ExecutorStrategy, TracingExecutor}, + opts::EvmOpts, +}; use reqwest::Url; use revm_primitives::{ db::Database, @@ -321,6 +325,7 @@ pub async fn get_tracing_executor( fork_blk_num: u64, evm_version: EvmVersion, evm_opts: EvmOpts, + strategy: Box, ) -> Result<(Env, TracingExecutor)> { fork_config.fork_block_number = Some(fork_blk_num); fork_config.evm_version = evm_version; @@ -335,6 +340,7 @@ pub async fn get_tracing_executor( false, false, is_alphanet, + strategy, ); Ok((env, executor)) diff --git a/crates/zksync/core/src/lib.rs b/crates/zksync/core/src/lib.rs index 7218ff60e..e6b15b428 100644 --- a/crates/zksync/core/src/lib.rs +++ b/crates/zksync/core/src/lib.rs @@ -86,6 +86,7 @@ pub struct ZkPaymasterData { /// Represents additional data for ZK transactions. #[derive(Clone, Debug, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct ZkTransactionMetadata { /// Factory Deps for ZK transactions. pub factory_deps: Vec>, diff --git a/crates/zksync/core/src/vm/runner.rs b/crates/zksync/core/src/vm/runner.rs index 0f117ec43..e39c409c3 100644 --- a/crates/zksync/core/src/vm/runner.rs +++ b/crates/zksync/core/src/vm/runner.rs @@ -35,7 +35,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");