From 87654dc229248e246daf0ec4281887306ee43d4e Mon Sep 17 00:00:00 2001 From: Alex Ostrovski Date: Fri, 11 Oct 2024 16:43:26 +0300 Subject: [PATCH] =?UTF-8?q?test(vm):=20Add=20tests=20for=20EVM=20emulator?= =?UTF-8?q?=20=E2=80=93=20multivm=20(#3045)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What ❔ Adds unit tests for the `multivm` crate testing a mock EVM emulator. ## Why ❔ Ensures that EVM emulation will work as expected. ## Checklist - [x] PR title corresponds to the body of PR (we generate changelog entries from PRs). - [x] Tests for the changes have been added / updated. - [x] Documentation comments have been added / updated. - [x] Code has been formatted via `zk_supervisor fmt` and `zk_supervisor lint`. --- Cargo.lock | 1 + core/lib/multivm/Cargo.toml | 1 + .../versions/vm_latest/tests/evm_emulator.rs | 453 +++++++++++++++++- core/node/consensus/src/registry/tests.rs | 2 +- core/node/consensus/src/tests/attestation.rs | 4 +- .../contracts/mock-evm/mock-evm.sol | 159 +++++- 6 files changed, 602 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bd9f2d5ef28..7b1a268afef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10488,6 +10488,7 @@ dependencies = [ "itertools 0.10.5", "once_cell", "pretty_assertions", + "test-casing", "thiserror", "tracing", "vise", diff --git a/core/lib/multivm/Cargo.toml b/core/lib/multivm/Cargo.toml index ab418d24cd1..e49086a6b8b 100644 --- a/core/lib/multivm/Cargo.toml +++ b/core/lib/multivm/Cargo.toml @@ -42,5 +42,6 @@ ethabi.workspace = true [dev-dependencies] assert_matches.workspace = true pretty_assertions.workspace = true +test-casing.workspace = true zksync_test_account.workspace = true zksync_eth_signer.workspace = true diff --git a/core/lib/multivm/src/versions/vm_latest/tests/evm_emulator.rs b/core/lib/multivm/src/versions/vm_latest/tests/evm_emulator.rs index ca8157b170d..4316558eda2 100644 --- a/core/lib/multivm/src/versions/vm_latest/tests/evm_emulator.rs +++ b/core/lib/multivm/src/versions/vm_latest/tests/evm_emulator.rs @@ -1,28 +1,44 @@ +use std::collections::HashMap; + use ethabi::Token; -use zksync_contracts::read_bytecode; -use zksync_system_constants::{CONTRACT_DEPLOYER_ADDRESS, KNOWN_CODES_STORAGE_ADDRESS}; -use zksync_types::{get_code_key, get_known_code_key, Execute, H256}; -use zksync_utils::{be_words_to_bytes, bytecode::hash_bytecode, h256_to_u256}; -use zksync_vm_interface::VmInterfaceExt; +use test_casing::{test_casing, Product}; +use zksync_contracts::{load_contract, read_bytecode, SystemContractCode}; +use zksync_system_constants::{ + CONTRACT_DEPLOYER_ADDRESS, KNOWN_CODES_STORAGE_ADDRESS, L2_BASE_TOKEN_ADDRESS, +}; +use zksync_test_account::TxType; +use zksync_types::{ + get_code_key, get_known_code_key, + utils::{key_for_eth_balance, storage_key_for_eth_balance}, + AccountTreeId, Address, Execute, StorageKey, H256, U256, +}; +use zksync_utils::{be_words_to_bytes, bytecode::hash_bytecode, bytes_to_be_words, h256_to_u256}; use crate::{ - interface::{storage::InMemoryStorage, TxExecutionMode}, + interface::{ + storage::InMemoryStorage, TxExecutionMode, VmExecutionResultAndLogs, VmInterfaceExt, + }, versions::testonly::default_system_env, - vm_latest::{tests::tester::VmTesterBuilder, utils::hash_evm_bytecode, HistoryEnabled}, + vm_latest::{ + tests::tester::{VmTester, VmTesterBuilder}, + utils::hash_evm_bytecode, + HistoryEnabled, + }, }; const MOCK_DEPLOYER_PATH: &str = "etc/contracts-test-data/artifacts-zk/contracts/mock-evm/mock-evm.sol/MockContractDeployer.json"; const MOCK_KNOWN_CODE_STORAGE_PATH: &str = "etc/contracts-test-data/artifacts-zk/contracts/mock-evm/mock-evm.sol/MockKnownCodeStorage.json"; +const MOCK_EMULATOR_PATH: &str = + "etc/contracts-test-data/artifacts-zk/contracts/mock-evm/mock-evm.sol/MockEvmEmulator.json"; +const RECURSIVE_CONTRACT_PATH: &str = "etc/contracts-test-data/artifacts-zk/contracts/mock-evm/mock-evm.sol/NativeRecursiveContract.json"; +const INCREMENTING_CONTRACT_PATH: &str = "etc/contracts-test-data/artifacts-zk/contracts/mock-evm/mock-evm.sol/IncrementingContract.json"; -#[test] -fn tracing_evm_contract_deployment() { +fn override_system_contracts(storage: &mut InMemoryStorage) { let mock_deployer = read_bytecode(MOCK_DEPLOYER_PATH); let mock_deployer_hash = hash_bytecode(&mock_deployer); let mock_known_code_storage = read_bytecode(MOCK_KNOWN_CODE_STORAGE_PATH); let mock_known_code_storage_hash = hash_bytecode(&mock_known_code_storage); - // Override - let mut storage = InMemoryStorage::with_system_contracts(hash_bytecode); storage.set_value(get_code_key(&CONTRACT_DEPLOYER_ADDRESS), mock_deployer_hash); storage.set_value( get_known_code_key(&mock_deployer_hash), @@ -38,6 +54,81 @@ fn tracing_evm_contract_deployment() { ); storage.store_factory_dep(mock_deployer_hash, mock_deployer); storage.store_factory_dep(mock_known_code_storage_hash, mock_known_code_storage); +} + +#[derive(Debug)] +struct EvmTestBuilder { + deploy_emulator: bool, + storage: InMemoryStorage, + evm_contract_addresses: Vec
, +} + +impl EvmTestBuilder { + fn new(deploy_emulator: bool, evm_contract_address: Address) -> Self { + Self { + deploy_emulator, + storage: InMemoryStorage::with_system_contracts(hash_bytecode), + evm_contract_addresses: vec![evm_contract_address], + } + } + + fn with_mock_deployer(mut self) -> Self { + override_system_contracts(&mut self.storage); + self + } + + fn with_evm_address(mut self, address: Address) -> Self { + self.evm_contract_addresses.push(address); + self + } + + fn build(self) -> VmTester { + let mock_emulator = read_bytecode(MOCK_EMULATOR_PATH); + let mut storage = self.storage; + let mut system_env = default_system_env(); + if self.deploy_emulator { + let evm_bytecode: Vec<_> = (0..=u8::MAX).collect(); + let evm_bytecode_hash = hash_evm_bytecode(&evm_bytecode); + storage.set_value( + get_known_code_key(&evm_bytecode_hash), + H256::from_low_u64_be(1), + ); + for evm_address in self.evm_contract_addresses { + storage.set_value(get_code_key(&evm_address), evm_bytecode_hash); + } + + system_env.base_system_smart_contracts.evm_emulator = Some(SystemContractCode { + hash: hash_bytecode(&mock_emulator), + code: bytes_to_be_words(mock_emulator), + }); + } else { + let emulator_hash = hash_bytecode(&mock_emulator); + storage.set_value(get_known_code_key(&emulator_hash), H256::from_low_u64_be(1)); + storage.store_factory_dep(emulator_hash, mock_emulator); + + for evm_address in self.evm_contract_addresses { + storage.set_value(get_code_key(&evm_address), emulator_hash); + // Set `isUserSpace` in the emulator storage to `true`, so that it skips emulator-specific checks + storage.set_value( + StorageKey::new(AccountTreeId::new(evm_address), H256::zero()), + H256::from_low_u64_be(1), + ); + } + } + + VmTesterBuilder::new(HistoryEnabled) + .with_system_env(system_env) + .with_storage(storage) + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_random_rich_accounts(1) + .build() + } +} + +#[test] +fn tracing_evm_contract_deployment() { + let mut storage = InMemoryStorage::with_system_contracts(hash_bytecode); + override_system_contracts(&mut storage); let mut system_env = default_system_env(); // The EVM emulator will not be accessed, so we set it to a dummy value. @@ -74,3 +165,343 @@ fn tracing_evm_contract_deployment() { evm_bytecode ); } + +#[test] +fn mock_emulator_basics() { + let called_address = Address::repeat_byte(0x23); + let mut vm = EvmTestBuilder::new(true, called_address).build(); + let account = &mut vm.rich_accounts[0]; + let tx = account.get_l2_tx_for_execute( + Execute { + contract_address: Some(called_address), + calldata: vec![], + value: 0.into(), + factory_deps: vec![], + }, + None, + ); + + let (_, vm_result) = vm + .vm + .execute_transaction_with_bytecode_compression(tx, true); + assert!(!vm_result.result.is_failed(), "{:?}", vm_result.result); +} + +const RECIPIENT_ADDRESS: Address = Address::repeat_byte(0x12); + +/// `deploy_emulator = false` here and below tests the mock emulator as an ordinary contract (i.e., sanity-checks its logic). +#[test_casing(2, [false, true])] +#[test] +fn mock_emulator_with_payment(deploy_emulator: bool) { + let mock_emulator_abi = load_contract(MOCK_EMULATOR_PATH); + let mut vm = EvmTestBuilder::new(deploy_emulator, RECIPIENT_ADDRESS).build(); + + let mut current_balance = U256::zero(); + for i in 1_u64..=5 { + let transferred_value = (1_000_000_000 * i).into(); + let vm_result = test_payment( + &mut vm, + &mock_emulator_abi, + &mut current_balance, + transferred_value, + ); + + let balance_storage_logs = vm_result.logs.storage_logs.iter().filter_map(|log| { + (*log.log.key.address() == L2_BASE_TOKEN_ADDRESS) + .then_some((*log.log.key.key(), h256_to_u256(log.log.value))) + }); + let balances: HashMap<_, _> = balance_storage_logs.collect(); + assert_eq!( + balances[&key_for_eth_balance(&RECIPIENT_ADDRESS)], + current_balance + ); + } +} + +fn test_payment( + vm: &mut VmTester, + mock_emulator_abi: ðabi::Contract, + balance: &mut U256, + transferred_value: U256, +) -> VmExecutionResultAndLogs { + *balance += transferred_value; + let test_payment_fn = mock_emulator_abi.function("testPayment").unwrap(); + let account = &mut vm.rich_accounts[0]; + let tx = account.get_l2_tx_for_execute( + Execute { + contract_address: Some(RECIPIENT_ADDRESS), + calldata: test_payment_fn + .encode_input(&[Token::Uint(transferred_value), Token::Uint(*balance)]) + .unwrap(), + value: transferred_value, + factory_deps: vec![], + }, + None, + ); + + let (_, vm_result) = vm + .vm + .execute_transaction_with_bytecode_compression(tx, true); + assert!(!vm_result.result.is_failed(), "{vm_result:?}"); + vm_result +} + +#[test_casing(4, Product(([false, true], [false, true])))] +#[test] +fn mock_emulator_with_recursion(deploy_emulator: bool, is_external: bool) { + let mock_emulator_abi = load_contract(MOCK_EMULATOR_PATH); + let recipient_address = Address::repeat_byte(0x12); + let mut vm = EvmTestBuilder::new(deploy_emulator, recipient_address).build(); + let account = &mut vm.rich_accounts[0]; + + let test_recursion_fn = mock_emulator_abi + .function(if is_external { + "testExternalRecursion" + } else { + "testRecursion" + }) + .unwrap(); + let mut expected_value = U256::one(); + let depth = 50_u32; + for i in 2..=depth { + expected_value *= i; + } + + let factory_deps = if is_external { + vec![read_bytecode(RECURSIVE_CONTRACT_PATH)] + } else { + vec![] + }; + let tx = account.get_l2_tx_for_execute( + Execute { + contract_address: Some(recipient_address), + calldata: test_recursion_fn + .encode_input(&[Token::Uint(depth.into()), Token::Uint(expected_value)]) + .unwrap(), + value: 0.into(), + factory_deps, + }, + None, + ); + let (_, vm_result) = vm + .vm + .execute_transaction_with_bytecode_compression(tx, true); + assert!(!vm_result.result.is_failed(), "{vm_result:?}"); +} + +#[test] +fn calling_to_mock_emulator_from_native_contract() { + let recipient_address = Address::repeat_byte(0x12); + let mut vm = EvmTestBuilder::new(true, recipient_address).build(); + let account = &mut vm.rich_accounts[0]; + + // Deploy a native contract. + let native_contract = read_bytecode(RECURSIVE_CONTRACT_PATH); + let native_contract_abi = load_contract(RECURSIVE_CONTRACT_PATH); + let deploy_tx = account.get_deploy_tx( + &native_contract, + Some(&[Token::Address(recipient_address)]), + TxType::L2, + ); + let (_, vm_result) = vm + .vm + .execute_transaction_with_bytecode_compression(deploy_tx.tx, true); + assert!(!vm_result.result.is_failed(), "{:?}", vm_result.result); + + // Call from the native contract to the EVM emulator. + let test_fn = native_contract_abi.function("recurse").unwrap(); + let test_tx = account.get_l2_tx_for_execute( + Execute { + contract_address: Some(deploy_tx.address), + calldata: test_fn.encode_input(&[Token::Uint(50.into())]).unwrap(), + value: Default::default(), + factory_deps: vec![], + }, + None, + ); + let (_, vm_result) = vm + .vm + .execute_transaction_with_bytecode_compression(test_tx, true); + assert!(!vm_result.result.is_failed(), "{:?}", vm_result.result); +} + +#[test] +fn mock_emulator_with_deployment() { + let contract_address = Address::repeat_byte(0xaa); + let mut vm = EvmTestBuilder::new(true, contract_address) + .with_mock_deployer() + .build(); + let account = &mut vm.rich_accounts[0]; + + let mock_emulator_abi = load_contract(MOCK_EMULATOR_PATH); + let new_evm_bytecode = vec![0xfe; 96]; + let new_evm_bytecode_hash = hash_evm_bytecode(&new_evm_bytecode); + + let test_fn = mock_emulator_abi.function("testDeploymentAndCall").unwrap(); + let test_tx = account.get_l2_tx_for_execute( + Execute { + contract_address: Some(contract_address), + calldata: test_fn + .encode_input(&[ + Token::FixedBytes(new_evm_bytecode_hash.0.into()), + Token::Bytes(new_evm_bytecode.clone()), + ]) + .unwrap(), + value: 0.into(), + factory_deps: vec![], + }, + None, + ); + let (_, vm_result) = vm + .vm + .execute_transaction_with_bytecode_compression(test_tx, true); + assert!(!vm_result.result.is_failed(), "{vm_result:?}"); + + let factory_deps = vm_result.new_known_factory_deps.unwrap(); + assert_eq!( + factory_deps, + HashMap::from([(new_evm_bytecode_hash, new_evm_bytecode)]) + ); +} + +#[test] +fn mock_emulator_with_delegate_call() { + let evm_contract_address = Address::repeat_byte(0xaa); + let other_evm_contract_address = Address::repeat_byte(0xbb); + let mut builder = EvmTestBuilder::new(true, evm_contract_address); + builder.storage.set_value( + storage_key_for_eth_balance(&evm_contract_address), + H256::from_low_u64_be(1_000_000), + ); + builder.storage.set_value( + storage_key_for_eth_balance(&other_evm_contract_address), + H256::from_low_u64_be(2_000_000), + ); + let mut vm = builder.with_evm_address(other_evm_contract_address).build(); + let account = &mut vm.rich_accounts[0]; + + // Deploy a native contract. + let native_contract = read_bytecode(INCREMENTING_CONTRACT_PATH); + let native_contract_abi = load_contract(INCREMENTING_CONTRACT_PATH); + let deploy_tx = account.get_deploy_tx(&native_contract, None, TxType::L2); + let (_, vm_result) = vm + .vm + .execute_transaction_with_bytecode_compression(deploy_tx.tx, true); + assert!(!vm_result.result.is_failed(), "{:?}", vm_result.result); + + let test_fn = native_contract_abi.function("testDelegateCall").unwrap(); + // Delegate to the native contract from EVM. + test_delegate_call(&mut vm, test_fn, evm_contract_address, deploy_tx.address); + // Delegate to EVM from the native contract. + test_delegate_call(&mut vm, test_fn, deploy_tx.address, evm_contract_address); + // Delegate to EVM from EVM. + test_delegate_call( + &mut vm, + test_fn, + evm_contract_address, + other_evm_contract_address, + ); +} + +fn test_delegate_call( + vm: &mut VmTester, + test_fn: ðabi::Function, + from: Address, + to: Address, +) { + let account = &mut vm.rich_accounts[0]; + let test_tx = account.get_l2_tx_for_execute( + Execute { + contract_address: Some(from), + calldata: test_fn.encode_input(&[Token::Address(to)]).unwrap(), + value: 0.into(), + factory_deps: vec![], + }, + None, + ); + let (_, vm_result) = vm + .vm + .execute_transaction_with_bytecode_compression(test_tx, true); + assert!(!vm_result.result.is_failed(), "{vm_result:?}"); +} + +#[test] +fn mock_emulator_with_static_call() { + let evm_contract_address = Address::repeat_byte(0xaa); + let other_evm_contract_address = Address::repeat_byte(0xbb); + let mut builder = EvmTestBuilder::new(true, evm_contract_address); + builder.storage.set_value( + storage_key_for_eth_balance(&evm_contract_address), + H256::from_low_u64_be(1_000_000), + ); + builder.storage.set_value( + storage_key_for_eth_balance(&other_evm_contract_address), + H256::from_low_u64_be(2_000_000), + ); + // Set differing read values for tested contracts. The slot index is defined in the contract. + let value_slot = H256::from_low_u64_be(0x123); + builder.storage.set_value( + StorageKey::new(AccountTreeId::new(evm_contract_address), value_slot), + H256::from_low_u64_be(100), + ); + builder.storage.set_value( + StorageKey::new(AccountTreeId::new(other_evm_contract_address), value_slot), + H256::from_low_u64_be(200), + ); + let mut vm = builder.with_evm_address(other_evm_contract_address).build(); + let account = &mut vm.rich_accounts[0]; + + // Deploy a native contract. + let native_contract = read_bytecode(INCREMENTING_CONTRACT_PATH); + let native_contract_abi = load_contract(INCREMENTING_CONTRACT_PATH); + let deploy_tx = account.get_deploy_tx(&native_contract, None, TxType::L2); + let (_, vm_result) = vm + .vm + .execute_transaction_with_bytecode_compression(deploy_tx.tx, true); + assert!(!vm_result.result.is_failed(), "{:?}", vm_result.result); + + let test_fn = native_contract_abi.function("testStaticCall").unwrap(); + // Call to the native contract from EVM. + test_static_call(&mut vm, test_fn, evm_contract_address, deploy_tx.address, 0); + // Call to EVM from the native contract. + test_static_call( + &mut vm, + test_fn, + deploy_tx.address, + evm_contract_address, + 100, + ); + // Call to EVM from EVM. + test_static_call( + &mut vm, + test_fn, + evm_contract_address, + other_evm_contract_address, + 200, + ); +} + +fn test_static_call( + vm: &mut VmTester, + test_fn: ðabi::Function, + from: Address, + to: Address, + expected_value: u64, +) { + let account = &mut vm.rich_accounts[0]; + let test_tx = account.get_l2_tx_for_execute( + Execute { + contract_address: Some(from), + calldata: test_fn + .encode_input(&[Token::Address(to), Token::Uint(expected_value.into())]) + .unwrap(), + value: 0.into(), + factory_deps: vec![], + }, + None, + ); + let (_, vm_result) = vm + .vm + .execute_transaction_with_bytecode_compression(test_tx, true); + assert!(!vm_result.result.is_failed(), "{vm_result:?}"); +} diff --git a/core/node/consensus/src/registry/tests.rs b/core/node/consensus/src/registry/tests.rs index 33392a7f206..89afc20e1d5 100644 --- a/core/node/consensus/src/registry/tests.rs +++ b/core/node/consensus/src/registry/tests.rs @@ -80,7 +80,7 @@ async fn test_attester_committee() { .wrap("wait_for_batch_info()")?; // Read the attester committee using the vm. - let batch = attester::BatchNumber(node.last_batch().0.into()); + let batch = attester::BatchNumber(node.last_batch().0); assert_eq!( Some(committee), registry diff --git a/core/node/consensus/src/tests/attestation.rs b/core/node/consensus/src/tests/attestation.rs index bd3886bd4c8..2701a986e9e 100644 --- a/core/node/consensus/src/tests/attestation.rs +++ b/core/node/consensus/src/tests/attestation.rs @@ -79,7 +79,7 @@ async fn test_attestation_status_api(version: ProtocolVersionId) { let status = fetch_status().await?; assert_eq!( status.next_batch_to_attest, - attester::BatchNumber(first_batch.0.into()) + attester::BatchNumber(first_batch.0) ); tracing::info!("Insert a cert"); @@ -237,7 +237,7 @@ async fn test_multiple_attesters(version: ProtocolVersionId, pregenesis: bool) { } tracing::info!("Wait for the batches to be attested"); - let want_last = attester::BatchNumber(validator.last_sealed_batch().0.into()); + let want_last = attester::BatchNumber(validator.last_sealed_batch().0); validator_pool .wait_for_batch_certificates_and_verify(ctx, want_last, Some(registry_addr)) .await?; diff --git a/etc/contracts-test-data/contracts/mock-evm/mock-evm.sol b/etc/contracts-test-data/contracts/mock-evm/mock-evm.sol index 5f4de59681f..baa0d37b753 100644 --- a/etc/contracts-test-data/contracts/mock-evm/mock-evm.sol +++ b/etc/contracts-test-data/contracts/mock-evm/mock-evm.sol @@ -68,7 +68,7 @@ contract MockContractDeployer { Version1 } - address constant CODE_ORACLE_ADDR = address(0x8012); + IAccountCodeStorage constant ACCOUNT_CODE_STORAGE_CONTRACT = IAccountCodeStorage(address(0x8002)); MockKnownCodeStorage constant KNOWN_CODE_STORAGE_CONTRACT = MockKnownCodeStorage(address(0x8004)); /// The returned value is obviously incorrect in the general case, but works well enough when called by the bootloader. @@ -78,15 +78,166 @@ contract MockContractDeployer { /// Replaces real deployment with publishing a surrogate EVM "bytecode". /// @param _salt bytecode hash - /// @param _bytecodeHash ignored, since it's not possible to set arbitrarily /// @param _input bytecode to publish function create( bytes32 _salt, - bytes32 _bytecodeHash, + bytes32, // ignored, since it's not possible to set arbitrarily bytes calldata _input ) external payable returns (address) { KNOWN_CODE_STORAGE_CONTRACT.setEVMBytecodeHash(_salt); KNOWN_CODE_STORAGE_CONTRACT.publishEVMBytecode(_input); - return address(0); + address newAddress = address(uint160(msg.sender) + 1); + ACCOUNT_CODE_STORAGE_CONTRACT.storeAccountConstructedCodeHash(newAddress, _salt); + return newAddress; + } +} + +interface IAccountCodeStorage { + function getRawCodeHash(address _address) external view returns (bytes32); + function storeAccountConstructedCodeHash(address _address, bytes32 _hash) external; +} + +interface IRecursiveContract { + function recurse(uint _depth) external returns (uint); +} + +/// Native incrementing library. Not actually a library to simplify deployment. +contract IncrementingContract { + // Should not collide with other storage slots + uint constant INCREMENTED_SLOT = 0x123; + + function getIncrementedValue() public view returns (uint _value) { + assembly { + _value := sload(INCREMENTED_SLOT) + } + } + + function increment(address _thisAddress, uint _thisBalance) external { + require(msg.sender == tx.origin, "msg.sender not retained"); + require(address(this) == _thisAddress, "this address"); + require(address(this).balance == _thisBalance, "this balance"); + assembly { + sstore(INCREMENTED_SLOT, add(sload(INCREMENTED_SLOT), 1)) + } + } + + /// Tests delegation to a native or EVM contract at the specified target. + function testDelegateCall(address _target) external { + uint valueSnapshot = getIncrementedValue(); + (bool success, ) = _target.delegatecall(abi.encodeCall( + IncrementingContract.increment, + (address(this), address(this).balance) + )); + require(success, "delegatecall reverted"); + require(getIncrementedValue() == valueSnapshot + 1, "invalid value"); + } + + function testStaticCall(address _target, uint _expectedValue) external { + (bool success, bytes memory rawValue) = _target.staticcall(abi.encodeCall( + this.getIncrementedValue, + () + )); + require(success, "static call reverted"); + (uint value) = abi.decode(rawValue, (uint)); + require(value == _expectedValue, "value mismatch"); + + (success, ) = _target.staticcall(abi.encodeCall( + IncrementingContract.increment, + (address(this), address(this).balance) + )); + require(!success, "staticcall should've reverted"); + } +} + +uint constant EVM_EMULATOR_STIPEND = 1 << 30; + +/** + * Mock EVM emulator used in low-level tests. + */ +contract MockEvmEmulator is IRecursiveContract, IncrementingContract { + IAccountCodeStorage constant ACCOUNT_CODE_STORAGE_CONTRACT = IAccountCodeStorage(address(0x8002)); + + /// Set to `true` for testing logic sanity. + bool isUserSpace; + + modifier validEvmEntry() { + if (!isUserSpace) { + require(gasleft() >= EVM_EMULATOR_STIPEND, "no stipend"); + // Fetch bytecode for the executed contract. + bytes32 bytecodeHash = ACCOUNT_CODE_STORAGE_CONTRACT.getRawCodeHash(address(this)); + require(bytecodeHash != bytes32(0), "called contract not deployed"); + uint bytecodeVersion = uint(bytecodeHash) >> 248; + require(bytecodeVersion == 2, "non-EVM bytecode"); + + // Check that members of the current address are well-defined. + require(address(this).code.length != 0, "invalid code"); + require(address(this).codehash == bytecodeHash, "bytecode hash mismatch"); + } + _; + } + + function testPayment(uint _expectedValue, uint _expectedBalance) public payable validEvmEntry { + require(msg.value == _expectedValue, "unexpected msg.value"); + require(address(this).balance == _expectedBalance, "unexpected balance"); + } + + IRecursiveContract recursionTarget; + + function recurse(uint _depth) public validEvmEntry returns (uint) { + require(gasleft() < 2 * EVM_EMULATOR_STIPEND, "stipend provided multiple times"); + + if (_depth <= 1) { + return 1; + } else { + IRecursiveContract target = (address(recursionTarget) == address(0)) ? this : recursionTarget; + // The real emulator limits amount of gas when performing far calls by EVM gas, so we emulate this behavior as well. + uint gasToSend = isUserSpace ? gasleft() : (gasleft() - EVM_EMULATOR_STIPEND); + return target.recurse{gas: gasToSend}(_depth - 1) * _depth; + } + } + + function testRecursion(uint _depth, uint _expectedValue) external validEvmEntry returns (uint) { + require(recurse(_depth) == _expectedValue, "incorrect recursion"); + } + + function testExternalRecursion(uint _depth, uint _expectedValue) external validEvmEntry returns (uint) { + recursionTarget = new NativeRecursiveContract(IRecursiveContract(this)); + uint returnedValue = recurse(_depth); + recursionTarget = this; // This won't work on revert, but for tests, it's good enough + require(returnedValue == _expectedValue, "incorrect recursion"); + } + + MockContractDeployer constant CONTRACT_DEPLOYER_CONTRACT = MockContractDeployer(address(0x8006)); + + /// Emulates EVM contract deployment and a subsequent call to it in a single transaction. + function testDeploymentAndCall(bytes32 _evmBytecodeHash, bytes calldata _evmBytecode) external validEvmEntry { + IRecursiveContract newContract = IRecursiveContract(CONTRACT_DEPLOYER_CONTRACT.create( + _evmBytecodeHash, + _evmBytecodeHash, + _evmBytecode + )); + require(uint160(address(newContract)) == uint160(address(this)) + 1, "unexpected address"); + require(address(newContract).code.length > 0, "contract code length"); + require(address(newContract).codehash != bytes32(0), "contract code hash"); + + uint gasToSend = gasleft() - EVM_EMULATOR_STIPEND; + require(newContract.recurse{gas: gasToSend}(5) == 120, "unexpected recursive result"); + } + + fallback() external validEvmEntry { + require(msg.data.length == 0, "unsupported call"); + } +} + +contract NativeRecursiveContract is IRecursiveContract { + IRecursiveContract target; + + constructor(IRecursiveContract _target) { + target = _target; + } + + function recurse(uint _depth) external returns (uint) { + require(gasleft() < EVM_EMULATOR_STIPEND, "stipend spilled to native contract"); + return (_depth <= 1) ? 1 : target.recurse(_depth - 1) * _depth; } }