code-423n4 / 2023-10-zksync-findings

4 stars 0 forks source link

Timestamp Constraints Leading to Number of Blocks Creation Limitations #316

Open c4-submissions opened 1 year ago

c4-submissions commented 1 year ago

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:

  1. 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.

  2. 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.

  3. 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 the SystemContext 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#L416

This 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 the SystemContext 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#L323

Once 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 than block.timestamp - COMMIT_TIMESTAMP_NOT_OLDER. Additionally, it ensures that the timestamp of the last block in this batch is not greater than block.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#L94

A 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 of X + COMMIT_TIMESTAMP_APPROXIMATION_DELTA - 1 and a fictive block with a timestamp of X + COMMIT_TIMESTAMP_APPROXIMATION_DELTA. Importantly, these values adhere to all the timestamp requirements outlined in the SystemContext 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 (assuming X <= blockTimestamp1000):

require(lastL2BlockTimestamp <= block.timestamp + COMMIT_TIMESTAMP_APPROXIMATION_DELTA, "h2");

https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/zksync/facets/Executor.sol#L94

Because:

assuming: X <= blockTimestamp1000 ==>
X + COMMIT_TIMESTAMP_APPROXIMATION_DELTA <= blockTimestamp1000 + COMMIT_TIMESTAMP_APPROXIMATION_DELTA

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 of X + COMMIT_TIMESTAMP_APPROXIMATION_DELTA + Y + K. To summarize:

During the timestamp verification of batch 1001 on L1, to meet the requirement:

require(lastL2BlockTimestamp <= block.timestamp + COMMIT_TIMESTAMP_APPROXIMATION_DELTA, "h2");

We must have:

X + COMMIT_TIMESTAMP_APPROXIMATION_DELTA + Y + K <= blockTimestamp1001 + COMMIT_TIMESTAMP_APPROXIMATION_DELTA

Simplifying the condition yields:

Y + K <= blockTimestamp1001 - X

Here, X represents the time set by the operator, where X <= blockTimestamp1000 as explained earlier. In the worst case scenario, if X = blockTimestamp1000, the condition becomes:

Y + K <= blockTimestamp1001 - blockTimestamp1000

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 of Y is 1 second. Assuming it is equal to 1 second (please note that if Y is higher than 1, the situation becomes even worse), the condition simplifies to:

K <= blockTimestamp1001 - blockTimestamp1000 - 1

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 is X + COMMIT_TIMESTAMP_APPROXIMATION_DELTA + Y + K (the fictive block). The difference between these two timestamps equals K, 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:

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#L94

Assessed type

Context

c4-pre-sort commented 1 year ago

bytes032 marked the issue as duplicate of #600

c4-pre-sort commented 1 year ago

bytes032 marked the issue as sufficient quality report

c4-judge commented 11 months ago

GalloDaSballo changed the severity to 2 (Med Risk)

c4-judge commented 11 months ago

GalloDaSballo marked the issue as satisfactory

HE1M commented 11 months ago

@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:

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.

0xunforgiven commented 11 months ago

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:

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.

miladpiri commented 11 months ago

@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:

  • 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.

c4-judge commented 11 months ago

GalloDaSballo marked the issue as selected for report

GalloDaSballo commented 11 months ago

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

c4-sponsor commented 8 months ago

vladbochok marked the issue as disagree with severity

vladbochok commented 8 months ago

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.

thebrittfactor commented 8 months ago

For transparency, the sponsor has notated via private discord channel that they are disputing this finding.