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