Skip to content

Latest commit

 

History

History
194 lines (139 loc) · 8.51 KB

075.md

File metadata and controls

194 lines (139 loc) · 8.51 KB

Able Tan Jaguar

Medium

Cross-Chain Message Queue Congestion via Gas Limit Mismatch

Summary

There's a mismatch between gas limit validation on L1 and actual gas consumption on L2, which could lead to message execution failures and subsequent message accumulation, this allow messages to pass the gas limit checks on L1 but fail during execution on L2 due to insufficient gas. Although the system incorporates fee requirements and relies on trusted sequencers, for mitigation against abuse, but the risk of gradual system congestion remains a concern. This issue doesn't present an immediate threat of system failure or fund loss but could lead to degraded performance and increased operational overhead if left unaddressed.

Vulnerability Details

Messages can pass the initial gas limit checks on L1, but fail during execution on L2 due to insufficient gas, and then remain in the system as skipped messages, this failed messages can lead to congestion. The vulnerability arises due to the interaction between gas limit validation on L1, message execution on L2, and the handling of failed messages.

Code Snippet

Step 1: Message Initiation in (L1CrossDomainMessenger.sol)

    function sendMessage(
        address _to,
        uint256 _value,
        bytes memory _message,
        uint256 _gasLimit
    ) external payable override whenNotPaused {
        _sendMessage(_to, _value, _message, _gasLimit, _msgSender());
    }

    /// @inheritdoc ICrossDomainMessenger
    function sendMessage(
        address _to,
        uint256 _value,
        bytes calldata _message,
        uint256 _gasLimit,
        address _refundAddress
    ) external payable override whenNotPaused {
        _sendMessage(_to, _value, _message, _gasLimit, _refundAddress);
    }

This is where messages are initiated, but in L1MessageQueueWithGasPriceOracle that validate the gas limit as follows

Step 2: Gas Limit Validation (L1MessageQueueWithGasPriceOracle.sol)

    function appendCrossDomainMessage(
        address _target,
        uint256 _gasLimit,
        bytes calldata _data
    ) external override onlyMessenger {
        // validate gas limit
        _validateGasLimit(_gasLimit, _data);

        // do address alias to avoid replay attack in L2.
        address _sender = AddressAliasHelper.applyL1ToL2Alias(_msgSender());

        _queueTransaction(_sender, _target, 0, _gasLimit, _data);
    }

    function _validateGasLimit(uint256 _gasLimit, bytes calldata _calldata) internal view {
        require(_gasLimit <= maxGasLimit, "Gas limit must not exceed maxGasLimit");
        // check if the gas limit is above intrinsic gas
        uint256 intrinsicGas = calculateIntrinsicGasFee(_calldata);
        require(_gasLimit >= intrinsicGas, "Insufficient gas limit, must be above intrinsic gas");
    }

Gas limit validation occurs, but only checks against a minimum and maximum intrinsic limit

Step 3: Message Queuing (L1MessageQueueWithGasPriceOracle.sol)

    function _queueTransaction(
        address _sender,
        address _target,
        uint256 _value,
        uint256 _gasLimit,
        bytes calldata _data
    ) internal {
        // compute transaction hash
        uint256 _queueIndex = messageQueue.length;
        bytes32 _hash = computeTransactionHash(_sender, _queueIndex, _value, _target, _gasLimit, _data);
        messageQueue.push(_hash);

        // emit event
        emit QueueTransaction(_sender, _target, _value, uint64(_queueIndex), _gasLimit, _data);
    }

Messages are queued after passing validation.

Step 4: How Message Execute on L2 (L2CrossDomainMessenger.sol)

    function _executeMessage(
        address _from,
        address _to,
        uint256 _value,
        bytes memory _message,
        bytes32 _xDomainCalldataHash
    ) internal {
        // @note check more `_to` address to avoid attack in the future when we add more gateways.
        require(_to != Predeploys.L2_TO_L1_MESSAGE_PASSER, "Forbid to call l2 to l1 message passer");
        _validateTargetAddress(_to);

        // @note This usually will never happen, just in case.
        require(_from != xDomainMessageSender, "Invalid message sender");

        xDomainMessageSender = _from;
        // solhint-disable-next-line avoid-low-level-calls
        (bool success, ) = _to.call{value: _value}(_message);
        // reset value to refund gas.
        xDomainMessageSender = Constants.DEFAULT_XDOMAIN_MESSAGE_SENDER;

        if (success) {
            isL1MessageExecuted[_xDomainCalldataHash] = true;
            emit RelayedMessage(_xDomainCalldataHash);
        } else {
            emit FailedRelayedMessage(_xDomainCalldataHash);
        }
    }

Messages are executed here but If gas is insufficient, the call fails.

Step 5: Handling of Failed Messages (L1MessageQueueWithGasPriceOracle.sol)

    function popCrossDomainMessage(uint256 _startIndex, uint256 _count, uint256 _skippedBitmap) external {
        require(_msgSender() == ROLLUP_CONTRACT, "Only callable by the rollup");

        require(_count <= 256, "pop too many messages");
        require(pendingQueueIndex == _startIndex, "start index mismatch");

        unchecked {
            // clear extra bits in `_skippedBitmap`, and if _count = 256, it's designed to overflow.
            uint256 mask = (1 << _count) - 1;
            _skippedBitmap &= mask;

            uint256 bucket = _startIndex >> 8;
            uint256 offset = _startIndex & 0xff;
            skippedMessageBitmap[bucket] |= _skippedBitmap << offset;
            if (offset + _count > 256) {
                skippedMessageBitmap[bucket + 1] = _skippedBitmap >> (256 - offset);
            }

            pendingQueueIndex = _startIndex + _count;
        }

        emit DequeueTransaction(_startIndex, _count, _skippedBitmap);
    }

Failed messages are marked as skipped but remain in the system

Attack Scenario

Heres how attacker can exploit this vulnerability through the following steps:

  1. The attacker prepares multiple cross-chain messages with gas limits that are just above the minimum required by _validateGasLimit but insufficient for actual execution on L2.

  2. The attacker sends these messages using sendMessage in L1CrossDomainMessenger, paying the required fees.

  3. These messages will pass the gas limit validation in _validateGasLimit but will be queued via _queueTransaction.

  4. When these messages are relayed to L2, the message will fail in _executeMessage due to insufficient gas

  5. Even though the failed messages are marked as skipped in popCrossDomainMessage, but they're not removed from the system.

  6. This process can be repeated, leading to an accumulation of failed messages in the system.

Impact

  1. Message Queue Congestion: The accumulation of failed messages could lead to congestion in the message queue, potentially delaying the processing of legitimate messages.

  2. Increased Operational Overhead: More resources would be required to manage and clear the backlog of failed messages.

  3. Network Congestion

Recommendations

  1. Improve Gas Estimation: Implement more accurate gas estimation for L2 execution in the L1 validation process.

  2. Batch Processing of Failed Messages: Develop mechanisms to efficiently process or clear backlogs of failed messages.