diff --git a/crates/forge/tests/cli/zk_cmd.rs b/crates/forge/tests/cli/zk_cmd.rs index f8f8c2c8e..0f5f3343e 100644 --- a/crates/forge/tests/cli/zk_cmd.rs +++ b/crates/forge/tests/cli/zk_cmd.rs @@ -122,3 +122,55 @@ contract CallEmptyCode is Test { .stdout_lossy() .contains("call may fail or behave unexpectedly due to empty code"); }); + +forgetest_async!(test_zk_can_send_eth_to_eoa_without_warnings, |prj, cmd| { + foundry_test_utils::util::initialize(prj.root()); + prj.add_test( + "SendEthToEOA.t.sol", + r#" +import "forge-std/Test.sol"; + +contract SendEthToEOA is Test { + function testSendEthToEOA() external { + address eoa = makeAddr("Juan's Account"); + vm.deal(address(this), 1 ether); + + (bool success,) = eoa.call{value: 1 ether}(""); + assertTrue(success, "ETH transfer failed"); + } +} +"#, + ) + .unwrap(); + + cmd.args(["test", "--zksync", "--match-test", "testSendEthToEOA"]); + let output = cmd.assert_success().get_output().stdout_lossy(); + + assert!(!output.contains("call may fail or behave unexpectedly due to empty code")); +}); + +forgetest_async!(test_zk_calling_empty_code_with_zero_value_issues_warning, |prj, cmd| { + foundry_test_utils::util::initialize(prj.root()); + prj.add_test( + "CallEmptyCodeWithZeroValue.t.sol", + r#" +import "forge-std/Test.sol"; + +contract CallEmptyCodeWithZeroValue is Test { + function testCallEmptyCodeWithZeroValue() external { + address eoa = makeAddr("Juan's Account"); + vm.deal(address(this), 1 ether); + + (bool success,) = eoa.call(""); + assertTrue(success, "call failed"); + } +} +"#, + ) + .unwrap(); + + cmd.args(["test", "--zksync", "--match-test", "testCallEmptyCodeWithZeroValue"]); + let output = cmd.assert_success().get_output().stdout_lossy(); + + assert!(output.contains("call may fail or behave unexpectedly due to empty code")); +}); diff --git a/crates/zksync/core/src/vm/tracers/cheatcode.rs b/crates/zksync/core/src/vm/tracers/cheatcode.rs index a2becb77e..873f7c835 100644 --- a/crates/zksync/core/src/vm/tracers/cheatcode.rs +++ b/crates/zksync/core/src/vm/tracers/cheatcode.rs @@ -167,11 +167,22 @@ impl CheatcodeTracer { } /// Check if the given address's code is empty - fn has_empty_code(&self, storage: StoragePtr, target: Address) -> bool { + fn has_empty_code( + &self, + storage: StoragePtr, + target: Address, + calldata: &[u8], + value: rU256, + ) -> bool { // The following addresses are expected to have empty bytecode let ignored_known_addresses = [foundry_evm_abi::HARDHAT_CONSOLE_ADDRESS, self.call_context.tx_caller]; + // Skip empty code check for empty calldata with non-zero value (Transfers) + if calldata.is_empty() && !value.is_zero() { + return false; + } + let contract_code = storage.borrow_mut().read_value(&get_code_key(&target.to_h160())); !ignored_known_addresses.contains(&target) && @@ -295,7 +306,7 @@ impl DynTracer> for Cheatcode // if we get here there was no matching mock call, // so we check if there's no code at the mocked address - if self.has_empty_code(storage, call_contract) { + if self.has_empty_code(storage, call_contract, &call_input, call_value) { // issue a more targeted // error if we already had some mocks there let had_mocks_message =