Era

ZKsync
FoundryLayer 2
500,000 USDC
View results
Submission Details
Severity: medium
Valid

Inverted use of boolean value in `L2NativeTokenVault.constructor` would cause revert or unnecessary deployment of `bridgedTokenBeacon`

Summary

The L2GatewayUpgradeHelper._getForceDeploymentsData function in L2GatewayUpgradeHelper provides the ForceDeployment struct used to deploy the L2NativeTokenVault. However, the boolean value passed as shouldDeployBeacon is interpreted as _contractsDeployedAlready in the L2NativeTokenVault constructor, resulting in an inverted deployment process.

Vulnerability Details

L2NativeTokenVault is deployed using L2GatewayUpgradeHelper.performForceDeployedContractsInit function and its constructor data is provided by _getForceDeploymentsData function as forceDeployments[3]:

function _getForceDeploymentsData(
bytes memory _fixedForceDeploymentsData,
bytes memory _additionalForceDeploymentsData
) internal returns (ForceDeployment[] memory forceDeployments) {
...
address deployedTokenBeacon;
if (additionalForceDeploymentsData.l2LegacySharedBridge != address(0)) {
deployedTokenBeacon = address(
IL2SharedBridgeLegacy(additionalForceDeploymentsData.l2LegacySharedBridge).l2TokenBeacon()
);
}
bool shouldDeployBeacon = deployedTokenBeacon == address(0);
// Configure the Native Token Vault deployment.
>> forceDeployments[3] = ForceDeployment({
bytecodeHash: fixedForceDeploymentsData.l2NtvBytecodeHash,
newAddress: L2_NATIVE_TOKEN_VAULT_ADDR,
callConstructor: true,
value: 0,
// solhint-disable-next-line func-named-parameters
input: abi.encode(
fixedForceDeploymentsData.l1ChainId,
fixedForceDeploymentsData.aliasedL1Governance,
fixedForceDeploymentsData.l2TokenProxyBytecodeHash,
additionalForceDeploymentsData.l2LegacySharedBridge,
deployedTokenBeacon,
>> shouldDeployBeacon,
wrappedBaseTokenAddress,
additionalForceDeploymentsData.baseTokenAssetId
)
});
}

As outlined above, shouldDeployBeacon represents whether to deploy tokenBeacon as it reflects the status of deployedTokenBeacon being address(0). However, on the receiving end (constructor of L2NativeTokenVault), shouldDeployBeacon is decoded as _contractsDeployedAlready:

constructor(
uint256 _l1ChainId,
address _aliasedOwner,
bytes32 _l2TokenProxyBytecodeHash,
address _legacySharedBridge,
address _bridgedTokenBeacon,
>> bool _contractsDeployedAlready,
address _wethToken,
bytes32 _baseTokenAssetId
) NativeTokenVault(_wethToken, L2_ASSET_ROUTER_ADDR, _baseTokenAssetId, _l1ChainId) {
L2_LEGACY_SHARED_BRIDGE = IL2SharedBridgeLegacy(_legacySharedBridge);
if (_l2TokenProxyBytecodeHash == bytes32(0)) {
revert EmptyBytes32();
}
if (_aliasedOwner == address(0)) {
revert EmptyAddress();
}
L2_TOKEN_PROXY_BYTECODE_HASH = _l2TokenProxyBytecodeHash;
_transferOwnership(_aliasedOwner);
>> if (_contractsDeployedAlready) {
if (_bridgedTokenBeacon == address(0)) {
revert EmptyAddress();
}
bridgedTokenBeacon = IBeacon(_bridgedTokenBeacon);
} else {
address l2StandardToken = address(new BridgedStandardERC20{salt: bytes32(0)}());
UpgradeableBeacon tokenBeacon = new UpgradeableBeacon{salt: bytes32(0)}(l2StandardToken);
tokenBeacon.transferOwnership(owner());
bridgedTokenBeacon = IBeacon(address(tokenBeacon));
emit L2TokenBeaconUpdated(address(bridgedTokenBeacon), _l2TokenProxyBytecodeHash);
}
}

The boolean value is interpreted in the opposite way, which leads to a whole inverted process of deployment. When

  • _contractsDeployedAlready is true (shouldDeployBeacon is true): it will revert as _bridgedTokenBeacon is address(0)

  • _contractsDeployedAlready is false (shouldDeployBeacon is false): it will create a new bridgedTokenBeacon despite having a valid _bridgedTokenBeacon passed in constructor

Impact

This causes an inverted deployment process, which leads to either a revert when _contractsDeployedAlready is true or the unnecessary creation of a new bridgedTokenBeacon when it is false. Ultimately it blocks the L2 genesis upgrade in the absence of a legacy shared bridge.

Tools Used

Manual Review

Recommendations

Provide the correct boolean value to ForceDeployment to properly configure the native token vault deployment, ensuring alignment with the logic defined in the L2NativeTokenVault constructor:

function _getForceDeploymentsData(
bytes memory _fixedForceDeploymentsData,
bytes memory _additionalForceDeploymentsData
) internal returns (ForceDeployment[] memory forceDeployments) {
...
address deployedTokenBeacon;
if (additionalForceDeploymentsData.l2LegacySharedBridge != address(0)) {
deployedTokenBeacon = address(
IL2SharedBridgeLegacy(additionalForceDeploymentsData.l2LegacySharedBridge).l2TokenBeacon()
);
}
- bool shouldDeployBeacon = deployedTokenBeacon == address(0);
+ bool contractsDeployedAlready = deployedTokenBeacon != address(0);
// Configure the Native Token Vault deployment.
forceDeployments[3] = ForceDeployment({
bytecodeHash: fixedForceDeploymentsData.l2NtvBytecodeHash,
newAddress: L2_NATIVE_TOKEN_VAULT_ADDR,
callConstructor: true,
value: 0,
// solhint-disable-next-line func-named-parameters
input: abi.encode(
fixedForceDeploymentsData.l1ChainId,
fixedForceDeploymentsData.aliasedL1Governance,
fixedForceDeploymentsData.l2TokenProxyBytecodeHash,
additionalForceDeploymentsData.l2LegacySharedBridge,
deployedTokenBeacon,
- shouldDeployBeacon,
+ contractsDeployedAlready,
wrappedBaseTokenAddress,
additionalForceDeploymentsData.baseTokenAssetId
)
});
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 5 months ago
Submission Judgement Published
Validated
Assigned finding tags:

`L2GatewayUpgradeHelper._getForceDeploymentsData` semantic mismatch during `L2NativeTokenVault` deployment will revert if the beacon is not deployed during a `L2GenesisUpgrade.genesisUpgrade`

Support

FAQs

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