Platform: Code4rena
Start Date: 02/10/2023
Pot Size: $1,100,000 USDC
Total HM: 28
Participants: 64
Period: 21 days
Judge: GalloDaSballo
Total Solo HM: 13
Id: 292
League: ETH
Rank: 14/64
Findings: 1
Award: $6,776.36
🌟 Selected for report: 0
🚀 Solo Findings: 0
6776.3633 USDC - $6,776.36
the executor contract can commit and revert batches.
When you revert a batch, which includes a system contract upgrade, the contract only deletes the batch number but not the transaction hash associated with the upgrade.
When you later try to commit new batches that are not meant to have a system contract upgrade, the contract's logic goes wrong.
It wrongly treats the new batch as if it should be committed with a system contract upgrade because it sees that there's still a transaction hash for the upgrade in the system.
because you already reverted a batch with upgrade, you dont expect further execution with that upgradeHash, most especially if reason for reverting is because the upgrade transaction hash may cause failure or unintended behaviours.
This can result in a successful commit and possible execution of the said commit with unintended system contract upgrades by validators or even cause reverts because the system contract upgrade transaction hash doesn't match the expected logValue.
tries to upgrade system contract ---> commits batch with system upgrade ---> notices fault ---> reverts batch with system contract upgrade ---> decides to no longer do upgrade for a while ---> tries to commit new batches without upgrades but cannot because system treats new batches commit as if it should have systems contract upgrade because previous systems contract upgrade hash from faulty upgrade is still present.
if we revert a committed batch or batches that has a system contracts upgrade committed, on the next commit, if the new batches to be committed are meant to have no system contracts upgrade, the commitBatch() logic will always work wrongly and try to commit the new batches as a batch with system contracts upgrade via _commitBatchesWithSystemContractsUpgrade()
instead of committing it as a batch with no system contracts upgrade. This happens because in revertBatches, only s.l2SystemContractsUpgradeBatchNumber
is deleted and s.l2SystemContractsUpgradeTxHash
is not.
function revertBatches(uint256 _newLastBatch) external nonReentrant onlyValidator { require(s.totalBatchesCommitted > _newLastBatch, "v1"); // The last committed batch is less than new last batch require(_newLastBatch >= s.totalBatchesExecuted, "v2"); // Already executed batches cannot be reverted //require (_newLastBatch < s.totalBatchesVerified, "v3"); if (_newLastBatch < s.totalBatchesVerified) { s.totalBatchesVerified = _newLastBatch; } s.totalBatchesCommitted = _newLastBatch; // Reset the batch number of the executed system contracts upgrade transaction if the batch // where the system contracts upgrade was committed is among the reverted batches. if (s.l2SystemContractsUpgradeBatchNumber > _newLastBatch) { delete s.l2SystemContractsUpgradeBatchNumber; } emit BlocksRevert(s.totalBatchesCommitted, s.totalBatchesVerified, s.totalBatchesExecuted); }
This will cause the commit to update s.l2SystemContractsUpgradeBatchNumber
wrongly (because the batch is not meant to be an upgrade batch) in _commitBatchesWithSystemContractsUpgrade()
or cause reverts because the expectedUpgradeTxHash
will not be eq to the logValue
as seen here
Note that s.l2SystemContractsUpgradeTxHash
is only set via s.l2SystemContractsUpgradeTxHash() in BaseZKSyncUpgrade.sol
and deleted/reset to bytes(0) upon sucessfull execution of the systems contracts upgrade batch via execute() in executor.sol. This means once s.l2SystemContractsUpgradeTxHash
is set it is impossible to revert the value if you indeed want to revert a committed upgrade batch and prevent a system contracts upgrade from executing. There is no possible way to set s.l2SystemContractsUpgradeTxHash
to bytes(0) again as if _l2ProtocolUpgradeTx.txType == 0
, no logic is performed in _setL2SystemContractUpgrade()
and s.l2SystemContractsUpgradeTxHash
remains as previously set in storage.
// If the type is 0, it is considered as noop and so will not be required to be executed. if (_l2ProtocolUpgradeTx.txType == 0) { return bytes32(0); }
This issue prevents a reversal of not yet executed system contract upgrades because the s.l2SystemContractsUpgradeTxHash
is not reverted too. In the case where the previously set l2SystemContractsUpgradeTxHash
is to be stopped from executing with a batch due to whatever reason, it is not possible to do so.
revertBatches()
is meant to be used to undo committed batches that have not been executed yet. It is possible that unexecuted batches may contain system contracts upgrades, in that case, revertBatches( handle that by deleting the s.l2SystemContractsUpgradeBatchNumber
but not deleting the s.l2SystemContractsUpgradeTxHash
.
https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/contracts/ethereum/contracts/zksync/facets/Executor.sol#L409C1-L412C1
// Reset the batch number of the executed system contracts upgrade transaction if the batch // where the system contracts upgrade was committed is among the reverted batches. if (s.l2SystemContractsUpgradeBatchNumber > _newLastBatch) { delete s.l2SystemContractsUpgradeBatchNumber; }
upon successful reverting of committed batches with a system contracts upgrade, we should be able to commit new batches. Lets assume a new batch (with no intention to have it as a system contracts upgrade batch) is supplied to the commitBatches()
fcn for committing. In the commitBatches()
logic depending on the value of s.l2SystemContractsUpgradeTxHash
and s.l2SystemContractsUpgradeBatchNumber
we either do the commit with system contracts upgrade via _commitBatchesWithSystemContractsUpgrade()
or without via _commitBatchesWithoutSystemContractsUpgrade()
.
In this case of trying to commit a batch with no system upgrade immediately after reverting a batch that contained a system contracts upgrade, the logic will force the new batch with no system contract upgrade to be committed with the _commitBatchesWithSystemContractsUpgrade()
function instead of _commitBatchesWithoutSystemContractsUpgrade()
function (because it is not supposed to be a system contracts upgrade batch). This happens because after the revert of a batch with system contracts upgrade via revertBatches()
, s.l2SystemContractsUpgradeBatchNumber
is deleted but the s.l2SystemContractsUpgradeTxHash
(systemContractsUpgradeTxHash) is not deleted. commitBatches()
has this conditional statement
if (systemContractsUpgradeTxHash == bytes32(0) || s.l2SystemContractsUpgradeBatchNumber != 0)
if the above statement is true it will commit batches without contracts upgrade via _commitBatchesWithoutSystemContractsUpgrade()
and if false it will commit batches via _commitBatchesWithSystemContractsUpgrade
.
Since only s.l2SystemContractsUpgradeBatchNumber
is deleted, s.l2SystemContractsUpgradeBatchNumber
= 0 and systemContractsUpgradeTxHash
!= bytes(0).
This means if (systemContractsUpgradeTxHash == bytes32(0) || s.l2SystemContractsUpgradeBatchNumber != 0)
will return false and _commitBatchesWithSystemContractsUpgrade
will be used to commit the new batches which originally are not meant to have system contract upgrades.
function commitBatches(StoredBatchInfo memory _lastCommittedBatchData, CommitBatchInfo[] calldata _newBatchesData) external override nonReentrant onlyValidator { // Check that we commit batches after last committed batch require(s.storedBatchHashes[s.totalBatchesCommitted] == _hashStoredBatchInfo(_lastCommittedBatchData), "i"); // incorrect previous batch data require(_newBatchesData.length > 0, "No batches to commit"); bytes32 systemContractsUpgradeTxHash = s.l2SystemContractsUpgradeTxHash; // Upgrades are rarely done so we optimize a case with no active system contracts upgrade. if (systemContractsUpgradeTxHash == bytes32(0) || s.l2SystemContractsUpgradeBatchNumber != 0) { _commitBatchesWithoutSystemContractsUpgrade(_lastCommittedBatchData, _newBatchesData); } else { _commitBatchesWithSystemContractsUpgrade( _lastCommittedBatchData, _newBatchesData, systemContractsUpgradeTxHash ); } s.totalBatchesCommitted = s.totalBatchesCommitted + _newBatchesData.length; }
Now that the new batches that weren't intended to be committed as a batch with system contract upgrades processed through _commitBatchesWithSystemContractsUpgrade()
. two things can happen:
s.l2SystemContractsUpgradeBatchNumber will be set to the new batch batchNumber as seen here. the state updates wrongly and the commit is sucessfull.
_commitBatchesWithSystemContractsUpgrade()
takes the systemContractsUpgradeTxHash and passes it into _commitOneBatch()
as the expected hash and uses it in a require statement as seen here in __processL2Logs(). If this require statement is reached, it will fail because the expected hash (systemContractsUpgradeTxHash) is not equal to the logValue
which is derived from the new batch _newBatch.systemLogs
as seen here, if not reached (in a case where by one of these if statements are true before the require statement that compares the expected hash (systemContractsUpgradeTxHash) to the logValue
is reached) the _commitBatchesWithSystemContractsUpgrade()
function logic will continue and the commit will be sucessful but s.l2SystemContractsUpgradeBatchNumber
will be updated wrongly because the batch commit was not meant to handle any system contract upgrades.
/// @dev Commits new batches with a system contracts upgrade transaction. /// @param _lastCommittedBatchData The data of the last committed batch. /// @param _newBatchesData An array of batch data that needs to be committed. /// @param _systemContractUpgradeTxHash The transaction hash of the system contract upgrade. function _commitBatchesWithSystemContractsUpgrade( StoredBatchInfo memory _lastCommittedBatchData, CommitBatchInfo[] calldata _newBatchesData, bytes32 _systemContractUpgradeTxHash ) internal { // The system contract upgrade is designed to be executed atomically with the new bootloader, a default account, // ZKP verifier, and other system parameters. Hence, we ensure that the upgrade transaction is // carried out within the first batch committed after the upgrade. // While the logic of the contract ensures that the s.l2SystemContractsUpgradeBatchNumber is 0 when this function is called, // this check is added just in case. Since it is a hot read, it does not encure noticable gas cost. require(s.l2SystemContractsUpgradeBatchNumber == 0, "ik"); // Save the batch number where the upgrade transaction was executed. s.l2SystemContractsUpgradeBatchNumber = _newBatchesData[0].batchNumber; for (uint256 i = 0; i < _newBatchesData.length; i = i.uncheckedInc()) { // The upgrade transaction must only be included in the first batch. bytes32 expectedUpgradeTxHash = i == 0 ? _systemContractUpgradeTxHash : bytes32(0); _lastCommittedBatchData = _commitOneBatch( _lastCommittedBatchData, _newBatchesData[i], expectedUpgradeTxHash ); s.storedBatchHashes[_lastCommittedBatchData.batchNumber] = _hashStoredBatchInfo(_lastCommittedBatchData); emit BlockCommit( _lastCommittedBatchData.batchNumber, _lastCommittedBatchData.batchHash, _lastCommittedBatchData.commitment ); } }
If sucessfully committed, it can be proven and executed by the validators and a faulty/not intended/ expected to be reverted l2SystemContractsUpgradeTxHash
will be used in the upgrade.
manual review
when reverting batches, delete the s.l2SystemContractsUpgradeTxHash
too along with the s.l2SystemContractsUpgradeBatchNumber
if the batch contains a systems contract upgrade.
function revertBatches(uint256 _newLastBatch) external nonReentrant onlyValidator { require(s.totalBatchesCommitted > _newLastBatch, "v1"); // The last committed batch is less than new last batch require(_newLastBatch >= s.totalBatchesExecuted, "v2"); // Already executed batches cannot be reverted if (_newLastBatch < s.totalBatchesVerified) { s.totalBatchesVerified = _newLastBatch; } s.totalBatchesCommitted = _newLastBatch; // Reset the batch number of the executed system contracts upgrade transaction if the batch // where the system contracts upgrade was committed is among the reverted batches. if (s.l2SystemContractsUpgradeBatchNumber > _newLastBatch) { delete s.l2SystemContractsUpgradeBatchNumber; delete s.l2SystemContractsUpgradeTxHash; } emit BlocksRevert(s.totalBatchesCommitted, s.totalBatchesVerified, s.totalBatchesExecuted); }
Error
#0 - c4-pre-sort
2023-10-31T08:43:31Z
bytes032 marked the issue as duplicate of #114
#1 - miladpiri
2023-11-09T11:23:49Z
By design. Not duplicate of 114.
#2 - c4-sponsor
2023-11-09T11:23:53Z
miladpiri (sponsor) disputed
#3 - c4-judge
2023-11-23T16:18:10Z
GalloDaSballo changed the severity to QA (Quality Assurance)
#4 - c4-judge
2023-11-23T16:18:39Z
GalloDaSballo marked the issue as not a duplicate
#5 - GalloDaSballo
2023-11-26T13:05:43Z
Have judged similar findings as QA
Ultimately there is an operative risk in how the batches, upgrades and reverting batches works
In practice, this is dependent on:
Meaning that I cannot determine a higher severity than QA, however such finding is worth monitoring long term
#6 - adeolu98
2023-12-01T10:55:22Z
Hi judge, i have noticed some bit of similarity between this issue and #221.
they both affect the commit, prove, revert, execute and system upgrade processes albeit in different ways.
#221 root cause seems to be the fact that upgrades are queued i.e one must be executed before the other. This is similar to the root cause via my revert() case scenario expained above. I explained that reverts will delete upgrade batch number but not the batch tx thus making reverts of batches with system upgrade non functional since the system tx hash may still be executed with new batches. This means that undesired system tx hashes, which are part of reverted batches will always be executed first especially if before a batch with new correct upgrade with tx hash may be executed.
i think infering per sponsor comment in #221, If a dangerous batch commit with upgrade hash is reverted but still processed for execution with new batches, The system is still susceptible to the effects.
also i dont think reverts will always happen because of "Admin mistake". It is possible there may be external events/scenarios/happening that may force admin to begin a revert, these may be no fault of the admin. i.e if the committed upgrade implements a new function that uses a 3rd party service, library, compiler etc and its determined to poses a risk.
Based on these, i hereby reason with the judge to classify this issue as med. The scenario is not like the usual admin function related functions that can be changed by a resetting a value or one that affects only the admin. The issue in the operation logic and consequences could be system/protocol wide.
#7 - GalloDaSballo
2023-12-02T10:20:59Z
Similar to #728 will double check
#8 - c4-judge
2023-12-07T18:54:32Z
This previously downgraded issue has been upgraded by GalloDaSballo
#9 - c4-judge
2023-12-07T18:54:32Z
This previously downgraded issue has been upgraded by GalloDaSballo
#10 - c4-judge
2023-12-07T18:54:51Z
GalloDaSballo marked the issue as duplicate of #214
#11 - c4-judge
2023-12-07T18:55:04Z
GalloDaSballo marked the issue as satisfactory