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

Factory deployments won't work correctly on the ZKsync chain

Summary

Factory contracts won't work correctly on the ZKsync chain as they use Solady's LibClone library, which is incompatible with ZKsync. The protocol has stated that it must be compatible with any EVM chain :

Vulnerability Details

The contract factories, namely NexusAccountFactory.sol, RegistryFactory.sol, and RegistryFactory.sol use Solady's LibClone library, which will not work correctly on the ZKsync chain. This is because, for the create/create2 opcodes to function correctly on the ZKsync chain, the compiler must be aware of the bytecode of the deployed contract in advance.

Quoting from ZKsync docs :

"On ZKsync Era, contract deployment is performed using the hash of the bytecode, and the factoryDeps field of EIP712 transactions contains the bytecode. The actual deployment occurs by providing the contract's hash to the ContractDeployer system contract.
To guarantee that create/create2 functions operate correctly, the compiler must be aware of the bytecode of the deployed contract in advance. The compiler interprets the calldata arguments as incomplete input for ContractDeployer, as the remaining part is filled in by the compiler internally. The Yul datasize and dataoffset instructions have been adjusted to return the constant size and bytecode hash rather than the bytecode itself."

Let's look at how the contracts are cloned in the factory contracts :

NexusAccountFactory.sol#L56 :

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);
}

RegistryFactory.sol#L123 :

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);
}

K1ValidatorFactory.sol#L93 :

function createAccount(
address eoaOwner,
uint256 index,
address[] calldata attesters,
uint8 threshold
) external payable 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 Nexus contract using the computed salt
@=> (bool alreadyDeployed, address account) = LibClone.createDeterministicERC1967(msg.value, ACCOUNT_IMPLEMENTATION, actualSalt);
// Create the validator configuration using the Bootstrap library
BootstrapConfig memory validator = BootstrapLib.createSingleConfig(K1_VALIDATOR, abi.encodePacked(eoaOwner));
bytes memory initData = BOOTSTRAPPER.getInitNexusWithSingleValidatorCalldata(validator, REGISTRY, attesters, threshold);
// Initialize the account if it was not already deployed
if (!alreadyDeployed) {
INexus(account).initializeAccount(initData);
emit AccountCreated(account, eoaOwner, index);
}
return payable(account);
}

They all use the LibClone.createDeterministicERC1967() method to create clones.

Now, let's look at the LibClone.createDeterministicERC1967() function :

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.
}
}

As you can see, the compiler will not be aware of the bytecode at compile time since the bytecode is stored in memory only on runtime in this function. The ZKsync docs recommend against this practice.

Since the compiler is unaware of the bytecode beforehand, this will lead to unexpected results on the ZKsync chain.

NOTE: This finding is heavily inspired by this issue

Impact

Factory deployments will not work/ deploy faulty contracts on the ZKsync chain.
Since the createDeterministicERC1967() method deploys ETH along with the contract, there will also be loss of funds.
Therefore, this is a High severity finding.

Tools Used

Manual Review

Recommendations

Make sure the compiler is aware of the bytecode beforehand.

Example (from ZKsync docs) :

bytes memory bytecode = type(MyContract).creationCode;
assembly {
addr := create2(0, add(bytecode, 32), mload(bytecode), salt)
}
Updates

Lead Judging Commences

0xnevi Lead Judge 10 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

Appeal created

adriro Judge
10 months ago
0xnevi Lead Judge
10 months ago
adriro Judge
10 months ago
0xnevi Lead Judge
10 months ago
0xnevi Lead Judge 10 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.