Skip to content

Latest commit

 

History

History
68 lines (41 loc) · 4.5 KB

046.md

File metadata and controls

68 lines (41 loc) · 4.5 KB

Tangy Coconut Crocodile

Medium

Griefer can permanently DoS the L2 message queue

Summary

When L2 to L1 messages are sent via the L2CrossDomainMessenger.sol contract, the messages are appended to the tree, which has a max depth of 32. This max depth limits the maximum number of elements in the tree to 2^32 - 1, which is not large enough to avoid being fully DoS-ed. Someone could fill all 2^32 - 1 slots by sending dummy transactions, making it impossible for legitimate L2 to L1 messages to be sent, as the entire queue would be full.

Root Cause

The MAX_DEPTH of the L2Messages tree is 32, meaning the number of transactions that can be placed in the tree is 2^32 - 1, which is not a large enough number to prevent a potential DoS attack.

Internal pre-conditions

None needed

External pre-conditions

None needed. Though, if the gas fee is lower the attack is more feasible.

Attack Path

  1. Call the L2CrossDomainMessenger contract for "x" times such that the L2 message tree is full and not accepting any L2 messages to L1. For efficiency make the calls simple and send batch calls in a single transaction to minimize gas

Impact

Sending messages from L2 to L1 will be impossible. Permanent DoS.

PoC

As we can see in L2CrossDomainMessenger.sol:114, the messages are appended to the L2ToL1MessagePasser contract's tree structure. L2ToL1MessagePasser.sol::appendMessage() calls the internal _appendMessageHash function, where the following check inside _appendMessageHash is our target for DoSing the entire L2 -> L1 message process:

function _appendMessageHash(bytes32 leafHash) internal {
         //..
        -> if (leafNodesCount >= _MAX_DEPOSIT_COUNT) {
            revert MerkleTreeFull();
        }
        //..
    }

If leafNodesCount reaches _MAX_DEPOSIT_COUNT, which is 2^32 - 1, any transaction attempting to pass from L2 -> L1 will automatically revert, as the tree is full.

Now, let's take the worst-case scenario, where the number of transactions sent from L2 -> L1 is "0", and calling L2CrossDomainMessenger::sendMessage() 500 times in a single transaction costs $0.10 (a reasonable estimate considering EIP-4844).

First, calculate 2^32 - 1, which is approximately $4.2949673 \times 10^9$, meaning we would need to send this many transactions to L2CrossDomainMessenger. Since we are sending 500 messages per transaction, the total number of calls would be: $4.2949673 \times 10^9 / 500 = 8,589,934.6$ calls

Each call incurs a $0.10 fee, so the total cost would be: $8,589,934.6 \times 0.1 = 858K$, which is quite significant. However, this is the worst-case estimate, assuming no L2 messages are sent and each transaction costs $0.10 for 500 calls. If the gas fee were lower, say $0.01 per call, the cost would be: $8,589,934.6 \times 0.01 = 85.8K$, which is not a significant amount to DoS the entire system. Moreover, if the gas fee drops to $0.001, the cost would be only $8.58K, which is a very small amount that could be exploited by anyone to permanently DoS the system.

In comparison, let's look at how Scroll implements its tree structure. The Scroll tree has a depth of 40 Link, which not only increases the cost of an attack due to the number of iterations required but also makes appending messages more expensive, as there are 8 additional iterations in the loop to calculate the root. However, let's assume the best-case scenario, where calling sendMessage 500 times in 1 transaction costs $0.01, and run the same calculation:

The maximum number of transactions needed is $2^40 - 1 = 1.09951163 \times 10^{12}$.

Dividing by 500 messages per transaction: $1.09951163 \times 10^{12} / 500 = 2.19902326 \times 10^9$ calls.

The overall cost to consume the entire tree would be: $2.19902326 \times 10^9 \times 0.01 = 21,990,232.6$, which is around $22M! In comparison, with a tree depth of 32, the cost was only $85.8K.

Mitigation

Increase the _MAX_TREE_DEPTH