Skip to content

Latest commit

 

History

History
122 lines (100 loc) · 5.33 KB

File metadata and controls

122 lines (100 loc) · 5.33 KB

Recumbent Blood Mouse

High

Re-proving Withdrawals in Optimism Portal

Summary

The Optimism Portal contract allows for re-proving withdrawals under certain conditions. While this feature provides flexibility, it also introduces potential vulnerabilities if not properly implemented or monitored.

Vulnerability Detail

The contract allows re-proving of withdrawals in the following cases:

  1. The withdrawal has never been proven before
  2. The previous dispute game was lost by the proposer
  3. The previous dispute game is blacklisted
  4. The previous dispute game type is no longer respected

While these conditions cover many scenarios, they may not account for all potential attack vectors or edge cases.

Impact

If exploited, vulnerabilities in the re-proving mechanism could lead to:

  1. Denial of service: Users could be prevented from finalizing their withdrawals
  2. Fund lockup: Legitimate withdrawals could be indefinitely delayed
  3. Increased gas costs: Users might need to re-prove multiple times
  4. Timestamp manipulation: Potential for attacks based on updating the proof timestamp

Code Snippet

 function proveWithdrawalTransaction(
        Types.WithdrawalTransaction memory _tx,
        uint256 _disputeGameIndex,
        Types.OutputRootProof calldata _outputRootProof,
        bytes[] calldata _withdrawalProof
    )
        external
        whenNotPaused
    {
        // Prevent users from creating a deposit transaction where this address is the message
        // sender on L2. Because this is checked here, we do not need to check again in
        // `finalizeWithdrawalTransaction`.
        require(_tx.target != address(this), "OptimismPortal: you cannot send messages to the portal contract");

        // Fetch the dispute game proxy from the `DisputeGameFactory` contract.
        (GameType gameType,, IDisputeGame gameProxy) = disputeGameFactory.gameAtIndex(_disputeGameIndex);
        Claim outputRoot = gameProxy.rootClaim();

        // The game type of the dispute game must be the respected game type.
        require(gameType.raw() == respectedGameType.raw(), "OptimismPortal: invalid game type");

        // Verify that the output root can be generated with the elements in the proof.
        require(
            outputRoot.raw() == Hashing.hashOutputRootProof(_outputRootProof),
            "OptimismPortal: invalid output root proof"
        );

        // Load the ProvenWithdrawal into memory, using the withdrawal hash as a unique identifier.
        bytes32 withdrawalHash = Hashing.hashWithdrawal(_tx);

        // We do not allow for proving withdrawals against dispute games that have resolved against the favor
        // of the root claim.
        require(
            gameProxy.status() != GameStatus.CHALLENGER_WINS,
            "OptimismPortal: cannot prove against invalid dispute games"
        );

        // Compute the storage slot of the withdrawal hash in the L2ToL1MessagePasser contract.
        // Refer to the Solidity documentation for more information on how storage layouts are
        // computed for mappings.
        bytes32 storageKey = keccak256(
            abi.encode(
                withdrawalHash,
                uint256(0) // The withdrawals mapping is at the first slot in the layout.
            )
        );

        // Verify that the hash of this withdrawal was stored in the L2toL1MessagePasser contract
        // on L2. If this is true, under the assumption that the SecureMerkleTrie does not have
        // bugs, then we know that this withdrawal was actually triggered on L2 and can therefore
        // be relayed on L1.
        require(
            SecureMerkleTrie.verifyInclusionProof({
                _key: abi.encode(storageKey),
                _value: hex"01",
                _proof: _withdrawalProof,
                _root: _outputRootProof.messagePasserStorageRoot
            }),
            "OptimismPortal: invalid withdrawal inclusion proof"
        );

        // Designate the withdrawalHash as proven by storing the `disputeGameProxy` & `timestamp` in the
        // `provenWithdrawals` mapping. A `withdrawalHash` can only be proven once unless the dispute game it proved
        // against resolves against the favor of the root claim.
        provenWithdrawals[withdrawalHash][msg.sender] =
            ProvenWithdrawal({ disputeGameProxy: gameProxy, timestamp: uint64(block.timestamp) });

        // Emit a `WithdrawalProven` event.
        emit WithdrawalProven(withdrawalHash, _tx.sender, _tx.target);
        // Emit a `WithdrawalProvenExtension1` event.
        emit WithdrawalProvenExtension1(withdrawalHash, msg.sender);

        // Add the proof submitter to the list of proof submitters for this withdrawal hash.
        proofSubmitters[withdrawalHash].push(msg.sender);
    }

Tool used

Manual Review

Recommendation

ProvenWithdrawal memory provenWithdrawal = provenWithdrawals[withdrawalHash][msg.sender];

IDisputeGame oldGame = provenWithdrawal.disputeGameProxy;
require(
    provenWithdrawal.timestamp == 0 || oldGame.status() == GameStatus.CHALLENGER_WINS
        || disputeGameBlacklist[oldGame] || oldGame.gameType().raw() != respectedGameType.raw(),
    "OptimismPortal: withdrawal hash has already been proven, and the old dispute game is not invalid"
);

provenWithdrawals[withdrawalHash][msg.sender] =
    ProvenWithdrawal({ disputeGameProxy: gameProxy, timestamp: uint64(block.timestamp) });