diff --git a/crates/forge/tests/fixtures/zk/Create2.t.sol b/crates/forge/tests/fixtures/zk/Create2.t.sol new file mode 100644 index 000000000..099c9096f --- /dev/null +++ b/crates/forge/tests/fixtures/zk/Create2.t.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.18; + +import "forge-std/Test.sol"; +import {L2ContractHelper} from "era-contracts/l2-contracts/contracts/L2ContractHelper.sol"; // =0.8.20 + +import {Greeter} from "../src/Greeter.sol"; +import {CustomNumber} from "../src/CustomNumber.sol"; + +import {Create2Utils} from "../src/Create2Utils.sol"; + +contract Create2Test is Test { + function getBytecodeHash(string memory path) internal returns (bytes32 bytecodeHash) { + string memory artifact = vm.readFile(path); + bytecodeHash = vm.parseJsonBytes32(artifact, ".hash"); + } + + function testCanDeployViaCreate2() public { + bytes32 bytecodeHash = getBytecodeHash("zkout/Greeter.sol/Greeter.json"); + address sender = address(0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496); + bytes32 salt = "12345"; + bytes32 constructorInputHash = keccak256(abi.encode()); + + address computedAddress = + Create2Utils.computeCreate2Address(sender, salt, bytes32(bytecodeHash), constructorInputHash); + + // deploy via create2 + address actualAddress = address(new Greeter{salt: salt}()); + + assertEq(actualAddress, computedAddress); + } + + function testComputeCreate2WithNoArgs() external { + bytes32 salt = bytes32(0x0); + + bytes32 bytecodeHash = getBytecodeHash("zkout/Greeter.sol/Greeter.json"); + + address computedAddress = + Create2Utils.computeCreate2Address(address(this), salt, bytes32(bytecodeHash), keccak256(abi.encode())); + address expectedAddress = + L2ContractHelper.computeCreate2Address(address(this), salt, bytes32(bytecodeHash), keccak256(abi.encode())); + + address actualAddress = address(new Greeter{salt: salt}()); + assertEq(actualAddress, expectedAddress); + assertEq(computedAddress, expectedAddress); + } + + function testComputeCreate2WithArgs() external { + bytes32 salt = bytes32(0x0); + uint8 value = 42; + + bytes32 bytecodeHash = getBytecodeHash("zkout/CustomNumber.sol/CustomNumber.json"); + + address computedAddress = + Create2Utils.computeCreate2Address(address(this), salt, bytecodeHash, keccak256(abi.encode(value))); + address expectedAddress = + L2ContractHelper.computeCreate2Address(address(this), salt, bytecodeHash, keccak256(abi.encode(value))); + + CustomNumber num = new CustomNumber{salt: salt}(value); + assertEq(address(num), expectedAddress); + assertEq(computedAddress, expectedAddress); + assertEq(num.number(), value); + } +} diff --git a/crates/forge/tests/it/zk/contracts.rs b/crates/forge/tests/it/zk/contracts.rs index 373fab699..0f7959c03 100644 --- a/crates/forge/tests/it/zk/contracts.rs +++ b/crates/forge/tests/it/zk/contracts.rs @@ -3,7 +3,7 @@ use crate::{config::*, test_helpers::TEST_DATA_DEFAULT}; use forge::revm::primitives::SpecId; use foundry_config::fs_permissions::PathPermission; -use foundry_test_utils::Filter; +use foundry_test_utils::{util, Filter}; #[tokio::test(flavor = "multi_thread")] async fn test_zk_contract_can_call_function() { @@ -52,12 +52,33 @@ async fn test_zk_contract_deployment_balance_transfer() { #[tokio::test(flavor = "multi_thread")] async fn test_zk_contract_create2() { - let mut zk_config = TEST_DATA_DEFAULT.zk_test_data.zk_config.clone(); - zk_config.fs_permissions.add(PathPermission::read_write("./zk/zkout/ConstantNumber.sol")); - let runner = TEST_DATA_DEFAULT.runner_with_zksync_config(zk_config); - let filter = Filter::new("testZkContractsCreate2", "ZkContractsTest", ".*"); + let (prj, mut cmd) = util::setup_forge( + "test_zk_contract_create2_with_deps", + foundry_test_utils::foundry_compilers::PathStyle::Dapptools, + ); + util::initialize(prj.root()); - TestConfig::with_filter(runner, filter).evm_spec(SpecId::SHANGHAI).run().await; + cmd.args(["install", "matter-labs/era-contracts", "--no-commit", "--shallow"]) + .ensure_execute_success() + .expect("able to install dependencies"); + cmd.forge_fuse(); + + let mut config = cmd.config(); + config.fs_permissions.add(PathPermission::read("./zkout")); + prj.write_config(config); + + prj.add_source("Greeter.sol", include_str!("../../../../../testdata/zk/Greeter.sol")).unwrap(); + + prj.add_source("CustomNumber.sol", include_str!("../../../../../testdata/zk/CustomNumber.sol")) + .unwrap(); + + prj.add_source("Create2Utils.sol", include_str!("../../../../../testdata/zk/Create2Utils.sol")) + .unwrap(); + + prj.add_test("Create2.t.sol", include_str!("../../fixtures/zk/Create2.t.sol")).unwrap(); + + cmd.args(["test", "--zk-startup", "--evm-version", "shanghai", "--mc", "Create2Test"]); + assert!(cmd.stdout_lossy().contains("Suite result: ok")); } #[tokio::test(flavor = "multi_thread")] diff --git a/crates/forge/tests/it/zk/create.rs b/crates/forge/tests/it/zk/create.rs new file mode 100644 index 000000000..83569a6bb --- /dev/null +++ b/crates/forge/tests/it/zk/create.rs @@ -0,0 +1,55 @@ +use foundry_test_utils::{forgetest_async, util, ZkSyncNode}; + +forgetest_async!(forge_zk_can_deploy_erc20, |prj, cmd| { + util::initialize(prj.root()); + prj.add_source("ERC20.sol", include_str!("../../../../../testdata/zk/ERC20.sol")).unwrap(); + + let node = ZkSyncNode::start(); + let url = node.url(); + + let private_key = + ZkSyncNode::rich_wallets().next().map(|(_, pk, _)| pk).expect("No rich wallets available"); + + cmd.forge_fuse().args([ + "create", + "--zk-startup", + "./src/ERC20.sol:MyToken", + "--rpc-url", + url.as_str(), + "--private-key", + private_key, + ]); + + let (stdout, _) = cmd.output_lossy(); + assert!(stdout.contains("Deployer: ")); + assert!(stdout.contains("Deployed to: ")); +}); + +forgetest_async!(forge_zk_can_deploy_token_receiver, |prj, cmd| { + util::initialize(prj.root()); + prj.add_source( + "TokenReceiver.sol", + include_str!("../../../../../testdata/zk/TokenReceiver.sol"), + ) + .unwrap(); + + let node = ZkSyncNode::start(); + let url = node.url(); + + let private_key = + ZkSyncNode::rich_wallets().next().map(|(_, pk, _)| pk).expect("No rich wallets available"); + + cmd.forge_fuse().args([ + "create", + "--zk-startup", + "./src/TokenReceiver.sol:TokenReceiver", + "--rpc-url", + url.as_str(), + "--private-key", + private_key, + ]); + + let (stdout, _) = cmd.output_lossy(); + assert!(stdout.contains("Deployer: ")); + assert!(stdout.contains("Deployed to: ")); +}); diff --git a/crates/forge/tests/it/zk/mod.rs b/crates/forge/tests/it/zk/mod.rs index ff3aa883d..4149c7dfd 100644 --- a/crates/forge/tests/it/zk/mod.rs +++ b/crates/forge/tests/it/zk/mod.rs @@ -2,6 +2,7 @@ mod basic; mod cheats; mod contracts; +mod create; mod factory; mod factory_deps; mod fork; diff --git a/testdata/zk/Contracts.t.sol b/testdata/zk/Contracts.t.sol index 651d60e8b..0113def5e 100644 --- a/testdata/zk/Contracts.t.sol +++ b/testdata/zk/Contracts.t.sol @@ -6,6 +6,7 @@ import "../cheats/Vm.sol"; import {ConstantNumber} from "./ConstantNumber.sol"; import {Greeter} from "./Greeter.sol"; +import {CustomNumber} from "./CustomNumber.sol"; import {Globals} from "./Globals.sol"; interface ISystemContractDeployer { @@ -65,18 +66,6 @@ contract PayableFixedNumber { } } -contract CustomNumber { - uint8 value; - - constructor(uint8 _value) { - value = _value; - } - - function number() public view returns (uint8) { - return value; - } -} - contract CustomStorage { uint8 num; string str; @@ -209,23 +198,6 @@ contract ZkContractsTest is DSTest { require(customStorage.getNum() == 10, "era inline contract value mismatch (complex args)"); } - function testZkContractsCreate2() public { - vm.selectFork(forkEra); - - string memory artifact = vm.readFile("zk/zkout/ConstantNumber.sol/ConstantNumber.json"); - bytes32 bytecodeHash = vm.parseJsonBytes32(artifact, ".hash"); - address sender = address(0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496); - bytes32 salt = "12345"; - bytes32 constructorInputHash = keccak256(abi.encode()); - address expectedDeployedAddress = - _computeCreate2Address(sender, salt, bytes32(bytecodeHash), constructorInputHash); - - // deploy via create2 - address actualDeployedAddress = address(new ConstantNumber{salt: salt}()); - - assertEq(expectedDeployedAddress, actualDeployedAddress); - } - function testZkContractsCallSystemContract() public { (bool success,) = address(vm).call(abi.encodeWithSignature("zkVm(bool)", true)); require(success, "zkVm() call failed"); @@ -274,20 +246,4 @@ contract ZkContractsTest is DSTest { assert(vm.getNonce(sender) == startingNonce + 3); vm.stopBroadcast(); } - - function _computeCreate2Address( - address sender, - bytes32 salt, - bytes32 creationCodeHash, - bytes32 constructorInputHash - ) private pure returns (address) { - bytes32 zksync_create2_prefix = keccak256("zksyncCreate2"); - bytes32 address_hash = keccak256( - bytes.concat( - zksync_create2_prefix, bytes32(uint256(uint160(sender))), salt, creationCodeHash, constructorInputHash - ) - ); - - return address(uint160(uint256(address_hash))); - } } diff --git a/testdata/zk/Create2Utils.sol b/testdata/zk/Create2Utils.sol new file mode 100644 index 000000000..6d658731e --- /dev/null +++ b/testdata/zk/Create2Utils.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity >=0.8.7 <0.9.0; + +library Create2Utils { + function computeCreate2Address(address sender, bytes32 salt, bytes32 creationCodeHash, bytes32 constructorInputHash) + internal + pure + returns (address) + { + bytes32 zksync_create2_prefix = keccak256("zksyncCreate2"); + bytes32 address_hash = keccak256( + bytes.concat( + zksync_create2_prefix, bytes32(uint256(uint160(sender))), salt, creationCodeHash, constructorInputHash + ) + ); + + return address(uint160(uint256(address_hash))); + } +} diff --git a/testdata/zk/CustomNumber.sol b/testdata/zk/CustomNumber.sol new file mode 100644 index 000000000..0c82bde9b --- /dev/null +++ b/testdata/zk/CustomNumber.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.7 <0.9.0; + +contract CustomNumber { + uint8 value; + + constructor(uint8 _value) { + value = _value; + } + + function number() public view returns (uint8) { + return value; + } +} diff --git a/testdata/zk/ERC20.sol b/testdata/zk/ERC20.sol new file mode 100644 index 000000000..1fe10b382 --- /dev/null +++ b/testdata/zk/ERC20.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +contract MyToken { + string public owner; + string public constant name = "MyToken"; + string public constant symbol = "MTK"; + uint8 public constant decimals = 18; + uint256 public totalSupply = 1000000 * (10 ** uint256(decimals)); + mapping(address => uint256) public balanceOf; + + event Transfer(address indexed from, address indexed to, uint256 value); + + constructor() { + balanceOf[msg.sender] = totalSupply; + } + + function setTotalSupply(uint256 amount) public { + totalSupply = amount; + } + + function transfer(address to, uint256 amount) public returns (bool) { + require(balanceOf[msg.sender] >= amount, "Not enough tokens"); + balanceOf[msg.sender] -= amount; + balanceOf[to] += amount; + emit Transfer(msg.sender, to, amount); + return true; + } +} diff --git a/testdata/zk/TokenReceiver.sol b/testdata/zk/TokenReceiver.sol new file mode 100644 index 000000000..efd75034d --- /dev/null +++ b/testdata/zk/TokenReceiver.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +interface IMyToken { + function transfer(address to, uint256 amount) external returns (bool); +} + +contract TokenReceiver { + function receiveAndHoldToken(address token, uint256 amount) external { + IMyToken(token).transfer(msg.sender, amount); + } +}