Era

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

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

Summary

During the L2 genesis upgrade process, the _getForceDeploymentsData function from the L2GatewayUpgradeHelper.sol contract contains a semantic mismatch in how it handles the deployment of the L2 Native Token Vault.

When constructing the force deployment data for L2_NATIVE_TOKEN_VAULT_ADDR, the helper function checks if a token beacon does not exist through the legacy bridge with bool shouldDeployBeacon = deployedTokenBeacon == address(0);, and forwards both arguments to the ForceDeployment input.

The problem is that the constructor of L2NativeTokenVault expects a bool _contractsDeployedAlready at the position passed by shouldDeployBeacon, which has the opposite meaning as the value passed by the L2GenesisUpgrade contract.

In the logic of the L2NativeTokenVault, if _contractsDeployedAlready is True and _bridgedTokenBeacon is address(0) the deployment will revert with EmptyAddress(). This will be the case if shouldDeployBeacon is passed as True.

Vulnerability Details

The semantic mismatch occurs in the following sequence:

  1. In L2GatewayUpgradeHelper._getForceDeploymentsData:

    • shouldDeployBeacon = deployedTokenBeacon == address(0) - True if no beacon exists

    • This value is passed as _contractsDeployedAlready to the constructor

  2. In L2NativeTokenVault constructor:

    • If _contractsDeployedAlready is True, it expects _bridgedTokenBeacon to be non-zero

    • However, shouldDeployBeacon being True means deployedTokenBeacon is zero

    • This causes the constructor to revert with EmptyAddress()

Proof of Concept

Change the L2GatewayUpgradeHelper to return additional information and run the L2GenesisUpgrade.spec.ts test. It should pass, but because of the wrong implementation, it will fail, showing the logic issue.

diff --git a/era-contracts/system-contracts/contracts/L2GatewayUpgradeHelper.sol b/era-contracts/system-contracts/contracts/L2GatewayUpgradeHelper.sol
index 75ad2e8..be4e3e2 100644
--- a/era-contracts/system-contracts/contracts/L2GatewayUpgradeHelper.sol
+++ b/era-contracts/system-contracts/contracts/L2GatewayUpgradeHelper.sol
@@ -28,9 +28,10 @@ library L2GatewayUpgradeHelper {
address _ctmDeployer,
bytes memory _fixedForceDeploymentsData,
bytes memory _additionalForceDeploymentsData
- ) internal {
+ ) internal returns (bool deployedTokenBeaconAlreadyDeployed, address deployedTokenBeacon) {
// Decode and retrieve the force deployments data.
- ForceDeployment[] memory forceDeployments = _getForceDeploymentsData(
+ ForceDeployment[] memory forceDeployments;
+ (forceDeployments, deployedTokenBeaconAlreadyDeployed, deployedTokenBeacon) = _getForceDeploymentsData(
_fixedForceDeploymentsData,
_additionalForceDeploymentsData
);
@@ -78,7 +79,7 @@ library L2GatewayUpgradeHelper {
function _getForceDeploymentsData(
bytes memory _fixedForceDeploymentsData,
bytes memory _additionalForceDeploymentsData
- ) internal returns (ForceDeployment[] memory forceDeployments) {
+ ) internal returns (ForceDeployment[] memory forceDeployments, bool deployedTokenBeaconAlreadyDeployed, address deployedTokenBeacon) {
// Decode the fixed and additional force deployments data.
FixedForceDeploymentsData memory fixedForceDeploymentsData = abi.decode(
_fixedForceDeploymentsData,
@@ -141,14 +142,13 @@ library L2GatewayUpgradeHelper {
_baseTokenSymbol: additionalForceDeploymentsData.baseTokenSymbol
});
- address deployedTokenBeacon;
if (additionalForceDeploymentsData.l2LegacySharedBridge != address(0)) {
deployedTokenBeacon = address(
IL2SharedBridgeLegacy(additionalForceDeploymentsData.l2LegacySharedBridge).l2TokenBeacon()
);
}
- bool shouldDeployBeacon = deployedTokenBeacon == address(0);
+ deployedTokenBeaconAlreadyDeployed = deployedTokenBeacon != address(0);
// Configure the Native Token Vault deployment.
forceDeployments[3] = ForceDeployment({
@@ -163,7 +163,7 @@ library L2GatewayUpgradeHelper {
fixedForceDeploymentsData.l2TokenProxyBytecodeHash,
additionalForceDeploymentsData.l2LegacySharedBridge,
deployedTokenBeacon,
- shouldDeployBeacon,
+ deployedTokenBeaconAlreadyDeployed,
wrappedBaseTokenAddress,
additionalForceDeploymentsData.baseTokenAssetId
)
diff --git a/era-contracts/system-contracts/contracts/L2GenesisUpgrade.sol b/era-contracts/system-contracts/contracts/L2GenesisUpgrade.sol
index 00a22c7..ad1e39f 100644
--- a/era-contracts/system-contracts/contracts/L2GenesisUpgrade.sol
+++ b/era-contracts/system-contracts/contracts/L2GenesisUpgrade.sol
@@ -30,12 +30,12 @@ contract L2GenesisUpgrade is IL2GenesisUpgrade {
}
ISystemContext(SYSTEM_CONTEXT_CONTRACT).setChainId(_chainId);
- L2GatewayUpgradeHelper.performForceDeployedContractsInit(
+ (bool deployedTokenBeaconAlreadyDeployed, address deployedTokenBeacon) = L2GatewayUpgradeHelper.performForceDeployedContractsInit(
_ctmDeployer,
_fixedForceDeploymentsData,
_additionalForceDeploymentsData
);
- emit UpgradeComplete(_chainId);
+ emit UpgradeComplete(_chainId, deployedTokenBeaconAlreadyDeployed, deployedTokenBeacon);
}
}
diff --git a/era-contracts/system-contracts/contracts/interfaces/IL2GenesisUpgrade.sol b/era-contracts/system-contracts/contracts/interfaces/IL2GenesisUpgrade.sol
index 14f0530..c92ef52 100644
--- a/era-contracts/system-contracts/contracts/interfaces/IL2GenesisUpgrade.sol
+++ b/era-contracts/system-contracts/contracts/interfaces/IL2GenesisUpgrade.sol
@@ -34,7 +34,7 @@ struct FixedForceDeploymentsData {
}
interface IL2GenesisUpgrade {
- event UpgradeComplete(uint256 _chainId);
+ event UpgradeComplete(uint256 _chainId, bool deployedTokenBeaconAlreadyDeployed, address deployedTokenBeacon);
function genesisUpgrade(
uint256 _chainId,
diff --git a/era-contracts/system-contracts/test/L2GenesisUpgrade.spec.ts b/era-contracts/system-contracts/test/L2GenesisUpgrade.spec.ts
index f2db34f..4c5037c 100644
--- a/era-contracts/system-contracts/test/L2GenesisUpgrade.spec.ts
+++ b/era-contracts/system-contracts/test/L2GenesisUpgrade.spec.ts
@@ -119,7 +119,7 @@ describe("L2GenesisUpgrade tests", function () {
);
});
- describe("upgrade", function () {
+ describe.only("upgrade", function () {
it("successfully upgraded", async () => {
const data = l2GenesisUpgrade.interface.encodeFunctionData("genesisUpgrade", [
chainId,
@@ -134,7 +134,7 @@ describe("L2GenesisUpgrade tests", function () {
new ethers.Contract(complexUpgrader.address, l2GenesisUpgrade.interface, complexUpgrader.signer),
"UpgradeComplete"
)
- .withArgs(chainId);
+ .withArgs(chainId, true, ethers.utils.AddresZero);
await network.provider.request({
method: "hardhat_stopImpersonatingAccount",

Impact

  • High severity as it prevents successful deployment of the L2NativeTokenVault during genesis upgrade when no legacy beacon exists (Step 9-10 here will revert)

  • This could block the entire upgrade process and system initialization

  • Would require redeployment with corrected parameters

Tools Used

Manual code review

Recommendations

Invert the boolean logic in L2GatewayUpgradeHelper._getForceDeploymentsData or in the L2NativeTokenVault constructor.

Updates

Lead Judging Commences

inallhonesty Lead Judge 6 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.