Closed c4-bot-5 closed 4 months ago
saxenism (sponsor) disputed
This looks like a misunderstanding of the purpose of the reservedGas
. It still should be refunded in full.
The Warden specifies that a subtraction when calculating the potential refund at the bootloader
level is incorrect.
The Sponsor specifies that the submission is incorrect, however, they focused on an incorrect aspect of the exhibit. I discussed this with the Sponsor directly, and after investigating, we concluded that the exhibit was indeed invalid.
The gasForExecution
variable is guaranteed to be greater than or equal to the gasSpentOnExecution + ergsSpentOnPubdata
sum due to the unique nature of the ZKSYNC_NEAR_CALL_executeL1Tx
function. An invocation of the function creates a new call frame whose gas limit (i.e. gas()
) is set to the ABI's value. Based on this line, the value is set to the gasForExecution
variable's value.
As the actual gas consumed during execution is evaluated as the delta between gas()
invocations, the maximum gasSpentOnExecution
value is going to be whatever ZKSYNC_NEAR_CALL_executeL1Tx
ends up consuming, which is up-to gasForExecution
.
The ZKSYNC_NEAR_CALL_executeL1Tx
function will invoke the isNotEnoughGasForPubdata
function at its end, supplying the gas()
result as an argument to it. The isNotEnoughGasForPubdata
function will ensure that the gas()
in the context of the ZKSYNC_NEAR_CALL_executeL1Tx
at its end is greater than or equal to the ergsSpentOnPubdata
value, as evidenced here.
Culminating the above, we have the following relations:
gas()
at beginning of ZKSYNC_NEAR_CALL_executeL1Tx
: gasForExecution
gas()
at end of ZKSYNC_NEAR_CALL_executeL1Tx
: someValue >= ergsSpentOnPubdata
Based on the above, we know that the delta between gas()
evaluates before and after the ZKSYNC_NEAR_CALL_executeL1Tx
is going to be a value for which the following will hold true:
$$ gasForExecution - gasSpentOnExecution >= ergsSpentOnPubdata $$
Taking the above equation one step further, we have:
$$ gasForExecution >= gasSpentOnExecution + ergsSpentOnPubdata $$
As a result, the Warden's assumption that the gasForExecution
may not cover the gasSpentOnExecution + ergsSpentOnPubdata
value required is invalid.
alex-ppg marked the issue as unsatisfactory: Invalid
Lines of code
https://github.com/code-423n4/2024-03-zksync/blob/4f0ba34f34a864c354c7e8c47643ed8f4a250e13/code/system-contracts/bootloader/bootloader.yul#L919-L1052
Vulnerability details
Proof of Concept
First take a look at https://github.com/code-423n4/2024-03-zksync/blob/4f0ba34f34a864c354c7e8c47643ed8f4a250e13/code/system-contracts/bootloader/bootloader.yul#L919-L1052
This is the updated method of processing L1 transactions in the bootloader, and in comparison to the previous implementation there are notable improvements including the implicit setting of pubdata prices, advanced calculation of
basePubdataSpent
for more precise gas and refund assessments, and a more complex approach to calculating potential refunds, among others.Now keep in mind that the
reservedGas
is the number of L2 gas that is beyond theMAX_GAS_PER_TRANSACTION
and beyond the operator's trust limit , i.e. gas which cannot be allowed to be used for the transaction and is to be refunded, as clearly documented in the docs and commented here: https://github.com/code-423n4/2024-03-zksync/blob/4f0ba34f34a864c354c7e8c47643ed8f4a250e13/code/system-contracts/bootloader/bootloader.yul#L1214-L1271Now issue with the logic applied to
processL1Tx()
is that it allows for part of thisreservedGas
to be used on the execution if not all.While executing
processL1Tx()
the functiongetExecuteL1TxAndNotifyResult()
is used to execute the L1 tx, also would be key to note that as the name hints thegetExecuteL1TxAndNotifyResult()
function does not return thepotentialRefund
value but rather only executes the transaction, notifies the result whether successful or not, while returning thegasSpentOnExecution
value, see https://github.com/code-423n4/2024-03-zksync/blob/4f0ba34f34a864c354c7e8c47643ed8f4a250e13/code/system-contracts/bootloader/bootloader.yul#L1053-L1076Back to
processL1Tx()
we can see how thepotentialRefund
https://github.com/code-423n4/2024-03-zksync/blob/4f0ba34f34a864c354c7e8c47643ed8f4a250e13/code/system-contracts/bootloader/bootloader.yul#L985-L989Evidently, this function erroneously subtracts both
gasSpentOnExecution
&ergsSpentOnPubdata
from the summation ofreservedGas
&gasForExecution
whereas it should only subtract from thegasForExecution
.To show that the above statement is true and that subtracting from the summation of
reservedGas
&gasForExecution
consider the next three paragraphs.Note that previous to this, both in
processL1Tx()
andgetExecuteL1TxAndNotifyResult()
no where is it enforced that the gas to be spent on execution is less than the availablegasForExecution
.Now considering this, from the
getGasLimitForTx
's function implementation we see thatgasLimitForTx
is the maximum number of L2 gas that should be spent on that transaction, while atleast thereservedGas
or more should be refunded to the operator.Now going down the line of
processL1Tx()
, after querying the value ofgasLimitForTx
fromgetGasLimitForTx
, it then calculates the gas meant for execution as a subtraction of the gas used on the tx preparation from thegasLimitForTx
, here https://github.com/code-423n4/2024-03-zksync/blob/4f0ba34f34a864c354c7e8c47643ed8f4a250e13/code/system-contracts/bootloader/bootloader.yul#L970-L971Impact
Core logic of having trust limits for operators is flawed.
This is cause the gas spent on the transaction could be higher than the maximum specified L2 gas for the transaction
gasLimitForTx
essentially meaning that the subtle invariant of having atrust limit
for an operator on the amount of gas that can be spent on a transaction is not properly enforced, currently the refunded gas for these transaction could be less than the transaction's supposedreservedGas
.Recommended Mitigation Steps
The
gasSpentOnExecution + ergsSpentOnPubdata
value should be checked with only thegasForExecution
,gasForExecution > (gasSpentOnExecution + ergsSpentOnPubdata)
should be enforced.And then
potentialRefund
should also be enforced to always be>= reservedGas
, potentially with the difference from "gasForExecution - (gasSpentOnExecution + ergsSpentOnPubdata)
", i.e when thedifference > 0
.Assessed type
Context