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);
>> forceDeployments[3] = ForceDeployment({
bytecodeHash: fixedForceDeploymentsData.l2NtvBytecodeHash,
newAddress: L2_NATIVE_TOKEN_VAULT_ADDR,
callConstructor: true,
value: 0,
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
)
});
}