HardhatFoundry
30,000 USDC
View results
Submission Details
Severity: medium
Valid

`create2` address derivation differs on zkEVm

Summary

The address derivation for the create2 opcode differs on zkEVM, breaking the factory contracts' functionality.

Note that the protocol is supposed to be compatible with any EVM chain.


Vulnerability Details

The address derivation formula on the zkSync chain differs from that used on other EVM chains. The LibClone library used in the factory contracts to create deterministic addresses of Nexus contracts uses the standard EVM formula for address prediction.

function createAccount(bytes calldata initData, bytes32 salt) external payable override returns (address payable) {
// Compute the actual salt for deterministic deployment
bytes32 actualSalt;
assembly {
let ptr := mload(0x40)
let calldataLength := sub(calldatasize(), 0x04)
mstore(0x40, add(ptr, calldataLength))
calldatacopy(ptr, 0x04, calldataLength)
actualSalt := keccak256(ptr, calldataLength)
}
// Deploy the account using the deterministic address
(bool alreadyDeployed, address account) = LibClone.createDeterministicERC1967(msg.value, ACCOUNT_IMPLEMENTATION, actualSalt);
if (!alreadyDeployed) {
INexus(account).initializeAccount(initData);
emit AccountCreated(account, initData, salt);
}
return payable(account);
}
function createDeterministicERC1967(uint256 value, address implementation, bytes32 salt)
internal
returns (bool alreadyDeployed, address instance)
{
/// @solidity memory-safe-assembly
assembly {
let m := mload(0x40) // Cache the free memory pointer.
mstore(0x60, 0xcc3735a920a3ca505d382bbc545af43d6000803e6038573d6000fd5b3d6000f3)
mstore(0x40, 0x5155f3363d3d373d3d363d7f360894a13ba1a3210667c828492db98dca3e2076)
mstore(0x20, 0x6009)
mstore(0x1e, implementation)
mstore(0x0a, 0x603d3d8160223d3973)
// Compute and store the bytecode hash.
mstore(add(m, 0x35), keccak256(0x21, 0x5f))
mstore(m, shl(88, address()))
mstore8(m, 0xff) // Write the prefix.
mstore(add(m, 0x15), salt)
instance := keccak256(m, 0x55)
for {} 1 {} {
if iszero(extcodesize(instance)) {
instance := create2(value, 0x21, 0x5f, salt)
if iszero(instance) {
mstore(0x00, 0x30116425) // `DeploymentFailed()`.
revert(0x1c, 0x04)
}
break
}
alreadyDeployed := 1
if iszero(value) { break }
if iszero(call(gas(), instance, value, codesize(), 0x00, codesize(), 0x00)) {
mstore(0x00, 0xb12d13eb) // `ETHTransferFailed()`.
revert(0x1c, 0x04)
}
break
}
mstore(0x40, m) // Restore the free memory pointer.
mstore(0x60, 0) // Restore the zero slot.
}
}

The zkSync docs explicitly state that the derivation for addresses created using create2 is different from Ethereum Mainnet.

From the docs :

Address derivation

For zkEVM bytecode, ZKsync Era uses a distinct address derivation method compared to Ethereum. The precise formulas can be found in our SDK, as demonstrated below:

export function create2Address(sender: Address, bytecodeHash: BytesLike, salt: BytesLike, input: BytesLike) {
const prefix = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("zksyncCreate2"));
const inputHash = ethers.utils.keccak256(input);
const addressBytes = ethers.utils.keccak256(ethers.utils.concat([prefix, ethers.utils.zeroPad(sender, 32), salt, bytecodeHash, inputHash])).slice(26);
return ethers.utils.getAddress(addressBytes);
}
export function createAddress(sender: Address, senderNonce: BigNumberish) {
const prefix = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("zksyncCreate"));
const addressBytes = ethers.utils
.keccak256(ethers.utils.concat([prefix, ethers.utils.zeroPad(sender, 32), ethers.utils.zeroPad(ethers.utils.hexlify(senderNonce), 32)]))
.slice(26);
return ethers.utils.getAddress(addressBytes);
}

Since the bytecode differs from Ethereum as ZKsync uses a modified version of the EVM, the address derived from the bytecode hash will also differ. This means that the same bytecode deployed on Ethereum and ZKsync will have different addresses and the Ethereum address will still be available and unused on ZKsync. If and when the zkEVM reaches parity with the EVM, the address derivation will be updated to match Ethereum and the same bytecode will have the same address on both chains, deployed bytecodes to different addresses on ZKsync could then be deployed to the same the Ethereum-matching addresses on ZKsync.

As you can see above, the prefix "zksyncCreate2" is added to the hash used to compute the address. Therefore, the address on zkEVM will be different from ETH mainnet. This means that the factory contracts will calculate the wrong address on zkSync chain and return the EVM equivalent address to the user in the createAccount() function call. This means that the user will be unaware of the actual address of the Nexus account.

Impact

Since some initial ETH may also be deployed along with the Nexus account, there will be a loss of funds for a naive user as the web2 frontend will return the incorrect address. However, an advanced user can observe the new address on the zkSync explorer and recover their account. Therefore, this issue breaks core functionality and qualifies as medium severity.

Tools Used

Manual Review

Recommendation

Handle address derivation differently on zkSync.
Use a zkEVM compatible cloning library.

Updates

Lead Judging Commences

0xnevi Lead Judge 11 months ago
Submission Judgement Published
Validated
Assigned finding tags:

finding-zksync-create-create2-opcode

Valid medium, since there is non-functionality on zkSync (cannot create accounts) since it is stated as follows > Blockchains: > - Ethereum/Any EVM

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.