Deployment
Address Derivation
Issue
ZKsync uses a different strategy for deriving addresses in CREATE
and CREATE2
operations compared to Ethereum. This can cause discrepancies if your project relies on hardcoded Ethereum-style addresses for contract deployment.
Problematic Code
In Ethereum, you might have code that calculates the address of a contract deployed using CREATE2
based on a salt and bytecode hash:
function computeCreate2Address(
address deployer,
bytes32 salt,
bytes memory bytecode
) public pure returns (address) {
return address(uint160(uint(keccak256(abi.encodePacked(
bytes1(0xff),
deployer,
salt,
keccak256(bytecode)
)))));
}
This method won’t work in ZKsync because the address derivation logic is different. To learn more about these differences refer to the documentation on Differences with address derivation.
Error
When deploying contracts using CREATE2
, the actual address of the deployed contract may differ from the expected address derived using Ethereum’s
method. This can cause failures when interacting with contracts by
their derived address.
Solution
To derive ZKsync-compatible CREATE2
addresses, you need to use ZKsync's specific hashing mechanism that includes a zksyncCreate2
prefix.
Fixed Code Example
The following is an example of how you can compute a valid CREATE2
address for ZKSync.
This function uses ZKsync’s specific address derivation logic and ensures the contract
addresses are correctly computed.
function computeCreate2Address(
address _sender,
bytes32 _salt,
bytes32 _bytecodeHash,
bytes32 _constructorInputHash
) external pure returns (address) {
bytes32 senderBytes = bytes32(uint256(uint160(_sender)));
bytes32 data = keccak256(
bytes.concat(keccak256("zksyncCreate2"), senderBytes, _salt, _bytecodeHash, _constructorInputHash)
);
return address(uint160(uint256(data)));
}
Batch Transactions
Issue
In Foundry ZKsync, batched transactions may be executed out of order, which can cause failures if subsequent transactions depend on the execution of prior transactions.
Problematic Code
In Ethereum, you might use batched transactions within your scripts like this:
contract Calculator {
function add(uint8 a, uint8 b) public pure returns (uint8) {
return a + b;
}
}
contract FooScript is Script {
function run() public {
vm.startBroadcast();
Calculator calc = new Calculator(); // tx1
uint8 sum = calc.add(1, 2); // tx2
vm.stopBroadcast();
}
}
In this example, the deployment of the Calculator
contract (tx1)
and the invocation of add
(tx2) are batched together. However,
on ZKsync, tx2 might execute before tx1, causing the script to fail.
Error
The error occurs when the second transaction (tx2
) is executed before the first
transaction (tx1
), leading to a failure since the contract isn't deployed yet:
Error: Transaction reverted: contract not deployed
Solution
To ensure transactions are executed correctly on ZKsync using Foundry ZKsync,
use the --slow
flag, which sends transactions one at a time and keeps them in order.
Fixed Code Example
Here’s how you can run the script with proper transaction execution:
forge script script/FooTest.s.sol:FooScript --zksync --rpc-url https://sepolia.era.zksync.dev --broadcast --slow
The --slow
flag forces transactions to be sent individually to the RPC endpoint, ensuring they are executed in the correct order.
Bytecode Hash
Bytecode hashes output by zksolc
are fundamentally different
from the hash obtained via solc. The most glaring difference is the first
(most-significant) byte denotes the version of the format, which at present is 1
.
This leads to all zksolc
bytecode hashes to begin with 1
, whereas solc
bytecodes are merely the keccak
hash of the bytecode.
Any code making assumptions about bytecode hashes around EVM-scope will need to be migrated to accommodate for ZKsync's bytecode hashes.