From 4f2145e829ed9674c008ef46768bc5e52dc43170 Mon Sep 17 00:00:00 2001 From: Nisheeth Barthwal Date: Fri, 26 Jul 2024 18:33:23 +0200 Subject: [PATCH 1/5] feat: cargo test for zksync functionality (#491) --- .github/workflows/test.yml | 21 +++ Cargo.lock | 8 +- Cargo.toml | 2 +- crates/cheatcodes/src/inspector.rs | 4 +- crates/forge/Cargo.toml | 5 + crates/forge/tests/cli/main.rs | 2 + crates/forge/tests/cli/script.rs | 115 +++++++------- crates/forge/tests/cli/zksync_node.rs | 210 ++++++++++++++++++++++++++ crates/forge/tests/it/test_helpers.rs | 150 +++++++++++++----- crates/forge/tests/it/zk.rs | 36 ++--- crates/zksync/core/src/vm/inspect.rs | 5 +- testdata/zk/Basic.t.sol | 5 +- testdata/zk/Cheatcodes.t.sol | 5 +- testdata/zk/Console.t.sol | 2 +- testdata/zk/Contracts.t.sol | 10 +- testdata/zk/Globals.sol | 8 + 16 files changed, 457 insertions(+), 131 deletions(-) create mode 100644 crates/forge/tests/cli/zksync_node.rs create mode 100644 testdata/zk/Globals.sol diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dc1e2ad8c..1167b4dcd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -116,6 +116,27 @@ jobs: # - name: Test ZK VM # run: RUST_LOG=1 cargo test --package forge --test it --jobs=1 -- zk + zk-cargo-test: + name: zk-cargo-test + runs-on: ubuntu-22.04-github-hosted-16core + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + submodules: recursive + ref: ${{ github.event.pull_request.head.sha }} + + - name: Install Rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: nightly-2024-04-28 + + - name: Run zk tests + env: + RUST_BACKTRACE: full + run: cargo test zk + zk-smoke-test: name: zk-smoke-test runs-on: ubuntu-22.04-github-hosted-16core diff --git a/Cargo.lock b/Cargo.lock index ec95b74d4..9e59fb4ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3801,8 +3801,8 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "era_test_node" -version = "0.1.0-alpha.21" -source = "git+https://github.com/matter-labs/era-test-node.git?rev=1d72eae6e9400b5b6caed5e60093e7c32eee8abb#1d72eae6e9400b5b6caed5e60093e7c32eee8abb" +version = "0.1.0-alpha.23" +source = "git+https://github.com/matter-labs/era-test-node.git?rev=dd6d2f463eb9697dc2365899a72ae12dae3ec809#dd6d2f463eb9697dc2365899a72ae12dae3ec809" dependencies = [ "anyhow", "bigdecimal", @@ -3832,6 +3832,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", + "zkevm_opcode_defs 1.5.0", "zksync_basic_types", "zksync_contracts", "zksync_node_fee_model", @@ -4577,6 +4578,7 @@ dependencies = [ "criterion", "dialoguer", "dunce", + "era_test_node", "ethers-contract-abigen", "evm-disassembler", "eyre", @@ -4603,6 +4605,8 @@ dependencies = [ "hyper 1.4.0", "indicatif", "itertools 0.13.0", + "jsonrpc-core", + "jsonrpc-http-server", "mockall", "once_cell", "opener", diff --git a/Cargo.toml b/Cargo.toml index e798875d1..4ede27933 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -210,7 +210,7 @@ alloy-rlp = "0.3.3" solang-parser = "=0.3.3" ## zksync -era_test_node = { git="https://github.com/matter-labs/era-test-node.git" , rev = "1d72eae6e9400b5b6caed5e60093e7c32eee8abb" } +era_test_node = { git="https://github.com/matter-labs/era-test-node.git" , rev = "dd6d2f463eb9697dc2365899a72ae12dae3ec809" } zksync-web3-rs = {git = "https://github.com/lambdaclass/zksync-web3-rs.git", rev = "fd7adf634c016f40ea01f702e0e05f57aa5ba614"} zksync_basic_types = { git = "https://github.com/matter-labs/zksync-era.git", rev = "e10bbdd1e863962552f37e768ae6af649353e4ea" } zksync_types = { git = "https://github.com/matter-labs/zksync-era.git", rev = "e10bbdd1e863962552f37e768ae6af649353e4ea" } diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index d511bdf26..e53d4ef54 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -473,7 +473,7 @@ impl Cheatcodes { data.env.block.timestamp = U256::from(block_timestamp); let test_contract = data.db.get_test_contract_address(); - for address in data.db.persistent_accounts() { + for address in data.db.persistent_accounts().into_iter().chain([data.env.tx.caller]) { info!(?address, "importing to evm state"); let zk_address = address.to_h160(); @@ -546,7 +546,7 @@ impl Cheatcodes { let mut known_codes_storage: rHashMap = Default::default(); let mut deployed_codes: HashMap = Default::default(); - for address in data.db.persistent_accounts() { + 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"); diff --git a/crates/forge/Cargo.toml b/crates/forge/Cargo.toml index 2efe93c41..c138ed69c 100644 --- a/crates/forge/Cargo.toml +++ b/crates/forge/Cargo.toml @@ -124,6 +124,11 @@ tikv-jemallocator = { workspace = true, optional = true } anvil.workspace = true foundry-test-utils.workspace = true +# zk +era_test_node.workspace = true +jsonrpc-core = { git = "https://github.com/matter-labs/jsonrpc.git", branch = "master" } +jsonrpc-http-server = { git = "https://github.com/matter-labs/jsonrpc.git", branch = "master" } + mockall = "0.12" criterion = "0.5" paste = "1.0" diff --git a/crates/forge/tests/cli/main.rs b/crates/forge/tests/cli/main.rs index 0ac67d81b..147bbae1b 100644 --- a/crates/forge/tests/cli/main.rs +++ b/crates/forge/tests/cli/main.rs @@ -20,4 +20,6 @@ mod svm; mod test_cmd; mod verify; +mod zksync_node; + mod ext_integration; diff --git a/crates/forge/tests/cli/script.rs b/crates/forge/tests/cli/script.rs index 8e71dcb9c..af8c6d017 100644 --- a/crates/forge/tests/cli/script.rs +++ b/crates/forge/tests/cli/script.rs @@ -1,6 +1,6 @@ //! Contains various tests related to `forge script`. -use crate::constants::TEMPLATE_CONTRACT; +use crate::{constants::TEMPLATE_CONTRACT, zksync_node}; use alloy_primitives::{Address, Bytes}; use anvil::{spawn, NodeConfig}; use foundry_test_utils::{rpc, util::OutputExt, ScriptOutcome, ScriptTester}; @@ -1452,39 +1452,35 @@ forgetest_async!(can_deploy_library_create2_different_sender, |prj, cmd| { .await; }); -// ignoring test as it requires a local era-test-node to be running on port 8011 -forgetest_async!( - #[ignore] - can_execute_zk_script_with_arguments, - |prj, cmd| { - #[derive(serde::Deserialize, Debug)] - #[allow(dead_code)] - struct ZkTransactions { - transactions: Vec, - } +forgetest_async!(test_zk_can_execute_script_with_arguments, |prj, cmd| { + #[derive(serde::Deserialize, Debug)] + #[allow(dead_code)] + struct ZkTransactions { + transactions: Vec, + } - #[derive(serde::Deserialize, Debug)] - #[allow(dead_code)] - struct ZkTransaction { - zk: Zk, - } + #[derive(serde::Deserialize, Debug)] + #[allow(dead_code)] + struct ZkTransaction { + zk: Zk, + } - #[derive(serde::Deserialize, Debug)] - #[serde(rename_all = "camelCase")] - #[allow(dead_code)] - struct Zk { - factory_deps: Vec>, - } + #[derive(serde::Deserialize, Debug)] + #[serde(rename_all = "camelCase")] + #[allow(dead_code)] + struct Zk { + factory_deps: Vec>, + } - cmd.args(["init", "--force"]).arg(prj.root()); - cmd.assert_non_empty_stdout(); - cmd.forge_fuse(); + let node = zksync_node::ZkSyncNode::start(); - let (_api, _handle) = spawn(NodeConfig::test()).await; - let script = prj - .add_script( - "Deploy.s.sol", - r#" + cmd.args(["init", "--force"]).arg(prj.root()); + cmd.assert_non_empty_stdout(); + cmd.forge_fuse(); + + prj.add_script( + "Deploy.s.sol", + r#" pragma solidity ^0.8.18; import {Script} from "forge-std/Script.sol"; @@ -1527,34 +1523,33 @@ contract DeployScript is Script { } } "#, - ) - .unwrap(); + ) + .unwrap(); - cmd.arg("script").arg(script).args([ - "--tc", - "DeployScript", - "--broadcast", - "--private-key", - "0x3d3cbc973389cb26f657686445bcc75662b415b656078503592ac8c1abb8810e", - "--chain", - "260", - "--gas-estimate-multiplier", - "310", - "--rpc-url", - "http://localhost:8011", - "--slow", - ]); - - assert!(cmd.stdout_lossy().contains("ONCHAIN EXECUTION COMPLETE & SUCCESSFUL")); - - let run_latest = foundry_common::fs::json_files(prj.root().join("broadcast").as_path()) - .find(|file| file.ends_with("run-latest.json")) - .expect("No broadcast artifacts"); - - let content = foundry_common::fs::read_to_string(run_latest).unwrap(); - - let transactions: ZkTransactions = serde_json::from_str(&content).unwrap(); - let transactions = transactions.transactions; - assert_eq!(transactions.len(), 3); - } -); + cmd.arg("script").args([ + "--zksync", + "DeployScript", + "--broadcast", + "--private-key", + "0x3d3cbc973389cb26f657686445bcc75662b415b656078503592ac8c1abb8810e", + "--chain", + "260", + "--gas-estimate-multiplier", + "310", + "--rpc-url", + node.url().as_str(), + "--slow", + ]); + + assert!(cmd.stdout_lossy().contains("ONCHAIN EXECUTION COMPLETE & SUCCESSFUL")); + + let run_latest = foundry_common::fs::json_files(prj.root().join("broadcast").as_path()) + .find(|file| file.ends_with("run-latest.json")) + .expect("No broadcast artifacts"); + + let content = foundry_common::fs::read_to_string(run_latest).unwrap(); + + let transactions: ZkTransactions = serde_json::from_str(&content).unwrap(); + let transactions = transactions.transactions; + assert_eq!(transactions.len(), 3); +}); diff --git a/crates/forge/tests/cli/zksync_node.rs b/crates/forge/tests/cli/zksync_node.rs new file mode 100644 index 000000000..5a75e15c8 --- /dev/null +++ b/crates/forge/tests/cli/zksync_node.rs @@ -0,0 +1,210 @@ +//! Contains in-memory implementation of era-test-node. +use std::{ + net::{IpAddr, Ipv4Addr, SocketAddr}, + str::FromStr, +}; + +use era_test_node::{ + http_fork_source::HttpForkSource, + namespaces::{ + ConfigurationApiNamespaceT, DebugNamespaceT, EthNamespaceT, EthTestNodeNamespaceT, + EvmNamespaceT, HardhatNamespaceT, NetNamespaceT, Web3NamespaceT, ZksNamespaceT, + }, + node::InMemoryNode, +}; +use futures::{SinkExt, StreamExt}; +use jsonrpc_core::IoHandler; +use zksync_types::H160; + +const DEFAULT_PORT: u16 = 8011; + +/// List of legacy wallets (address, private key) that we seed with tokens at start. +const LEGACY_RICH_WALLETS: [(&str, &str); 10] = [ + ( + "0x36615Cf349d7F6344891B1e7CA7C72883F5dc049", + "0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110", + ), + ( + "0xa61464658AfeAf65CccaaFD3a512b69A83B77618", + "0xac1e735be8536c6534bb4f17f06f6afc73b2b5ba84ac2cfb12f7461b20c0bbe3", + ), + ( + "0x0D43eB5B8a47bA8900d84AA36656c92024e9772e", + "0xd293c684d884d56f8d6abd64fc76757d3664904e309a0645baf8522ab6366d9e", + ), + ( + "0xA13c10C0D5bd6f79041B9835c63f91de35A15883", + "0x850683b40d4a740aa6e745f889a6fdc8327be76e122f5aba645a5b02d0248db8", + ), + ( + "0x8002cD98Cfb563492A6fB3E7C8243b7B9Ad4cc92", + "0xf12e28c0eb1ef4ff90478f6805b68d63737b7f33abfa091601140805da450d93", + ), + ( + "0x4F9133D1d3F50011A6859807C837bdCB31Aaab13", + "0xe667e57a9b8aaa6709e51ff7d093f1c5b73b63f9987e4ab4aa9a5c699e024ee8", + ), + ( + "0xbd29A1B981925B94eEc5c4F1125AF02a2Ec4d1cA", + "0x28a574ab2de8a00364d5dd4b07c4f2f574ef7fcc2a86a197f65abaec836d1959", + ), + ( + "0xedB6F5B4aab3dD95C7806Af42881FF12BE7e9daa", + "0x74d8b3a188f7260f67698eb44da07397a298df5427df681ef68c45b34b61f998", + ), + ( + "0xe706e60ab5Dc512C36A4646D719b889F398cbBcB", + "0xbe79721778b48bcc679b78edac0ce48306a8578186ffcb9f2ee455ae6efeace1", + ), + ( + "0xE90E12261CCb0F3F7976Ae611A29e84a6A85f424", + "0x3eb15da85647edd9a1159a4a13b9e7c56877c4eb33f614546d4db06a51868b1c", + ), +]; + +/// List of wallets (address, private key, mnemonic) that we seed with tokens at start. +const RICH_WALLETS: [(&str, &str, &str); 10] = [ + ( + "0xBC989fDe9e54cAd2aB4392Af6dF60f04873A033A", + "0x3d3cbc973389cb26f657686445bcc75662b415b656078503592ac8c1abb8810e", + "mass wild lava ripple clog cabbage witness shell unable tribe rubber enter", + ), + ( + "0x55bE1B079b53962746B2e86d12f158a41DF294A6", + "0x509ca2e9e6acf0ba086477910950125e698d4ea70fa6f63e000c5a22bda9361c", + "crumble clutch mammal lecture lazy broken nominee visit gentle gather gym erupt", + ), + ( + "0xCE9e6063674DC585F6F3c7eaBe82B9936143Ba6C", + "0x71781d3a358e7a65150e894264ccc594993fbc0ea12d69508a340bc1d4f5bfbc", + "illegal okay stereo tattoo between alien road nuclear blind wolf champion regular", + ), + ( + "0xd986b0cB0D1Ad4CCCF0C4947554003fC0Be548E9", + "0x379d31d4a7031ead87397f332aab69ef5cd843ba3898249ca1046633c0c7eefe", + "point donor practice wear alien abandon frozen glow they practice raven shiver", + ), + ( + "0x87d6ab9fE5Adef46228fB490810f0F5CB16D6d04", + "0x105de4e75fe465d075e1daae5647a02e3aad54b8d23cf1f70ba382b9f9bee839", + "giraffe organ club limb install nest journey client chunk settle slush copy", + ), + ( + "0x78cAD996530109838eb016619f5931a03250489A", + "0x7becc4a46e0c3b512d380ca73a4c868f790d1055a7698f38fb3ca2b2ac97efbb", + "awful organ version habit giraffe amused wire table begin gym pistol clean", + ), + ( + "0xc981b213603171963F81C687B9fC880d33CaeD16", + "0xe0415469c10f3b1142ce0262497fe5c7a0795f0cbfd466a6bfa31968d0f70841", + "exotic someone fall kitten salute nerve chimney enlist pair display over inside", + ), + ( + "0x42F3dc38Da81e984B92A95CBdAAA5fA2bd5cb1Ba", + "0x4d91647d0a8429ac4433c83254fb9625332693c848e578062fe96362f32bfe91", + "catch tragic rib twelve buffalo also gorilla toward cost enforce artefact slab", + ), + ( + "0x64F47EeD3dC749d13e49291d46Ea8378755fB6DF", + "0x41c9f9518aa07b50cb1c0cc160d45547f57638dd824a8d85b5eb3bf99ed2bdeb", + "arrange price fragile dinner device general vital excite penalty monkey major faculty", + ), + ( + "0xe2b8Cb53a43a56d4d2AB6131C81Bd76B86D3AFe5", + "0xb0680d66303a0163a19294f1ef8c95cd69a9d7902a4aca99c05f3e134e68a11a", + "increase pulp sing wood guilt cement satoshi tiny forum nuclear sudden thank", + ), +]; + +/// In-memory era-test-node that is stopped when dropped. +pub struct ZkSyncNode { + close_tx: futures::channel::mpsc::Sender<()>, +} + +impl Drop for ZkSyncNode { + fn drop(&mut self) { + self.stop(); + } +} + +impl ZkSyncNode { + /// Returns the server url. + #[inline] + pub fn url(&self) -> String { + format!("http://127.0.0.1:{DEFAULT_PORT}") + } + + /// Start era-test-node in memory at the [DEFAULT_PORT]. The server is automatically stopped + /// when the instance is dropped. + pub fn start() -> Self { + let (tx, mut rx) = futures::channel::mpsc::channel::<()>(1); + + let io_handler = { + let node: InMemoryNode = + InMemoryNode::new(None, None, Default::default()); + + tracing::info!(""); + tracing::info!("Rich Accounts"); + tracing::info!("============="); + for wallet in LEGACY_RICH_WALLETS.iter() { + let address = wallet.0; + node.set_rich_account(H160::from_str(address).unwrap()); + } + for (index, wallet) in RICH_WALLETS.iter().enumerate() { + let address = wallet.0; + let private_key = wallet.1; + let mnemonic_phrase = wallet.2; + node.set_rich_account(H160::from_str(address).unwrap()); + tracing::info!("Account #{}: {} ({})", index, address, "1_000_000_000_000 ETH"); + tracing::info!("Private Key: {}", private_key); + tracing::info!("Mnemonic: {}", &mnemonic_phrase); + tracing::info!(""); + } + + let mut io = IoHandler::default(); + + io.extend_with(NetNamespaceT::to_delegate(node.clone())); + io.extend_with(Web3NamespaceT::to_delegate(node.clone())); + io.extend_with(ConfigurationApiNamespaceT::to_delegate(node.clone())); + io.extend_with(DebugNamespaceT::to_delegate(node.clone())); + io.extend_with(EthNamespaceT::to_delegate(node.clone())); + io.extend_with(EthTestNodeNamespaceT::to_delegate(node.clone())); + io.extend_with(EvmNamespaceT::to_delegate(node.clone())); + io.extend_with(HardhatNamespaceT::to_delegate(node.clone())); + io.extend_with(ZksNamespaceT::to_delegate(node)); + io + }; + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .worker_threads(1) + .build() + .unwrap(); + + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), DEFAULT_PORT); + std::thread::spawn(move || { + let server = jsonrpc_http_server::ServerBuilder::new(io_handler) + .threads(1) + .event_loop_executor(runtime.handle().clone()) + .start_http(&addr) + .unwrap(); + + futures::executor::block_on(async { + let _ = rx.next().await; + }); + + server.close(); + }); + + // wait for server to start + std::thread::sleep(std::time::Duration::from_millis(600)); + + Self { close_tx: tx } + } + + /// Stop the running era-test-node. + pub fn stop(&mut self) { + futures::executor::block_on(async { + let _ = self.close_tx.send(()).await; + }); + } +} diff --git a/crates/forge/tests/it/test_helpers.rs b/crates/forge/tests/it/test_helpers.rs index b1b00b7aa..9bee2e638 100644 --- a/crates/forge/tests/it/test_helpers.rs +++ b/crates/forge/tests/it/test_helpers.rs @@ -7,8 +7,13 @@ use forge::{ }; use foundry_compilers::{ artifacts::{EvmVersion, Libraries, Settings}, - zksync::compile::output::ProjectCompileOutput as ZkProjectCompileOutput, - Project, ProjectCompileOutput, SolcConfig, + multi::MultiCompilerLanguage, + solc::SolcCompiler, + zksync::{ + cache::ZKSYNC_SOLIDITY_FILES_CACHE_FILENAME, + compile::output::ProjectCompileOutput as ZkProjectCompileOutput, + }, + Project, ProjectCompileOutput, ProjectPathsConfig, SolcConfig, }; use foundry_config::{ fs_permissions::PathPermission, Config, FsPermissions, FuzzConfig, FuzzDictionaryConfig, @@ -19,7 +24,9 @@ use foundry_evm::{ opts::{Env, EvmOpts}, }; use foundry_test_utils::{fd_lock, init_tracing}; +use foundry_zksync_compiler::DualCompiledContracts; use once_cell::sync::Lazy; +use semver::Version; use std::{ env, fmt, io::Write, @@ -27,6 +34,8 @@ use std::{ sync::Arc, }; +type ZkProject = Project; + pub const RE_PATH_SEPARATOR: &str = "/"; const TESTDATA: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../testdata"); @@ -76,6 +85,22 @@ impl ForgeTestProfile { self.config().project().expect("Failed to build project") } + pub fn zk_project(&self) -> ZkProject { + let zk_config = self.zk_config(); + let mut zk_project = + foundry_zksync_compiler::create_project(&zk_config, zk_config.cache, false) + .expect("failed creating zksync project"); + zk_project.paths.zksync_artifacts = zk_config.root.as_ref().join("zk").join("zkout"); + zk_project.paths.zksync_cache = zk_config + .root + .as_ref() + .join("zk") + .join("cache") + .join(ZKSYNC_SOLIDITY_FILES_CACHE_FILENAME); + + zk_project + } + pub fn test_opts(&self, output: &ProjectCompileOutput) -> TestOptions { TestOptionsBuilder::default() .fuzz(FuzzConfig { @@ -159,6 +184,40 @@ impl ForgeTestProfile { config } + + /// Build [Config] for zksync test profile. + /// + /// Project source files are read from testdata/zk + /// Project output files are written to testdata/zk/out and testdata/zk/zkout + /// Cache is written to testdata/zk/cache + /// + /// AST output is enabled by default to support inline configs. + pub fn zk_config(&self) -> Config { + let mut zk_config = Config::with_root(self.root()); + + zk_config.ast = true; + zk_config.src = self.root().join("./zk"); + zk_config.test = self.root().join("./zk"); + zk_config.out = self.root().join("zk").join("out"); + zk_config.cache_path = self.root().join("zk").join("cache"); + zk_config.evm_version = EvmVersion::London; + + zk_config.zksync.startup = true; + zk_config.zksync.fallback_oz = true; + zk_config.zksync.optimizer_mode = '3'; + zk_config.zksync.zksolc = Some(foundry_config::SolcReq::Version(Version::new(1, 5, 1))); + + zk_config + } +} + +/// Container for test data for zkSync specific tests. +pub struct ZkTestData { + pub dual_compiled_contracts: DualCompiledContracts, + pub zk_config: Config, + pub zk_project: ZkProject, + pub output: ProjectCompileOutput, + pub zk_output: ZkProjectCompileOutput, } /// Container for test data for a specific test profile. @@ -169,6 +228,7 @@ pub struct ForgeTestData { pub evm_opts: EvmOpts, pub config: Config, pub profile: ForgeTestProfile, + pub zk_test_data: ZkTestData, } impl ForgeTestData { @@ -182,7 +242,34 @@ impl ForgeTestData { let config = profile.config(); let evm_opts = profile.evm_opts(); - Self { project, output, test_opts, evm_opts, config, profile } + let zk_test_data = { + let zk_config = profile.zk_config(); + let zk_project = profile.zk_project(); + + let project = zk_config.project().expect("failed obtaining project"); + let output = get_compiled(&project); + let zk_output = get_zk_compiled(&zk_project); + let layout = ProjectPathsConfig { + root: zk_project.paths.root.clone(), + cache: zk_project.paths.cache.clone(), + artifacts: zk_project.paths.artifacts.clone(), + build_infos: zk_project.paths.build_infos.clone(), + sources: zk_project.paths.sources.clone(), + tests: zk_project.paths.tests.clone(), + scripts: zk_project.paths.scripts.clone(), + libraries: zk_project.paths.libraries.clone(), + remappings: zk_project.paths.remappings.clone(), + include_paths: zk_project.paths.include_paths.clone(), + allowed_paths: zk_project.paths.allowed_paths.clone(), + zksync_artifacts: zk_project.paths.zksync_artifacts.clone(), + zksync_cache: zk_project.paths.zksync_cache.clone(), + _l: std::marker::PhantomData::, + }; + let dual_compiled_contracts = DualCompiledContracts::new(&output, &zk_output, &layout); + ZkTestData { dual_compiled_contracts, zk_config, zk_project, output, zk_output } + }; + + Self { project, output, test_opts, evm_opts, config, profile, zk_test_data } } /// Builds a base runner @@ -209,10 +296,10 @@ impl ForgeTestData { /// Builds a non-tracing zksync runner /// TODO: This needs to be implemented as currently it is a copy of the original function pub fn runner_zksync(&self) -> MultiContractRunner { - let mut config = self.config.clone(); - config.fs_permissions = + let mut zk_config = self.zk_test_data.zk_config.clone(); + zk_config.fs_permissions = FsPermissions::new(vec![PathPermission::read_write(manifest_root())]); - self.runner_with_zksync_config(config) + self.runner_with_zksync_config(zk_config) } /// Builds a non-tracing runner @@ -247,32 +334,34 @@ impl ForgeTestData { /// Builds a non-tracing runner with zksync /// TODO: This needs to be added as currently it is a copy of the original function - pub fn runner_with_zksync_config(&self, mut config: Config) -> MultiContractRunner { - config.rpc_endpoints = rpc_endpoints(); - config.allow_paths.push(manifest_root().to_path_buf()); + pub fn runner_with_zksync_config(&self, mut zk_config: Config) -> MultiContractRunner { + zk_config.rpc_endpoints = rpc_endpoints(); + zk_config.allow_paths.push(manifest_root().to_path_buf()); // no prompt testing - config.prompt_timeout = 0; + zk_config.prompt_timeout = 0; - let root = self.project.root(); + let root = self.zk_test_data.zk_project.root(); let mut opts = self.evm_opts.clone(); - if config.isolate { + if zk_config.isolate { opts.isolate = true; } let env = opts.local_evm_env(); - let output = self.output.clone(); + let output = self.zk_test_data.output.clone(); + let zk_output = self.zk_test_data.zk_output.clone(); + let dual_compiled_contracts = self.zk_test_data.dual_compiled_contracts.clone(); - let sender = config.sender; + let sender = zk_config.sender; let mut builder = self.base_runner(); - builder.config = Arc::new(config); + builder.config = Arc::new(zk_config); builder .enable_isolation(opts.isolate) .sender(sender) .with_test_options(self.test_opts.clone()) - .build(root, output, None, env, opts.clone(), Default::default()) + .build(root, output, Some(zk_output), env, opts.clone(), dual_compiled_contracts) .unwrap() } @@ -336,41 +425,34 @@ pub fn get_compiled(project: &Project) -> ProjectCompileOutput { out } -pub static COMPILED_ZK: Lazy = Lazy::new(|| { - const LOCK: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../testdata/.lock-zk"); - - // let libs = - // ["fork/Fork.t.sol:DssExecLib:0xfD88CeE74f7D78697775aBDAE53f9Da1559728E4".to_string()]; - - // TODO: fix this to adapt to new way of testing - let config = ForgeTestData::new(ForgeTestProfile::Default).config; - let zk_project = foundry_zksync_compiler::create_project(&config, true, false).unwrap(); - let zk_compiler = foundry_common::compile::ProjectCompiler::new(); - assert!(zk_project.cached); - +pub fn get_zk_compiled(zk_project: &ZkProject) -> ZkProjectCompileOutput { + let lock_file_path = zk_project.sources_path().join(".lock-zk"); // Compile only once per test run. // We need to use a file lock because `cargo-nextest` runs tests in different processes. // This is similar to [`foundry_test_utils::util::initialize`], see its comments for more // details. - let mut lock = fd_lock::new_lock(LOCK); + let mut lock = fd_lock::new_lock(&lock_file_path); let read = lock.read().unwrap(); let out; - if zk_project.cache_path().exists() && std::fs::read(LOCK).unwrap() == b"1" { - out = zk_compiler.zksync_compile(&zk_project, None).unwrap(); + + let zk_compiler = foundry_common::compile::ProjectCompiler::new(); + if zk_project.paths.zksync_cache.exists() && std::fs::read(&lock_file_path).unwrap() == b"1" { + out = zk_compiler.zksync_compile(zk_project, None); drop(read); } else { drop(read); let mut write = lock.write().unwrap(); write.write_all(b"1").unwrap(); - out = zk_compiler.zksync_compile(&zk_project, None).unwrap(); + out = zk_compiler.zksync_compile(zk_project, None); drop(write); - }; + } + let out = out.expect("failed compiling zksync project"); if out.has_compiler_errors() { panic!("Compiled with errors:\n{out}"); } out -}); +} pub static EVM_OPTS: Lazy = Lazy::new(|| EvmOpts { env: Env { diff --git a/crates/forge/tests/it/zk.rs b/crates/forge/tests/it/zk.rs index da988fcdd..8a1ad0435 100644 --- a/crates/forge/tests/it/zk.rs +++ b/crates/forge/tests/it/zk.rs @@ -2,21 +2,16 @@ use std::collections::BTreeMap; -use crate::{ - config::*, - test_helpers::{RE_PATH_SEPARATOR, TEST_DATA_DEFAULT}, -}; +use crate::{config::*, test_helpers::TEST_DATA_DEFAULT}; use forge::revm::primitives::SpecId; -use foundry_config::{fs_permissions::PathPermission, FsPermissions}; +use foundry_config::fs_permissions::PathPermission; use foundry_test_utils::Filter; /// Executes all zk basic tests #[tokio::test(flavor = "multi_thread")] async fn test_zk_basic() { - let mut config = TEST_DATA_DEFAULT.config.clone(); - config.fs_permissions = FsPermissions::new(vec![PathPermission::read_write("./")]); - let runner = TEST_DATA_DEFAULT.runner_with_zksync_config(config); - let filter = Filter::new(".*", "ZkBasicTest", &format!(".*zk{RE_PATH_SEPARATOR}*")); + let runner = TEST_DATA_DEFAULT.runner_zksync(); + let filter = Filter::new(".*", "ZkBasicTest", ".*"); TestConfig::with_filter(runner, filter).evm_spec(SpecId::SHANGHAI).run().await; } @@ -24,10 +19,10 @@ async fn test_zk_basic() { /// Executes all zk contract tests #[tokio::test(flavor = "multi_thread")] async fn test_zk_contracts() { - let mut config = TEST_DATA_DEFAULT.config.clone(); - config.fs_permissions = FsPermissions::new(vec![PathPermission::read_write("./")]); - let runner = TEST_DATA_DEFAULT.runner_with_zksync_config(config); - let filter = Filter::new(".*", "ZkContractsTest", &format!(".*zk{RE_PATH_SEPARATOR}*")); + let mut zk_config = TEST_DATA_DEFAULT.zk_test_data.zk_config.clone(); + zk_config.fs_permissions.add(PathPermission::read_write("./zk/zkout/ConstantNumber.sol")); + let runner = TEST_DATA_DEFAULT.runner_with_zksync_config(zk_config); + let filter = Filter::new(".*", "ZkContractsTest", ".*"); TestConfig::with_filter(runner, filter).evm_spec(SpecId::SHANGHAI).run().await; } @@ -35,10 +30,10 @@ async fn test_zk_contracts() { /// Executes all zk cheatcode tests #[tokio::test(flavor = "multi_thread")] async fn test_zk_cheats() { - let mut config = TEST_DATA_DEFAULT.config.clone(); - config.fs_permissions = FsPermissions::new(vec![PathPermission::read_write("./")]); - let runner = TEST_DATA_DEFAULT.runner_with_zksync_config(config); - let filter = Filter::new(".*", "ZkCheatcodesTest", &format!(".*zk{RE_PATH_SEPARATOR}*")); + let mut zk_config = TEST_DATA_DEFAULT.zk_test_data.zk_config.clone(); + zk_config.fs_permissions.add(PathPermission::read_write("./zk/zkout/ConstantNumber.sol")); + let runner = TEST_DATA_DEFAULT.runner_with_zksync_config(zk_config); + let filter = Filter::new(".*", "ZkCheatcodesTest", ".*"); TestConfig::with_filter(runner, filter).evm_spec(SpecId::SHANGHAI).run().await; } @@ -46,13 +41,10 @@ async fn test_zk_cheats() { /// Executes all zk console tests #[tokio::test(flavor = "multi_thread")] async fn test_zk_logs() { - let mut config = TEST_DATA_DEFAULT.config.clone(); - config.fs_permissions = FsPermissions::new(vec![PathPermission::read_write("./")]); - let runner = TEST_DATA_DEFAULT.runner_with_zksync_config(config); - let filter = Filter::new(".*", "ZkConsoleTest", &format!(".*zk{RE_PATH_SEPARATOR}*")); + let runner = TEST_DATA_DEFAULT.runner_zksync(); + let filter = Filter::new(".*", "ZkConsoleTest", ".*"); let results = TestConfig::with_filter(runner, filter).evm_spec(SpecId::SHANGHAI).test(); - assert_multiple( &results, BTreeMap::from([( diff --git a/crates/zksync/core/src/vm/inspect.rs b/crates/zksync/core/src/vm/inspect.rs index 1d5603c6a..4d8742dfe 100644 --- a/crates/zksync/core/src/vm/inspect.rs +++ b/crates/zksync/core/src/vm/inspect.rs @@ -420,7 +420,10 @@ fn inspect_inner( let bytecodes = vm .get_last_tx_compressed_bytecodes() .iter() - .map(|b| bytecode_to_factory_dep(b.original.clone())) + .map(|b| { + bytecode_to_factory_dep(b.original.clone()) + .expect("failed converting bytecode to factory dep") + }) .collect(); let modified_keys = if is_static { Default::default() diff --git a/testdata/zk/Basic.t.sol b/testdata/zk/Basic.t.sol index 86b2f31cb..03bec35fb 100644 --- a/testdata/zk/Basic.t.sol +++ b/testdata/zk/Basic.t.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.18; import "ds-test/test.sol"; import "../cheats/Vm.sol"; +import {Globals} from "./Globals.sol"; contract ZkBasicTest is DSTest { Vm constant vm = Vm(HEVM_ADDRESS); @@ -19,8 +20,8 @@ contract ZkBasicTest is DSTest { uint256 forkEth; function setUp() public { - forkEra = vm.createFork("https://mainnet.era.zksync.io", ERA_FORK_BLOCK); - forkEth = vm.createFork("https://eth-mainnet.alchemyapi.io/v2/Lc7oIGYeL_QvInzI0Wiu_pOZZDEKBrdf", ETH_FORK_BLOCK); + forkEra = vm.createFork(Globals.ZKSYNC_MAINNET_URL, ERA_FORK_BLOCK); + forkEth = vm.createFork(Globals.ETHEREUM_MAINNET_URL, ETH_FORK_BLOCK); } function testZkBasicBlockNumber() public { diff --git a/testdata/zk/Cheatcodes.t.sol b/testdata/zk/Cheatcodes.t.sol index 9935dd8dc..6ed2e518c 100644 --- a/testdata/zk/Cheatcodes.t.sol +++ b/testdata/zk/Cheatcodes.t.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.18; import "ds-test/test.sol"; import "../cheats/Vm.sol"; +import {Globals} from "./Globals.sol"; contract ZkCheatcodesTest is DSTest { Vm constant vm = Vm(HEVM_ADDRESS); @@ -19,8 +20,8 @@ contract ZkCheatcodesTest is DSTest { uint256 forkEth; function setUp() public { - forkEra = vm.createFork("https://mainnet.era.zksync.io", ERA_FORK_BLOCK); - forkEth = vm.createFork("https://eth-mainnet.alchemyapi.io/v2/Lc7oIGYeL_QvInzI0Wiu_pOZZDEKBrdf", ETH_FORK_BLOCK); + forkEra = vm.createFork(Globals.ZKSYNC_MAINNET_URL, ERA_FORK_BLOCK); + forkEth = vm.createFork(Globals.ETHEREUM_MAINNET_URL, ETH_FORK_BLOCK); } function testZkCheatcodesRoll() public { diff --git a/testdata/zk/Console.t.sol b/testdata/zk/Console.t.sol index 935ec4cdb..df3f4e7c1 100644 --- a/testdata/zk/Console.t.sol +++ b/testdata/zk/Console.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.18; import "ds-test/test.sol"; import "../cheats/Vm.sol"; -import "../logs/console.sol"; +import "../default/logs/console.sol"; contract Printer { function print() public view { diff --git a/testdata/zk/Contracts.t.sol b/testdata/zk/Contracts.t.sol index d52cd7178..30ae02b5d 100644 --- a/testdata/zk/Contracts.t.sol +++ b/testdata/zk/Contracts.t.sol @@ -5,6 +5,7 @@ import "ds-test/test.sol"; import "../cheats/Vm.sol"; import {ConstantNumber} from "./ConstantNumber.sol"; +import {Globals} from "./Globals.sol"; contract Greeter { string name; @@ -105,8 +106,8 @@ contract ZkContractsTest is DSTest { vm.makePersistent(address(number)); vm.makePersistent(address(customNumber)); - forkEra = vm.createFork("https://mainnet.era.zksync.io", ERA_FORK_BLOCK); - forkEth = vm.createFork("https://eth-mainnet.alchemyapi.io/v2/Lc7oIGYeL_QvInzI0Wiu_pOZZDEKBrdf", ETH_FORK_BLOCK); + forkEra = vm.createFork(Globals.ZKSYNC_MAINNET_URL, ERA_FORK_BLOCK); + forkEth = vm.createFork(Globals.ETHEREUM_MAINNET_URL, ETH_FORK_BLOCK); } function testZkContractsPersistedDeployedContractNoArgs() public { @@ -168,8 +169,9 @@ contract ZkContractsTest is DSTest { function testZkContractsCreate2() public { vm.selectFork(forkEra); - bytes32 bytecodeHash = 0x0100000fbb43aa073340811284a4666183e306fbe9c950df1d05ac3210ef9fc5; - address sender = address(0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38); + string memory artifact = vm.readFile("zk/zkout/ConstantNumber.sol/ConstantNumber.json"); + bytes32 bytecodeHash = vm.parseJsonBytes32(artifact, ".hash"); + address sender = address(0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496); bytes32 salt = "12345"; bytes32 constructorInputHash = keccak256(abi.encode()); address expectedDeployedAddress = diff --git a/testdata/zk/Globals.sol b/testdata/zk/Globals.sol new file mode 100644 index 000000000..076f703d2 --- /dev/null +++ b/testdata/zk/Globals.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.18; + +library Globals { + string public constant ETHEREUM_MAINNET_URL = + "https://eth-mainnet.alchemyapi.io/v2/Lc7oIGYeL_QvInzI0Wiu_pOZZDEKBrdf"; // trufflehog:ignore + string public constant ZKSYNC_MAINNET_URL = "https://mainnet.era.zksync.io"; +} From 9ab540a3f5e12464a0c41549dd3b34f13b500962 Mon Sep 17 00:00:00 2001 From: Karrq Date: Wed, 31 Jul 2024 16:15:58 +0200 Subject: [PATCH 2/5] fix: remove unused `Send` bound on `DB` (#496) --- crates/cheatcodes/src/inspector.rs | 4 ++-- crates/zksync/core/src/vm/farcall.rs | 6 +++--- crates/zksync/core/src/vm/inspect.rs | 10 +++++----- crates/zksync/core/src/vm/runner.rs | 14 +++++++------- crates/zksync/core/src/vm/tracer.rs | 4 ++-- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index e53d4ef54..9b9dd6db0 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -626,7 +626,7 @@ impl Cheatcodes { } } -impl Inspector for Cheatcodes { +impl Inspector for Cheatcodes { #[inline] fn initialize_interp(&mut self, _: &mut Interpreter, ecx: &mut EvmContext) { // When the first interpreter is initialized we've circumvented the balance and gas checks, @@ -2083,7 +2083,7 @@ impl Inspector for Cheatcodes { } } -impl InspectorExt for Cheatcodes { +impl InspectorExt for Cheatcodes { fn should_use_create2_factory( &mut self, ecx: &mut EvmContext, diff --git a/crates/zksync/core/src/vm/farcall.rs b/crates/zksync/core/src/vm/farcall.rs index b4df85563..c9d05d2f4 100644 --- a/crates/zksync/core/src/vm/farcall.rs +++ b/crates/zksync/core/src/vm/farcall.rs @@ -151,7 +151,7 @@ impl FarCallHandler { /// Attempts to return the preset data ignoring any following opcodes, if set. /// Must be called during `finish_cycle`. - pub(crate) fn maybe_return_early( + pub(crate) fn maybe_return_early( &mut self, state: &mut ZkSyncVmState, _bootloader_state: &mut BootloaderState, @@ -211,7 +211,7 @@ impl FarCallHandler { /// Returns immediate [CallAction]s for the currently active FarCall. /// Must be called during `finish_cycle`. - pub(crate) fn take_immediate_actions( + pub(crate) fn take_immediate_actions( &mut self, state: &mut ZkSyncVmState, _bootloader_state: &mut BootloaderState, @@ -220,7 +220,7 @@ impl FarCallHandler { } } -fn populate_page_with_data( +fn populate_page_with_data( state: &mut ZkSyncVmState, page: MemoryPage, data: Vec, diff --git a/crates/zksync/core/src/vm/inspect.rs b/crates/zksync/core/src/vm/inspect.rs index 4d8742dfe..e3207c5d2 100644 --- a/crates/zksync/core/src/vm/inspect.rs +++ b/crates/zksync/core/src/vm/inspect.rs @@ -75,8 +75,8 @@ pub fn inspect_as_batch( call_ctx: CallContext, ) -> ZKVMResult where - DB: Database + Send, - ::Error: Send + Debug, + DB: Database, + ::Error: Debug, { let txns = split_tx_by_factory_deps(tx); let total_txns = txns.len(); @@ -141,8 +141,8 @@ pub fn inspect( call_ctx: CallContext, ) -> ZKVMResult where - DB: Database + Send, - ::Error: Send + Debug, + DB: Database, + ::Error: Debug, { let chain_id = if ecx.env.cfg.chain_id <= u32::MAX as u64 { L2ChainId::from(ecx.env.cfg.chain_id as u32) @@ -329,7 +329,7 @@ where Ok(execution_result) } -fn inspect_inner( +fn inspect_inner( l2_tx: L2Tx, storage: StoragePtr>, chain_id: L2ChainId, diff --git a/crates/zksync/core/src/vm/runner.rs b/crates/zksync/core/src/vm/runner.rs index 17da2ae5d..368d9c8b9 100644 --- a/crates/zksync/core/src/vm/runner.rs +++ b/crates/zksync/core/src/vm/runner.rs @@ -32,8 +32,8 @@ pub fn transact<'a, DB>( db: &'a mut DB, ) -> eyre::Result where - DB: Database + Send, - ::Error: Send + Debug, + DB: Database, + ::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"); @@ -124,8 +124,8 @@ pub fn create( mut ccx: CheatcodeTracerContext, ) -> ZKVMResult where - DB: Database + Send, - ::Error: Send + Debug, + DB: Database, + ::Error: Debug, { info!(?call, "create tx {}", hex::encode(&call.init_code)); let constructor_input = call.init_code[contract.evm_bytecode.len()..].to_vec(); @@ -175,8 +175,8 @@ pub fn call( mut ccx: CheatcodeTracerContext, ) -> ZKVMResult where - DB: Database + Send, - ::Error: Send + Debug, + DB: Database, + ::Error: Debug, { info!(?call, "call tx {}", hex::encode(&call.input)); let caller = ecx.env.tx.caller; @@ -229,7 +229,7 @@ where /// Assign gas parameters that satisfy zkSync's fee model. fn gas_params(ecx: &mut EvmContext, caller: Address) -> (U256, U256) where - DB: Database + Send, + DB: Database, ::Error: Debug, { let value = ecx.env.tx.value.to_u256(); diff --git a/crates/zksync/core/src/vm/tracer.rs b/crates/zksync/core/src/vm/tracer.rs index 2004c2898..4bae9a46a 100644 --- a/crates/zksync/core/src/vm/tracer.rs +++ b/crates/zksync/core/src/vm/tracer.rs @@ -141,7 +141,7 @@ impl CheatcodeTracer { } } -impl DynTracer> for CheatcodeTracer { +impl DynTracer> for CheatcodeTracer { fn before_decoding(&mut self, _state: VmLocalStateData<'_>, _memory: &SimpleMemory) {} fn after_decoding( @@ -328,7 +328,7 @@ impl DynTracer> for CheatcodeTracer } } -impl VmTracer for CheatcodeTracer { +impl VmTracer for CheatcodeTracer { fn initialize_tracer(&mut self, _state: &mut ZkSyncVmState) {} fn finish_cycle( From 199f02685792d34ac47f71fc0fa3f5ea500c9486 Mon Sep 17 00:00:00 2001 From: Nisheeth Barthwal Date: Wed, 31 Jul 2024 17:35:41 +0200 Subject: [PATCH 3/5] fix: tokio panic and split up tests (#495) Co-authored-by: Karrq --- .github/workflows/test.yml | 11 ++ crates/evm/core/src/backend/fork_type.rs | 11 +- crates/forge/tests/it/test_helpers.rs | 31 ++++- crates/forge/tests/it/zk.rs | 68 ----------- crates/forge/tests/it/zk/basic.rs | 34 ++++++ crates/forge/tests/it/zk/cheats.rs | 92 ++++++++++++++ crates/forge/tests/it/zk/contracts.rs | 85 +++++++++++++ crates/forge/tests/it/zk/logs.rs | 35 ++++++ crates/forge/tests/it/zk/mod.rs | 5 + crates/zksync/core/src/cheatcodes.rs | 4 +- testdata/zk/Basic.t.sol | 70 +++++++++++ testdata/zk/Cheatcodes.t.sol | 147 +++++++++++++++++++++-- testdata/zk/Contracts.t.sol | 136 ++++++++++++++++----- testdata/zk/Globals.sol | 2 +- testdata/zk/Greeter.sol | 35 ++++++ zk-tests/src/Cheatcodes.t.sol | 2 +- 16 files changed, 649 insertions(+), 119 deletions(-) delete mode 100644 crates/forge/tests/it/zk.rs create mode 100644 crates/forge/tests/it/zk/basic.rs create mode 100644 crates/forge/tests/it/zk/cheats.rs create mode 100644 crates/forge/tests/it/zk/contracts.rs create mode 100644 crates/forge/tests/it/zk/logs.rs create mode 100644 crates/forge/tests/it/zk/mod.rs create mode 100644 testdata/zk/Greeter.sol diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1167b4dcd..adbe72804 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -131,10 +131,21 @@ jobs: uses: actions-rust-lang/setup-rust-toolchain@v1 with: toolchain: nightly-2024-04-28 + + - name: Run era-test-node + uses: dutterbutter/era-test-node-action@v1 + with: + mode: fork + network: mainnet + log: info + logFilePath: era_test_node.log + target: x86_64-unknown-linux-gnu + releaseTag: v0.1.0-alpha.25 - name: Run zk tests env: RUST_BACKTRACE: full + TEST_MAINNET_URL: http://localhost:8011 run: cargo test zk zk-smoke-test: diff --git a/crates/evm/core/src/backend/fork_type.rs b/crates/evm/core/src/backend/fork_type.rs index f559adb9e..75c1b60d4 100644 --- a/crates/evm/core/src/backend/fork_type.rs +++ b/crates/evm/core/src/backend/fork_type.rs @@ -37,13 +37,10 @@ impl CachedForkType { let is_zk_url = foundry_common::provider::try_get_http_provider(fork_url) .map(|provider| { - let is_zk_url = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .unwrap() - .block_on(provider.raw_request("zks_L1ChainId".into(), ())) - .map(|_: String| true) - .unwrap_or_default(); + let is_zk_url = + futures::executor::block_on(provider.raw_request("zks_L1ChainId".into(), ())) + .map(|_: String| true) + .unwrap_or_default(); is_zk_url }) diff --git a/crates/forge/tests/it/test_helpers.rs b/crates/forge/tests/it/test_helpers.rs index 9bee2e638..26cad2158 100644 --- a/crates/forge/tests/it/test_helpers.rs +++ b/crates/forge/tests/it/test_helpers.rs @@ -206,6 +206,7 @@ impl ForgeTestProfile { zk_config.zksync.fallback_oz = true; zk_config.zksync.optimizer_mode = '3'; zk_config.zksync.zksolc = Some(foundry_config::SolcReq::Version(Version::new(1, 5, 1))); + zk_config.fuzz.no_zksync_reserved_addresses = true; zk_config } @@ -335,7 +336,7 @@ impl ForgeTestData { /// Builds a non-tracing runner with zksync /// TODO: This needs to be added as currently it is a copy of the original function pub fn runner_with_zksync_config(&self, mut zk_config: Config) -> MultiContractRunner { - zk_config.rpc_endpoints = rpc_endpoints(); + zk_config.rpc_endpoints = rpc_endpoints_zk(); zk_config.allow_paths.push(manifest_root().to_path_buf()); // no prompt testing @@ -352,7 +353,8 @@ impl ForgeTestData { let output = self.zk_test_data.output.clone(); let zk_output = self.zk_test_data.zk_output.clone(); let dual_compiled_contracts = self.zk_test_data.dual_compiled_contracts.clone(); - + let mut test_opts = self.test_opts.clone(); + test_opts.fuzz.no_zksync_reserved_addresses = zk_config.fuzz.no_zksync_reserved_addresses; let sender = zk_config.sender; let mut builder = self.base_runner(); @@ -360,7 +362,7 @@ impl ForgeTestData { builder .enable_isolation(opts.isolate) .sender(sender) - .with_test_options(self.test_opts.clone()) + .with_test_options(test_opts) .build(root, output, Some(zk_output), env, opts.clone(), dual_compiled_contracts) .unwrap() } @@ -511,3 +513,26 @@ pub fn rpc_endpoints() -> RpcEndpoints { ("rpcEnvAlias", RpcEndpoint::Env("${RPC_ENV_ALIAS}".to_string())), ]) } + +/// the RPC endpoints used during tests +pub fn rpc_endpoints_zk() -> RpcEndpoints { + // use mainnet url from env to avoid rate limiting in CI + let mainnet_url = + std::env::var("TEST_MAINNET_URL").unwrap_or("https://mainnet.era.zksync.io".to_string()); // trufflehog:ignore + RpcEndpoints::new([ + ("mainnet", RpcEndpoint::Url(mainnet_url)), + ( + "rpcAlias", + RpcEndpoint::Url( + "https://eth-mainnet.alchemyapi.io/v2/Lc7oIGYeL_QvInzI0Wiu_pOZZDEKBrdf".to_string(), /* trufflehog:ignore */ + ), + ), + ( + "rpcAliasSepolia", + RpcEndpoint::Url( + "https://eth-sepolia.g.alchemy.com/v2/Lc7oIGYeL_QvInzI0Wiu_pOZZDEKBrdf".to_string(), /* trufflehog:ignore */ + ), + ), + ("rpcEnvAlias", RpcEndpoint::Env("${RPC_ENV_ALIAS}".to_string())), + ]) +} diff --git a/crates/forge/tests/it/zk.rs b/crates/forge/tests/it/zk.rs deleted file mode 100644 index 8a1ad0435..000000000 --- a/crates/forge/tests/it/zk.rs +++ /dev/null @@ -1,68 +0,0 @@ -//! Forge tests for cheatcodes. - -use std::collections::BTreeMap; - -use crate::{config::*, test_helpers::TEST_DATA_DEFAULT}; -use forge::revm::primitives::SpecId; -use foundry_config::fs_permissions::PathPermission; -use foundry_test_utils::Filter; - -/// Executes all zk basic tests -#[tokio::test(flavor = "multi_thread")] -async fn test_zk_basic() { - let runner = TEST_DATA_DEFAULT.runner_zksync(); - let filter = Filter::new(".*", "ZkBasicTest", ".*"); - - TestConfig::with_filter(runner, filter).evm_spec(SpecId::SHANGHAI).run().await; -} - -/// Executes all zk contract tests -#[tokio::test(flavor = "multi_thread")] -async fn test_zk_contracts() { - let mut zk_config = TEST_DATA_DEFAULT.zk_test_data.zk_config.clone(); - zk_config.fs_permissions.add(PathPermission::read_write("./zk/zkout/ConstantNumber.sol")); - let runner = TEST_DATA_DEFAULT.runner_with_zksync_config(zk_config); - let filter = Filter::new(".*", "ZkContractsTest", ".*"); - - TestConfig::with_filter(runner, filter).evm_spec(SpecId::SHANGHAI).run().await; -} - -/// Executes all zk cheatcode tests -#[tokio::test(flavor = "multi_thread")] -async fn test_zk_cheats() { - let mut zk_config = TEST_DATA_DEFAULT.zk_test_data.zk_config.clone(); - zk_config.fs_permissions.add(PathPermission::read_write("./zk/zkout/ConstantNumber.sol")); - let runner = TEST_DATA_DEFAULT.runner_with_zksync_config(zk_config); - let filter = Filter::new(".*", "ZkCheatcodesTest", ".*"); - - TestConfig::with_filter(runner, filter).evm_spec(SpecId::SHANGHAI).run().await; -} - -/// Executes all zk console tests -#[tokio::test(flavor = "multi_thread")] -async fn test_zk_logs() { - let runner = TEST_DATA_DEFAULT.runner_zksync(); - let filter = Filter::new(".*", "ZkConsoleTest", ".*"); - - let results = TestConfig::with_filter(runner, filter).evm_spec(SpecId::SHANGHAI).test(); - assert_multiple( - &results, - BTreeMap::from([( - "zk/Console.t.sol:ZkConsoleTest", - vec![( - "testZkConsoleOutput()", - true, - None, - Some(vec![ - "print".into(), - "outer print".into(), - "0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496".into(), - "print".into(), - "0xff".into(), - "print".into(), - ]), - None, - )], - )]), - ); -} diff --git a/crates/forge/tests/it/zk/basic.rs b/crates/forge/tests/it/zk/basic.rs new file mode 100644 index 000000000..62c82b792 --- /dev/null +++ b/crates/forge/tests/it/zk/basic.rs @@ -0,0 +1,34 @@ +//! Forge tests for basic zkysnc functionality. + +use crate::{config::*, test_helpers::TEST_DATA_DEFAULT}; +use forge::revm::primitives::SpecId; +use foundry_test_utils::Filter; + +#[tokio::test(flavor = "multi_thread")] +async fn test_zk_block_information_is_consistent() { + let runner = TEST_DATA_DEFAULT.runner_zksync(); + let filter = + Filter::new("testZkBasicBlockNumber|testZkBasicBlockTimestamp", "ZkBasicTest", ".*"); + + TestConfig::with_filter(runner, filter).evm_spec(SpecId::SHANGHAI).run().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_zk_address_balance_is_consistent() { + let runner = TEST_DATA_DEFAULT.runner_zksync(); + let filter = Filter::new("testZkBasicAddressBalance", "ZkBasicTest", ".*"); + + TestConfig::with_filter(runner, filter).evm_spec(SpecId::SHANGHAI).run().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_zk_propagated_block_env_is_consistent() { + let runner = TEST_DATA_DEFAULT.runner_zksync(); + let filter = Filter::new( + "testZkPropagatedBlockEnv|testZkBasicBlockBaseFee|testZkBlockHashWithNewerBlocks", + "ZkBasicTest", + ".*", + ); + + TestConfig::with_filter(runner, filter).evm_spec(SpecId::SHANGHAI).run().await; +} diff --git a/crates/forge/tests/it/zk/cheats.rs b/crates/forge/tests/it/zk/cheats.rs new file mode 100644 index 000000000..bfa27b7b4 --- /dev/null +++ b/crates/forge/tests/it/zk/cheats.rs @@ -0,0 +1,92 @@ +//! Forge tests for cheatcodes. + +use crate::{config::*, test_helpers::TEST_DATA_DEFAULT}; +use forge::revm::primitives::SpecId; +use foundry_config::fs_permissions::PathPermission; +use foundry_test_utils::Filter; + +#[tokio::test(flavor = "multi_thread")] +async fn test_zk_cheat_roll_works() { + let runner = TEST_DATA_DEFAULT.runner_zksync(); + let filter = Filter::new("testZkCheatcodesRoll", "ZkCheatcodesTest", ".*"); + + TestConfig::with_filter(runner, filter).evm_spec(SpecId::SHANGHAI).run().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_zk_cheat_warp_works() { + let runner = TEST_DATA_DEFAULT.runner_zksync(); + let filter = Filter::new("testZkCheatcodesWarp", "ZkCheatcodesTest", ".*"); + + TestConfig::with_filter(runner, filter).evm_spec(SpecId::SHANGHAI).run().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_zk_cheat_deal_works() { + let runner = TEST_DATA_DEFAULT.runner_zksync(); + let filter = Filter::new("testZkCheatcodesDeal", "ZkCheatcodesTest", ".*"); + + TestConfig::with_filter(runner, filter).evm_spec(SpecId::SHANGHAI).run().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_zk_cheat_set_nonce_works() { + let runner = TEST_DATA_DEFAULT.runner_zksync(); + let filter = Filter::new("testZkCheatcodesSetNonce", "ZkCheatcodesTest", ".*"); + + TestConfig::with_filter(runner, filter).evm_spec(SpecId::SHANGHAI).run().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_zk_cheat_etch_works() { + let mut zk_config = TEST_DATA_DEFAULT.zk_test_data.zk_config.clone(); + zk_config.fs_permissions.add(PathPermission::read_write("./zk/zkout/ConstantNumber.sol")); + let runner = TEST_DATA_DEFAULT.runner_with_zksync_config(zk_config); + let filter = Filter::new("testZkCheatcodesEtch", "ZkCheatcodesTest", ".*"); + + TestConfig::with_filter(runner, filter).evm_spec(SpecId::SHANGHAI).run().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_zk_cheat_record_works() { + let runner = TEST_DATA_DEFAULT.runner_zksync(); + let filter = Filter::new("testRecord", "ZkCheatcodesTest", ".*"); + + TestConfig::with_filter(runner, filter).evm_spec(SpecId::SHANGHAI).run().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_zk_cheat_expect_emit_works() { + let runner = TEST_DATA_DEFAULT.runner_zksync(); + let filter = Filter::new("testExpectEmit|testExpectEmitOnCreate", "ZkCheatcodesTest", ".*"); + + TestConfig::with_filter(runner, filter).evm_spec(SpecId::SHANGHAI).run().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_zk_cheat_mock_with_value_function() { + let runner = TEST_DATA_DEFAULT.runner_zksync(); + let filter = Filter::new("testZkCheatcodesValueFunctionMockReturn", "ZkCheatcodesTest", ".*"); + + TestConfig::with_filter(runner, filter).evm_spec(SpecId::SHANGHAI).run().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_zk_cheat_mock_calls() { + let runner = TEST_DATA_DEFAULT.runner_zksync(); + let filter = Filter::new( + "testZkCheatcodesCanMockCallTestContract|testZkCheatcodesCanMockCall", + "ZkCheatcodesTest", + ".*", + ); + + TestConfig::with_filter(runner, filter).evm_spec(SpecId::SHANGHAI).run().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_zk_cheat_works_after_fork() { + let runner = TEST_DATA_DEFAULT.runner_zksync(); + let filter = Filter::new("testZkCheatcodesCanBeUsedAfterFork", "ZkCheatcodesTest", ".*"); + + TestConfig::with_filter(runner, filter).evm_spec(SpecId::SHANGHAI).run().await; +} diff --git a/crates/forge/tests/it/zk/contracts.rs b/crates/forge/tests/it/zk/contracts.rs new file mode 100644 index 000000000..373fab699 --- /dev/null +++ b/crates/forge/tests/it/zk/contracts.rs @@ -0,0 +1,85 @@ +//! Forge tests for zksync contracts. + +use crate::{config::*, test_helpers::TEST_DATA_DEFAULT}; +use forge::revm::primitives::SpecId; +use foundry_config::fs_permissions::PathPermission; +use foundry_test_utils::Filter; + +#[tokio::test(flavor = "multi_thread")] +async fn test_zk_contract_can_call_function() { + let runner = TEST_DATA_DEFAULT.runner_zksync(); + let filter = Filter::new( + "testZkContractCanCallMethod|testZkContractsMultipleTransactions", + "ZkContractsTest", + ".*", + ); + + TestConfig::with_filter(runner, filter).evm_spec(SpecId::SHANGHAI).run().await; +} + +#[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", ".*"); + + TestConfig::with_filter(runner, filter).evm_spec(SpecId::SHANGHAI).run().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_zk_contract_deployment() { + let runner = TEST_DATA_DEFAULT.runner_zksync(); + let filter = Filter::new("testZkContractsInlineDeployedContractNoArgs|testZkContractsInlineDeployedContractComplexArgs", "ZkContractsTest", ".*"); + + TestConfig::with_filter(runner, filter).evm_spec(SpecId::SHANGHAI).run().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_zk_contract_deployment_balance() { + let runner = TEST_DATA_DEFAULT.runner_zksync(); + let filter = + Filter::new("testZkContractsInlineDeployedContractBalance", "ZkContractsTest", ".*"); + + TestConfig::with_filter(runner, filter).evm_spec(SpecId::SHANGHAI).run().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_zk_contract_deployment_balance_transfer() { + let runner = TEST_DATA_DEFAULT.runner_zksync(); + let filter = Filter::new("testZkContractsExpectedBalances", "ZkContractsTest", ".*"); + + TestConfig::with_filter(runner, filter).evm_spec(SpecId::SHANGHAI).run().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_zk_contract_create2() { + let mut zk_config = TEST_DATA_DEFAULT.zk_test_data.zk_config.clone(); + zk_config.fs_permissions.add(PathPermission::read_write("./zk/zkout/ConstantNumber.sol")); + let runner = TEST_DATA_DEFAULT.runner_with_zksync_config(zk_config); + let filter = Filter::new("testZkContractsCreate2", "ZkContractsTest", ".*"); + + TestConfig::with_filter(runner, filter).evm_spec(SpecId::SHANGHAI).run().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_zk_contract_can_call_system_contracts() { + let runner = TEST_DATA_DEFAULT.runner_zksync(); + let filter = Filter::new("testZkContractsCallSystemContract", "ZkContractsTest", ".*"); + + TestConfig::with_filter(runner, filter).evm_spec(SpecId::SHANGHAI).run().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_zk_contract_deployed_in_setup_can_be_mocked() { + let runner = TEST_DATA_DEFAULT.runner_zksync(); + let filter = Filter::new("testZkContractsDeployedInSetupAreMockable", "ZkContractsTest", ".*"); + + TestConfig::with_filter(runner, filter).evm_spec(SpecId::SHANGHAI).run().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_zk_contract_static_calls_keep_nonce_consistent() { + let runner = TEST_DATA_DEFAULT.runner_zksync(); + let filter = Filter::new("testZkStaticCalls", "ZkContractsTest", ".*"); + + TestConfig::with_filter(runner, filter).evm_spec(SpecId::SHANGHAI).run().await; +} diff --git a/crates/forge/tests/it/zk/logs.rs b/crates/forge/tests/it/zk/logs.rs new file mode 100644 index 000000000..8c0c2a159 --- /dev/null +++ b/crates/forge/tests/it/zk/logs.rs @@ -0,0 +1,35 @@ +//! Forge tests for zksync logs. + +use std::collections::BTreeMap; + +use crate::{config::*, test_helpers::TEST_DATA_DEFAULT}; +use forge::revm::primitives::SpecId; +use foundry_test_utils::Filter; + +#[tokio::test(flavor = "multi_thread")] +async fn test_zk_logs_work() { + let runner = TEST_DATA_DEFAULT.runner_zksync(); + let filter = Filter::new(".*", "ZkConsoleTest", ".*"); + + let results = TestConfig::with_filter(runner, filter).evm_spec(SpecId::SHANGHAI).test(); + assert_multiple( + &results, + BTreeMap::from([( + "zk/Console.t.sol:ZkConsoleTest", + vec![( + "testZkConsoleOutput()", + true, + None, + Some(vec![ + "print".into(), + "outer print".into(), + "0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496".into(), + "print".into(), + "0xff".into(), + "print".into(), + ]), + None, + )], + )]), + ); +} diff --git a/crates/forge/tests/it/zk/mod.rs b/crates/forge/tests/it/zk/mod.rs new file mode 100644 index 000000000..a0e1ba532 --- /dev/null +++ b/crates/forge/tests/it/zk/mod.rs @@ -0,0 +1,5 @@ +//! Forge tests for zkysnc functionality. +mod basic; +mod cheats; +mod contracts; +mod logs; diff --git a/crates/zksync/core/src/cheatcodes.rs b/crates/zksync/core/src/cheatcodes.rs index dc50d2cb7..fc55cac3d 100644 --- a/crates/zksync/core/src/cheatcodes.rs +++ b/crates/zksync/core/src/cheatcodes.rs @@ -161,7 +161,9 @@ where ::Error: Debug, { if address == caller { - tracing::error!("using `mockCall` cheatcode on caller isn't supported in zkVM"); + tracing::error!( + "using `mockCall` cheatcode on caller ({address:?}) isn't supported in zkVM" + ); } let account_code_addr = zksync_types::ACCOUNT_CODE_STORAGE_ADDRESS.to_address(); diff --git a/testdata/zk/Basic.t.sol b/testdata/zk/Basic.t.sol index 03bec35fb..0c79e9756 100644 --- a/testdata/zk/Basic.t.sol +++ b/testdata/zk/Basic.t.sol @@ -5,6 +5,24 @@ import "ds-test/test.sol"; import "../cheats/Vm.sol"; import {Globals} from "./Globals.sol"; +contract BlockEnv { + uint256 public number; + uint256 public timestamp; + uint256 public basefee; + uint256 public chainid; + + constructor() { + number = block.number; + timestamp = block.timestamp; + basefee = block.basefee; + chainid = block.chainid; + } + + function zkBlockhash(uint256 _blockNumber) public view returns (bytes32) { + return blockhash(_blockNumber); + } +} + contract ZkBasicTest is DSTest { Vm constant vm = Vm(HEVM_ADDRESS); @@ -18,10 +36,12 @@ contract ZkBasicTest is DSTest { uint256 forkEra; uint256 forkEth; + uint256 latestForkEth; function setUp() public { forkEra = vm.createFork(Globals.ZKSYNC_MAINNET_URL, ERA_FORK_BLOCK); forkEth = vm.createFork(Globals.ETHEREUM_MAINNET_URL, ETH_FORK_BLOCK); + latestForkEth = vm.createFork(Globals.ETHEREUM_MAINNET_URL); } function testZkBasicBlockNumber() public { @@ -50,4 +70,54 @@ contract ZkBasicTest is DSTest { vm.selectFork(forkEth); require(TEST_ADDRESS.balance == 100, "eth balance mismatch"); } + + function testZkPropagatedBlockEnv() public { + BlockEnv be = new BlockEnv(); + require(be.number() == block.number, "propagated block number is the same as current"); + require(be.timestamp() == block.timestamp, "propagated block timestamp is the same as current"); + require(be.basefee() == block.basefee, "propagated block basefee is the same as current"); + require(be.chainid() == block.chainid, "propagated block chainid is the same as current"); + + require( + be.zkBlockhash(block.number) == blockhash(block.number), "blockhash of the current block should be zero" + ); + + // this corresponds to the the genesis block since the test runs in block #1 + require( + be.zkBlockhash(block.number - 1) == blockhash(block.number - 1), + "blockhash of the previous block should be equal" + ); + + require(be.zkBlockhash(0) == blockhash(0), "blockhash of the genesis block should be equal"); + + be = new BlockEnv(); + require(be.number() == block.number, "propagated block number stays constant"); + require(be.timestamp() == block.timestamp, "propagated block timestamp stays constant"); + require(be.basefee() == block.basefee, "propagated block basefee stays constant"); + require(be.chainid() == block.chainid, "propagated block chainid stays constant"); + + vm.roll(42); + vm.warp(42); + + be = new BlockEnv(); + require(be.number() == block.number, "propagated block number rolls"); + require(be.timestamp() == block.timestamp, "propagated block timestamp warps"); + require(be.basefee() == block.basefee, "propagated block basefee warps"); + } + + function testZkBasicBlockBaseFee() public { + BlockEnv beBefore = new BlockEnv(); + require(beBefore.basefee() == block.basefee, "propagated block basefee is the same as current"); + + vm.selectFork(forkEra); + BlockEnv beAfter = new BlockEnv(); + require(beAfter.basefee() == block.basefee, "propagated block basefee is the same as before"); + require(beAfter.basefee() == block.basefee, "propagated block basefee is the same as before"); + } + + function testZkBlockHashWithNewerBlocks() public { + vm.selectFork(latestForkEth); + BlockEnv be = new BlockEnv(); + require(be.zkBlockhash(block.number) == blockhash(block.number), "blockhash mismatch"); + } } diff --git a/testdata/zk/Cheatcodes.t.sol b/testdata/zk/Cheatcodes.t.sol index 6ed2e518c..2e82a4a99 100644 --- a/testdata/zk/Cheatcodes.t.sol +++ b/testdata/zk/Cheatcodes.t.sol @@ -4,10 +4,71 @@ pragma solidity ^0.8.18; import "ds-test/test.sol"; import "../cheats/Vm.sol"; import {Globals} from "./Globals.sol"; +import "../default/logs/console.sol"; + +contract FixedSlot { + uint8 num; // slot index: 0 + + function setSlot0(uint8 _num) public { + num = _num; + } +} + +contract InnerMock { + function getBytes() public payable returns (bytes memory) { + bytes memory r = bytes(hex"abcd"); + return r; + } +} + +contract Mock { + InnerMock private inner; + + constructor(InnerMock _inner) payable { + inner = _inner; + } + + function getBytes() public returns (bytes memory) { + return inner.getBytes{value: 10}(); + } +} + +interface IMyProxyCaller { + function transact(uint8 _data) external; +} + +contract MyProxyCaller { + address inner; + + constructor(address _inner) { + inner = _inner; + } + + function transact() public { + IMyProxyCaller(inner).transact(10); + } +} + +contract Emitter { + event EventConstructor(string message); + event EventFunction(string message); + + constructor() { + emit EventConstructor("constructor"); + } + + function functionEmit() public { + emit EventFunction("function"); + } +} contract ZkCheatcodesTest is DSTest { Vm constant vm = Vm(HEVM_ADDRESS); + event EventConstructor(string message); + event EventFunction(string message); + + uint256 testSlot = 0; //0x000000000000000000000000000000000000000000000000000000000000001e slot uint256 constant ERA_FORK_BLOCK = 19579636; uint256 constant ERA_FORK_BLOCK_TS = 1700601590; @@ -41,7 +102,7 @@ contract ZkCheatcodesTest is DSTest { } function testZkCheatcodesDeal() public { - vm.zkVm(true); + vm.selectFork(forkEra); require(TEST_ADDRESS.balance == 0, "era balance mismatch"); vm.deal(TEST_ADDRESS, 100); @@ -49,7 +110,7 @@ contract ZkCheatcodesTest is DSTest { } function testZkCheatcodesSetNonce() public { - vm.zkVm(true); + vm.selectFork(forkEra); require(vm.getNonce(TEST_ADDRESS) == 0, "era nonce mismatch"); vm.setNonce(TEST_ADDRESS, 10); @@ -60,16 +121,11 @@ contract ZkCheatcodesTest is DSTest { } function testZkCheatcodesEtch() public { - vm.zkVm(true); - - bytes32 emptyHash = hex"0000000000000000000000000000000000000000000000000000000000000000"; - bytes memory emptyBytes = hex"00"; - bytes32 zkBytecodeHash = hex"0100000f6d092b2cd44547a312320ad99c9587b40e0d03b0c17f09afd286d660"; - bytes memory zkDeployedBytecode = - hex"0000008003000039000000400030043f0000000102200190000000120000c13d000000000201001900000009022001980000001a0000613d000000000101043b0000000a011001970000000b0110009c0000001a0000c13d0000000001000416000000000101004b0000001a0000c13d0000000a01000039000000800010043f0000000c010000410000001d0001042e0000000001000416000000000101004b0000001a0000c13d00000020010000390000010000100443000001200000044300000008010000410000001d0001042e00000000010000190000001e000104300000001c000004320000001d0001042e0000001e000104300000000000000000000000020000000000000000000000000000004000000100000000000000000000000000000000000000000000000000fffffffc000000000000000000000000ffffffff00000000000000000000000000000000000000000000000000000000643ceff9000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000800000000000000000000000000000000000000000000000000000000000000000000000000000000075b6ac057b6098db0e2fae836aa00e54c6eec4973fc9e5e2b4c8baee23515b65"; - vm.zkRegisterContract("ConstantNumber", emptyHash, emptyBytes, emptyBytes, zkBytecodeHash, zkDeployedBytecode); + vm.selectFork(forkEra); - vm.etch(TEST_ADDRESS, zkDeployedBytecode); + string memory artifact = vm.readFile("./zk/zkout/ConstantNumber.sol/ConstantNumber.json"); + bytes memory constantNumberCode = vm.parseJsonBytes(artifact, ".bytecode.object"); + vm.etch(TEST_ADDRESS, constantNumberCode); (bool success, bytes memory output) = TEST_ADDRESS.call(abi.encodeWithSignature("ten()")); require(success, "ten() call failed"); @@ -77,4 +133,73 @@ contract ZkCheatcodesTest is DSTest { uint8 number = abi.decode(output, (uint8)); require(number == 10, "era etched code incorrect"); } + + function testRecord() public { + FixedSlot fs = new FixedSlot(); + vm.record(); + fs.setSlot0(10); + (bytes32[] memory reads, bytes32[] memory writes) = vm.accesses(address(fs)); + bytes32 keySlot0 = bytes32(uint256(0)); + assertEq(reads[0], keySlot0); + assertEq(writes[0], keySlot0); + } + + function testExpectEmit() public { + vm.expectEmit(true, true, true, true); + emit EventFunction("function"); + Emitter emitter = new Emitter(); + emitter.functionEmit(); + } + + function testExpectEmitOnCreate() public { + vm.expectEmit(true, true, true, true); + emit EventConstructor("constructor"); + new Emitter(); + } + + function testZkCheatcodesValueFunctionMockReturn() public { + InnerMock inner = new InnerMock(); + // Send some funds to so it can pay for the inner call + Mock target = new Mock{value: 50}(inner); + + bytes memory dataBefore = target.getBytes(); + assertEq(dataBefore, bytes(hex"abcd")); + + vm.mockCall(address(inner), abi.encodeWithSelector(inner.getBytes.selector), abi.encode(bytes(hex"a1b1"))); + + bytes memory dataAfter = target.getBytes(); + assertEq(dataAfter, bytes(hex"a1b1")); + } + + function testZkCheatcodesCanMockCallTestContract() public { + address thisAddress = address(this); + MyProxyCaller transactor = new MyProxyCaller(thisAddress); + + vm.mockCall(thisAddress, abi.encodeWithSelector(IMyProxyCaller.transact.selector), abi.encode()); + + transactor.transact(); + } + + function testZkCheatcodesCanMockCall(address mockMe) public { + vm.assume(mockMe != address(vm)); + + // zkVM currently doesn't support mocking the transaction sender + vm.assume(mockMe != msg.sender); + + MyProxyCaller transactor = new MyProxyCaller(mockMe); + + vm.mockCall(mockMe, abi.encodeWithSelector(IMyProxyCaller.transact.selector), abi.encode()); + + transactor.transact(); + } + + function testZkCheatcodesCanBeUsedAfterFork() public { + assertEq(0, address(0x4e59b44847b379578588920cA78FbF26c0B4956C).balance); + + vm.createSelectFork(Globals.ETHEREUM_MAINNET_URL, ETH_FORK_BLOCK); + assertEq(0, address(0x4e59b44847b379578588920cA78FbF26c0B4956C).balance); + + vm.deal(0x4e59b44847b379578588920cA78FbF26c0B4956C, 1 ether); + assertEq(1 ether, address(0x4e59b44847b379578588920cA78FbF26c0B4956C).balance); + } } diff --git a/testdata/zk/Contracts.t.sol b/testdata/zk/Contracts.t.sol index 30ae02b5d..651d60e8b 100644 --- a/testdata/zk/Contracts.t.sol +++ b/testdata/zk/Contracts.t.sol @@ -5,28 +5,14 @@ import "ds-test/test.sol"; import "../cheats/Vm.sol"; import {ConstantNumber} from "./ConstantNumber.sol"; +import {Greeter} from "./Greeter.sol"; import {Globals} from "./Globals.sol"; -contract Greeter { - string name; - uint256 age; - - event Greet(string greet); - - function greeting(string memory _name) public returns (string memory) { - name = _name; - string memory greet = string(abi.encodePacked("Hello ", _name)); - emit Greet(greet); - return greet; - } - - function setAge(uint256 _age) public { - age = _age; - } - - function getAge() public view returns (uint256) { - return age; - } +interface ISystemContractDeployer { + function getNewAddressCreate2(address _sender, bytes32 _bytecodeHash, bytes32 _salt, bytes calldata _input) + external + view + returns (address newAddress); } contract Number { @@ -41,15 +27,39 @@ contract FixedNumber { } } +contract FixedGreeter { + function greet(string memory _name) public pure returns (string memory) { + string memory greeting = string(abi.encodePacked("Hello ", _name)); + return greeting; + } +} + +contract MultiNumber { + function one() public pure returns (uint8) { + return 1; + } + + function two() public pure returns (uint8) { + return 2; + } +} + contract PayableFixedNumber { address sender; uint256 value; + receive() external payable {} + constructor() payable { sender = msg.sender; value = msg.value; } + function transfer(address payable dest, uint256 amt) public { + (bool success,) = dest.call{value: amt}(""); + require(success); + } + function five() public pure returns (uint8) { return 5; } @@ -90,6 +100,7 @@ contract ZkContractsTest is DSTest { Number number; CustomNumber customNumber; + MultiNumber multiNumber; uint256 constant ERA_FORK_BLOCK = 19579636; uint256 constant ERA_FORK_BLOCK_TS = 1700601590; @@ -103,6 +114,7 @@ contract ZkContractsTest is DSTest { function setUp() public { number = new Number(); customNumber = new CustomNumber(20); + multiNumber = new MultiNumber(); vm.makePersistent(address(number)); vm.makePersistent(address(customNumber)); @@ -110,6 +122,23 @@ contract ZkContractsTest is DSTest { forkEth = vm.createFork(Globals.ETHEREUM_MAINNET_URL, ETH_FORK_BLOCK); } + function testZkContractCanCallMethod() public { + FixedGreeter g = new FixedGreeter(); + vm.makePersistent(address(g)); + vm.selectFork(forkEra); + assertEq("Hello hi", g.greet("hi")); + } + + function testZkContractsMultipleTransactions() external { + vm.zkVm(true); + Greeter greeter = new Greeter(); + greeter.setAge(10); + string memory greeting = greeter.greeting("john"); + assertEq("Hello john", greeting); + greeter.setAge(60); + assertEq(60, greeter.getAge()); + } + function testZkContractsPersistedDeployedContractNoArgs() public { require(number.ten() == 10, "base setUp contract value mismatch"); @@ -142,6 +171,20 @@ contract ZkContractsTest is DSTest { require(address(payableFixedNumber).balance == 10, "incorrect balance"); } + function testZkContractsExpectedBalances() public { + vm.selectFork(forkEra); + uint256 balanceBefore = address(this).balance; + + PayableFixedNumber one = new PayableFixedNumber{value: 10}(); + + PayableFixedNumber two = new PayableFixedNumber(); + one.transfer(payable(address(two)), 5); + + require(address(one).balance == 5, "first contract's balance not decreased"); + require(address(two).balance == 5, "second contract's balance not increased"); + require(address(this).balance == balanceBefore - 10, "test address balance not decreased"); + } + function testZkContractsInlineDeployedContractComplexArgs() public { CustomStorage customStorage = new CustomStorage("hello", 10); vm.makePersistent(address(customStorage)); @@ -183,14 +226,53 @@ contract ZkContractsTest is DSTest { assertEq(expectedDeployedAddress, actualDeployedAddress); } - function testZkContractsMultipleTransactions() external { - vm.zkVm(true); + function testZkContractsCallSystemContract() public { + (bool success,) = address(vm).call(abi.encodeWithSignature("zkVm(bool)", true)); + require(success, "zkVm() call failed"); + + ISystemContractDeployer deployer = ISystemContractDeployer(address(0x0000000000000000000000000000000000008006)); + + address addr = deployer.getNewAddressCreate2( + address(this), + 0x0100000781e55a60f3f14fd7dd67e3c8caab896b7b0fca4a662583959299eede, + 0x0100000781e55a60f3f14fd7dd67e3c8caab896b7b0fca4a662583959299eede, + "" + ); + + assertEq(address(0x46efB6258A2A539f7C8b44e2EF659D778fb5BAAd), addr); + } + + function testZkContractsDeployedInSetupAreMockable() public { + vm.mockCall(address(multiNumber), abi.encodeWithSelector(MultiNumber.one.selector), abi.encode(42)); + + assertEq(42, multiNumber.one()); + assertEq(2, multiNumber.two()); + } + + function testZkStaticCalls() public { + (bool success,) = address(vm).call(abi.encodeWithSignature("zkVm(bool)", true)); + require(success, "zkVm() call failed"); + address sender = address(this); + uint64 startingNonce = vm.getNonce(sender); + + //this ensures calls & deployments increase the nonce + vm.startBroadcast(sender); + Greeter greeter = new Greeter(); - greeter.setAge(10); - string memory greeting = greeter.greeting("john"); - assertEq("Hello john", greeting); - greeter.setAge(60); - assertEq(60, greeter.getAge()); + assert(vm.getNonce(sender) == startingNonce + 1); + + greeter.setAge(42); + assert(vm.getNonce(sender) == startingNonce + 2); + + // static-call, nonce shouldn't change + uint256 age = greeter.getAge(); + assert(age == 42); + assert(vm.getNonce(sender) == startingNonce + 2); + + uint256 num = greeter.greeting2("zksync", 2); + assert(num == 4); + assert(vm.getNonce(sender) == startingNonce + 3); + vm.stopBroadcast(); } function _computeCreate2Address( diff --git a/testdata/zk/Globals.sol b/testdata/zk/Globals.sol index 076f703d2..5f45e65ca 100644 --- a/testdata/zk/Globals.sol +++ b/testdata/zk/Globals.sol @@ -4,5 +4,5 @@ pragma solidity ^0.8.18; library Globals { string public constant ETHEREUM_MAINNET_URL = "https://eth-mainnet.alchemyapi.io/v2/Lc7oIGYeL_QvInzI0Wiu_pOZZDEKBrdf"; // trufflehog:ignore - string public constant ZKSYNC_MAINNET_URL = "https://mainnet.era.zksync.io"; + string public constant ZKSYNC_MAINNET_URL = "mainnet"; } diff --git a/testdata/zk/Greeter.sol b/testdata/zk/Greeter.sol new file mode 100644 index 000000000..996760700 --- /dev/null +++ b/testdata/zk/Greeter.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.18; + +contract Greeter { + string name; + uint256 age; + + event Greet(string greet); + + function greeting(string memory _name) public returns (string memory) { + name = _name; + + string memory greet = string(abi.encodePacked("Hello ", _name)); + emit Greet(greet); + + return greet; + } + + function greeting2(string memory _name, uint256 n) public returns (uint256) { + name = _name; + + string memory greet = string(abi.encodePacked("Hello ", _name)); + emit Greet(greet); + + return n * 2; + } + + function setAge(uint256 _age) public { + age = _age; + } + + function getAge() public view returns (uint256) { + return age; + } +} diff --git a/zk-tests/src/Cheatcodes.t.sol b/zk-tests/src/Cheatcodes.t.sol index 8f4141660..d2ff9e7c7 100644 --- a/zk-tests/src/Cheatcodes.t.sol +++ b/zk-tests/src/Cheatcodes.t.sol @@ -207,7 +207,7 @@ contract ZkCheatcodesTest is Test { vm.assume(mockMe != address(vm)); //zkVM currently doesn't support mocking the transaction sender - vm.assume(mockMe != tx.origin); + vm.assume(mockMe != msg.sender); MyProxyCaller transactor = new MyProxyCaller(mockMe); From bde6d976683cc970f5d100f52b2f2ad57e93fd61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Federico=20Rodr=C3=ADguez?= Date: Thu, 1 Aug 2024 11:20:40 -0300 Subject: [PATCH 4/5] feat: make bytecode_hash None by default (#494) --- crates/config/src/zksync.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/config/src/zksync.rs b/crates/config/src/zksync.rs index d0349c7b0..ac73cfd01 100644 --- a/crates/config/src/zksync.rs +++ b/crates/config/src/zksync.rs @@ -29,7 +29,7 @@ pub struct ZkSyncConfig { pub solc_path: Option, /// Whether to include the metadata hash for zksolc compiled bytecode. - pub bytecode_hash: BytecodeHash, + pub bytecode_hash: Option, /// Whether to try to recompile with -Oz if the bytecode is too large. pub fallback_oz: bool, @@ -111,7 +111,7 @@ impl ZkSyncConfig { libraries, optimizer, evm_version: Some(evm_version), - metadata: Some(SettingsMetadata { bytecode_hash: Some(self.bytecode_hash) }), + metadata: Some(SettingsMetadata { bytecode_hash: self.bytecode_hash }), via_ir: Some(via_ir), // Set in project paths. remappings: Vec::new(), From 4144021661c62d7f54f33bf5c021a871d93290b5 Mon Sep 17 00:00:00 2001 From: Tomi Moreno <166135794+tomimor@users.noreply.github.com> Date: Tue, 6 Aug 2024 11:20:34 -0300 Subject: [PATCH 5/5] docs: windows compatibility clarification (#500) Co-authored-by: Nisheeth Barthwal --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6ffc311d7..7c020d69a 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This repository enhances Foundry to support zkSync Era, enabling Solidity-based > 🔧 **Fork Notice:** This is a Foundry fork with added zkSync support. > -> ⚠️ **Alpha Stage:** The project is in alpha, so you might encounter issues. +> ⚠️ **Alpha Stage:** The project is in alpha, so you might encounter issues. For more information, please review [Limitations](#limitations) section. > > 🐞 **Found an Issue?** Please report it to help us improve. @@ -59,6 +59,7 @@ While `foundry-zksync` is **alpha stage**, there are some limitations to be awar - **Contract Verification**: Currently contract verification via the `--verify` flag do not work as expected but will be added shortly. - **Specific Foundry Features**: Currently features such as `--gas-report`, `--coverage` may not work as intended. We are actively working on providing support for these feature types. - **Solc Compatibility**: `zksolc` requires a `solc` binary to be run as a child process. The version/path to use for each can be specified by the `zksolc` and `solc` options in `foundry.toml`. Not all `solc` versions are supported by all `zksolc` versions, compiling with a `solc` version higher than the one supported may lead to unexpected errors. [Read the docs](https://docs.zksync.io/zk-stack/components/compiler/toolchain/solidity.html#limitations) about version limitations and check the [zksolc changelog](https://github.com/matter-labs/era-compiler-solidity/blob/main/CHANGELOG.md) to see the latest supported `solc` version. +- **Windows Compatibility**: Windows is not officially supported yet. The reported issues would be investigated on a best-effort basis. For the most effective use of our library, we recommend familiarizing yourself with these features and limitations.