diff --git a/Cargo.lock b/Cargo.lock index 001c1323b..a28e01546 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3292,22 +3292,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] -name = "era_revm" -version = "0.0.1-alpha" -source = "git+https://github.com/matter-labs/era-revm?rev=2662bb19427a421662a7e463cf1ed6a15da361e5#2662bb19427a421662a7e463cf1ed6a15da361e5" +name = "era_cheatcodes" +version = "0.2.0" dependencies = [ "era_test_node", "ethabi 18.0.0", "ethers", "eyre", + "foundry-evm-core", "hashbrown 0.14.2", "hex", + "itertools 0.12.0", + "maplit", "multivm", "revm", "serde", "serde_json", "tracing", - "zk_evm 1.3.3 (git+https://github.com/matter-labs/era-zk_evm.git?tag=v1.3.3-rc1)", "zksync_basic_types", "zksync_state", "zksync_types", @@ -4277,7 +4278,6 @@ dependencies = [ "const-hex", "dirs 5.0.1", "dunce", - "era_revm", "ethers-core", "ethers-middleware", "ethers-providers", @@ -4293,6 +4293,7 @@ dependencies = [ "rand 0.8.5", "regex", "reqwest", + "revm", "semver 1.0.20", "serde", "serde_json", @@ -4304,6 +4305,8 @@ dependencies = [ "walkdir", "yansi 0.5.1", "zksync-web3-rs", + "zksync_basic_types", + "zksync_utils", ] [[package]] @@ -4402,6 +4405,8 @@ dependencies = [ "alloy-primitives", "alloy-sol-types", "const-hex", + "era_cheatcodes", + "era_test_node", "ethers-core", "ethers-signers", "eyre", @@ -4414,11 +4419,13 @@ dependencies = [ "foundry-evm-fuzz", "foundry-evm-traces", "hashbrown 0.14.2", + "multivm", "parking_lot 0.12.1", "proptest", "revm", "thiserror", "tracing", + "zksync_state", ] [[package]] @@ -4429,9 +4436,10 @@ dependencies = [ "alloy-json-abi", "alloy-primitives", "alloy-sol-types", + "auto_impl", "const-hex", "derive_more", - "era_revm", + "era_test_node", "ethers-core", "ethers-providers", "eyre", @@ -4442,6 +4450,8 @@ dependencies = [ "foundry-macros", "futures 0.3.28", "itertools 0.11.0", + "maplit", + "multivm", "once_cell", "parking_lot 0.12.1", "revm", @@ -4451,6 +4461,11 @@ dependencies = [ "tokio", "tracing", "url", + "zksync_basic_types", + "zksync_state", + "zksync_types", + "zksync_utils", + "zksync_web3_decl", ] [[package]] @@ -5972,6 +5987,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.9" @@ -9141,11 +9165,9 @@ dependencies = [ "bitflags 2.4.1", "bitvec 1.0.1", "c-kzg", - "derive_more", "enumn", "hashbrown 0.14.2", "hex", - "once_cell", "serde", ] @@ -12507,21 +12529,6 @@ dependencies = [ "zkevm_opcode_defs 1.3.1", ] -[[package]] -name = "zk_evm" -version = "1.3.3" -source = "git+https://github.com/matter-labs/era-zk_evm.git?tag=v1.3.3-rc1#fe8215a7047d24430ad470cf15a19bedb4d6ba0b" -dependencies = [ - "anyhow", - "lazy_static", - "num 0.4.1", - "serde", - "serde_json", - "static_assertions", - "zk_evm_abstractions", - "zkevm_opcode_defs 1.3.2", -] - [[package]] name = "zk_evm" version = "1.3.3" diff --git a/Cargo.toml b/Cargo.toml index f4e7fb25b..cbbcc771d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ members = [ "crates/forge/", "crates/macros/", "crates/test-utils/", + "crates/era-cheatcodes/", "crates/zkcast", "crates/zkforge", ] @@ -126,6 +127,7 @@ foundry-evm-fuzz = { path = "crates/evm/fuzz" } foundry-evm-traces = { path = "crates/evm/traces" } foundry-macros = { path = "crates/macros" } foundry-test-utils = { path = "crates/test-utils" } +era_cheatcodes = { path = "crates/era-cheatcodes" } foundry-block-explorers = { version = "0.1.2", default-features = false } @@ -133,7 +135,6 @@ foundry-block-explorers = { version = "0.1.2", default-features = false } # no default features to avoid c-kzg revm = { version = "3", default-features = false } revm-primitives = { version = "1", default-features = false } -era_revm = { git="https://github.com/matter-labs/era-revm", rev = "2662bb19427a421662a7e463cf1ed6a15da361e5" } ## ethers ethers = { version = "2.0", default-features = false } @@ -157,6 +158,15 @@ alloy-chains = "0.1.4" 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 = "21b48af90a9f9d98ec38cb93a32c119a3266f401" } +zksync_basic_types = { git = "https://github.com/matter-labs/zksync-era.git", rev = "bd268ac02bc3530c1d3247cb9496c3e13c2e52d9" } +zksync_types = { git = "https://github.com/matter-labs/zksync-era.git", rev = "bd268ac02bc3530c1d3247cb9496c3e13c2e52d9" } +zksync_state = { git = "https://github.com/matter-labs/zksync-era.git", rev = "bd268ac02bc3530c1d3247cb9496c3e13c2e52d9" } +multivm = { git = "https://github.com/matter-labs/zksync-era.git", rev = "bd268ac02bc3530c1d3247cb9496c3e13c2e52d9" } +zksync_web3_decl = { git = "https://github.com/matter-labs/zksync-era.git", rev = "bd268ac02bc3530c1d3247cb9496c3e13c2e52d9" } +zksync_utils = { git = "https://github.com/matter-labs/zksync-era.git", rev = "bd268ac02bc3530c1d3247cb9496c3e13c2e52d9" } + ## misc chrono = { version = "0.4", default-features = false, features = ["clock", "std"] } color-eyre = "0.6" diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 87dea878d..ca7551c72 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -20,7 +20,10 @@ ethers-providers = { workspace = true, features = ["ws", "ipc"] } # zksync zksync-web3-rs = {git = "https://github.com/lambdaclass/zksync-web3-rs.git", rev = "70327ae5413c517bd4d27502507cdd96ee40cd22"} -era_revm = { workspace = true } +zksync_basic_types.workspace = true +zksync_utils.workspace = true +revm.workspace = true + anyhow = {version = "1.0.70"} dirs = {version = "5.0.0"} ansi_term = "0.12.1" diff --git a/crates/common/src/zk_compile.rs b/crates/common/src/zk_compile.rs index c6f35df40..cf70c0d80 100644 --- a/crates/common/src/zk_compile.rs +++ b/crates/common/src/zk_compile.rs @@ -487,7 +487,7 @@ impl ZkSolc { .collect(); let packed_bytecode = Bytes::from( - era_revm::factory_deps::PackedEraBytecode::new( + foundry_common::zk_utils::factory_deps::PackedEraBytecode::new( contract.hash.as_ref().unwrap().clone(), contract.evm.bytecode.as_ref().unwrap().object.clone(), factory_deps, diff --git a/crates/common/src/zk_utils/conversion_utils.rs b/crates/common/src/zk_utils/conversion_utils.rs new file mode 100644 index 000000000..80fa69814 --- /dev/null +++ b/crates/common/src/zk_utils/conversion_utils.rs @@ -0,0 +1,101 @@ +/// Conversion between REVM units and zkSync units. +use revm::primitives::U256 as revmU256; +use revm::primitives::{Address, B256}; + +use zksync_basic_types::{H160, H256, U256}; +use zksync_utils::h256_to_u256; + +/// Convert address to h160 +pub fn address_to_h160(i: Address) -> H160 { + H160::from(i.0 .0) +} + +/// Convert h160 to address +pub fn h160_to_address(i: H160) -> Address { + i.as_fixed_bytes().into() +} + +/// Convert u256 to b256 +pub fn u256_to_b256(i: U256) -> B256 { + let mut payload: [u8; 32] = [0; 32]; + i.to_big_endian(&mut payload); + B256::from_slice(&payload) +} + +/// Convert u256 to revm u256 +pub fn u256_to_revm_u256(i: U256) -> revmU256 { + let mut payload: [u8; 32] = [0; 32]; + i.to_big_endian(&mut payload); + revmU256::from_be_bytes(payload) +} + +/// Convert revm u256 to u256 +pub fn revm_u256_to_u256(i: revmU256) -> U256 { + U256::from_big_endian(&i.to_be_bytes::<32>()) +} + +/// Convert revm u256 to h256 +pub fn revm_u256_to_h256(i: revmU256) -> H256 { + i.to_be_bytes::<32>().into() +} + +/// Convert h256 to revm u256 +pub fn h256_to_revm_u256(i: H256) -> revmU256 { + u256_to_revm_u256(h256_to_u256(i)) +} + +/// Convert h256 to b256 +pub fn h256_to_b256(i: H256) -> B256 { + i.to_fixed_bytes().into() +} + +/// Convert h256 to h160 +pub fn h256_to_h160(i: &H256) -> H160 { + H160::from_slice(&i.0[12..32]) +} + +#[cfg(test)] +mod test { + use std::str::FromStr; + + use zksync_utils::u256_to_h256; + + use super::*; + + #[test] + fn test_160_conversion() { + let b = Address::from_str("0x000000000000000000000000000000000000800b").unwrap(); + let h = address_to_h160(b); + assert_eq!(h.to_string(), "0x0000…800b"); + let b2 = h160_to_address(h); + assert_eq!(b, b2); + } + + #[test] + fn test_256_conversion() { + let h = + H256::from_str("0xb99acb716b354b9be88d3eaba99ad36792ccdd4349404cbb812adf0b0b14d601") + .unwrap(); + let b = h256_to_b256(h); + assert_eq!( + b.to_string(), + "0xb99acb716b354b9be88d3eaba99ad36792ccdd4349404cbb812adf0b0b14d601" + ); + let u = h256_to_u256(h); + assert_eq!( + u.to_string(), + "83951375548152864551218308881540843734370423742152710934930688330188941743617" + ); + + let revm_u = u256_to_revm_u256(u); + assert_eq!( + revm_u.to_string(), + "83951375548152864551218308881540843734370423742152710934930688330188941743617" + ); + assert_eq!(u, revm_u256_to_u256(revm_u)); + + assert_eq!(h, revm_u256_to_h256(revm_u)); + + assert_eq!(h, u256_to_h256(u)); + } +} diff --git a/crates/common/src/zk_utils/factory_deps.rs b/crates/common/src/zk_utils/factory_deps.rs new file mode 100644 index 000000000..857143742 --- /dev/null +++ b/crates/common/src/zk_utils/factory_deps.rs @@ -0,0 +1,75 @@ +use std::str::FromStr; + +use zksync_basic_types::H256; +use zksync_utils::bytecode::hash_bytecode; + +/// Factory deps packer. +/// +/// EVM assumes that all the necessary bytecodes (factory deps) are present within the original +/// bytecode. In case of Era - they are actually returned separate from the compiler. +/// +/// So in order to fit to the REVM / Forge - we "serialize" all the factory deps into +/// one huge "fake" bytecode string - and then pass them around. + +/// Struct with the contract bytecode, and all the other factory deps. +#[derive(serde::Serialize, serde::Deserialize, Debug)] +pub struct PackedEraBytecode { + hash: String, + bytecode: String, + factory_deps: Vec, +} + +impl PackedEraBytecode { + /// Create a new instance of the `PackedEraBytecode`. + pub fn new(hash: String, bytecode: String, factory_deps: Vec) -> Self { + Self { hash, bytecode, factory_deps } + } + + /// Convert the `PackedEraBytecode` into a `Vec`. + pub fn to_vec(&self) -> Vec { + serde_json::to_vec(self).unwrap() + } + + /// Convert a `Vec` into a `PackedEraBytecode`. + pub fn from_vec(input: &[u8]) -> Self { + serde_json::from_slice(input).unwrap() + } + + /// Convert the `PackedEraBytecode` into a `Vec`. + pub fn bytecode(&self) -> Vec { + hex::decode(self.bytecode.clone()).unwrap() + } + + /// Get the bytecode hash. + pub fn bytecode_hash(&self) -> H256 { + let h = hash_bytecode(&self.bytecode()); + assert_eq!(h, H256::from_str(&self.hash).unwrap()); + h + } + + /// Get the factory deps. + pub fn factory_deps(&self) -> Vec> { + self.factory_deps + .iter() + .chain([&self.bytecode]) + .map(|entry| hex::decode(entry).unwrap()) + .collect() + } +} + +fn ensure_chunkable(bytes: &[u8]) { + assert!(bytes.len() % 32 == 0, "Bytes must be divisible by 32 to split into chunks"); +} + +/// Convert bytes into 32 bytes chunks. +pub fn bytes_to_chunks(bytes: &[u8]) -> Vec<[u8; 32]> { + ensure_chunkable(bytes); + bytes + .chunks(32) + .map(|el| { + let mut chunk = [0u8; 32]; + chunk.copy_from_slice(el); + chunk + }) + .collect() +} diff --git a/crates/common/src/zk_utils.rs b/crates/common/src/zk_utils/mod.rs similarity index 97% rename from crates/common/src/zk_utils.rs rename to crates/common/src/zk_utils/mod.rs index 5b9664044..5f8f285d4 100644 --- a/crates/common/src/zk_utils.rs +++ b/crates/common/src/zk_utils/mod.rs @@ -32,6 +32,10 @@ use foundry_config::Chain; use std::num::ParseIntError; use url::Url; use zksync_web3_rs::types::H256; +/// Utils for conversion between zksync types and revm types +pub mod conversion_utils; +/// Tools for working with factory deps +pub mod factory_deps; /// Gets the RPC URL for Ethereum. /// /// If the `eth.rpc_url` is `None`, an error is returned. diff --git a/crates/era-cheatcodes/.gitignore b/crates/era-cheatcodes/.gitignore new file mode 100644 index 000000000..c9b090484 --- /dev/null +++ b/crates/era-cheatcodes/.gitignore @@ -0,0 +1,7 @@ +/target +/Cargo.lock +tests/* +!tests/src +!tests/lib +!tests/test.sh +!tests/foundry.toml diff --git a/crates/era-cheatcodes/Cargo.toml b/crates/era-cheatcodes/Cargo.toml new file mode 100644 index 000000000..fd3d4e383 --- /dev/null +++ b/crates/era-cheatcodes/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "era_cheatcodes" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +revm.workspace = true +era_test_node.workspace = true +zksync_basic_types.workspace = true +zksync_types.workspace = true +zksync_state.workspace = true +multivm.workspace = true +zksync_web3_decl.workspace = true +zksync_utils.workspace = true +foundry-evm-core.workspace = true + +ethabi = "18.0.0" +itertools = "0.12.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +hex = "0.4" +eyre = "0.6" +tracing = { version = "0.1.26", features = ["log"] } +ethers = { version = "2.0.4", features = ["rustls"] } +hashbrown = { version = "0.14" } +maplit = "1.0.2" + diff --git a/crates/era-cheatcodes/src/cheatcodes.rs b/crates/era-cheatcodes/src/cheatcodes.rs new file mode 100644 index 000000000..a271f09b1 --- /dev/null +++ b/crates/era-cheatcodes/src/cheatcodes.rs @@ -0,0 +1,530 @@ +use era_test_node::{fork::ForkStorage, utils::bytecode_to_factory_dep}; +use ethers::{abi::AbiDecode, prelude::abigen, utils::to_checksum}; +use foundry_evm_core::{backend::DatabaseExt, era_revm::db::RevmDatabaseForEra}; +use itertools::Itertools; +use multivm::{ + interface::{dyn_tracers::vm_1_3_3::DynTracer, tracer::TracerExecutionStatus}, + vm_refunds_enhancement::{BootloaderState, HistoryMode, SimpleMemory, VmTracer, ZkSyncVmState}, + zk_evm_1_3_3::{ + tracing::{BeforeExecutionData, VmLocalStateData}, + vm_state::PrimitiveValue, + zkevm_opcode_defs::{ + FatPointer, Opcode, CALL_IMPLICIT_CALLDATA_FAT_PTR_REGISTER, + RET_IMPLICIT_RETURNDATA_PARAMS_REGISTER, + }, + }, +}; +use std::{cell::RefMut, collections::HashMap, fmt::Debug}; +use zksync_basic_types::{AccountTreeId, Address, H160, H256, U256}; +use zksync_state::{ReadStorage, StoragePtr, StorageView, WriteStorage}; +use zksync_types::{ + block::{pack_block_info, unpack_block_info}, + get_code_key, get_nonce_key, + utils::{decompose_full_nonce, nonces_to_full_nonce, storage_key_for_eth_balance}, + LogQuery, StorageKey, Timestamp, +}; +use zksync_utils::{h256_to_u256, u256_to_h256}; + +type EraDb = StorageView>>; + +// address(uint160(uint256(keccak256('hevm cheat code')))) +const CHEATCODE_ADDRESS: H160 = H160([ + 113, 9, 112, 158, 207, 169, 26, 128, 98, 111, 243, 152, 157, 104, 246, 127, 91, 29, 209, 45, +]); + +const INTERNAL_CONTRACT_ADDRESSES: [H160; 20] = [ + zksync_types::BOOTLOADER_ADDRESS, + zksync_types::ACCOUNT_CODE_STORAGE_ADDRESS, + zksync_types::NONCE_HOLDER_ADDRESS, + zksync_types::KNOWN_CODES_STORAGE_ADDRESS, + zksync_types::IMMUTABLE_SIMULATOR_STORAGE_ADDRESS, + zksync_types::CONTRACT_DEPLOYER_ADDRESS, + zksync_types::CONTRACT_FORCE_DEPLOYER_ADDRESS, + zksync_types::L1_MESSENGER_ADDRESS, + zksync_types::MSG_VALUE_SIMULATOR_ADDRESS, + zksync_types::KECCAK256_PRECOMPILE_ADDRESS, + zksync_types::L2_ETH_TOKEN_ADDRESS, + zksync_types::SYSTEM_CONTEXT_ADDRESS, + zksync_types::BOOTLOADER_UTILITIES_ADDRESS, + zksync_types::EVENT_WRITER_ADDRESS, + zksync_types::COMPRESSOR_ADDRESS, + zksync_types::COMPLEX_UPGRADER_ADDRESS, + zksync_types::ECRECOVER_PRECOMPILE_ADDRESS, + zksync_types::SHA256_PRECOMPILE_ADDRESS, + zksync_types::MINT_AND_BURN_ADDRESS, + H160::zero(), +]; + +abigen!( + CheatcodeContract, + r#"[ + function addr(uint256 privateKey) + function deal(address who, uint256 newBalance) + function etch(address who, bytes calldata code) + function getNonce(address account) + function load(address account, bytes32 slot) + function roll(uint256 blockNumber) + function serializeAddress(string objectKey, string valueKey, address value) + function serializeBool(string objectKey, string valueKey, bool value) + function serializeUint(string objectKey, string valueKey, uint256 value) + function setNonce(address account, uint64 nonce) + function store(address account, bytes32 slot, bytes32 value) + function startPrank(address sender) + function startPrank(address sender, address origin) + function stopPrank() + function toString(address value) + function toString(bool value) + function toString(uint256 value) + function toString(int256 value) + function toString(bytes32 value) + function toString(bytes value) + function warp(uint256 timestamp) + ]"# +); + +#[derive(Debug, Default, Clone)] +pub struct CheatcodeTracer { + one_time_actions: Vec, + permanent_actions: FinishCyclePermanentActions, + return_data: Option>, + return_ptr: Option, + near_calls: usize, + serialized_objects: HashMap, +} + +#[derive(Debug, Clone)] +enum FinishCycleOneTimeActions { + StorageWrite { key: StorageKey, read_value: H256, write_value: H256 }, + StoreFactoryDep { hash: U256, bytecode: Vec }, +} + +#[derive(Debug, Default, Clone)] +struct FinishCyclePermanentActions { + start_prank: Option, +} + +#[derive(Debug, Clone)] +struct StartPrankOpts { + sender: H160, + origin: Option, +} + +impl DynTracer, SimpleMemory> + for CheatcodeTracer +{ + fn before_execution( + &mut self, + state: VmLocalStateData<'_>, + data: BeforeExecutionData, + memory: &SimpleMemory, + storage: StoragePtr>, + ) { + if let Opcode::NearCall(_call) = data.opcode.variant.opcode { + let current = state.vm_local_state.callstack.current; + if current.this_address != CHEATCODE_ADDRESS { + return + } + if current.code_page.0 == 0 || current.ergs_remaining == 0 { + tracing::error!("cheatcode triggered, but no calldata or ergs available"); + return + } + tracing::info!("near call: cheatcode triggered"); + let calldata = { + let ptr = state.vm_local_state.registers + [CALL_IMPLICIT_CALLDATA_FAT_PTR_REGISTER as usize]; + assert!(ptr.is_pointer); + let fat_data_pointer = FatPointer::from_u256(ptr.value); + memory.read_unaligned_bytes( + fat_data_pointer.memory_page as usize, + fat_data_pointer.start as usize, + fat_data_pointer.length as usize, + ) + }; + + // try to dispatch the cheatcode + if let Ok(call) = CheatcodeContractCalls::decode(calldata.clone()) { + self.dispatch_cheatcode(state, data, memory, storage, call) + } else { + tracing::error!( + "Failed to decode cheatcode calldata (near call): {}", + hex::encode(calldata), + ); + } + } + } + + fn after_execution( + &mut self, + state: VmLocalStateData<'_>, + data: multivm::zk_evm_1_3_3::tracing::AfterExecutionData, + _memory: &SimpleMemory, + _storage: StoragePtr>, + ) { + if self.return_data.is_some() { + if let Opcode::Ret(_call) = data.opcode.variant.opcode { + if self.near_calls == 0 { + let ptr = state.vm_local_state.registers + [RET_IMPLICIT_RETURNDATA_PARAMS_REGISTER as usize]; + let fat_data_pointer = FatPointer::from_u256(ptr.value); + self.return_ptr = Some(fat_data_pointer); + } else { + self.near_calls = self.near_calls.saturating_sub(1); + } + } + } + + if let Opcode::NearCall(_call) = data.opcode.variant.opcode { + if self.return_data.is_some() { + self.near_calls += 1; + } + } + } +} + +impl VmTracer, H> for CheatcodeTracer { + fn finish_cycle( + &mut self, + state: &mut ZkSyncVmState, H>, + _bootloader_state: &mut BootloaderState, + ) -> TracerExecutionStatus { + while let Some(action) = self.one_time_actions.pop() { + match action { + FinishCycleOneTimeActions::StorageWrite { key, read_value, write_value } => { + state.storage.write_value(LogQuery { + timestamp: Timestamp(state.local_state.timestamp), + tx_number_in_block: state.local_state.tx_number_in_block, + aux_byte: Default::default(), + shard_id: Default::default(), + address: *key.address(), + key: h256_to_u256(*key.key()), + read_value: h256_to_u256(read_value), + written_value: h256_to_u256(write_value), + rw_flag: true, + rollback: false, + is_service: false, + }); + } + FinishCycleOneTimeActions::StoreFactoryDep { hash, bytecode } => state + .decommittment_processor + .populate(vec![(hash, bytecode)], Timestamp(state.local_state.timestamp)), + } + } + + // Set return data, if any + if let Some(mut fat_pointer) = self.return_ptr.take() { + let timestamp = Timestamp(state.local_state.timestamp); + + let elements = self.return_data.take().unwrap(); + fat_pointer.length = (elements.len() as u32) * 32; + state.local_state.registers[RET_IMPLICIT_RETURNDATA_PARAMS_REGISTER as usize] = + PrimitiveValue { value: fat_pointer.to_u256(), is_pointer: true }; + state.memory.populate_page( + fat_pointer.memory_page as usize, + elements.into_iter().enumerate().collect_vec(), + timestamp, + ); + } + + // Sets the sender address for startPrank cheatcode + if let Some(start_prank_call) = &self.permanent_actions.start_prank { + let this_address = state.local_state.callstack.current.this_address; + if !INTERNAL_CONTRACT_ADDRESSES.contains(&this_address) { + state.local_state.callstack.current.msg_sender = start_prank_call.sender; + } + } + + TracerExecutionStatus::Continue + } +} + +impl CheatcodeTracer { + pub fn new() -> Self { + CheatcodeTracer { + one_time_actions: vec![], + permanent_actions: FinishCyclePermanentActions { start_prank: None }, + near_calls: 0, + return_data: None, + return_ptr: None, + serialized_objects: HashMap::new(), + } + } + + pub fn dispatch_cheatcode( + &mut self, + _state: VmLocalStateData<'_>, + _data: BeforeExecutionData, + _memory: &SimpleMemory, + storage: StoragePtr>, + call: CheatcodeContractCalls, + ) { + use CheatcodeContractCalls::*; + match call { + Addr(AddrCall { private_key }) => { + tracing::info!("👷 Getting address for private key"); + let Ok(address) = zksync_types::PackedEthSignature::address_from_private_key( + &u256_to_h256(private_key), + ) else { + tracing::error!("Failed generating address for private key"); + return + }; + self.return_data = Some(vec![h256_to_u256(address.into())]); + } + Deal(DealCall { who, new_balance }) => { + tracing::info!("👷 Setting balance for {who:?} to {new_balance}"); + self.write_storage( + storage_key_for_eth_balance(&who), + u256_to_h256(new_balance), + &mut storage.borrow_mut(), + ); + } + Etch(EtchCall { who, code }) => { + tracing::info!("👷 Setting address code for {who:?}"); + let code_key = get_code_key(&who); + let (hash, code) = bytecode_to_factory_dep(code.0.into()); + self.store_factory_dep(hash, code); + self.write_storage(code_key, u256_to_h256(hash), &mut storage.borrow_mut()); + } + GetNonce(GetNonceCall { account }) => { + tracing::info!("👷 Getting nonce for {account:?}"); + let mut storage = storage.borrow_mut(); + let nonce_key = get_nonce_key(&account); + let full_nonce = storage.read_value(&nonce_key); + let (account_nonce, _) = decompose_full_nonce(h256_to_u256(full_nonce)); + tracing::info!( + "👷 Nonces for account {:?} are {}", + account, + account_nonce.as_u64() + ); + tracing::info!("👷 Setting returndata",); + tracing::info!("👷 Returndata is {:?}", account_nonce); + self.return_data = Some(vec![account_nonce]); + } + Load(LoadCall { account, slot }) => { + tracing::info!("👷 Getting storage slot {:?} for account {:?}", slot, account); + let key = StorageKey::new(AccountTreeId::new(account), H256(slot)); + let mut storage = storage.borrow_mut(); + let value = storage.read_value(&key); + self.return_data = Some(vec![h256_to_u256(value)]); + } + Roll(RollCall { block_number }) => { + tracing::info!("👷 Setting block number to {}", block_number); + + let key = StorageKey::new( + AccountTreeId::new(zksync_types::SYSTEM_CONTEXT_ADDRESS), + zksync_types::CURRENT_VIRTUAL_BLOCK_INFO_POSITION, + ); + let mut storage = storage.borrow_mut(); + let (_, block_timestamp) = + unpack_block_info(h256_to_u256(storage.read_value(&key))); + self.write_storage( + key, + u256_to_h256(pack_block_info(block_number.as_u64(), block_timestamp)), + &mut storage, + ); + } + SerializeAddress(SerializeAddressCall { object_key, value_key, value }) => { + tracing::info!( + "👷 Serializing address {:?} with key {:?} to object {:?}", + value, + value_key, + object_key + ); + let json_value = serde_json::json!({ + value_key: value + }); + + //write to serialized_objects + self.serialized_objects.insert(object_key.clone(), json_value.to_string()); + + let address = Address::from(value); + let address_with_checksum = to_checksum(&address, None); + self.add_trimmed_return_data(address_with_checksum.as_bytes()); + } + SerializeBool(SerializeBoolCall { object_key, value_key, value }) => { + tracing::info!( + "👷 Serializing bool {:?} with key {:?} to object {:?}", + value, + value_key, + object_key + ); + let json_value = serde_json::json!({ + value_key: value + }); + + self.serialized_objects.insert(object_key.clone(), json_value.to_string()); + + let bool_value = value.to_string(); + self.add_trimmed_return_data(bool_value.as_bytes()); + } + SerializeUint(SerializeUintCall { object_key, value_key, value }) => { + tracing::info!( + "👷 Serializing uint256 {:?} with key {:?} to object {:?}", + value, + value_key, + object_key + ); + let json_value = serde_json::json!({ + value_key: value + }); + + self.serialized_objects.insert(object_key.clone(), json_value.to_string()); + + let uint_value = value.to_string(); + self.add_trimmed_return_data(uint_value.as_bytes()); + } + SetNonce(SetNonceCall { account, nonce }) => { + tracing::info!("👷 Setting nonce for {account:?} to {nonce}"); + let mut storage = storage.borrow_mut(); + let nonce_key = get_nonce_key(&account); + let full_nonce = storage.read_value(&nonce_key); + let (mut account_nonce, mut deployment_nonce) = + decompose_full_nonce(h256_to_u256(full_nonce)); + if account_nonce.as_u64() >= nonce { + tracing::error!( + "SetNonce cheatcode failed: Account nonce is already set to a higher value ({}, requested {})", + account_nonce, + nonce + ); + return + } + account_nonce = nonce.into(); + if deployment_nonce.as_u64() >= nonce { + tracing::error!( + "SetNonce cheatcode failed: Deployment nonce is already set to a higher value ({}, requested {})", + deployment_nonce, + nonce + ); + return + } + deployment_nonce = nonce.into(); + let enforced_full_nonce = nonces_to_full_nonce(account_nonce, deployment_nonce); + tracing::info!("👷 Nonces for account {:?} have been set to {}", account, nonce); + self.write_storage(nonce_key, u256_to_h256(enforced_full_nonce), &mut storage); + } + StartPrank(StartPrankCall { sender }) => { + tracing::info!("👷 Starting prank to {sender:?}"); + self.permanent_actions.start_prank = Some(StartPrankOpts { sender, origin: None }); + } + StartPrankWithOrigin(StartPrankWithOriginCall { sender, origin }) => { + tracing::info!("👷 Starting prank to {sender:?} with origin {origin:?}"); + let key = StorageKey::new( + AccountTreeId::new(zksync_types::SYSTEM_CONTEXT_ADDRESS), + zksync_types::SYSTEM_CONTEXT_TX_ORIGIN_POSITION, + ); + let original_tx_origin = storage.borrow_mut().read_value(&key); + self.write_storage(key, origin.into(), &mut storage.borrow_mut()); + + self.permanent_actions.start_prank = + Some(StartPrankOpts { sender, origin: Some(original_tx_origin) }); + } + StopPrank(StopPrankCall) => { + tracing::info!("👷 Stopping prank"); + + if let Some(origin) = + self.permanent_actions.start_prank.as_ref().and_then(|v| v.origin) + { + let key = StorageKey::new( + AccountTreeId::new(zksync_types::SYSTEM_CONTEXT_ADDRESS), + zksync_types::SYSTEM_CONTEXT_TX_ORIGIN_POSITION, + ); + self.write_storage(key, origin, &mut storage.borrow_mut()); + } + + self.permanent_actions.start_prank = None; + } + Store(StoreCall { account, slot, value }) => { + tracing::info!( + "👷 Setting storage slot {:?} for account {:?} to {:?}", + slot, + account, + value + ); + let mut storage = storage.borrow_mut(); + let key = StorageKey::new(AccountTreeId::new(account), H256(slot)); + self.write_storage(key, H256(value), &mut storage); + } + ToString0(ToString0Call { value }) => { + tracing::info!("Converting address into string"); + let address = Address::from(value); + let address_with_checksum = to_checksum(&address, None); + self.add_trimmed_return_data(address_with_checksum.as_bytes()); + } + ToString1(ToString1Call { value }) => { + tracing::info!("Converting bool into string"); + let bool_value = value.to_string(); + self.add_trimmed_return_data(bool_value.as_bytes()); + } + ToString2(ToString2Call { value }) => { + tracing::info!("Converting uint256 into string"); + let uint_value = value.to_string(); + self.add_trimmed_return_data(uint_value.as_bytes()); + } + ToString3(ToString3Call { value }) => { + tracing::info!("Converting int256 into string"); + let int_value = value.to_string(); + self.add_trimmed_return_data(int_value.as_bytes()); + } + ToString4(ToString4Call { value }) => { + tracing::info!("Converting bytes32 into string"); + let bytes_value = format!("0x{}", hex::encode(value)); + self.add_trimmed_return_data(bytes_value.as_bytes()); + } + ToString5(ToString5Call { value }) => { + tracing::info!("Converting bytes into string"); + let bytes_value = format!("0x{}", hex::encode(value)); + self.add_trimmed_return_data(bytes_value.as_bytes()); + } + Warp(WarpCall { timestamp }) => { + tracing::info!("👷 Setting block timestamp {}", timestamp); + + let key = StorageKey::new( + AccountTreeId::new(zksync_types::SYSTEM_CONTEXT_ADDRESS), + zksync_types::CURRENT_VIRTUAL_BLOCK_INFO_POSITION, + ); + let mut storage = storage.borrow_mut(); + let (block_number, _) = unpack_block_info(h256_to_u256(storage.read_value(&key))); + self.write_storage( + key, + u256_to_h256(pack_block_info(block_number, timestamp.as_u64())), + &mut storage, + ); + } + }; + } + + fn store_factory_dep(&mut self, hash: U256, bytecode: Vec) { + self.one_time_actions.push(FinishCycleOneTimeActions::StoreFactoryDep { hash, bytecode }); + } + + fn write_storage( + &mut self, + key: StorageKey, + write_value: H256, + storage: &mut RefMut, + ) { + self.one_time_actions.push(FinishCycleOneTimeActions::StorageWrite { + key, + read_value: storage.read_value(&key), + write_value, + }); + } + + fn add_trimmed_return_data(&mut self, data: &[u8]) { + let data_length = data.len(); + let mut data: Vec = data + .chunks(32) + .map(|b| { + // Copies the bytes into a 32 byte array + // padding with zeros to the right if necessary + let mut bytes = [0u8; 32]; + bytes[..b.len()].copy_from_slice(b); + bytes.into() + }) + .collect_vec(); + + // Add the length of the data to the end of the return data + data.push(data_length.into()); + + self.return_data = Some(data); + } +} diff --git a/crates/era-cheatcodes/src/lib.rs b/crates/era-cheatcodes/src/lib.rs new file mode 100644 index 000000000..b89a2eeb2 --- /dev/null +++ b/crates/era-cheatcodes/src/lib.rs @@ -0,0 +1 @@ +pub mod cheatcodes; diff --git a/crates/era-cheatcodes/tests/foundry.toml b/crates/era-cheatcodes/tests/foundry.toml new file mode 100644 index 000000000..ea122abec --- /dev/null +++ b/crates/era-cheatcodes/tests/foundry.toml @@ -0,0 +1,9 @@ +[profile.default] +src = 'src' +out = 'out' +libs = ['lib'] + +[rpc_endpoints] +local = "${ERA_TEST_NODE_RPC_URL}" + +# See more config options https://github.com/foundry-rs/foundry/tree/master/config \ No newline at end of file diff --git a/crates/era-cheatcodes/tests/src/cheatcodes/Addr.t.sol b/crates/era-cheatcodes/tests/src/cheatcodes/Addr.t.sol new file mode 100644 index 000000000..e6c0418d7 --- /dev/null +++ b/crates/era-cheatcodes/tests/src/cheatcodes/Addr.t.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test, console2 as console} from "../../lib/forge-std/src/Test.sol"; +import {Constants} from "./Constants.sol"; + +contract AddrTest is Test { + function testAddr() public { + uint256 pk = 77814517325470205911140941194401928579557062014761831930645393041380819009408; + address expected = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266; + + (bool success, bytes memory data) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature("addr(uint256)", pk) + ); + require(success, "addr failed"); + address addr = abi.decode(data, (address)); + assertEq(addr, expected, "expected address did not match"); + console.log("failed?", failed()); + } +} \ No newline at end of file diff --git a/crates/era-cheatcodes/tests/src/cheatcodes/Constants.sol b/crates/era-cheatcodes/tests/src/cheatcodes/Constants.sol new file mode 100644 index 000000000..e56f43961 --- /dev/null +++ b/crates/era-cheatcodes/tests/src/cheatcodes/Constants.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +library Constants { + address constant CHEATCODE_ADDRESS = + 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D; +} diff --git a/crates/era-cheatcodes/tests/src/cheatcodes/Deal.t.sol b/crates/era-cheatcodes/tests/src/cheatcodes/Deal.t.sol new file mode 100644 index 000000000..0b83e3218 --- /dev/null +++ b/crates/era-cheatcodes/tests/src/cheatcodes/Deal.t.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test, console2 as console} from "../../lib/forge-std/src/Test.sol"; +import {Constants} from "./Constants.sol"; + +contract CheatcodeDealTest is Test { + address constant TEST_ADDRESS = 0x6Eb28604685b1F182dAB800A1Bfa4BaFdBA8a79a; + uint256 constant NEW_BALANCE = 10; + + function testDeal() public { + uint256 balanceBefore = address(TEST_ADDRESS).balance; + console.log("balance before:", balanceBefore); + + (bool success, ) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature( + "deal(address,uint256)", + TEST_ADDRESS, + NEW_BALANCE + ) + ); + uint256 balanceAfter = address(TEST_ADDRESS).balance; + console.log("balance after :", balanceAfter); + + require(balanceAfter == NEW_BALANCE, "balance mismatch"); + require(balanceAfter != balanceBefore, "balance unchanged"); + require(success, "deal failed"); + console.log("failed?", failed()); + } +} diff --git a/crates/era-cheatcodes/tests/src/cheatcodes/Etch.t.sol b/crates/era-cheatcodes/tests/src/cheatcodes/Etch.t.sol new file mode 100644 index 000000000..199e2118a --- /dev/null +++ b/crates/era-cheatcodes/tests/src/cheatcodes/Etch.t.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test, console2 as console} from "../../lib/forge-std/src/Test.sol"; +import {Constants} from "./Constants.sol"; + +contract CheatcodeEtchTest is Test { + address constant TEST_ADDRESS = 0x6Eb28604685b1F182dAB800A1Bfa4BaFdBA8a79a; + bytes constant GREETER_CODE = + hex"00050000000000020000000003010019000000600330027000000060033001970000000102200190000000950000c13d000000040230008c000000e00000413d000000000201043b0000006702200197000000680220009c000000e00000c13d0000000002000416000000000202004b000000e00000c13d000000040230008a000000200220008c000000e00000413d0000000402100370000000000502043b000000640250009c000000e00000213d00000023025000390000006904000041000000000632004b000000000600001900000000060480190000006902200197000000000702004b0000000004008019000000690220009c00000000020600190000000002046019000000000202004b000000e00000c13d0000000406500039000000000261034f000000000402043b000000640240009c0000009d0000213d0000001f07400039000000200200008a000000000727016f000000bf07700039000000000727016f0000006a087000410000006b0880009c0000009d0000413d000000400070043f000000800040043f00000000054500190000002405500039000000000335004b000000e00000213d0000002003600039000000000131034f0000001f0340018f0000000505400272000000440000613d00000000060000190000000507600210000000000871034f000000000808043b000000a00770003900000000008704350000000106600039000000000756004b0000003c0000413d000000000603004b000000530000613d0000000505500210000000000151034f0000000303300210000000a005500039000000000605043300000000063601cf000000000636022f000000000101043b0000010003300089000000000131022f00000000013101cf000000000161019f0000000000150435000000a0014000390000000000010435000000800100043d000000640310009c0000009d0000213d000000000400041a000000010340019000000001034002700000007f0330618f0000001f0530008c00000000050000190000000105002039000000000454013f0000000104400190000000fc0000c13d000000200430008c000000740000413d0000001f0410003900000005044002700000006c044000410000006c05000041000000200610008c000000000405401900000000000004350000001f0330003900000005033002700000006c03300041000000000534004b000000740000813d000000000004041b0000000104400039000000000534004b000000700000413d0000001f0310008c000001000000a13d0000000003210170000000a0040000390000006c020000410000000000000435000000880000613d0000006c0200004100000020060000390000000004000019000000000506001900000080065000390000000006060433000000000062041b000000200650003900000001022000390000002004400039000000000734004b0000007e0000413d000000a004500039000000000313004b000000920000813d0000000303100210000000f80330018f000000010500008a000000000335022f000000000353013f0000000004040433000000000334016f000000000032041b000000010200003900000001031002100000010a0000013d0000008002000039000000400020043f0000000002000416000000000202004b000000e00000c13d0000006102300041000000620220009c000000a30000213d0000006d0100004100000000001004350000004101000039000000040010043f0000006e010000410000017b000104300000009f023000390000006302200197000000400020043f0000001f0230018f0000000504300272000000b20000613d00000000050000190000000506500210000000000761034f000000000707043b000000800660003900000000007604350000000105500039000000000645004b000000aa0000413d000000000502004b000000c10000613d0000000504400210000000000141034f00000003022002100000008004400039000000000504043300000000052501cf000000000525022f000000000101043b0000010002200089000000000121022f00000000012101cf000000000151019f0000000000140435000000200130008c000000e00000413d000000800400043d000000640140009c000000e00000213d00000080033000390000009f01400039000000000131004b000000e00000813d00000080024000390000000001020433000000640510009c0000009d0000213d0000003f05100039000000200900008a000000000595016f000000400800043d0000000005580019000000000685004b00000000060000190000000106004039000000640750009c0000009d0000213d00000001066001900000009d0000c13d000000400050043f00000000061804360000000004140019000000a004400039000000000334004b000000e20000a13d00000000010000190000017b00010430000000000301004b000000ec0000613d000000000300001900000000046300190000002003300039000000000523001900000000050504330000000000540435000000000413004b000000e50000413d000000000116001900000000000104350000000004080433000000640140009c0000009d0000213d000000000100041a000000010210019000000001011002700000007f0310018f000000000301c0190000001f0130008c00000000010000190000000101002039000000010110018f000000000112004b0000010e0000613d0000006d0100004100000000001004350000002201000039000000a00000013d000000000201004b0000000002000019000001040000613d000000a00200043d0000000303100210000000010400008a000000000334022f000000000343013f000000000332016f0000000102100210000000000123019f000000000010041b00000000010000190000017a0001042e000000200130008c000001340000413d000100000003001d000300000004001d000000000000043500000060010000410000000002000414000000600320009c0000000001024019000000c00110021000000065011001c70000801002000039000500000008001d000400000009001d000200000006001d017901740000040f0000000206000029000000040900002900000005080000290000000102200190000000e00000613d00000003040000290000001f024000390000000502200270000000200340008c0000000002004019000000000301043b00000001010000290000001f01100039000000050110027000000000011300190000000002230019000000000312004b000001340000813d000000000002041b0000000102200039000000000312004b000001300000413d0000001f0140008c000001630000a13d000300000004001d000000000000043500000060010000410000000002000414000000600320009c0000000001024019000000c00110021000000065011001c70000801002000039000500000008001d000400000009001d017901740000040f000000040300002900000005060000290000000102200190000000e00000613d000000030700002900000000033701700000002002000039000000000101043b000001550000613d0000002002000039000000000400001900000000056200190000000005050433000000000051041b000000200220003900000001011000390000002004400039000000000534004b0000014d0000413d000000000373004b000001600000813d0000000303700210000000f80330018f000000010400008a000000000334022f000000000343013f00000000026200190000000002020433000000000232016f000000000021041b000000010100003900000001027002100000016d0000013d000000000104004b0000000001000019000001670000613d00000000010604330000000302400210000000010300008a000000000223022f000000000232013f000000000221016f0000000101400210000000000112019f000000000010041b00000020010000390000010000100443000001200000044300000066010000410000017a0001042e00000177002104230000000102000039000000000001042d0000000002000019000000000001042d00000179000004320000017a0001042e0000017b00010430000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff000000000000009fffffffffffffffffffffffffffffffffffffffffffffffff000000000000007f00000000000000000000000000000000000000000000000000000001ffffffe0000000000000000000000000000000000000000000000000ffffffffffffffff02000000000000000000000000000000000000200000000000000000000000000000000200000000000000000000000000000040000001000000000000000000ffffffff00000000000000000000000000000000000000000000000000000000a4136862000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000ffffffffffffffffffffffffffffffffffffffffffffffff0000000000000080290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e5634e487b71000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051358bfd296e885430dddecb908ce82d20e6832374027da7514aebac3689d51f"; + + function testEtch() public { + (bool success, ) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature( + "etch(address,bytes)", + TEST_ADDRESS, + GREETER_CODE + ) + ); + require(success, "etch failed"); + + (success, ) = TEST_ADDRESS.call( + abi.encodeWithSignature("setGreeting(string)", "hello world") + ); + require(success, "setGreeting failed"); + console.log("failed?", failed()); + } +} diff --git a/crates/era-cheatcodes/tests/src/cheatcodes/GetNonce.t.sol b/crates/era-cheatcodes/tests/src/cheatcodes/GetNonce.t.sol new file mode 100644 index 000000000..12c0f9f10 --- /dev/null +++ b/crates/era-cheatcodes/tests/src/cheatcodes/GetNonce.t.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test, console2 as console} from "../../lib/forge-std/src/Test.sol"; +import {Constants} from "./Constants.sol"; + +contract CheatcodeSetNonceTest is Test { + address constant TEST_ADDRESS = 0x6Eb28604685b1F182dAB800A1Bfa4BaFdBA8a79a; + uint256 constant NEW_NONCE = uint256(123456); + + function testSetNonce() public { + (bool success, ) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature( + "setNonce(address,uint64)", + TEST_ADDRESS, + NEW_NONCE + ) + ); + require(success, "setNonce failed"); + console.log("failed?", failed()); + + //test getNonce + (bool success2, bytes memory data2) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature("getNonce(address)", TEST_ADDRESS) + ); + require(success2, "getNonce failed"); + uint256 nonce = abi.decode(data2, (uint256)); + console.log("nonce: 0x", nonce); + require(nonce == NEW_NONCE, "nonce was not changed"); + console.log("failed?", failed()); + } +} + + diff --git a/crates/era-cheatcodes/tests/src/cheatcodes/Load.t.sol b/crates/era-cheatcodes/tests/src/cheatcodes/Load.t.sol new file mode 100644 index 000000000..a99a123d9 --- /dev/null +++ b/crates/era-cheatcodes/tests/src/cheatcodes/Load.t.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test, console2 as console} from "../../lib/forge-std/src/Test.sol"; +import {Constants} from "./Constants.sol"; + +contract Storage { + uint256 slot0 = 10; +} + +contract LoadTest is Test { + address constant TEST_ADDRESS = 0x6Eb28604685b1F182dAB800A1Bfa4BaFdBA8a79a; + uint256 slot0 = 20; + Storage store; + + function setUp() public { + store = new Storage(); + } + + function testLoadOwnStorage() public { + uint256 slot; + assembly { + slot := slot0.slot + } + (bool success, bytes memory data) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature( + "load(address,bytes32)", + address(this), + bytes32(slot) + ) + ); + require(success, "load failed"); + uint256 val = abi.decode(data, (uint256)); + assertEq(val, 20, "load failed"); + } + + function testLoadOtherStorage() public { + (bool success, bytes memory data) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature( + "load(address,bytes32)", + address(store), + bytes32(0) + ) + ); + require(success, "load failed"); + uint256 val = abi.decode(data, (uint256)); + assertEq(val, 10, "load failed"); + } +} diff --git a/crates/era-cheatcodes/tests/src/cheatcodes/Roll.t.sol b/crates/era-cheatcodes/tests/src/cheatcodes/Roll.t.sol new file mode 100644 index 000000000..9a706cc93 --- /dev/null +++ b/crates/era-cheatcodes/tests/src/cheatcodes/Roll.t.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test, console2 as console} from "../../lib/forge-std/src/Test.sol"; +import {Constants} from "./Constants.sol"; + +contract CheatcodeRollTest is Test { + address constant TEST_ADDRESS = 0x6Eb28604685b1F182dAB800A1Bfa4BaFdBA8a79a; + uint256 constant NEW_BLOCK_NUMBER = 10; + + function testRoll() public { + uint256 initialBlockNumber = block.number; + console.log("blockNumber before:", initialBlockNumber); + + require( + NEW_BLOCK_NUMBER != initialBlockNumber, + "block number must be different than current block number" + ); + + (bool success, ) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature("roll(uint256)", NEW_BLOCK_NUMBER) + ); + require(success, "roll failed"); + uint256 finalBlockNumber = block.number; + console.log("blockNumber after :", finalBlockNumber); + + require( + finalBlockNumber == NEW_BLOCK_NUMBER, + "block number was not changed" + ); + console.log("failed?", failed()); + } +} diff --git a/crates/era-cheatcodes/tests/src/cheatcodes/Serialize.t.sol b/crates/era-cheatcodes/tests/src/cheatcodes/Serialize.t.sol new file mode 100644 index 000000000..c2c95b1c8 --- /dev/null +++ b/crates/era-cheatcodes/tests/src/cheatcodes/Serialize.t.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test, console2 as console} from "../../lib/forge-std/src/Test.sol"; +import {Constants} from "./Constants.sol"; + +contract CheatcodeSerializeTest is Test { + address constant TEST_ADDRESS = 0x6Eb28604685b1F182dAB800A1Bfa4BaFdBA8a79a; + + function testSerializeAddress() external { + (bool success, bytes memory rawData) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature( + "serializeAddress(string,string,address)", + "obj1", + "address", + TEST_ADDRESS + ) + ); + require(success, "serializeAddress failed"); + bytes memory data = trimReturnBytes(rawData); + string memory testString = string(abi.encodePacked(data)); + require( + keccak256(bytes(testString)) == + keccak256(bytes("0x6Eb28604685b1F182dAB800A1Bfa4BaFdBA8a79a")), + "serializeAddress mismatch" + ); + console.log("failed?", failed()); + } + + function testSerializeBool() external { + (bool success, bytes memory rawData) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature( + "serializeBool(string,string,bool)", + "obj1", + "boolean", + true + ) + ); + require(success, "serializeBool failed"); + bytes memory data = trimReturnBytes(rawData); + string memory testString = string(abi.encodePacked(data)); + require( + keccak256(bytes(testString)) == keccak256(bytes("true")), + "serializeBool mismatch" + ); + console.log("failed?", failed()); + } + + function testSerializeUint() external { + (bool success, bytes memory rawData) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature( + "serializeUint(string,string,uint256)", + "obj1", + "uint", + 99 + ) + ); + require(success, "serializeUint failed"); + bytes memory data = trimReturnBytes(rawData); + string memory testString = string(abi.encodePacked(data)); + require( + keccak256(bytes(testString)) == keccak256(bytes("99")), + "serializeUint mismatch" + ); + console.log("failed?", failed()); + } + + function trimReturnBytes( + bytes memory rawData + ) internal pure returns (bytes memory) { + uint256 lengthStartingPos = rawData.length - 32; + bytes memory lengthSlice = new bytes(32); + for (uint256 i = 0; i < 32; i++) { + lengthSlice[i] = rawData[lengthStartingPos + i]; + } + uint256 length = abi.decode(lengthSlice, (uint256)); + bytes memory data = new bytes(length); + for (uint256 i = 0; i < length; i++) { + data[i] = rawData[i]; + } + return data; + } +} diff --git a/crates/era-cheatcodes/tests/src/cheatcodes/SetNonce.t.sol b/crates/era-cheatcodes/tests/src/cheatcodes/SetNonce.t.sol new file mode 100644 index 000000000..29a99ee0e --- /dev/null +++ b/crates/era-cheatcodes/tests/src/cheatcodes/SetNonce.t.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test, console2 as console} from "../../lib/forge-std/src/Test.sol"; +import {Constants} from "./Constants.sol"; + +contract CheatcodeSetNonceTest is Test { + address constant TEST_ADDRESS = 0x6Eb28604685b1F182dAB800A1Bfa4BaFdBA8a79a; + uint256 constant NEW_NONCE = uint256(123456); + + function testSetNonce() public { + (bool success, ) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature( + "setNonce(address,uint64)", + TEST_ADDRESS, + NEW_NONCE + ) + ); + require(success, "setNonce failed"); + console.log("failed?", failed()); + } +} diff --git a/crates/era-cheatcodes/tests/src/cheatcodes/StartPrank.t.sol b/crates/era-cheatcodes/tests/src/cheatcodes/StartPrank.t.sol new file mode 100644 index 000000000..3b5a73b82 --- /dev/null +++ b/crates/era-cheatcodes/tests/src/cheatcodes/StartPrank.t.sol @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test, console2 as console} from "../../lib/forge-std/src/Test.sol"; +import {Constants} from "./Constants.sol"; + +contract CheatcodeStartPrankTest is Test { + address constant TEST_ADDRESS = 0x6Eb28604685b1F182dAB800A1Bfa4BaFdBA8a79a; + address constant TEST_ORIGIN = 0xdEBe90b7BFD87Af696B1966082F6515a6E72F3d8; + + function testStartPrank() public { + address original_msg_sender = msg.sender; + address original_tx_origin = tx.origin; + + PrankVictim victim = new PrankVictim(); + + // Verify that the victim is set up correctly + victim.assertCallerAndOrigin( + address(this), + "startPrank failed: victim.assertCallerAndOrigin failed", + original_tx_origin, + "startPrank failed: victim.assertCallerAndOrigin failed" + ); + + // Start prank without tx.origin + (bool success1, ) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature("startPrank(address)", TEST_ADDRESS) + ); + require(success1, "startPrank failed"); + + require( + msg.sender == TEST_ADDRESS, + "startPrank failed: msg.sender unchanged" + ); + require( + tx.origin == original_tx_origin, + "startPrank failed tx.origin changed" + ); + victim.assertCallerAndOrigin( + TEST_ADDRESS, + "startPrank failed: victim.assertCallerAndOrigin failed", + original_tx_origin, + "startPrank failed: victim.assertCallerAndOrigin failed" + ); + + // Stop prank + (bool success2, ) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature("stopPrank()") + ); + require(success2, "stopPrank failed"); + + require( + msg.sender == original_msg_sender, + "stopPrank failed: msg.sender didn't return to original" + ); + require( + tx.origin == original_tx_origin, + "stopPrank failed tx.origin changed" + ); + victim.assertCallerAndOrigin( + address(this), + "startPrank failed: victim.assertCallerAndOrigin failed", + original_tx_origin, + "startPrank failed: victim.assertCallerAndOrigin failed" + ); + + console.log("failed?", failed()); + } + + function testStartPrankWithOrigin() external { + address original_msg_sender = msg.sender; + address original_tx_origin = tx.origin; + + PrankVictim victim = new PrankVictim(); + + // Verify that the victim is set up correctly + victim.assertCallerAndOrigin( + address(this), + "startPrank failed: victim.assertCallerAndOrigin failed", + original_tx_origin, + "startPrank failed: victim.assertCallerAndOrigin failed" + ); + + // Start prank with tx.origin + (bool success1, ) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature( + "startPrank(address,address)", + TEST_ADDRESS, + TEST_ORIGIN + ) + ); + require(success1, "startPrank failed"); + + require( + msg.sender == TEST_ADDRESS, + "startPrank failed: msg.sender unchanged" + ); + require( + tx.origin == TEST_ORIGIN, + "startPrank failed: tx.origin unchanged" + ); + victim.assertCallerAndOrigin( + TEST_ADDRESS, + "startPrank failed: victim.assertCallerAndOrigin failed", + TEST_ORIGIN, + "startPrank failed: victim.assertCallerAndOrigin failed" + ); + + // Stop prank + (bool success2, ) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature("stopPrank()") + ); + require(success2, "stopPrank failed"); + + require( + msg.sender == original_msg_sender, + "stopPrank failed: msg.sender didn't return to original" + ); + require( + tx.origin == original_tx_origin, + "stopPrank failed: tx.origin didn't return to original" + ); + victim.assertCallerAndOrigin( + address(this), + "startPrank failed: victim.assertCallerAndOrigin failed", + original_tx_origin, + "startPrank failed: victim.assertCallerAndOrigin failed" + ); + + console.log("failed?", failed()); + } +} + +contract PrankVictim { + function assertCallerAndOrigin( + address expectedSender, + string memory senderMessage, + address expectedOrigin, + string memory originMessage + ) public view { + console.log("msg.sender", msg.sender); + console.log("tx.origin", tx.origin); + require(msg.sender == expectedSender, senderMessage); + require(tx.origin == expectedOrigin, originMessage); + } +} diff --git a/crates/era-cheatcodes/tests/src/cheatcodes/Store.t.sol b/crates/era-cheatcodes/tests/src/cheatcodes/Store.t.sol new file mode 100644 index 000000000..7e22a7e41 --- /dev/null +++ b/crates/era-cheatcodes/tests/src/cheatcodes/Store.t.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.13; + +import {Test, console2 as console} from "../../lib/forge-std/src/Test.sol"; +import {Constants} from "./Constants.sol"; + +contract Storage { + uint256 public slot0 = 10; + uint256 public slot1 = 20; +} + +contract StoreTest is Test { + address constant TEST_ADDRESS = 0x6Eb28604685b1F182dAB800A1Bfa4BaFdBA8a79a; + Storage store; + + function setUp() public { + store = new Storage(); + } + + function testStore() public { + assertEq(store.slot0(), 10, "initial value for slot 0 is incorrect"); + assertEq(store.slot1(), 20, "initial value for slot 1 is incorrect"); + + (bool success, ) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature( + "store(address,bytes32,bytes32)", + address(store), + bytes32(0), + bytes32(uint256(1)) + ) + ); + require(success, "store failed"); + assertEq(store.slot0(), 1, "store failed"); + assertEq(store.slot1(), 20, "store failed"); + console.log("failed?", failed()); + } +} diff --git a/crates/era-cheatcodes/tests/src/cheatcodes/ToString.t.sol b/crates/era-cheatcodes/tests/src/cheatcodes/ToString.t.sol new file mode 100644 index 000000000..ff9305123 --- /dev/null +++ b/crates/era-cheatcodes/tests/src/cheatcodes/ToString.t.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test, console2 as console} from "../../lib/forge-std/src/Test.sol"; +import {Constants} from "./Constants.sol"; + +contract CheatcodeToStringTest is Test { + function testToStringFromAddress() external { + address testAddress = 0x413D15117be7a498e68A64FcfdB22C6e2AaE1808; + (bool success, bytes memory rawData) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature("toString(address)", testAddress) + ); + require(success, "toString failed"); + bytes memory data = trimReturnBytes(rawData); + string memory testString = string(abi.encodePacked(data)); + require( + keccak256(bytes(testString)) == + keccak256(bytes("0x413D15117be7a498e68A64FcfdB22C6e2AaE1808")), + "toString mismatch" + ); + console.log("failed?", failed()); + } + + function testToStringFromBool() external { + (bool success, bytes memory rawData) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature("toString(bool)", false) + ); + require(success, "toString failed"); + bytes memory data = trimReturnBytes(rawData); + string memory testString = string(abi.encodePacked(data)); + require( + keccak256(bytes(testString)) == keccak256(bytes("false")), + "toString mismatch" + ); + + (success, rawData) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature("toString(bool)", true) + ); + require(success, "toString failed"); + data = trimReturnBytes(rawData); + testString = string(abi.encodePacked(data)); + require( + keccak256(bytes(testString)) == keccak256(bytes("true")), + "toString mismatch" + ); + console.log("failed?", failed()); + } + + function testToStringFromUint256() external { + uint256 value = 99; + string memory stringValue = "99"; + (bool success, bytes memory rawData) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature("toString(uint256)", value) + ); + require(success, "toString failed"); + bytes memory data = trimReturnBytes(rawData); + string memory testString = string(abi.encodePacked(data)); + require( + keccak256(bytes(testString)) == keccak256(bytes(stringValue)), + "toString mismatch" + ); + console.log("failed?", failed()); + } + + function testToStringFromInt256() external { + int256 value = -99; + string memory stringValue = "-99"; + (bool success, bytes memory rawData) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature("toString(int256)", value) + ); + require(success, "toString failed"); + bytes memory data = trimReturnBytes(rawData); + string memory testString = string(abi.encodePacked(data)); + require( + keccak256(bytes(testString)) == keccak256(bytes(stringValue)), + "toString mismatch" + ); + console.log("failed?", failed()); + } + + function testToStringFromBytes32() external { + bytes32 testBytes = hex"4ec893b0a778b562e893cee722869c3e924e9ee46ec897cabda6b765a6624324"; + (bool success, bytes memory rawData) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature("toString(bytes32)", testBytes) + ); + require(success, "toString failed"); + bytes memory data = trimReturnBytes(rawData); + string memory testString = string(abi.encodePacked(data)); + require( + keccak256(bytes(testString)) == + keccak256( + bytes( + "0x4ec893b0a778b562e893cee722869c3e924e9ee46ec897cabda6b765a6624324" + ) + ), + "toString mismatch" + ); + console.log("failed?", failed()); + } + + function testToStringFromBytes() external { + bytes + memory testBytes = hex"89987299ea14decf0e11d068474a6e459439802edca8aacf9644222e490d8ef6db"; + (bool success, bytes memory rawData) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature("toString(bytes)", testBytes) + ); + require(success, "toString failed"); + bytes memory data = trimReturnBytes(rawData); + string memory testString = string(abi.encodePacked(data)); + require( + keccak256(bytes(testString)) == + keccak256( + bytes( + "0x89987299ea14decf0e11d068474a6e459439802edca8aacf9644222e490d8ef6db" + ) + ), + "toString mismatch" + ); + console.log("failed?", failed()); + } + + function trimReturnBytes( + bytes memory rawData + ) internal pure returns (bytes memory) { + uint256 lengthStartingPos = rawData.length - 32; + bytes memory lengthSlice = new bytes(32); + for (uint256 i = 0; i < 32; i++) { + lengthSlice[i] = rawData[lengthStartingPos + i]; + } + uint256 length = abi.decode(lengthSlice, (uint256)); + bytes memory data = new bytes(length); + for (uint256 i = 0; i < length; i++) { + data[i] = rawData[i]; + } + return data; + } +} diff --git a/crates/era-cheatcodes/tests/src/cheatcodes/Warp.t.sol b/crates/era-cheatcodes/tests/src/cheatcodes/Warp.t.sol new file mode 100644 index 000000000..336dec06c --- /dev/null +++ b/crates/era-cheatcodes/tests/src/cheatcodes/Warp.t.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test, console2 as console} from "../../lib/forge-std/src/Test.sol"; +import {Constants} from "./Constants.sol"; + +contract CheatcodeWarpTest is Test { + uint256 constant NEW_BLOCK_TIMESTAMP = uint256(10000); + + function testWarp() public { + uint256 initialTimestamp = block.timestamp; + console.log("timestamp before:", initialTimestamp); + require( + NEW_BLOCK_TIMESTAMP != initialTimestamp, + "timestamp must be different than current block timestamp" + ); + + (bool success, ) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature("warp(uint256)", NEW_BLOCK_TIMESTAMP) + ); + require(success, "warp failed"); + + uint256 finalTimestamp = block.timestamp; + console.log("timestamp after:", finalTimestamp); + require( + finalTimestamp == NEW_BLOCK_TIMESTAMP, + "timestamp was not changed" + ); + console.log("failed?", failed()); + } +} diff --git a/crates/era-cheatcodes/tests/test.sh b/crates/era-cheatcodes/tests/test.sh new file mode 100755 index 000000000..e45cfb8fe --- /dev/null +++ b/crates/era-cheatcodes/tests/test.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash + +# Fail fast and on piped commands +set -o pipefail -e + +TEST_REPO=${1:-$TEST_REPO} +TEST_REPO_DIR=${2:-$TEST_REPO_DIR} +SOLC_VERSION=${SOLC_VERSION:-"v0.8.20"} +SOLC="solc-${SOLC_VERSION}" +BINARY_PATH="../target/release/zkforge" + +if [ "${TEST_REPO}" == "foundry-zksync" ]; then + BINARY_PATH="${TEST_REPO_DIR}/target/release/zkforge" +fi + +function cleanup() { + echo "Cleaning up..." + rm -rf "./foundry-zksync" + rm "./${SOLC}" +} + +function download_solc() { + if [ ! -x "${SOLC}" ]; then + wget --quiet -O "${SOLC}" "https://github.com/ethereum/solidity/releases/download/${1}/solc-static-macos" + chmod +x "${SOLC}" + fi +} + +function wait_for_build() { + local timeout=$1 + while ! [ -x "${BINARY_PATH}" ]; do + ((timeout--)) + if [ $timeout -le 0 ]; then + echo "Build timed out waiting for binary to be created." + exit 1 + fi + sleep 1 + done +} + +# We want this to fail-fast and hence are put on separate lines +# See https://unix.stackexchange.com/questions/312631/bash-script-with-set-e-doesnt-stop-on-command +function build_zkforge() { + echo "Building ${1}..." + cargo build --release --manifest-path="${1}/Cargo.toml" + wait_for_build 30 +} + +# trap cleanup ERR + +echo "Solc: ${SOLC_VERSION}" +echo "Zkforge binary: ${BINARY_PATH}" + +# Download solc +download_solc "${SOLC_VERSION}" + +# Check for necessary tools +command -v cargo &>/dev/null || { + echo "cargo not found, exiting" + exit 1 +} +command -v git &>/dev/null || { + echo "git not found, exiting" + exit 1 +} + + +build_zkforge "../" + +echo "Running tests..." +"${BINARY_PATH}" zkbuild --use "./${SOLC}" +RUST_LOG=debug "${BINARY_PATH}" test --use "./${SOLC}" + +# cleanup diff --git a/crates/evm/core/Cargo.toml b/crates/evm/core/Cargo.toml index 1aff62b5b..82e6ee021 100644 --- a/crates/evm/core/Cargo.toml +++ b/crates/evm/core/Cargo.toml @@ -34,7 +34,13 @@ revm = { workspace = true, default-features = false, features = [ ethers-core.workspace = true ethers-providers.workspace = true -era_revm.workspace = true +multivm.workspace = true +zksync_basic_types.workspace = true +zksync_types.workspace = true +era_test_node.workspace = true +zksync_utils.workspace = true +zksync_web3_decl.workspace = true +zksync_state.workspace = true derive_more.workspace = true eyre = "0.6" @@ -49,3 +55,7 @@ thiserror = "1" tokio = { version = "1", features = ["time", "macros"] } tracing = "0.1" url = "2" +auto_impl = "1" + +[dev-dependencies] +maplit = "1" diff --git a/crates/evm/core/src/backend/fuzz.rs b/crates/evm/core/src/backend/fuzz.rs index ff1999b22..c31205784 100644 --- a/crates/evm/core/src/backend/fuzz.rs +++ b/crates/evm/core/src/backend/fuzz.rs @@ -15,6 +15,11 @@ use revm::{ }; use std::{borrow::Cow, collections::HashMap}; +use crate::era_revm::db::RevmDatabaseForEra; +use era_test_node::fork::ForkStorage; +use multivm::vm_refunds_enhancement::{HistoryDisabled, ToTracerPointer}; +use zksync_state::StorageView; + /// A wrapper around `Backend` that ensures only `revm::DatabaseRef` functions are called. /// /// Any changes made during its existence that affect the caching layer of the underlying Database @@ -47,18 +52,22 @@ impl<'a> FuzzBackendWrapper<'a> { } /// Executes the configured transaction of the `env` without committing state changes - pub fn inspect_ref( - &mut self, - env: &mut Env, + pub fn inspect_ref<'b, INSP>( + &'b mut self, + env: &'b mut Env, inspector: INSP, ) -> eyre::Result where - INSP: Inspector, + INSP: Inspector + + ToTracerPointer< + StorageView>>, + HistoryDisabled, + >, { self.is_initialized = false; let result: EVMResult = - era_revm::transactions::run_era_transaction(env, self, inspector); + crate::era_revm::transactions::run_era_transaction(env, self, inspector); Ok(result.unwrap()) } diff --git a/crates/evm/core/src/backend/mod.rs b/crates/evm/core/src/backend/mod.rs index 30d14a270..394910a34 100644 --- a/crates/evm/core/src/backend/mod.rs +++ b/crates/evm/core/src/backend/mod.rs @@ -28,6 +28,11 @@ use revm::{ }; use std::collections::{HashMap, HashSet}; +use crate::era_revm::db::RevmDatabaseForEra; +use era_test_node::fork::ForkStorage; +use multivm::vm_refunds_enhancement::{HistoryDisabled, ToTracerPointer}; +use zksync_state::StorageView; + mod diagnostic; pub use diagnostic::RevertDiagnostic; @@ -65,6 +70,7 @@ const GLOBAL_FAILURE_SLOT: B256 = b256!("6661696c65640000000000000000000000000000000000000000000000000000"); /// An extension trait that allows us to easily extend the `revm::Inspector` capabilities +#[auto_impl::auto_impl(&mut, Box)] pub trait DatabaseExt: Database { /// Creates a new snapshot at the current point of execution. /// @@ -756,18 +762,22 @@ impl Backend { } /// Executes the configured test call of the `env` without committing state changes - pub fn inspect_ref( - &mut self, - env: &mut Env, + pub fn inspect_ref<'a, INSP>( + &'a mut self, + env: &'a mut Env, inspector: INSP, ) -> eyre::Result where - INSP: Inspector, + INSP: Inspector + + ToTracerPointer< + StorageView>>, + HistoryDisabled, + >, { self.initialize(env); let result: EVMResult = - era_revm::transactions::run_era_transaction(env, self, inspector); + crate::era_revm::transactions::run_era_transaction(env, self, inspector); Ok(result.unwrap()) } diff --git a/crates/evm/core/src/era_revm/db.rs b/crates/evm/core/src/era_revm/db.rs new file mode 100644 index 000000000..02a7ac6ed --- /dev/null +++ b/crates/evm/core/src/era_revm/db.rs @@ -0,0 +1,477 @@ +/// RevmDatabaseForEra allows era VM to use the revm "Database" object +/// as a read-only fork source. +/// This way, we can run transaction on top of the chain that is persisted +/// in the Database object. +/// This code doesn't do any mutatios to Database: after each transaction run, the Revm +/// is usually collecing all the diffs - and applies them to database itself. +use std::{ + collections::HashMap, + fmt::Debug, + sync::{Arc, Mutex}, +}; + +use era_test_node::fork::ForkSource; +use eyre::ErrReport; +use foundry_common::zk_utils::conversion_utils::{ + h160_to_address, h256_to_b256, h256_to_h160, revm_u256_to_h256, u256_to_revm_u256, +}; +use revm::{ + primitives::{Bytecode, Bytes}, + Database, +}; +use zksync_basic_types::{ + web3::signing::keccak256, AccountTreeId, MiniblockNumber, H160, H256, U256, +}; +use zksync_types::{ + api::{BlockIdVariant, Transaction, TransactionDetails}, + StorageKey, ACCOUNT_CODE_STORAGE_ADDRESS, L2_ETH_TOKEN_ADDRESS, NONCE_HOLDER_ADDRESS, + SYSTEM_CONTEXT_ADDRESS, +}; + +use zksync_utils::{address_to_h256, h256_to_u256, u256_to_h256}; + +#[derive(Default)] +pub struct RevmDatabaseForEra { + pub db: Arc>>, + pub current_block: u64, +} + +impl Clone for RevmDatabaseForEra { + fn clone(&self) -> Self { + Self { db: self.db.clone(), current_block: self.current_block } + } +} + +impl Debug for RevmDatabaseForEra { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RevmDatabaseForEra") + .field("db", &"db") + .field("current_block", &self.current_block) + .finish() + } +} + +impl RevmDatabaseForEra +where + ::Error: Debug, +{ + /// Returns the current block number and timestamp from the database. + /// Reads it directly from the SYSTEM_CONTEXT storage. + pub fn block_number_and_timestamp(&self) -> (u64, u64) { + let num_and_ts = self.read_storage_internal(SYSTEM_CONTEXT_ADDRESS, U256::from(7)); + let num_and_ts_bytes = num_and_ts.as_fixed_bytes(); + let num: [u8; 8] = num_and_ts_bytes[24..32].try_into().unwrap(); + let ts: [u8; 8] = num_and_ts_bytes[8..16].try_into().unwrap(); + + (u64::from_be_bytes(num), u64::from_be_bytes(ts)) + } + + /// Returns the nonce for a given account from NonceHolder storage. + pub fn get_nonce_for_address(&self, address: H160) -> u64 { + // Nonce is stored in the first mapping of the Nonce contract. + let storage_idx = [&[0; 12], address.as_bytes(), &[0; 32]].concat(); + let storage_idx = H256::from_slice(&keccak256(storage_idx.as_slice())); + + let nonce_storage = + self.read_storage_internal(NONCE_HOLDER_ADDRESS, h256_to_u256(storage_idx)); + let nonces: [u8; 8] = nonce_storage.as_fixed_bytes()[24..32].try_into().unwrap(); + u64::from_be_bytes(nonces) + } + + fn read_storage_internal(&self, address: H160, idx: U256) -> H256 { + let mut db = self.db.lock().unwrap(); + let result = db.storage(h160_to_address(address), u256_to_revm_u256(idx)).unwrap(); + revm_u256_to_h256(result) + } + + /// Tries to fetch the bytecode that belongs to a given account. + /// Start, by looking into account code storage - to see if there is any information about the + /// bytecode for this account. If there is none - check if any of the bytecode hashes are + /// matching the account. And as the final step - read the bytecode from the database + /// itself. + pub fn fetch_account_code( + &self, + account: H160, + modified_keys: &HashMap, + bytecodes: &HashMap>, + ) -> Option<(H256, Bytecode)> { + // First - check if the bytecode was set/changed in the recent block. + if let Some(v) = modified_keys.get(&StorageKey::new( + AccountTreeId::new(ACCOUNT_CODE_STORAGE_ADDRESS), + address_to_h256(&account), + )) { + let new_bytecode_hash = *v; + if let Some(new_bytecode) = bytecodes.get(&h256_to_u256(new_bytecode_hash)) { + let u8_bytecode: Vec = + new_bytecode.iter().flat_map(|x| u256_to_h256(*x).to_fixed_bytes()).collect(); + + return Some(( + new_bytecode_hash, + Bytecode { + bytecode: Bytes::copy_from_slice(u8_bytecode.as_slice()), + state: revm::primitives::BytecodeState::Raw, + }, + )) + } + } + + // Check if maybe we got a bytecode with this hash. + // Unfortunately the accounts are mapped as "last 20 bytes of the 32 byte hash". + // so we have to iterate over all the bytecodes, truncate their hash and then compare. + for (k, v) in bytecodes { + if h256_to_h160(&u256_to_h256(*k)) == account { + let u8_bytecode: Vec = + v.iter().flat_map(|x| u256_to_h256(*x).to_fixed_bytes()).collect(); + + return Some(( + u256_to_h256(*k), + Bytecode { + bytecode: Bytes::copy_from_slice(u8_bytecode.as_slice()), + state: revm::primitives::BytecodeState::Raw, + }, + )) + } + } + + let account = h160_to_address(account); + + let mut db = self.db.lock().unwrap(); + db.basic(account) + .ok() + .and_then(|db_account| { + db_account.map(|a| a.code.map(|b| (H256::from(a.code_hash.0), b))) + }) + .flatten() + } +} + +impl ForkSource for RevmDatabaseForEra +where + ::Error: Debug, +{ + fn get_storage_at( + &self, + address: H160, + idx: U256, + block: Option, + ) -> eyre::Result { + // We cannot support historical lookups. Only the most recent block is supported. + let current_block = self.current_block; + if let Some(block) = &block { + match block { + BlockIdVariant::BlockNumber(zksync_types::api::BlockNumber::Number(num)) => { + let current_block_number_l2 = current_block * 2; + if num.as_u64() != current_block_number_l2 { + eyre::bail!("Only fetching of the most recent L2 block {} is supported - but queried for {}", current_block_number_l2, num) + } + } + _ => eyre::bail!("Only fetching most recent block is implemented"), + } + } + let mut result = self.read_storage_internal(address, idx); + + if L2_ETH_TOKEN_ADDRESS == address && result.is_zero() { + // TODO: here we should read the account information from the Database trait + // and lookup how many token it holds. + // Unfortunately the 'idx' here is a hash of the account and Database doesn't + // support getting a list of active accounts. + // So for now - simply assume that every user has infinite money. + result = u256_to_h256(U256::from(9_223_372_036_854_775_808_u64)); + } + Ok(result) + } + + fn get_raw_block_transactions( + &self, + _block_number: MiniblockNumber, + ) -> eyre::Result> { + todo!() + } + + fn get_bytecode_by_hash(&self, hash: H256) -> eyre::Result>> { + let mut db = self.db.lock().unwrap(); + let result = db.code_by_hash(h256_to_b256(hash)).unwrap(); + Ok(Some(result.bytecode.to_vec())) + } + + fn get_transaction_by_hash(&self, _hash: H256) -> eyre::Result> { + todo!() + } + + fn get_transaction_details( + &self, + _hash: H256, + ) -> Result, ErrReport> { + todo!() + } + + fn get_block_by_hash( + &self, + _hash: H256, + _full_transactions: bool, + ) -> eyre::Result>> { + todo!() + } + + fn get_block_by_number( + &self, + _block_number: zksync_types::api::BlockNumber, + _full_transactions: bool, + ) -> eyre::Result>> { + todo!() + } + + fn get_block_details( + &self, + _miniblock: MiniblockNumber, + ) -> eyre::Result> { + todo!() + } + + fn get_block_transaction_count_by_hash(&self, _block_hash: H256) -> eyre::Result> { + todo!() + } + + fn get_block_transaction_count_by_number( + &self, + _block_number: zksync_types::api::BlockNumber, + ) -> eyre::Result> { + todo!() + } + + fn get_transaction_by_block_hash_and_index( + &self, + _block_hash: H256, + _index: zksync_basic_types::web3::types::Index, + ) -> eyre::Result> { + todo!() + } + + fn get_transaction_by_block_number_and_index( + &self, + _block_number: zksync_types::api::BlockNumber, + _index: zksync_basic_types::web3::types::Index, + ) -> eyre::Result> { + todo!() + } + + fn get_bridge_contracts(&self) -> eyre::Result { + todo!() + } + + fn get_confirmed_tokens( + &self, + _from: u32, + _limit: u8, + ) -> eyre::Result> { + todo!() + } +} + +#[cfg(test)] +#[allow(clippy::box_default)] +mod tests { + use maplit::hashmap; + use revm::primitives::AccountInfo; + + use super::*; + use crate::era_revm::testing::MockDatabase; + + #[test] + fn test_fetch_account_code_returns_hash_and_code_if_present_in_modified_keys_and_bytecodes() { + let bytecode_hash = H256::repeat_byte(0x3); + let bytecode = vec![U256::from(4)]; + let account = H160::repeat_byte(0xa); + let modified_keys = hashmap! { + StorageKey::new( + AccountTreeId::new(ACCOUNT_CODE_STORAGE_ADDRESS), + address_to_h256(&account), + ) => bytecode_hash, + }; + let bytecodes = hashmap! { + h256_to_u256(bytecode_hash) => bytecode.clone(), + }; + let db = RevmDatabaseForEra { + current_block: 0, + db: Arc::new(Mutex::new(Box::new(MockDatabase::default()))), + }; + + let actual = db.fetch_account_code(account, &modified_keys, &bytecodes); + + let expected = Some(( + bytecode_hash, + Bytecode::new_raw( + bytecode + .into_iter() + .flat_map(|v| u256_to_h256(v).to_fixed_bytes()) + .collect::>() + .into(), + ), + )); + assert_eq!(expected, actual) + } + + #[test] + fn test_fetch_account_code_returns_hash_and_code_if_not_in_modified_keys_but_in_bytecodes() { + let bytecode_hash = H256::repeat_byte(0x3); + let bytecode = vec![U256::from(4)]; + let account = h256_to_h160(&bytecode_hash); // accounts are mapped as last 20 bytes of the 32-byte hash + let modified_keys = Default::default(); + let bytecodes = hashmap! { + h256_to_u256(bytecode_hash) => bytecode.clone(), + }; + let db = RevmDatabaseForEra { + current_block: 0, + db: Arc::new(Mutex::new(Box::new(MockDatabase::default()))), + }; + + let actual = db.fetch_account_code(account, &modified_keys, &bytecodes); + + let expected = Some(( + bytecode_hash, + Bytecode::new_raw( + bytecode + .into_iter() + .flat_map(|v| u256_to_h256(v).to_fixed_bytes()) + .collect::>() + .into(), + ), + )); + assert_eq!(expected, actual) + } + + #[test] + fn test_fetch_account_code_returns_hash_and_code_from_db_if_not_in_modified_keys_or_bytecodes() + { + let bytecode_hash = H256::repeat_byte(0x3); + let bytecode = vec![U256::from(4)]; + let account = h256_to_h160(&bytecode_hash); // accounts are mapped as last 20 bytes of the 32-byte hash + let modified_keys = Default::default(); + let bytecodes = Default::default(); + let db = RevmDatabaseForEra { + current_block: 0, + db: Arc::new(Mutex::new(Box::new(MockDatabase { + basic: hashmap! { + h160_to_address(account) => AccountInfo { + code_hash: bytecode_hash.to_fixed_bytes().into(), + code: Some(Bytecode::new_raw( + bytecode + .clone() + .into_iter() + .flat_map(|v| u256_to_h256(v).to_fixed_bytes()) + .collect::>() + .into())), + ..Default::default() + } + }, + }))), + }; + + let actual = db.fetch_account_code(account, &modified_keys, &bytecodes); + + let expected = Some(( + bytecode_hash, + Bytecode::new_raw( + bytecode + .into_iter() + .flat_map(|v| u256_to_h256(v).to_fixed_bytes()) + .collect::>() + .into(), + ), + )); + assert_eq!(expected, actual) + } + + #[test] + fn test_fetch_account_code_returns_hash_and_code_from_db_if_address_in_modified_keys_but_not_in_bytecodes( + ) { + let bytecode_hash = H256::repeat_byte(0x3); + let bytecode = vec![U256::from(4)]; + let account = h256_to_h160(&bytecode_hash); // accounts are mapped as last 20 bytes of the 32-byte hash + let modified_keys = hashmap! { + StorageKey::new( + AccountTreeId::new(ACCOUNT_CODE_STORAGE_ADDRESS), + address_to_h256(&account), + ) => bytecode_hash, + }; + let bytecodes = Default::default(); // nothing in bytecodes + let db = RevmDatabaseForEra { + current_block: 0, + db: Arc::new(Mutex::new(Box::new(MockDatabase { + basic: hashmap! { + h160_to_address(account) => AccountInfo { + code_hash: bytecode_hash.to_fixed_bytes().into(), + code: Some(Bytecode::new_raw( + bytecode + .clone() + .into_iter() + .flat_map(|v| u256_to_h256(v).to_fixed_bytes()) + .collect::>() + .into())), + ..Default::default() + } + }, + }))), + }; + + let actual = db.fetch_account_code(account, &modified_keys, &bytecodes); + + let expected = Some(( + bytecode_hash, + Bytecode::new_raw( + bytecode + .into_iter() + .flat_map(|v| u256_to_h256(v).to_fixed_bytes()) + .collect::>() + .into(), + ), + )); + assert_eq!(expected, actual) + } + + #[test] + fn test_get_storage_at_does_not_panic_when_even_numbered_blocks_are_requested() { + // This test exists because era-test-node creates two L2 (virtual) blocks per transaction. + // See https://github.com/matter-labs/era-test-node/pull/111/files#diff-af08c3181737aa5783b96dfd920cd5ef70829f46cd1b697bdb42414c97310e13R1333 + + let db = &RevmDatabaseForEra { + current_block: 1, + db: Arc::new(Mutex::new(Box::new(MockDatabase::default()))), + }; + + let actual = db + .get_storage_at( + H160::zero(), + U256::zero(), + Some(BlockIdVariant::BlockNumber(zksync_types::api::BlockNumber::Number( + zksync_basic_types::U64::from(2), + ))), + ) + .expect("failed getting storage"); + + assert_eq!(H256::zero(), actual) + } + + #[test] + #[should_panic( + expected = "Only fetching of the most recent L2 block 2 is supported - but queried for 1" + )] + fn test_get_storage_at_panics_when_odd_numbered_blocks_are_requested() { + // This test exists because era-test-node creates two L2 (virtual) blocks per transaction. + // See https://github.com/matter-labs/era-test-node/pull/111/files#diff-af08c3181737aa5783b96dfd920cd5ef70829f46cd1b697bdb42414c97310e13R1333 + + let db = &RevmDatabaseForEra { + current_block: 1, + db: Arc::new(Mutex::new(Box::new(MockDatabase::default()))), + }; + + db.get_storage_at( + H160::zero(), + U256::zero(), + Some(BlockIdVariant::BlockNumber(zksync_types::api::BlockNumber::Number( + zksync_basic_types::U64::from(1), + ))), + ) + .unwrap(); + } +} diff --git a/crates/evm/core/src/era_revm/mod.rs b/crates/evm/core/src/era_revm/mod.rs new file mode 100644 index 000000000..32e6350f5 --- /dev/null +++ b/crates/evm/core/src/era_revm/mod.rs @@ -0,0 +1,5 @@ +pub mod db; +pub mod transactions; + +#[cfg(test)] +mod testing; diff --git a/crates/evm/core/src/era_revm/testing.rs b/crates/evm/core/src/era_revm/testing.rs new file mode 100644 index 000000000..5ac1070b4 --- /dev/null +++ b/crates/evm/core/src/era_revm/testing.rs @@ -0,0 +1,161 @@ +use crate::{ + backend::{Backend, DatabaseError, DatabaseExt, LocalForkId}, + fork::{CreateFork, ForkId}, +}; + +use crate::backend::RevertDiagnostic; +use ethers_core::utils::GenesisAccount; +use revm::{ + primitives::{AccountInfo, Address, Bytecode, Env, B256, U256}, + Inspector, JournaledState, +}; +use std::collections::HashMap; + +#[derive(Default)] +pub struct MockDatabase { + pub basic: HashMap, +} + +impl revm::Database for MockDatabase { + type Error = DatabaseError; + + fn basic(&mut self, address: Address) -> Result, Self::Error> { + Ok(self.basic.get(&address).cloned()) + } + + fn code_by_hash(&mut self, _code_hash: B256) -> Result { + todo!() + } + + fn storage(&mut self, _address: Address, _index: U256) -> Result { + Ok(U256::ZERO) + } + + fn block_hash(&mut self, _number: U256) -> Result { + todo!() + } +} + +impl DatabaseExt for MockDatabase { + fn snapshot(&mut self, _journaled_state: &JournaledState, _env: &Env) -> U256 { + todo!() + } + + fn revert( + &mut self, + _id: U256, + _journaled_state: &JournaledState, + _env: &mut Env, + ) -> Option { + todo!() + } + + fn create_fork(&mut self, _fork: CreateFork) -> eyre::Result { + todo!() + } + + fn create_fork_at_transaction( + &mut self, + _fork: CreateFork, + _transaction: B256, + ) -> eyre::Result { + todo!() + } + + fn select_fork( + &mut self, + _id: LocalForkId, + _env: &mut Env, + _journaled_state: &mut JournaledState, + ) -> eyre::Result<()> { + todo!() + } + + fn roll_fork( + &mut self, + _id: Option, + _block_number: U256, + _env: &mut Env, + _journaled_state: &mut JournaledState, + ) -> eyre::Result<()> { + todo!() + } + + fn roll_fork_to_transaction( + &mut self, + _id: Option, + _transaction: B256, + _env: &mut Env, + _journaled_state: &mut JournaledState, + ) -> eyre::Result<()> { + todo!() + } + + fn transact>( + &mut self, + _id: Option, + _transaction: B256, + _env: &mut Env, + _journaled_state: &mut JournaledState, + _inspector: &mut I, + ) -> eyre::Result<()> { + todo!() + } + + fn active_fork_id(&self) -> Option { + todo!() + } + + fn active_fork_url(&self) -> Option { + todo!() + } + + fn ensure_fork(&self, _id: Option) -> eyre::Result { + todo!() + } + + fn ensure_fork_id(&self, _id: LocalForkId) -> eyre::Result<&ForkId> { + todo!() + } + + fn diagnose_revert( + &self, + _callee: Address, + _journaled_state: &JournaledState, + ) -> Option { + todo!() + } + + fn load_allocs( + &mut self, + _allocs: &HashMap, + _journaled_state: &mut JournaledState, + ) -> Result<(), DatabaseError> { + todo!() + } + + fn is_persistent(&self, _acc: &Address) -> bool { + todo!() + } + + fn remove_persistent_account(&mut self, _account: &Address) -> bool { + todo!() + } + + #[doc = " Marks the given account as persistent."] + fn add_persistent_account(&mut self, _account: Address) -> bool { + todo!() + } + + fn allow_cheatcode_access(&mut self, _account: Address) -> bool { + todo!() + } + + fn revoke_cheatcode_access(&mut self, _account: Address) -> bool { + todo!() + } + + fn has_cheatcode_access(&self, _account: Address) -> bool { + todo!() + } +} diff --git a/crates/evm/core/src/era_revm/transactions.rs b/crates/evm/core/src/era_revm/transactions.rs new file mode 100644 index 000000000..d68256cee --- /dev/null +++ b/crates/evm/core/src/era_revm/transactions.rs @@ -0,0 +1,395 @@ +use era_test_node::{ + fork::{ForkDetails, ForkStorage}, + node::{ + InMemoryNode, InMemoryNodeConfig, ShowCalls, ShowGasDetails, ShowStorageLogs, ShowVMDetails, + }, + system_contracts, +}; +use ethers_core::abi::ethabi::{self, ParamType}; +use multivm::{ + interface::VmExecutionResultAndLogs, + vm_refunds_enhancement::{HistoryDisabled, ToTracerPointer}, +}; +use revm::primitives::{ + Account, AccountInfo, Address, Bytes, EVMResult, Env, Eval, Halt, HashMap as rHashMap, + OutOfGasError, ResultAndState, StorageSlot, TxEnv, B256, KECCAK_EMPTY, U256 as rU256, + U256 as revmU256, +}; +use std::{ + collections::{HashMap, HashSet}, + fmt::Debug, + sync::{Arc, Mutex}, +}; +use zksync_basic_types::{web3::signing::keccak256, L1BatchNumber, L2ChainId, H160, H256, U256}; +use zksync_state::StorageView; +use zksync_types::{ + api::Block, fee::Fee, l2::L2Tx, transaction_request::PaymasterParams, PackedEthSignature, + StorageKey, StorageLogQueryType, ACCOUNT_CODE_STORAGE_ADDRESS, +}; +use zksync_utils::{h256_to_account_address, u256_to_h256}; + +use foundry_common::zk_utils::{ + conversion_utils::{ + address_to_h160, h160_to_address, h256_to_h160, h256_to_revm_u256, revm_u256_to_u256, + }, + factory_deps::PackedEraBytecode, +}; + +use super::db::RevmDatabaseForEra; +use crate::backend::DatabaseExt; + +fn contract_address_from_tx_result(execution_result: &VmExecutionResultAndLogs) -> Option { + for query in execution_result.logs.storage_logs.iter().rev() { + if query.log_type == StorageLogQueryType::InitialWrite && + query.log_query.address == ACCOUNT_CODE_STORAGE_ADDRESS + { + return Some(h256_to_account_address(&u256_to_h256(query.log_query.key))) + } + } + None +} + +/// Prepares calldata to invoke deployer contract. +/// This method encodes parameters for the `create` method. +pub fn encode_deploy_params_create( + salt: H256, + contract_hash: H256, + constructor_input: Vec, +) -> Vec { + // TODO (SMA-1608): We should not re-implement the ABI parts in different places, instead have + // the ABI available from the `zksync_contracts` crate. + let signature = ethabi::short_signature( + "create", + &[ + ethabi::ParamType::FixedBytes(32), + ethabi::ParamType::FixedBytes(32), + ethabi::ParamType::Bytes, + ], + ); + let params = ethabi::encode(&[ + ethabi::Token::FixedBytes(salt.as_bytes().to_vec()), + ethabi::Token::FixedBytes(contract_hash.as_bytes().to_vec()), + ethabi::Token::Bytes(constructor_input), + ]); + + signature.iter().copied().chain(params).collect() +} + +/// Extract the zkSync Fee based off the Revm transaction. +pub fn tx_env_to_fee(tx_env: &TxEnv) -> Fee { + Fee { + // Currently zkSync doesn't allow gas limits larger than u32. + gas_limit: U256::min(tx_env.gas_limit.into(), U256::from(2147483640)), + // Block base fee on L2 is 0.25 GWei - make sure that the max_fee_per_gas is set to higher + // value. + max_fee_per_gas: U256::max(revm_u256_to_u256(tx_env.gas_price), U256::from(260_000_000)), + max_priority_fee_per_gas: revm_u256_to_u256(tx_env.gas_priority_fee.unwrap_or_default()), + gas_per_pubdata_limit: U256::from(800), + } +} + +/// Translates Revm transaction into era's L2Tx. +pub fn tx_env_to_era_tx(tx_env: TxEnv, nonce: u64) -> L2Tx { + let mut l2tx = match tx_env.transact_to { + revm::primitives::TransactTo::Call(contract_address) => L2Tx::new( + H160::from(contract_address.0 .0), + tx_env.data.to_vec(), + (tx_env.nonce.unwrap_or(nonce) as u32).into(), + tx_env_to_fee(&tx_env), + H160::from(tx_env.caller.0 .0), + revm_u256_to_u256(tx_env.value), + None, // factory_deps + PaymasterParams::default(), + ), + revm::primitives::TransactTo::Create(_scheme) => { + // TODO: support create / create2. + let packed_bytecode = PackedEraBytecode::from_vec(tx_env.data.as_ref()); + L2Tx::new( + H160::from_low_u64_be(0x8006), + encode_deploy_params_create( + Default::default(), + packed_bytecode.bytecode_hash(), + Default::default(), + ), + (tx_env.nonce.unwrap_or(nonce) as u32).into(), + tx_env_to_fee(&tx_env), + H160::from(tx_env.caller.0 .0), + revm_u256_to_u256(tx_env.value), + Some(packed_bytecode.factory_deps()), + PaymasterParams::default(), + ) + } + }; + l2tx.set_input(tx_env.data.to_vec(), H256(keccak256(tx_env.data.to_vec().as_slice()))); + l2tx +} + +#[derive(Debug, Clone)] +pub enum DatabaseError { + MissingCode(bool), +} + +pub fn run_era_transaction(env: &mut Env, db: DB, inspector: INSP) -> EVMResult +where + DB: DatabaseExt + Send, + ::Error: Debug, + INSP: ToTracerPointer>>, HistoryDisabled>, +{ + let (num, ts) = (env.block.number.to::(), env.block.timestamp.to::()); + let era_db = RevmDatabaseForEra { db: Arc::new(Mutex::new(Box::new(db))), current_block: num }; + + let nonces = era_db.get_nonce_for_address(address_to_h160(env.tx.caller)); + + println!( + "*** Starting ERA transaction: block: {:?} timestamp: {:?} - but using {:?} and {:?} instead with nonce {:?}", + env.block.number.to::(), + env.block.timestamp.to::(), + num, + ts, + nonces + ); + + // Update the environment timestamp and block number. + // Check if this should be done at the end? + env.block.number = env.block.number.saturating_add(rU256::from(1)); + env.block.timestamp = env.block.timestamp.saturating_add(rU256::from(1)); + + let chain_id_u32 = if env.cfg.chain_id <= u32::MAX as u64 { + env.cfg.chain_id as u32 + } else { + // TODO: FIXME + 31337 + }; + + let (l2_num, l2_ts) = (num * 2, ts * 2); + let fork_details = ForkDetails { + fork_source: era_db.clone(), + l1_block: L1BatchNumber(num as u32), + l2_block: Block::default(), + l2_miniblock: l2_num, + l2_miniblock_hash: Default::default(), + block_timestamp: l2_ts, + overwrite_chain_id: Some(L2ChainId::from(chain_id_u32)), + // Make sure that l1 gas price is set to reasonable values. + l1_gas_price: u64::max(env.block.basefee.to::(), 1000), + }; + + let config = InMemoryNodeConfig { + show_calls: ShowCalls::None, + show_storage_logs: ShowStorageLogs::None, + show_vm_details: ShowVMDetails::None, + show_gas_details: ShowGasDetails::None, + resolve_hashes: false, + system_contracts_options: system_contracts::Options::BuiltInWithoutSecurity, + }; + let node = InMemoryNode::new(Some(fork_details), None, config); + + let mut l2_tx = tx_env_to_era_tx(env.tx.clone(), nonces); + + if l2_tx.common_data.signature.is_empty() { + // FIXME: This is a hack to make sure that the signature is not empty. + // Fails without a signature here: https://github.com/matter-labs/zksync-era/blob/73a1e8ff564025d06e02c2689da238ae47bb10c3/core/lib/types/src/transaction_request.rs#L381 + l2_tx.common_data.signature = PackedEthSignature::default().serialize_packed().into(); + } + let tracer = inspector.into_tracer_pointer(); + let era_execution_result = node + .run_l2_tx_raw(l2_tx, multivm::interface::TxExecutionMode::VerifyExecute, vec![tracer]) + .unwrap(); + + let (modified_keys, tx_result, _call_traces, _block, bytecodes, _block_ctx) = + era_execution_result; + let maybe_contract_address = contract_address_from_tx_result(&tx_result); + + let execution_result = match tx_result.result { + multivm::interface::ExecutionResult::Success { output, .. } => { + revm::primitives::ExecutionResult::Success { + reason: Eval::Return, + gas_used: env.tx.gas_limit - tx_result.refunds.gas_refunded as u64, + gas_refunded: tx_result.refunds.gas_refunded as u64, + logs: vec![], + output: revm::primitives::Output::Create( + Bytes::from(decode_l2_tx_result(output)), + maybe_contract_address.map(h160_to_address), + ), + } + } + multivm::interface::ExecutionResult::Revert { output } => { + let output = match output { + multivm::interface::VmRevertReason::General { data, .. } => data, + multivm::interface::VmRevertReason::Unknown { data, .. } => data, + _ => Vec::new(), + }; + + revm::primitives::ExecutionResult::Revert { + gas_used: env.tx.gas_limit - tx_result.refunds.gas_refunded as u64, + output: Bytes::from(output), + } + } + multivm::interface::ExecutionResult::Halt { reason } => { + // Need to decide what to do in the case of a halt. This might depend on the reason for + // the halt. TODO: FIXME + tracing::error!("tx execution halted: {}", reason); + revm::primitives::ExecutionResult::Halt { + reason: match reason { + multivm::interface::Halt::NotEnoughGasProvided => { + Halt::OutOfGas(OutOfGasError::BasicOutOfGas) + } + _ => panic!("HALT: {}", reason), + }, + gas_used: env.tx.gas_limit - tx_result.refunds.gas_refunded as u64, + } + } + }; + + let account_to_keys: HashMap> = + modified_keys.iter().fold(HashMap::new(), |mut acc, (storage_key, value)| { + acc.entry(*storage_key.address()).or_default().insert(*storage_key, *value); + acc + }); + + // List of touched accounts + let mut accounts_touched: HashSet = Default::default(); + // All accounts where storage was modified. + for x in account_to_keys.keys() { + accounts_touched.insert(*x); + } + // Also insert 'fake' accounts for bytecodes (to make sure that factory bytecodes get + // persisted). + for k in bytecodes.keys() { + accounts_touched.insert(h256_to_h160(&u256_to_h256(*k))); + } + + let account_code_storage = ACCOUNT_CODE_STORAGE_ADDRESS; + + if let Some(account_bytecodes) = account_to_keys.get(&account_code_storage) { + for k in account_bytecodes.keys() { + let account_address = H160::from_slice(&k.key().0[12..32]); + accounts_touched.insert(account_address); + } + } + + let state: rHashMap = accounts_touched + .iter() + .map(|account| { + let acc: Address = h160_to_address(*account); + + let storage: Option> = + account_to_keys.get(account).map(|slot_changes| { + slot_changes + .iter() + .map(|(slot, value)| { + ( + h256_to_revm_u256(*slot.key()), + StorageSlot { + previous_or_original_value: revm::primitives::U256::ZERO, // FIXME + present_value: h256_to_revm_u256(*value), + }, + ) + }) + .collect() + }); + + let account_code = era_db.fetch_account_code(*account, &modified_keys, &bytecodes); + + let (code_hash, code) = account_code + .map(|(hash, bytecode)| (B256::from(&hash.0), Some(bytecode))) + .unwrap_or((KECCAK_EMPTY, None)); + if code.is_none() { + println!("*** No bytecode for account: {:?}", account); + } + + ( + acc, + Account { + info: AccountInfo { + balance: revm::primitives::U256::ZERO, // FIXME + nonce: era_db.get_nonce_for_address(*account), + code_hash, + code, + }, + storage: storage.unwrap_or_default(), + status: revm::primitives::AccountStatus::Touched, + }, + ) + }) + .collect(); + + Ok(ResultAndState { result: execution_result, state }) +} + +fn decode_l2_tx_result(output: Vec) -> Vec { + ethabi::decode(&[ParamType::Bytes], &output) + .ok() + .and_then(|result| result.first().cloned()) + .and_then(|result| result.into_bytes()) + .unwrap_or_default() +} + +#[cfg(test)] +mod tests { + use core::marker::PhantomData; + use multivm::{ + interface::dyn_tracers::vm_1_3_3::DynTracer, + vm_refunds_enhancement::{HistoryMode, SimpleMemory, VmTracer}, + }; + use zksync_state::WriteStorage; + + use super::*; + use crate::era_revm::testing::MockDatabase; + use zksync_utils::bytecode::hash_bytecode; + + #[test] + fn test_env_number_and_timestamp_is_incremented_after_transaction_and_marks_storage_as_touched() + { + let mut env = Env::default(); + + env.block.number = rU256::from(0); + env.block.timestamp = rU256::from(0); + + env.tx = TxEnv { + caller: Address(H160::repeat_byte(0x1).to_fixed_bytes().into()), + gas_limit: 1_000_000, + gas_price: rU256::from(250_000_000), + transact_to: revm::primitives::TransactTo::Create( + revm::primitives::CreateScheme::Create, + ), + data: serde_json::to_vec(&PackedEraBytecode::new( + hex::encode(hash_bytecode(&[0; 32])), + hex::encode([0; 32]), + vec![hex::encode([0; 32])], + )) + .unwrap() + .into(), + ..Default::default() + }; + let mock_db = MockDatabase::default(); + + let res = run_era_transaction::<_, ResultAndState, _>(&mut env, mock_db, Noop::default()) + .expect("failed executing"); + + assert!(!res.state.is_empty(), "unexpected failure: no states were touched"); + for (address, account) in res.state { + assert!( + account.is_touched(), + "unexpected failure: account {} was not marked as touched; it will not be updated", + address + ); + } + + assert_eq!(1, env.block.number.to::()); + assert_eq!(1, env.block.timestamp.to::()); + } + + struct Noop { + _phantom: PhantomData<(S, H)>, + } + + impl Default for Noop { + fn default() -> Self { + Self { _phantom: Default::default() } + } + } + + impl DynTracer> for Noop {} + impl VmTracer for Noop {} +} diff --git a/crates/evm/core/src/lib.rs b/crates/evm/core/src/lib.rs index 9864ded06..20081406a 100644 --- a/crates/evm/core/src/lib.rs +++ b/crates/evm/core/src/lib.rs @@ -12,6 +12,7 @@ pub mod backend; pub mod constants; pub mod debug; pub mod decode; +pub mod era_revm; pub mod fork; pub mod opts; pub mod snapshot; diff --git a/crates/evm/evm/Cargo.toml b/crates/evm/evm/Cargo.toml index 859457687..ac1a99108 100644 --- a/crates/evm/evm/Cargo.toml +++ b/crates/evm/evm/Cargo.toml @@ -44,3 +44,9 @@ parking_lot = "0.12" proptest = "1" thiserror = "1" tracing = "0.1" + +# zksync +multivm.workspace = true +era_cheatcodes.workspace = true +zksync_state.workspace = true +era_test_node.workspace = true diff --git a/crates/evm/evm/src/inspectors/stack.rs b/crates/evm/evm/src/inspectors/stack.rs index 9f30d7830..be1d3a885 100644 --- a/crates/evm/evm/src/inspectors/stack.rs +++ b/crates/evm/evm/src/inspectors/stack.rs @@ -569,3 +569,24 @@ impl Inspector for InspectorStack { ); } } + +use era_test_node::fork::ForkStorage; +use foundry_evm_core::era_revm::db::RevmDatabaseForEra; + +use era_cheatcodes::cheatcodes::CheatcodeTracer; +use multivm::vm_refunds_enhancement::{HistoryDisabled, ToTracerPointer}; +use zksync_state::StorageView; + +impl + ToTracerPointer>>, HistoryDisabled> + for &mut InspectorStack +{ + fn into_tracer_pointer( + self, + ) -> multivm::vm_refunds_enhancement::TracerPointer< + StorageView>>, + HistoryDisabled, + > { + CheatcodeTracer::new().into_tracer_pointer() + } +}