From 1e5e1021dbd09b47f0b64303b8d55fea152483ad Mon Sep 17 00:00:00 2001 From: Karrq Date: Thu, 18 Apr 2024 14:12:40 +0200 Subject: [PATCH] fix: fetch all factory deps for a given contract (#316) Co-authored-by: Jrigada Co-authored-by: Herman Obst Demaestri <70286869+HermanObst@users.noreply.github.com> Co-authored-by: Nisheeth Barthwal --- crates/cheatcodes/src/config.rs | 6 +- crates/cheatcodes/src/inspector.rs | 57 +++++---- crates/cheatcodes/src/test.rs | 2 + crates/forge/bin/cmd/create.rs | 53 ++++---- crates/forge/bin/cmd/script/broadcast.rs | 8 +- crates/forge/bin/cmd/script/build.rs | 6 +- crates/forge/bin/cmd/script/cmd.rs | 6 +- crates/forge/bin/cmd/script/executor.rs | 10 +- crates/forge/bin/cmd/test/mod.rs | 4 +- crates/forge/tests/it/config.rs | 5 +- crates/zksync/compiler/src/zksolc/mod.rs | 149 ++++++++++++++-------- crates/zksync/core/src/cheatcodes.rs | 2 +- crates/zksync/core/src/lib.rs | 1 + crates/zksync/core/src/vm/db.rs | 7 + crates/zksync/core/src/vm/runner.rs | 18 +-- crates/zksync/core/src/vm/storage_view.rs | 8 +- crates/zksync/core/src/vm/tracer.rs | 2 + zk-tests/script/Factory.s.sol | 72 +++++++++++ zk-tests/src/Factory.sol | 76 +++++++++++ zk-tests/src/Factory.t.sol | 49 +++++++ 20 files changed, 401 insertions(+), 140 deletions(-) create mode 100644 zk-tests/script/Factory.s.sol create mode 100644 zk-tests/src/Factory.sol create mode 100644 zk-tests/src/Factory.t.sol diff --git a/crates/cheatcodes/src/config.rs b/crates/cheatcodes/src/config.rs index f44ce1652..7690ee5fd 100644 --- a/crates/cheatcodes/src/config.rs +++ b/crates/cheatcodes/src/config.rs @@ -8,7 +8,7 @@ use foundry_config::{ ResolvedRpcEndpoints, }; use foundry_evm_core::opts::EvmOpts; -use foundry_zksync_compiler::DualCompiledContract; +use foundry_zksync_compiler::DualCompiledContracts; use std::{ collections::HashMap, path::{Path, PathBuf}, @@ -42,7 +42,7 @@ pub struct CheatsConfig { /// Script wallets pub script_wallets: Option, /// ZKSolc -> Solc Contract codes - pub dual_compiled_contracts: Vec, + pub dual_compiled_contracts: DualCompiledContracts, /// Use ZK-VM on startup pub use_zk: bool, } @@ -53,7 +53,7 @@ impl CheatsConfig { config: &Config, evm_opts: EvmOpts, script_wallets: Option, - dual_compiled_contracts: Vec, + dual_compiled_contracts: DualCompiledContracts, use_zk: bool, ) -> Self { let mut allowed_paths = vec![config.__root.0.clone()]; diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index fc4c47d43..c02f79f2b 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -28,10 +28,10 @@ use foundry_evm_core::{ HARDHAT_CONSOLE_ADDRESS, }, }; -use foundry_zksync_compiler::{DualCompiledContract, FindContract}; +use foundry_zksync_compiler::DualCompiledContracts; use foundry_zksync_core::{ convert::{ConvertAddress, ConvertH160, ConvertH256, ConvertRU256, ConvertU256}, - ZkTransactionMetadata, + hash_bytecode, ZkTransactionMetadata, }; use itertools::Itertools; use revm::{ @@ -58,7 +58,7 @@ use zksync_types::{ get_code_key, get_nonce_key, utils::{decompose_full_nonce, nonces_to_full_nonce, storage_key_for_eth_balance}, ACCOUNT_CODE_STORAGE_ADDRESS, CONTRACT_DEPLOYER_ADDRESS, CURRENT_VIRTUAL_BLOCK_INFO_POSITION, - KNOWN_CODES_STORAGE_ADDRESS, L2_ETH_TOKEN_ADDRESS, NONCE_HOLDER_ADDRESS, + H256, KNOWN_CODES_STORAGE_ADDRESS, L2_ETH_TOKEN_ADDRESS, NONCE_HOLDER_ADDRESS, SYSTEM_CONTEXT_ADDRESS, }; @@ -231,7 +231,7 @@ pub struct Cheatcodes { pub use_zk_vm: bool, /// Dual compiled contracts - pub dual_compiled_contracts: Vec, + pub dual_compiled_contracts: DualCompiledContracts, /// Logs printed during ZK-VM execution. /// EVM logs have the value `None` so they can be interpolated later, since @@ -240,6 +240,13 @@ pub struct Cheatcodes { /// Starts the cheatcode inspector in ZK mode pub startup_zk: bool, + + /// 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>, } impl Cheatcodes { @@ -404,10 +411,8 @@ impl Cheatcodes { .map(|(value, _)| value) .ok() .and_then(|zk_bytecode_hash| { - let zk_bytecode_hash = zk_bytecode_hash.to_h256(); self.dual_compiled_contracts - .iter() - .find(|c| c.zk_bytecode_hash == zk_bytecode_hash) + .find_by_zk_bytecode_hash(zk_bytecode_hash.to_h256()) .map(|contract| { ( contract.evm_bytecode_hash, @@ -1189,31 +1194,15 @@ impl Inspector for Cheatcodes { } info!("running call in zk vm {:#?}", call); - - let code_hash = data - .journaled_state - .load_account(call.contract, data.db) - .map(|(account, _)| account.info.code_hash) - .unwrap_or_default(); - let contract = if code_hash != KECCAK_EMPTY { - self.dual_compiled_contracts - .find_zk_bytecode_hash(zksync_types::H256::from(code_hash.0)) - } else { - None - }; - if let Some(contract) = contract { - tracing::debug!(contract = contract.name, "using dual compiled contract"); - } else { - error!("no zk contract was found for {code_hash:?}"); - } + let persisted_factory_deps = self.persisted_factory_deps.clone(); let ccx = foundry_zksync_core::vm::CheatcodeTracerContext { mocked_calls: self.mocked_calls.clone(), expected_calls: Some(&mut self.expected_calls), + persisted_factory_deps, }; if let Ok(result) = foundry_zksync_core::vm::call::<_, DatabaseError>( call, - contract, data.env, data.db, &mut data.journaled_state, @@ -1569,10 +1558,13 @@ impl Inspector for Cheatcodes { ) as u64; let contract = self .dual_compiled_contracts - .find_evm_bytecode(&call.init_code.0) + .find_by_evm_bytecode(&call.init_code.0) .unwrap_or_else(|| { panic!("failed finding contract for {:?}", call.init_code) }); + let factory_deps = + self.dual_compiled_contracts.fetch_all_factory_deps(contract); + let constructor_input = call.init_code[contract.evm_bytecode.len()..].to_vec(); let create_input = foundry_zksync_core::encode_create_params( @@ -1581,7 +1573,6 @@ impl Inspector for Cheatcodes { constructor_input, ); bytecode = Bytes::from(create_input); - let factory_deps = vec![contract.zk_deployed_bytecode.clone()]; Some(ZkTransactionMetadata { factory_deps }) } else { @@ -1674,17 +1665,27 @@ impl Inspector for Cheatcodes { let zk_contract = self .dual_compiled_contracts - .find_evm_bytecode(&call.init_code.0) + .find_by_evm_bytecode(&call.init_code.0) .unwrap_or_else(|| panic!("failed finding contract for {:?}", call.init_code)); + let factory_deps = self.dual_compiled_contracts.fetch_all_factory_deps(zk_contract); + + // get the current persisted factory deps to pass to zk create + let persisted_factory_deps = self.persisted_factory_deps.clone(); + // and extend it for future calls + self.persisted_factory_deps + .extend(factory_deps.iter().map(|dep| (hash_bytecode(dep), dep.clone()))); + tracing::debug!(contract = zk_contract.name, "using dual compiled contract"); let ccx = foundry_zksync_core::vm::CheatcodeTracerContext { mocked_calls: self.mocked_calls.clone(), expected_calls: Some(&mut self.expected_calls), + persisted_factory_deps, }; if let Ok(result) = foundry_zksync_core::vm::create::<_, DatabaseError>( call, zk_contract, + factory_deps, data.env, data.db, &mut data.journaled_state, diff --git a/crates/cheatcodes/src/test.rs b/crates/cheatcodes/src/test.rs index f72f8a833..69d52126e 100644 --- a/crates/cheatcodes/src/test.rs +++ b/crates/cheatcodes/src/test.rs @@ -38,6 +38,8 @@ impl Cheatcode for zkRegisterContractCall { name: name.clone(), zk_bytecode_hash: zkBytecodeHash.0.into(), zk_deployed_bytecode: zkDeployedBytecode.clone(), + //TODO: add argument to cheatcode + zk_factory_deps: vec![], evm_bytecode_hash: *evmBytecodeHash, evm_deployed_bytecode: evmDeployedBytecode.clone(), evm_bytecode: evmBytecode.clone(), diff --git a/crates/forge/bin/cmd/create.rs b/crates/forge/bin/cmd/create.rs index 65f343914..3c9e14fc8 100644 --- a/crates/forge/bin/cmd/create.rs +++ b/crates/forge/bin/cmd/create.rs @@ -30,9 +30,7 @@ use foundry_compilers::{ utils::canonicalized, }; use foundry_wallets::WalletSigner; -use foundry_zksync_compiler::{ - new_dual_compiled_contracts, DualCompiledContract, FindContract, ZkSolc, -}; +use foundry_zksync_compiler::{DualCompiledContract, DualCompiledContracts, ZkSolc}; use serde_json::json; use std::{borrow::Borrow, marker::PhantomData, path::PathBuf, sync::Arc}; @@ -123,7 +121,7 @@ impl CreateArgs { Ok(compiled) => compiled, Err(e) => return Err(eyre::eyre!("Failed to compile with zksolc: {}", e)), }; - let dual_compiled_contracts = new_dual_compiled_contracts(&output, &zk_output); + let dual_compiled_contracts = DualCompiledContracts::new(&output, &zk_output); if let Some(ref mut path) = self.contract.path { // paths are absolute in the project's output @@ -136,11 +134,12 @@ impl CreateArgs { let contract = bin .object .as_bytes() - .and_then(|bytes| dual_compiled_contracts.find_evm_bytecode(&bytes.0)) + .and_then(|bytes| dual_compiled_contracts.find_by_evm_bytecode(&bytes.0)) .ok_or(eyre::eyre!( "Could not find zksolc contract for contract {}", self.contract.name ))?; + let zk_bin = CompactBytecode { object: BytecodeObject::Bytecode(Bytes::from( contract.zk_deployed_bytecode.clone(), @@ -149,8 +148,9 @@ impl CreateArgs { source_map: Default::default(), }; - let mut factory_deps = Vec::with_capacity(self.factory_deps.len()); + let mut factory_deps = dual_compiled_contracts.fetch_all_factory_deps(contract); + // for manual specified factory deps for mut contract in std::mem::take(&mut self.factory_deps) { if let Some(path) = contract.path.as_mut() { *path = canonicalized(project.root().join(&path)).to_string_lossy().to_string(); @@ -163,16 +163,27 @@ impl CreateArgs { let zk = bin .object .as_bytes() - .and_then(|bytes| dual_compiled_contracts.find_evm_bytecode(&bytes.0)) + .and_then(|bytes| dual_compiled_contracts.find_by_evm_bytecode(&bytes.0)) .ok_or(eyre::eyre!( "Could not find zksolc contract for contract {}", contract.name ))?; - factory_deps.push(zk.zk_deployed_bytecode.clone()); + // if the dep isn't already present, + // fetch all deps and add them to the final list + if !factory_deps.contains(&zk.zk_deployed_bytecode) { + let additional_factory_deps = + dual_compiled_contracts.fetch_all_factory_deps(zk); + factory_deps.extend(additional_factory_deps); + factory_deps.dedup(); + } } - (abi, zk_bin, Some((contract, factory_deps))) + ( + abi, + zk_bin, + Some((contract, factory_deps.into_iter().map(|bc| bc.to_vec()).collect())), + ) } else { (abi, bin, None) }; @@ -297,16 +308,7 @@ impl CreateArgs { let factory = ContractFactory::new(abi.clone(), bin.clone(), provider.clone()); let is_args_empty = args.is_empty(); - let (zk_contract, factory_deps) = match zk_data { - Some((zk_contract, mut factory_deps)) => { - //add this contract to the list of factory deps - factory_deps.push(zk_contract.zk_deployed_bytecode.clone()); - (Some(zk_contract), factory_deps) - } - None => (None, vec![]), - }; - - let deployer = if let Some(contract) = zk_contract { + let deployer = if let Some((contract, factory_deps)) = &zk_data { factory.deploy_tokens_zk(args.clone(), contract).context("failed to deploy contract") .map(|deployer| deployer.set_zk_factory_deps(factory_deps.clone())) } else { @@ -328,9 +330,9 @@ impl CreateArgs { deployer.tx.set_value(value.to_ethers()); } - match zk_contract { + match zk_data { None => provider.fill_transaction(&mut deployer.tx, None).await?, - Some(contract) => { + Some((contract, factory_deps)) => { let chain_id = provider.get_chainid().await?.as_u64(); deployer.tx.set_chain_id(chain_id); @@ -358,9 +360,12 @@ impl CreateArgs { .tx .set_to(NameOrAddress::from(foundry_zksync_core::CONTRACT_DEPLOYER_ADDRESS)); - let estimated_gas = - foundry_zksync_core::estimate_gas(&deployer.tx, factory_deps, &provider) - .await?; + let estimated_gas = foundry_zksync_core::estimate_gas( + &deployer.tx, + factory_deps.clone(), + &provider, + ) + .await?; deployer.tx.set_gas(estimated_gas.limit.to_ethers()); deployer.tx.set_gas_price(estimated_gas.price.to_ethers()); } diff --git a/crates/forge/bin/cmd/script/broadcast.rs b/crates/forge/bin/cmd/script/broadcast.rs index 8dce97b61..9480e000d 100644 --- a/crates/forge/bin/cmd/script/broadcast.rs +++ b/crates/forge/bin/cmd/script/broadcast.rs @@ -29,7 +29,7 @@ use foundry_common::{ use foundry_compilers::{artifacts::Libraries, ArtifactId}; use foundry_config::Config; use foundry_wallets::WalletSigner; -use foundry_zksync_compiler::DualCompiledContract; +use foundry_zksync_compiler::DualCompiledContracts; use futures::StreamExt; use std::{ cmp::min, @@ -317,7 +317,7 @@ impl ScriptArgs { mut script_config: ScriptConfig, verify: VerifyBundle, signers: &HashMap, - dual_compiled_contracts: Option>, + dual_compiled_contracts: Option, ) -> Result<()> { if let Some(txs) = result.transactions.take() { script_config.collect_rpcs(&txs); @@ -419,7 +419,7 @@ impl ScriptArgs { script_config: &mut ScriptConfig, decoder: &CallTraceDecoder, known_contracts: &ContractsByArtifact, - dual_compiled_contracts: Option>, + dual_compiled_contracts: Option, ) -> Result> { if !txs.is_empty() { let gas_filled_txs = self @@ -458,7 +458,7 @@ impl ScriptArgs { script_config: &ScriptConfig, decoder: &CallTraceDecoder, known_contracts: &ContractsByArtifact, - dual_compiled_contracts: Option>, + dual_compiled_contracts: Option, ) -> Result> { let gas_filled_txs = if self.skip_simulation { shell::println("\nSKIPPING ON CHAIN SIMULATION.")?; diff --git a/crates/forge/bin/cmd/script/build.rs b/crates/forge/bin/cmd/script/build.rs index 038463af2..356b22d77 100644 --- a/crates/forge/bin/cmd/script/build.rs +++ b/crates/forge/bin/cmd/script/build.rs @@ -11,7 +11,7 @@ use foundry_compilers::{ info::ContractInfo, ArtifactId, Project, ProjectCompileOutput, }; -use foundry_zksync_compiler::{new_dual_compiled_contracts, DualCompiledContract, ZkSolc}; +use foundry_zksync_compiler::{DualCompiledContracts, ZkSolc}; use std::str::FromStr; impl ScriptArgs { @@ -46,7 +46,7 @@ impl ScriptArgs { Ok(compiled) => compiled, Err(e) => return Err(eyre::eyre!("Failed to compile with zksolc: {}", e)), }; - let dual_compiled_contracts = new_dual_compiled_contracts(&output, &zk_output); + let dual_compiled_contracts = DualCompiledContracts::new(&output, &zk_output); let sources = ContractSources::from_project_output(&output, root)?; let contracts = output.into_artifacts().collect(); @@ -228,5 +228,5 @@ pub struct BuildOutput { pub libraries: Libraries, pub predeploy_libraries: Vec, pub sources: ContractSources, - pub dual_compiled_contracts: Option>, + pub dual_compiled_contracts: Option, } diff --git a/crates/forge/bin/cmd/script/cmd.rs b/crates/forge/bin/cmd/script/cmd.rs index 5b34638e8..c46dfe1a0 100644 --- a/crates/forge/bin/cmd/script/cmd.rs +++ b/crates/forge/bin/cmd/script/cmd.rs @@ -19,7 +19,7 @@ use foundry_compilers::{ use foundry_debugger::Debugger; use foundry_evm::inspectors::cheatcodes::{BroadcastableTransaction, ScriptWallets}; use foundry_wallets::WalletSigner; -use foundry_zksync_compiler::DualCompiledContract; +use foundry_zksync_compiler::DualCompiledContracts; use std::{collections::HashMap, sync::Arc}; /// Helper alias type for the collection of data changed due to the new sender. @@ -159,7 +159,7 @@ impl ScriptArgs { predeploy_libraries: Vec, result: &mut ScriptResult, script_wallets: ScriptWallets, - dual_compiled_contracts: Option>, + dual_compiled_contracts: Option, ) -> Result> { if let Some(new_sender) = self.maybe_new_sender( &script_config.evm_opts, @@ -332,7 +332,7 @@ impl ScriptArgs { first_run_result: &mut ScriptResult, linker: Linker, script_wallets: ScriptWallets, - dual_compiled_contracts: Option>, + dual_compiled_contracts: Option, ) -> Result<(Libraries, ArtifactContracts)> { // if we had a new sender that requires relinking, we need to // get the nonce mainnet for accurate addresses for predeploy libs diff --git a/crates/forge/bin/cmd/script/executor.rs b/crates/forge/bin/cmd/script/executor.rs index 93652dc5f..108cdf286 100644 --- a/crates/forge/bin/cmd/script/executor.rs +++ b/crates/forge/bin/cmd/script/executor.rs @@ -18,7 +18,7 @@ use foundry_cli::utils::{ensure_clean_constructor, needs_setup}; use foundry_common::{get_contract_name, provider::ethers::RpcUrl, shell, ContractsByArtifact}; use foundry_compilers::artifacts::ContractBytecodeSome; use foundry_evm::inspectors::cheatcodes::ScriptWallets; -use foundry_zksync_compiler::DualCompiledContract; +use foundry_zksync_compiler::DualCompiledContracts; use futures::future::join_all; use parking_lot::RwLock; use std::{ @@ -36,7 +36,7 @@ impl ScriptArgs { sender: Address, predeploy_libraries: &[Bytes], script_wallets: ScriptWallets, - dual_compiled_contracts: Option>, + dual_compiled_contracts: Option, ) -> Result { trace!(target: "script", "start executing script"); @@ -104,7 +104,7 @@ impl ScriptArgs { script_config: &ScriptConfig, decoder: &CallTraceDecoder, contracts: &ContractsByArtifact, - dual_compiled_contracts: Option>, + dual_compiled_contracts: Option, ) -> Result> { trace!(target: "script", "executing onchain simulation"); @@ -253,7 +253,7 @@ impl ScriptArgs { async fn build_runners( &self, script_config: &ScriptConfig, - dual_compiled_contracts: Option>, + dual_compiled_contracts: Option, ) -> Result> { let sender = script_config.evm_opts.sender; @@ -292,7 +292,7 @@ impl ScriptArgs { sender: Address, stage: SimulationStage, script_wallets: Option, - dual_compiled_contracts: Option>, + dual_compiled_contracts: Option, ) -> Result { trace!("preparing script runner"); let env = script_config.evm_opts.evm_env().await?; diff --git a/crates/forge/bin/cmd/test/mod.rs b/crates/forge/bin/cmd/test/mod.rs index e677c5d98..df6cc0f8a 100644 --- a/crates/forge/bin/cmd/test/mod.rs +++ b/crates/forge/bin/cmd/test/mod.rs @@ -31,7 +31,7 @@ use foundry_config::{ get_available_profiles, Config, }; use foundry_debugger::Debugger; -use foundry_zksync_compiler::{new_dual_compiled_contracts, ZkSolc}; +use foundry_zksync_compiler::{DualCompiledContracts, ZkSolc}; use regex::Regex; use std::{sync::mpsc::channel, time::Instant}; use watchexec::config::{InitConfig, RuntimeConfig}; @@ -185,7 +185,7 @@ impl TestArgs { Ok(compiled) => compiled, Err(e) => return Err(eyre::eyre!("Failed to compile with zksolc: {}", e)), }; - let dual_compiled_contracts = new_dual_compiled_contracts(&output, &zk_output); + let dual_compiled_contracts = DualCompiledContracts::new(&output, &zk_output); // Create test options from general project settings and compiler output. let project_root = &project.paths.root; diff --git a/crates/forge/tests/it/config.rs b/crates/forge/tests/it/config.rs index 69e774390..c0b1a3f5e 100644 --- a/crates/forge/tests/it/config.rs +++ b/crates/forge/tests/it/config.rs @@ -18,7 +18,7 @@ use foundry_evm::{ traces::{render_trace_arena, CallTraceDecoderBuilder}, }; use foundry_test_utils::{init_tracing, Filter}; -use foundry_zksync_compiler::{DualCompiledContract, PackedEraBytecode}; +use foundry_zksync_compiler::{DualCompiledContract, DualCompiledContracts, PackedEraBytecode}; use futures::future::join_all; use itertools::Itertools; use std::{ @@ -210,7 +210,7 @@ pub async fn runner_with_config_and_zk(mut config: Config) -> MultiContractRunne let zk_output = COMPILED_ZK.clone(); // Dual compiled contracts - let mut dual_compiled_contracts = vec![]; + let mut dual_compiled_contracts = DualCompiledContracts::default(); let mut solc_bytecodes = HashMap::new(); for (contract_name, artifact) in output.artifacts() { let contract_name = @@ -242,6 +242,7 @@ pub async fn runner_with_config_and_zk(mut config: Config) -> MultiContractRunne name: contract_name, zk_bytecode_hash: packed_bytecode.bytecode_hash(), zk_deployed_bytecode: packed_bytecode.bytecode(), + zk_factory_deps: packed_bytecode.factory_deps(), evm_bytecode_hash: keccak256(solc_deployed_bytecode), evm_bytecode: solc_bytecode.to_vec(), evm_deployed_bytecode: solc_deployed_bytecode.to_vec(), diff --git a/crates/zksync/compiler/src/zksolc/mod.rs b/crates/zksync/compiler/src/zksolc/mod.rs index b07c554db..b35c86a69 100644 --- a/crates/zksync/compiler/src/zksolc/mod.rs +++ b/crates/zksync/compiler/src/zksolc/mod.rs @@ -5,7 +5,7 @@ mod config; mod factory_deps; mod manager; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet, VecDeque}; pub use compile::*; pub use config::*; @@ -14,6 +14,7 @@ use foundry_compilers::{Artifact, ProjectCompileOutput}; pub use manager::*; use alloy_primitives::{keccak256, B256}; +use tracing::debug; use zksync_types::H256; /// Defines a contract that has been dual compiled with both zksolc and solc @@ -25,6 +26,8 @@ pub struct DualCompiledContract { pub zk_bytecode_hash: H256, /// Deployed bytecode hash with zksolc pub zk_deployed_bytecode: Vec, + /// Deployed bytecode factory deps + pub zk_factory_deps: Vec>, /// Deployed bytecode hash with solc pub evm_bytecode_hash: B256, /// Deployed bytecode with solc @@ -33,67 +36,113 @@ pub struct DualCompiledContract { pub evm_bytecode: Vec, } -/// Creates a list of [DualCompiledContract]s from the provided solc and zksolc output. -pub fn new_dual_compiled_contracts( - output: &ProjectCompileOutput, - zk_output: &ProjectCompileOutput, -) -> Vec { - let mut dual_compiled_contracts = vec![]; - let mut solc_bytecodes = HashMap::new(); - for (contract_name, artifact) in output.artifacts() { - let contract_name = - contract_name.split('.').next().expect("name cannot be empty").to_string(); - let deployed_bytecode = artifact.get_deployed_bytecode(); - let deployed_bytecode = deployed_bytecode - .as_ref() - .and_then(|d| d.bytecode.as_ref().and_then(|b| b.object.as_bytes())); - let bytecode = artifact.get_bytecode().and_then(|b| b.object.as_bytes().cloned()); - if let Some(bytecode) = bytecode { - if let Some(deployed_bytecode) = deployed_bytecode { - solc_bytecodes.insert(contract_name.clone(), (bytecode, deployed_bytecode.clone())); +/// A collection of `[DualCompiledContract]`s +#[derive(Debug, Default, Clone)] +pub struct DualCompiledContracts { + contracts: Vec, +} + +impl DualCompiledContracts { + /// Creates a collection of `[DualCompiledContract]`s from the provided solc and zksolc output. + pub fn new(output: &ProjectCompileOutput, zk_output: &ProjectCompileOutput) -> Self { + let mut dual_compiled_contracts = vec![]; + let mut solc_bytecodes = HashMap::new(); + for (contract_name, artifact) in output.artifacts() { + let contract_name = + contract_name.split('.').next().expect("name cannot be empty").to_string(); + let deployed_bytecode = artifact.get_deployed_bytecode(); + let deployed_bytecode = deployed_bytecode + .as_ref() + .and_then(|d| d.bytecode.as_ref().and_then(|b| b.object.as_bytes())); + let bytecode = artifact.get_bytecode().and_then(|b| b.object.as_bytes().cloned()); + if let Some(bytecode) = bytecode { + if let Some(deployed_bytecode) = deployed_bytecode { + solc_bytecodes + .insert(contract_name.clone(), (bytecode, deployed_bytecode.clone())); + } } } - } - for (contract_name, artifact) in zk_output.artifacts() { - let deployed_bytecode = artifact.get_deployed_bytecode(); - let deployed_bytecode = deployed_bytecode - .as_ref() - .and_then(|d| d.bytecode.as_ref().and_then(|b| b.object.as_bytes())); - if let Some(deployed_bytecode) = deployed_bytecode { - let packed_bytecode = PackedEraBytecode::from_vec(deployed_bytecode); - if let Some((solc_bytecode, solc_deployed_bytecode)) = - solc_bytecodes.get(&contract_name) - { - dual_compiled_contracts.push(DualCompiledContract { - name: contract_name, - zk_bytecode_hash: packed_bytecode.bytecode_hash(), - zk_deployed_bytecode: packed_bytecode.bytecode(), - evm_bytecode_hash: keccak256(solc_deployed_bytecode), - evm_bytecode: solc_bytecode.to_vec(), - evm_deployed_bytecode: solc_deployed_bytecode.to_vec(), - }); + for (contract_name, artifact) in zk_output.artifacts() { + let deployed_bytecode = artifact.get_deployed_bytecode(); + let deployed_bytecode = deployed_bytecode + .as_ref() + .and_then(|d| d.bytecode.as_ref().and_then(|b| b.object.as_bytes())); + if let Some(deployed_bytecode) = deployed_bytecode { + let packed_bytecode = PackedEraBytecode::from_vec(deployed_bytecode); + if let Some((solc_bytecode, solc_deployed_bytecode)) = + solc_bytecodes.get(&contract_name) + { + dual_compiled_contracts.push(DualCompiledContract { + name: contract_name, + zk_bytecode_hash: packed_bytecode.bytecode_hash(), + zk_deployed_bytecode: packed_bytecode.bytecode(), + zk_factory_deps: packed_bytecode.factory_deps(), + evm_bytecode_hash: keccak256(solc_deployed_bytecode), + evm_bytecode: solc_bytecode.to_vec(), + evm_deployed_bytecode: solc_deployed_bytecode.to_vec(), + }); + } } } + + Self { contracts: dual_compiled_contracts } } - dual_compiled_contracts -} + /// Finds a contract matching the ZK deployed bytecode + pub fn find_by_zk_deployed_bytecode(&self, bytecode: &[u8]) -> Option<&DualCompiledContract> { + self.contracts.iter().find(|contract| bytecode.starts_with(&contract.zk_deployed_bytecode)) + } -/// Implements methods to look for contracts -pub trait FindContract { /// Finds a contract matching the EVM bytecode - fn find_evm_bytecode(&self, bytecode: &[u8]) -> Option<&DualCompiledContract>; + pub fn find_by_evm_bytecode(&self, bytecode: &[u8]) -> Option<&DualCompiledContract> { + self.contracts.iter().find(|contract| bytecode.starts_with(&contract.evm_bytecode)) + } /// Finds a contract matching the ZK bytecode hash - fn find_zk_bytecode_hash(&self, code_hash: H256) -> Option<&DualCompiledContract>; -} + pub fn find_by_zk_bytecode_hash(&self, code_hash: H256) -> Option<&DualCompiledContract> { + self.contracts.iter().find(|contract| code_hash == contract.zk_bytecode_hash) + } + + /// Finds a contract own and nested factory deps + pub fn fetch_all_factory_deps(&self, root: &DualCompiledContract) -> Vec> { + let mut visited = HashSet::new(); + let mut queue = VecDeque::new(); + + for dep in &root.zk_factory_deps { + queue.push_back(dep); + } + + while let Some(dep) = queue.pop_front() { + // try to insert in the list of visited, if it's already present, skip + if visited.insert(dep) { + if let Some(contract) = self.find_by_zk_deployed_bytecode(dep) { + debug!( + name = contract.name, + deps = contract.zk_factory_deps.len(), + "new factory depdendency" + ); + + for nested_dep in &contract.zk_factory_deps { + // check that the nested dependency is inserted + if !visited.contains(nested_dep) { + // if not, add it to queue for processing + queue.push_back(nested_dep); + } + } + } + } + } + + visited.into_iter().cloned().collect() + } -impl FindContract for Vec { - fn find_evm_bytecode(&self, bytecode: &[u8]) -> Option<&DualCompiledContract> { - self.iter().find(|contract| bytecode.starts_with(&contract.evm_bytecode)) + /// Returns an iterator over all `[DualCompiledContract]`s in the collection + pub fn iter(&self) -> impl Iterator { + self.contracts.iter() } - fn find_zk_bytecode_hash(&self, code_hash: H256) -> Option<&DualCompiledContract> { - self.iter().find(|contract| code_hash == contract.zk_bytecode_hash) + /// Adds a new `[DualCompiledContract]` to the collection + pub fn push(&mut self, contract: DualCompiledContract) { + self.contracts.push(contract); } } diff --git a/crates/zksync/core/src/cheatcodes.rs b/crates/zksync/core/src/cheatcodes.rs index 0596054ca..57cdd6caf 100644 --- a/crates/zksync/core/src/cheatcodes.rs +++ b/crates/zksync/core/src/cheatcodes.rs @@ -149,7 +149,7 @@ pub fn etch<'a, DB>( info!(?address, bytecode = hex::encode(bytecode), "cheatcode etch"); let bytecode_hash = hash_bytecode(bytecode).to_ru256(); - let bytecode = Bytecode::new_raw(Bytes::copy_from_slice(bytecode)).to_checked(); + let bytecode = Bytecode::new_raw(Bytes::copy_from_slice(bytecode)); let account_code_addr = ACCOUNT_CODE_STORAGE_ADDRESS.to_address(); let known_codes_addr = KNOWN_CODES_STORAGE_ADDRESS.to_address(); diff --git a/crates/zksync/core/src/lib.rs b/crates/zksync/core/src/lib.rs index ee32dc98e..d031282b7 100644 --- a/crates/zksync/core/src/lib.rs +++ b/crates/zksync/core/src/lib.rs @@ -29,6 +29,7 @@ pub use zksync_types::{ ACCOUNT_CODE_STORAGE_ADDRESS, CONTRACT_DEPLOYER_ADDRESS, L2_ETH_TOKEN_ADDRESS, NONCE_HOLDER_ADDRESS, }; +pub use zksync_utils::bytecode::hash_bytecode; use zksync_web3_rs::{ eip712::{Eip712Meta, Eip712Transaction, Eip712TransactionRequest}, providers::Middleware, diff --git a/crates/zksync/core/src/vm/db.rs b/crates/zksync/core/src/vm/db.rs index 1b6d0020d..385c761d8 100644 --- a/crates/zksync/core/src/vm/db.rs +++ b/crates/zksync/core/src/vm/db.rs @@ -48,6 +48,7 @@ where { /// Create a new instance of [ZKEVMData]. pub fn new(db: &'a mut DB, journaled_state: &'a mut JournaledState) -> Self { + // load all deployed contract bytecodes from the JournaledState as factory deps let mut factory_deps = journaled_state .state @@ -157,6 +158,12 @@ where account } + /// Extends the currently known factory deps with the provided input + pub fn with_extra_factory_deps(mut self, extra_factory_deps: HashMap>) -> Self { + self.factory_deps.extend(extra_factory_deps); + self + } + fn read_db(&mut self, address: H160, idx: U256) -> H256 { let addr = address.to_address(); self.journaled_state.load_account(addr, self.db).expect("failed loading account"); diff --git a/crates/zksync/core/src/vm/runner.rs b/crates/zksync/core/src/vm/runner.rs index feb480cb5..c79e54aa7 100644 --- a/crates/zksync/core/src/vm/runner.rs +++ b/crates/zksync/core/src/vm/runner.rs @@ -41,11 +41,7 @@ use tracing::{info, trace}; use zksync_basic_types::{L2ChainId, H256}; use zksync_state::{ReadStorage, StoragePtr, WriteStorage}; use zksync_types::{ - ethabi::{self}, - fee::Fee, - l2::L2Tx, - transaction_request::PaymasterParams, - vm_trace::Call, + ethabi, fee::Fee, l2::L2Tx, transaction_request::PaymasterParams, vm_trace::Call, PackedEthSignature, StorageKey, Transaction, VmEvent, ACCOUNT_CODE_STORAGE_ADDRESS, CONTRACT_DEPLOYER_ADDRESS, H160, U256, }; @@ -164,6 +160,7 @@ where pub fn create<'a, DB, E>( call: &CreateInputs, contract: &DualCompiledContract, + factory_deps: Vec>, env: &'a mut Env, db: &'a mut DB, journaled_state: &'a mut JournaledState, @@ -177,7 +174,6 @@ where let constructor_input = call.init_code[contract.evm_bytecode.len()..].to_vec(); let caller = env.tx.caller; let calldata = encode_create_params(&call.scheme, contract.zk_bytecode_hash, constructor_input); - let factory_deps = vec![contract.zk_deployed_bytecode.clone()]; let nonce = ZKVMData::new(db, journaled_state).get_tx_nonce(caller); let (gas_limit, max_fee_per_gas) = gas_params(env, db, journaled_state, caller); @@ -210,7 +206,6 @@ where /// Executes a CALL opcode on the ZK-VM. pub fn call<'a, DB, E>( call: &CallInputs, - contract: Option<&DualCompiledContract>, env: &'a mut Env, db: &'a mut DB, journaled_state: &'a mut JournaledState, @@ -222,7 +217,6 @@ where { info!(?call, "call tx {}", hex::encode(&call.input)); let caller = env.tx.caller; - let factory_deps = contract.map(|contract| vec![contract.zk_deployed_bytecode.clone()]); let nonce: zksync_types::Nonce = ZKVMData::new(db, journaled_state).get_tx_nonce(caller); let (gas_limit, max_fee_per_gas) = gas_params(env, db, journaled_state, caller); @@ -238,7 +232,7 @@ where }, caller.to_h160(), call.transfer.value.to_u256(), - factory_deps, + None, PaymasterParams::default(), ); @@ -286,14 +280,16 @@ fn inspect<'a, DB, E>( env: &'a mut Env, db: &'a mut DB, journaled_state: &'a mut JournaledState, - ccx: CheatcodeTracerContext, + mut ccx: CheatcodeTracerContext, call_ctx: CallContext, ) -> ZKVMResult where DB: Database + Send, ::Error: Debug, { - let mut era_db = ZKVMData::new_with_system_contracts(db, journaled_state); + let mut era_db = ZKVMData::new_with_system_contracts(db, journaled_state) + .with_extra_factory_deps(std::mem::take(&mut ccx.persisted_factory_deps)); + let is_create = tx.execute.contract_address == zksync_types::CONTRACT_DEPLOYER_ADDRESS; tracing::info!(?call_ctx, "executing transaction in zk vm"); diff --git a/crates/zksync/core/src/vm/storage_view.rs b/crates/zksync/core/src/vm/storage_view.rs index 2968ec3a9..7bcfea892 100644 --- a/crates/zksync/core/src/vm/storage_view.rs +++ b/crates/zksync/core/src/vm/storage_view.rs @@ -19,13 +19,13 @@ use crate::convert::ConvertH160; #[derive(Debug)] pub(crate) struct StorageView { pub(crate) storage_handle: S, - // Used for caching and to get the list/count of modified keys + /// Used for caching and to get the list/count of modified keys pub(crate) modified_storage_keys: HashMap, - // Used purely for caching + /// Used purely for caching pub(crate) read_storage_keys: HashMap, - // Cache for `contains_key()` checks. The cache is only valid within one L1 batch execution. + /// Cache for `contains_key()` checks. The cache is only valid within one L1 batch execution. initial_writes_cache: HashMap, - + /// The tx caller. caller: H160, } diff --git a/crates/zksync/core/src/vm/tracer.rs b/crates/zksync/core/src/vm/tracer.rs index 866caebce..2d4f73956 100644 --- a/crates/zksync/core/src/vm/tracer.rs +++ b/crates/zksync/core/src/vm/tracer.rs @@ -40,6 +40,8 @@ pub struct CheatcodeTracerContext<'a> { pub mocked_calls: HashMap>, /// Expected calls recorder. pub expected_calls: Option<&'a mut ExpectedCallTracker>, + /// Factory deps that were persisted across calls + pub persisted_factory_deps: HashMap>, } /// Tracer result to return back to foundry. diff --git a/zk-tests/script/Factory.s.sol b/zk-tests/script/Factory.s.sol new file mode 100644 index 000000000..b0ef29adb --- /dev/null +++ b/zk-tests/script/Factory.s.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.0; + +import {Script} from 'forge-std/Script.sol'; + +import '../src/Factory.sol'; + +contract ZkClassicFactoryScript is Script { + function run() external { + vm.startBroadcast(); + MyClassicFactory factory = new MyClassicFactory(); + factory.create(42); + + vm.stopBroadcast(); + assert(factory.getNumber() == 42); + } +} + +contract ZkConstructorFactoryScript is Script { + function run() external { + vm.startBroadcast(); + MyConstructorFactory factory = new MyConstructorFactory(42); + + vm.stopBroadcast(); + assert(factory.getNumber() == 42); + } +} + +contract ZkNestedFactoryScript is Script{ + function run() external { + vm.startBroadcast(); + MyNestedFactory factory = new MyNestedFactory(); + factory.create(42); + + vm.stopBroadcast(); + assert(factory.getNumber() == 42); + } +} + +contract ZkNestedConstructorFactoryScript is Script{ + function run() external { + vm.startBroadcast(); + MyNestedConstructorFactory factory = new MyNestedConstructorFactory(42); + + vm.stopBroadcast(); + assert(factory.getNumber() == 42); + } +} + +//FIXME: fails with 'trying to decode unexisting hash' +contract ZkUserFactoryScript is Script { + function run() external { + vm.startBroadcast(); + MyClassicFactory factory = new MyClassicFactory(); + MyUserFactory user = new MyUserFactory(); + user.create(address(factory), 42); + + vm.stopBroadcast(); + assert(user.getNumber(address(factory)) == 42); + } +} + +contract ZkUserConstructorFactoryScript is Script{ + function run() external { + vm.startBroadcast(); + MyConstructorFactory factory = new MyConstructorFactory(42); + MyUserFactory user = new MyUserFactory(); + + vm.stopBroadcast(); + assert(user.getNumber(address(factory)) == 42); + } +} diff --git a/zk-tests/src/Factory.sol b/zk-tests/src/Factory.sol new file mode 100644 index 000000000..68e93dc74 --- /dev/null +++ b/zk-tests/src/Factory.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.0; + +/// Set of tests for factory contracts +/// +/// *Constructor factories build their dependencies in their constructors +/// *User factories don't deploy but assume the given address to be a deployed factory + +contract MyContract { + uint256 public number; + constructor(uint256 _number) { + number = _number; + } +} + +contract MyClassicFactory { + MyContract item; + + function create(uint256 _number) public { + item = new MyContract(_number); + } + + function getNumber() public view returns (uint256) { + return item.number(); + } +} + +contract MyConstructorFactory { + MyContract item; + + constructor(uint256 _number) { + item = new MyContract(_number); + } + + function getNumber() public view returns (uint256) { + return item.number(); + } +} + +contract MyNestedFactory { + MyClassicFactory nested; + + function create(uint256 _number) public { + nested = new MyClassicFactory(); + + nested.create(_number); + } + + function getNumber() public view returns (uint256) { + return nested.getNumber(); + } +} + +contract MyNestedConstructorFactory { + MyClassicFactory nested; + + constructor(uint256 _number) { + nested = new MyClassicFactory(); + + nested.create(_number); + } + + function getNumber() public view returns (uint256) { + return nested.getNumber(); + } +} + +contract MyUserFactory { + function create(address classicFactory, uint256 _number) public { + MyClassicFactory(classicFactory).create(_number); + } + + function getNumber(address classicFactory) public view returns (uint256) { + return MyClassicFactory(classicFactory).getNumber(); + } +} diff --git a/zk-tests/src/Factory.t.sol b/zk-tests/src/Factory.t.sol new file mode 100644 index 000000000..64f7a4d5e --- /dev/null +++ b/zk-tests/src/Factory.t.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.0; + +import {Test} from 'forge-std/Test.sol'; + +import './Factory.sol'; + +contract ZkFactoryTest is Test { + function testClassicFactory() public { + MyClassicFactory factory = new MyClassicFactory(); + factory.create(42); + + assert(factory.getNumber() == 42); + } + + function testConstructorFactory() public { + MyConstructorFactory factory = new MyConstructorFactory(42); + + assert(factory.getNumber() == 42); + } + + function testNestedFactory() public { + MyNestedFactory factory = new MyNestedFactory(); + factory.create(42); + + assert(factory.getNumber() == 42); + } + + function testNestedConstructorFactory() public { + MyNestedConstructorFactory factory = new MyNestedConstructorFactory(42); + + assert(factory.getNumber() == 42); + } + + function testUserFactory() public { + MyClassicFactory factory = new MyClassicFactory(); + MyUserFactory user = new MyUserFactory(); + user.create(address(factory), 42); + + assert(user.getNumber(address(factory)) == 42); + } + + function testUserConstructorFactory() public { + MyConstructorFactory factory = new MyConstructorFactory(42); + MyUserFactory user = new MyUserFactory(); + + assert(user.getNumber(address(factory)) == 42); + } +}