Open c4-submissions opened 1 year ago
bytes032 marked the issue as duplicate of #600
bytes032 marked the issue as sufficient quality report
GalloDaSballo changed the severity to 2 (Med Risk)
GalloDaSballo marked the issue as satisfactory
@GalloDaSballo thank you for your judging,
This report is not a complete duplication of issue-600; only the root cause is similar, but the described impacts differ. Concerning this issue:
If the timestamp of a batch is set to its maximum valid value (which is block.timestamp + COMMIT_TIMESTAMP_APPROXIMATION_DELTA
, in this case, block.timestamp + 365 days
), it results in limitations on the speed of blocks and batches creation. Given that L2_TX_MAX_GAS_LIMIT
is equal to 80,000,000, and the maximum computation gas per batch is equal to 2^32 (explained here and set here), roughly 53 transactions (calculated as 2^32 / 80,000,000
), each consuming the maximum 80,000,000 gas, will exhaust the gas capacity of a batch. Consequently, the batch must be sealed, and another batch needs to be created to include additional transactions. If there is a high volume of transactions on the network, numerous batches must be created and committed on L1. Since the timestamp of the batch is set to the current timestamp plus 365 days, committing batches in a single L1 transaction becomes impossible. This is due to each batch's timestamp needing to be higher than the previous batch's timestamp, requiring the validator to wait between each batch commitment for the timestamp on L1 to elapse, thus passing this check. This scenario can lead to several issues:
Firstly, as indicated in the documentation, the value of 2^32 is arbitrary to prevent the state keeper from taking too much time.
The maximum number of computation gas per batch. This is the maximal number of gas that can be spent within a batch. This constant is rather arbitrary and is needed to prevent transactions from taking too much time from the state keeper. It can not be larger than the hard limit of 2^32 of gas for VM.
The operator may also seal the batch earlier as mentioned here. While this speeds up batch commitment on L1, the timestamp of the batch being set to the far future necessitates a delay between each commitment for the timestamp on L1 to elapse.
Secondly, the assumption so far was that each batch consists of one block containing all transactions. However, the primary objective of having multiple blocks in a batch is to enhance the speed of soft confirmation, as explained here:
L2 blocks were created for fast soft confirmation on wallets and block explorer. For example, MetaMask shows transactions as confirmed only after the block in which transaction execution was mined. So if the user needs to wait for the batch confirmation it would take at least minutes (for soft confirmation) and hours for full confirmation which is very bad UX. But API could return soft confirmation much earlier through L2 blocks.
If the operator includes multiple blocks in a batch, it becomes limited on the L1 side during timestamp verification. Suppose batch 100 has a timestamp of timestamp of the to-be-committed batch 100 + 365 days
and has 20 blocks. The 20th block must have a timestamp of at least timestamp of the to-be-committed batch 100 + 365 days + 20 seconds
. When the validator commits this batch, it will be reverted here because block.timestamp + 365 days + 20 seconds > block.timestamp + 365 days
. This indicates that having multiple blocks in a batch while the timestamp of the batch is set to the far future (to the edge of acceptable timestamp) will limit the frequency of batch commitment on L1 and restrict the number of blocks in a batch. In such a case, the validator should wait between each batch commitment on L1, potentially causing slow finality, impacting third parties dependent on that.
Summary:
Setting the timestamp of a batch to the far future will limit the speed of blocks and batches creation:
Furthermore, in the case of a high volume of transactions on the network, this issue will be a significant limiting factor. If such a batch with a timestamp in the far future is executed, this limitation will persist in the protocol, as an executed batch cannot be reverted.
hey @HE1M, @GalloDaSballo. Thanks for judging.
I want to add that the underlying cause of Issue #316 is a security check that restricts the operator to submitting batches with timestamp far in the future(ZKbatch.timestamp < Etherem.timestamp + DELTA
). This issue persists regardless of the DELTA value, even if set to zero. While this vulnerability exists, there are mitigating factors that diminish its severity:
Etherem.timestamp + DELTA
, increases by 12 seconds with each Ethereum block.Etherem.timestamp + DELTA
, Etherem.timestamp + DELTA +1
, Etherem.timestamp + DELTA +2
,... Etherem.timestamp + DELTA +11
and submitted to Ethereum. These timestamps will pass the batch.timestamp < Etherem.timestamp + DELTA
check in the next Ethereum blocks due to the 12-second increase in Ethereum's timestamp.In conclusion, the security check ZKbatch.timestamp < Etherem.timestamp + DELTA
does not impacts the average 12 batch submissions per Ethereum block, even if the operator sets batch timestamps to Etherem.timestamp + DELTA
.
@GalloDaSballo thank you for your judging,
This report is not a complete duplication of issue-600; only the root cause is similar, but the described impacts differ. Concerning this issue:
If the timestamp of a batch is set to its maximum valid value (which is
block.timestamp + COMMIT_TIMESTAMP_APPROXIMATION_DELTA
, in this case,block.timestamp + 365 days
), it results in limitations on the speed of blocks and batches creation. Given thatL2_TX_MAX_GAS_LIMIT
is equal to 80,000,000, and the maximum computation gas per batch is equal to 2^32 (explained here and set here), roughly 53 transactions (calculated as2^32 / 80,000,000
), each consuming the maximum 80,000,000 gas, will exhaust the gas capacity of a batch. Consequently, the batch must be sealed, and another batch needs to be created to include additional transactions. If there is a high volume of transactions on the network, numerous batches must be created and committed on L1. Since the timestamp of the batch is set to the current timestamp plus 365 days, committing batches in a single L1 transaction becomes impossible. This is due to each batch's timestamp needing to be higher than the previous batch's timestamp, requiring the validator to wait between each batch commitment for the timestamp on L1 to elapse, thus passing this check. This scenario can lead to several issues:
Firstly, as indicated in the documentation, the value of 2^32 is arbitrary to prevent the state keeper from taking too much time.
The maximum number of computation gas per batch. This is the maximal number of gas that can be spent within a batch. This constant is rather arbitrary and is needed to prevent transactions from taking too much time from the state keeper. It can not be larger than the hard limit of 2^32 of gas for VM.
The operator may also seal the batch earlier as mentioned here. While this speeds up batch commitment on L1, the timestamp of the batch being set to the far future necessitates a delay between each commitment for the timestamp on L1 to elapse.
Secondly, the assumption so far was that each batch consists of one block containing all transactions. However, the primary objective of having multiple blocks in a batch is to enhance the speed of soft confirmation, as explained here:
L2 blocks were created for fast soft confirmation on wallets and block explorer. For example, MetaMask shows transactions as confirmed only after the block in which transaction execution was mined. So if the user needs to wait for the batch confirmation it would take at least minutes (for soft confirmation) and hours for full confirmation which is very bad UX. But API could return soft confirmation much earlier through L2 blocks.
If the operator includes multiple blocks in a batch, it becomes limited on the L1 side during timestamp verification. Suppose batch 100 has a timestamp of
timestamp of the to-be-committed batch 100 + 365 days
and has 20 blocks. The 20th block must have a timestamp of at leasttimestamp of the to-be-committed batch 100 + 365 days + 20 seconds
. When the validator commits this batch, it will be reverted here becauseblock.timestamp + 365 days + 20 seconds > block.timestamp + 365 days
. This indicates that having multiple blocks in a batch while the timestamp of the batch is set to the far future (to the edge of acceptable timestamp) will limit the frequency of batch commitment on L1 and restrict the number of blocks in a batch. In such a case, the validator should wait between each batch commitment on L1, potentially causing slow finality, impacting third parties dependent on that.Summary:
Setting the timestamp of a batch to the far future will limit the speed of blocks and batches creation:
- Including multiple blocks in a batch will be restricted during timestamp verification on L1 since the timestamp of the batch is already at the edge of the accepted timestamp.
- Having only one block in a batch (to bypass the limitation mentioned above) contradicts the goal of having a block for fast finality.
Furthermore, in the case of a high volume of transactions on the network, this issue will be a significant limiting factor. If such a batch with a timestamp in the far future is executed, this limitation will persist in the protocol, as an executed batch cannot be reverted.
Agree.
GalloDaSballo marked the issue as selected for report
The finding highlights a way for batch creation to be interrupted, this pertains to any configuration and is something that the operator could do under specific circumnstances either willingly or by mistake
Due to the above, I believe this report should be considered primary and of Medium Severity
vladbochok marked the issue as disagree with severity
The issue is highly theoretical and has a low impact in principle.
Firstly validator should submit the batch with a timestamp in the future (+365 days). Even the validator is not trusted - this already has a huge impact on L2 protocols (all time-sensitive projects such as DeFi or oracles).
And then the only impact is that the validator can't commit
batches for times of high load. Please note, that the validator can create batches and generate zkp for them, just can't submit commitBatches
transaction by that moment. So, the validator can wait for a couple of minutes and then submit all batches. Moreover, currently, there is a delay between batch commits and batch execution by 21 hours. Due to this delay minutes in commits don't affect the time of execution, not saying that proof generation takes minutes, and sequencing in such a high load is questionable.
For transparency, the sponsor has notated via private discord channel that they are disputing this finding.
Lines of code
https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/zksync/facets/Executor.sol#L94
Vulnerability details
Impact
The timestamp constraints and batch creation limitations in zkSync have several significant impacts on the platform's functionality and operations:
Limited Block Inclusion: The constraints on timestamp differences between batches and their respective blocks restrict the number of blocks that can be included in each batch. This leads to smaller batch sizes, making it challenging to efficiently process transactions and utilize the available block space.
Responsiveness Issue: Due to these constraints, if two batches are intended to be committed on L1 at the same time (in the same Ethereum block), it's not allowed. This can create bottlenecks during batch commitments, especially when zkSync experiences high transaction volumes leading to an increased number of pending transactions and potentially longer finality times, affecting zkSync's overall responsiveness.
Operator-Induced Limitations: These constraints can be exploited by the operator by setting timestamps much further in the future to intentionally limit the number of blocks in batches. This could lead to various operational issues and inefficiencies.
Proof of Concept
Upon initiating transaction processing in the bootloader, the first step involves creating a new batch using the
setNewBatch
function in theSystemContext
contract. https://github.com/code-423n4/2023-10-zksync/blob/main/code/system-contracts/bootloader/bootloader.yul#L3675 https://github.com/code-423n4/2023-10-zksync/blob/main/code/system-contracts/contracts/SystemContext.sol#L416This operation enforces that the timestamp of the new batch must be greater than the timestamp of the previous batch and the timestamp of the last block in the previous batch. https://github.com/code-423n4/2023-10-zksync/blob/main/code/system-contracts/contracts/SystemContext.sol#L423 https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/system-contracts/contracts/SystemContext.sol#L402
Subsequently, when processing a specific transaction, an L2 block is designated by invoking the
setL2Block
function within theSystemContext
contract. This action ensures that the timestamp of the block is not lower than the timestamp of the current batch. https://github.com/code-423n4/2023-10-zksync/blob/main/code/system-contracts/bootloader/bootloader.yul#L559 https://github.com/code-423n4/2023-10-zksync/blob/main/code/system-contracts/contracts/SystemContext.sol#L323Once the processing of all transactions is completed, an additional fictive block is generated, serving as the final block within the batch. https://github.com/code-423n4/2023-10-zksync/blob/main/code/system-contracts/bootloader/bootloader.yul#L3812
Finally, the timestamp data is disclosed to L1 for verification. https://github.com/code-423n4/2023-10-zksync/blob/main/code/system-contracts/bootloader/bootloader.yul#L3816
On the L1 side, during batch commitment, the
_verifyBatchTimestamp
function is called to confirm the accuracy of the timestamps. It enforces that the batch's timestamp is later than the previous batch and not less thanblock.timestamp - COMMIT_TIMESTAMP_NOT_OLDER
. Additionally, it ensures that the timestamp of the last block in this batch is not greater thanblock.timestamp + COMMIT_TIMESTAMP_APPROXIMATION_DELTA
. https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/zksync/facets/Executor.sol#L85 https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/zksync/facets/Executor.sol#L93 https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/zksync/facets/Executor.sol#L94A critical concern arises when the operator, during the creation of a new batch on L2, sets its timestamp in close proximity to the value
block.timestamp + COMMIT_TIMESTAMP_APPROXIMATION_DELTA
. To illustrate this point, consider the following example:Imagine a new batch, numbered 1000, is created with a timestamp of
X + COMMIT_TIMESTAMP_APPROXIMATION_DELTA - 1
. This batch includes a block with a timestamp ofX + COMMIT_TIMESTAMP_APPROXIMATION_DELTA - 1
and a fictive block with a timestamp ofX + COMMIT_TIMESTAMP_APPROXIMATION_DELTA
. Importantly, these values adhere to all the timestamp requirements outlined in theSystemContext
contract, as explained earlier.When this batch 1000, is committed on L1 at a specific time
blockTimestamp1000
(meaning the time at which the batch 1000 is committed on L1), during timestamp verification, the following requirement is met (assumingX <= blockTimestamp1000
):https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/zksync/facets/Executor.sol#L94
Because:
This results in a successful proof and execution of the batch 1000. Now, when a new batch, 1001, is to be created on L2, its timestamp must be higher than
X + COMMIT_TIMESTAMP_APPROXIMATION_DELTA
because a batch's timestamp should surpass the timestamp of the last block in the previous batch.Suppose the timestamp of batch 1001 is
X + COMMIT_TIMESTAMP_APPROXIMATION_DELTA + Y
, and the last block within this batch carries a timestamp ofX + COMMIT_TIMESTAMP_APPROXIMATION_DELTA + Y + K
. To summarize:Batch 1000:
X + COMMIT_TIMESTAMP_APPROXIMATION_DELTA - 1
X + COMMIT_TIMESTAMP_APPROXIMATION_DELTA
blockTimestamp1000
X <= blockTimestamp1000
Batch 1001:
X + COMMIT_TIMESTAMP_APPROXIMATION_DELTA + Y
X + COMMIT_TIMESTAMP_APPROXIMATION_DELTA + Y + K
blockTimestamp1001
During the timestamp verification of batch 1001 on L1, to meet the requirement:
We must have:
Simplifying the condition yields:
Here,
X
represents the time set by the operator, whereX <= blockTimestamp1000
as explained earlier. In the worst case scenario, ifX = blockTimestamp1000
, the condition becomes:The variable
Y
signifies the amount of time required for the timestamp of batch 1001 to be higher than the timestamp of the last block in batch 1000. The minimum value ofY
is 1 second. Assuming it is equal to 1 second (please note that ifY
is higher than 1, the situation becomes even worse), the condition simplifies to:This condition imposes a critical limitation on the number of blocks that can be included in a batch. As a reminder, the timestamp of the first block in batch 1001 is
X + COMMIT_TIMESTAMP_APPROXIMATION_DELTA + Y
, while the timestamp of the last block in this batch isX + COMMIT_TIMESTAMP_APPROXIMATION_DELTA + Y + K
(the fictive block). The difference between these two timestamps equalsK
, and since each block's timestamp must be greater than the previous block,K
defines the maximum number of blocks allowed in a batch.This condition has several implications:
If both batches 1001 and 1000 are to be committed on L1 at the same time
blockTimestamp1000 = blockTimestamp1001
(in the same Ethereum block), it is not allowed, asK <= -1
.Examining zkSync Era Explorer, it is evident that batches are frequently committed in the same Ethereum block. For example, by observing Ethereum block number 18383261, the function
commitBlocks
is called 19 times, with block positions ranging from 0 to 18. The following two transactions show just the first and the lastcommitBlocks
in this Ethereum block. https://etherscan.io/tx/0xbfec43599bb73af95eaf4ac9ff83a62cdbe084382dd6f5d12bc8e817ce3574e5 https://etherscan.io/tx/0x1826d459ce7f2ab233374595569a13c4098e8e1eeb26517a98e42d9b5aab7374The explorer demonstrates that the interval between committing batches is relatively short. For instance, if the interval is 30 seconds, a maximum of 29 blocks can be included in a batch.
It's important to emphasize that batch 1000 has already undergone commitment, proof, and execution processes, and once these steps are completed, they are irreversible. Therefore, this issue will persist within the system.
One potential solution is to commit batches on L1 with a longer delay to allow for more blocks to be included in batches. However, this approach may lead to other issues, such as an increased number of pending transactions and significantly extended finality.
Addressing this issue is not straightforward, and it would necessitate an upgrade of the
Executor
facet and a redesign of the timestamp verification process.In summary, if the operator sets the timestamp of a batch too far into the future while still complying with L1 requirements, it can restrict the number of blocks that can be included in batches, resulting in a variety of challenges.
Tools Used
Recommended Mitigation Steps
It is recommended that a more stricter timestamp constraint currently enforced in the
_verifyBatchTimestamp
function on L1 to also apply on L2. This will help prevent the creation and submission of batches with timestamps set in the distant future to L1. https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/zksync/facets/Executor.sol#L94Assessed type
Context