Commit ea17584c authored by clabby's avatar clabby Committed by GitHub

feat(ctb): `OptimismPortal2` test migration (#9401)

* New portal tests

* Invariant docs

* test updates

* moar tests

* Invariant docs

* Allow for re-proving if the respected game type changed

* update spec tests / re-prove cond
parent 37647587
# `OptimismPortal2` Invariants
## Deposits of any value should always succeed unless `_to` = `address(0)` or `_isCreation` = `true`.
**Test:** [`OptimismPortal2.t.sol#L158`](../test/invariants/OptimismPortal2.t.sol#L158)
All deposits, barring creation transactions and transactions sent to `address(0)`, should always succeed.
## `finalizeWithdrawalTransaction` should revert if the proof maturity period has not elapsed.
**Test:** [`OptimismPortal2.t.sol#L180`](../test/invariants/OptimismPortal2.t.sol#L180)
A withdrawal that has been proven should not be able to be finalized until after the proof maturity period has elapsed.
## `finalizeWithdrawalTransaction` should revert if the withdrawal has already been finalized.
**Test:** [`OptimismPortal2.t.sol#L209`](../test/invariants/OptimismPortal2.t.sol#L209)
Ensures that there is no chain of calls that can be made that allows a withdrawal to be finalized twice.
## A withdrawal should **always** be able to be finalized `PROOF_MATURITY_DELAY_SECONDS` after it was successfully proven, if the game has resolved and passed the air-gap.
**Test:** [`OptimismPortal2.t.sol#L237`](../test/invariants/OptimismPortal2.t.sol#L237)
This invariant asserts that there is no chain of calls that can be made that will prevent a withdrawal from being finalized exactly `PROOF_MATURITY_DELAY_SECONDS` after it was successfully proven and the game has resolved and passed the air-gap.
\ No newline at end of file
......@@ -16,6 +16,7 @@ This directory contains documentation for all defined invariant tests within `co
- [InvariantTest.sol](./InvariantTest.sol.md)
- [L2OutputOracle](./L2OutputOracle.md)
- [OptimismPortal](./OptimismPortal.md)
- [OptimismPortal2](./OptimismPortal2.md)
- [ResourceMetering](./ResourceMetering.md)
- [SafeCall](./SafeCall.md)
- [SystemConfig](./SystemConfig.md)
......
......@@ -32,8 +32,8 @@
"sourceCodeHash": "0xdc27421279afb6c3b26fc8c589c5d213695f666c74d2c2c41cb7df719d172f37"
},
"src/L1/OptimismPortal2.sol": {
"initCodeHash": "0x48b052d78aafe26222a58fb01fee937402e43479c611739567551445ac235986",
"sourceCodeHash": "0xfb5b8bfa0ae30cd5969376d26aff9c9e2c44e5c1ebecdc95a83efad0eaaf0e85"
"initCodeHash": "0xbbf753c1df3e4eabdd910124948afc5bfda7e219ece0f3a16804ea2129d512ed",
"sourceCodeHash": "0x78457c09e7e80b1e950f794999dcd5f8426dde850793fbaf9aba9718d29d9141"
},
"src/L1/ProtocolVersions.sol": {
"initCodeHash": "0x72cd467e8bcf019c02675d72ab762e088bcc9cc0f1a4e9f587fa4589f7fdd1b8",
......
......@@ -154,6 +154,19 @@
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "disputeGameFinalityDelaySeconds",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "donateETH",
......@@ -322,13 +335,26 @@
"outputs": [
{
"internalType": "bool",
"name": "paused_",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "proofMaturityDelaySeconds",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
......@@ -448,6 +474,19 @@
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "sauron",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [
{
......
......@@ -183,10 +183,27 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver {
return superchainConfig.guardian();
}
/// @notice Getter function for the address of the sauron role.
/// Public getter is legacy and will be removed in the future. Use `SuperchainConfig.sauron()` instead
/// once it's added.
/// @custom:deprecated
function sauron() public pure returns (address) {
return SAURON;
}
/// @notice Getter for the current paused status.
/// @return paused_ Whether or not the contract is paused.
function paused() public view returns (bool paused_) {
paused_ = superchainConfig.paused();
function paused() public view returns (bool) {
return superchainConfig.paused();
}
/// @notice Getter for the proof maturity delay.
function proofMaturityDelaySeconds() public view returns (uint256) {
return PROOF_MATURITY_DELAY_SECONDS;
}
/// @notice Getter for the dispute game finality delay.
function disputeGameFinalityDelaySeconds() public view returns (uint256) {
return DISPUTE_GAME_FINALITY_DELAY_SECONDS;
}
/// @notice Computes the minimum gas limit for a deposit.
......@@ -266,9 +283,10 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver {
// in the case that an honest user proves their withdrawal against a dispute game that
// resolves against the root claim, or the dispute game is blacklisted, we allow
// re-proving the withdrawal against a new proposal.
IDisputeGame game = provenWithdrawal.disputeGameProxy;
require(
provenWithdrawal.timestamp == 0 || gameProxy.status() == GameStatus.CHALLENGER_WINS
|| disputeGameBlacklist[gameProxy],
|| disputeGameBlacklist[gameProxy] || game.gameType().raw() != respectedGameType.raw(),
"OptimismPortal: withdrawal hash has already been proven, and dispute game is not invalid"
);
......@@ -424,9 +442,8 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver {
respectedGameType = _gameType;
}
/// @notice Checks if a withdrawal can be finalized. NOTE: Decision was made to have this
/// function revert rather than returning a boolean so that was more obvious why the
/// function failed.
/// @notice Checks if a withdrawal can be finalized. This function will revert if the withdrawal cannot be
/// finalized, and otherwise has no side-effects.
/// @param _withdrawalHash Hash of the withdrawal to check.
function checkWithdrawal(bytes32 _withdrawalHash) public view {
ProvenWithdrawal memory provenWithdrawal = provenWithdrawals[_withdrawalHash];
......@@ -445,7 +462,7 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver {
// safety against weird bugs in the proving step.
require(
provenWithdrawal.timestamp > disputeGameProxy.createdAt().raw(),
"OptimismPortal: withdrawal timestamp less than L2 Oracle starting timestamp"
"OptimismPortal: withdrawal timestamp less than dispute game creation timestamp"
);
// A proven withdrawal must wait at least `PROOF_MATURITY_DELAY_SECONDS` before finalizing.
......@@ -462,6 +479,11 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver {
"OptimismPortal: output proposal has not been finalized yet"
);
// The game type of the dispute game must be the respected game type. This was also checked in
// `proveWithdrawalTransaction`, but we check it again in case the respected game type has changed since
// the withdrawal was proven.
require(disputeGameProxy.gameType().raw() == respectedGameType.raw(), "OptimismPortal: invalid game type");
// Before a withdrawal can be finalized, the dispute game it was proven against must have been
// resolved for at least `DISPUTE_GAME_FINALITY_DELAY_SECONDS`. This is to allow for manual
// intervention in the event that a dispute game is resolved incorrectly.
......
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
// Testing utilities
import { stdError } from "forge-std/Test.sol";
import { CommonTest } from "test/setup/CommonTest.sol";
import { NextImpl } from "test/mocks/NextImpl.sol";
import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol";
// Libraries
import { Types } from "src/libraries/Types.sol";
import { Hashing } from "src/libraries/Hashing.sol";
import { Constants } from "src/libraries/Constants.sol";
// Target contract dependencies
import { Proxy } from "src/universal/Proxy.sol";
import { ResourceMetering } from "src/L1/ResourceMetering.sol";
import { AddressAliasHelper } from "src/vendor/AddressAliasHelper.sol";
import { SystemConfig } from "src/L1/SystemConfig.sol";
import { SuperchainConfig } from "src/L1/SuperchainConfig.sol";
import { OptimismPortal2 } from "src/L1/OptimismPortal2.sol";
import { FaultDisputeGame, IDisputeGame } from "src/dispute/FaultDisputeGame.sol";
import "src/libraries/DisputeTypes.sol";
contract OptimismPortal2_Test is CommonTest {
address depositor;
function setUp() public override {
super.enableFaultProofs();
super.setUp();
depositor = makeAddr("depositor");
}
/// @dev Tests that the constructor sets the correct values.
/// @notice Marked virtual to be overridden in
/// test/kontrol/deployment/DeploymentSummary.t.sol
function test_constructor_succeeds() external virtual {
OptimismPortal2 opImpl = OptimismPortal2(payable(deploy.mustGetAddress("OptimismPortal2")));
assertEq(address(opImpl.disputeGameFactory()), address(0));
assertEq(address(opImpl.SYSTEM_CONFIG()), address(0));
assertEq(address(opImpl.systemConfig()), address(0));
assertEq(address(opImpl.superchainConfig()), address(0));
assertEq(opImpl.l2Sender(), Constants.DEFAULT_L2_SENDER);
assertEq(opImpl.respectedGameType().raw(), deploy.cfg().respectedGameType());
}
/// @dev Tests that the initializer sets the correct values.
/// @notice Marked virtual to be overridden in
/// test/kontrol/deployment/DeploymentSummary.t.sol
function test_initialize_succeeds() external virtual {
address guardian = deploy.cfg().superchainConfigGuardian();
assertEq(address(optimismPortal2.disputeGameFactory()), address(disputeGameFactory));
assertEq(address(optimismPortal2.SYSTEM_CONFIG()), address(systemConfig));
assertEq(address(optimismPortal2.systemConfig()), address(systemConfig));
assertEq(optimismPortal2.GUARDIAN(), guardian);
assertEq(optimismPortal2.guardian(), guardian);
assertEq(address(optimismPortal2.superchainConfig()), address(superchainConfig));
assertEq(optimismPortal2.l2Sender(), Constants.DEFAULT_L2_SENDER);
assertEq(optimismPortal2.paused(), false);
assertEq(optimismPortal2.respectedGameType().raw(), deploy.cfg().respectedGameType());
}
/// @dev Tests that `pause` successfully pauses
/// when called by the GUARDIAN.
function test_pause_succeeds() external {
address guardian = optimismPortal2.GUARDIAN();
assertEq(optimismPortal2.paused(), false);
vm.expectEmit(address(superchainConfig));
emit Paused("identifier");
vm.prank(guardian);
superchainConfig.pause("identifier");
assertEq(optimismPortal2.paused(), true);
}
/// @dev Tests that `pause` reverts when called by a non-GUARDIAN.
function test_pause_onlyGuardian_reverts() external {
assertEq(optimismPortal2.paused(), false);
assertTrue(optimismPortal2.GUARDIAN() != alice);
vm.expectRevert("SuperchainConfig: only guardian can pause");
vm.prank(alice);
superchainConfig.pause("identifier");
assertEq(optimismPortal2.paused(), false);
}
/// @dev Tests that `unpause` successfully unpauses
/// when called by the GUARDIAN.
function test_unpause_succeeds() external {
address guardian = optimismPortal2.GUARDIAN();
vm.prank(guardian);
superchainConfig.pause("identifier");
assertEq(optimismPortal2.paused(), true);
vm.expectEmit(address(superchainConfig));
emit Unpaused();
vm.prank(guardian);
superchainConfig.unpause();
assertEq(optimismPortal2.paused(), false);
}
/// @dev Tests that `unpause` reverts when called by a non-GUARDIAN.
function test_unpause_onlyGuardian_reverts() external {
address guardian = optimismPortal2.GUARDIAN();
vm.prank(guardian);
superchainConfig.pause("identifier");
assertEq(optimismPortal2.paused(), true);
assertTrue(optimismPortal2.GUARDIAN() != alice);
vm.expectRevert("SuperchainConfig: only guardian can unpause");
vm.prank(alice);
superchainConfig.unpause();
assertEq(optimismPortal2.paused(), true);
}
/// @dev Tests that `receive` successdully deposits ETH.
function testFuzz_receive_succeeds(uint256 _value) external {
vm.expectEmit(address(optimismPortal2));
emitTransactionDeposited({
_from: alice,
_to: alice,
_value: _value,
_mint: _value,
_gasLimit: 100_000,
_isCreation: false,
_data: hex""
});
// give alice money and send as an eoa
vm.deal(alice, _value);
vm.prank(alice, alice);
(bool s,) = address(optimismPortal2).call{ value: _value }(hex"");
assertTrue(s);
assertEq(address(optimismPortal2).balance, _value);
}
/// @dev Tests that `depositTransaction` reverts when the destination address is non-zero
/// for a contract creation deposit.
function test_depositTransaction_contractCreation_reverts() external {
// contract creation must have a target of address(0)
vm.expectRevert("OptimismPortal: must send to address(0) when creating a contract");
optimismPortal2.depositTransaction(address(1), 1, 0, true, hex"");
}
/// @dev Tests that `depositTransaction` reverts when the data is too large.
/// This places an upper bound on unsafe blocks sent over p2p.
function test_depositTransaction_largeData_reverts() external {
uint256 size = 120_001;
uint64 gasLimit = optimismPortal2.minimumGasLimit(uint64(size));
vm.expectRevert("OptimismPortal: data too large");
optimismPortal2.depositTransaction({
_to: address(0),
_value: 0,
_gasLimit: gasLimit,
_isCreation: false,
_data: new bytes(size)
});
}
/// @dev Tests that `depositTransaction` reverts when the gas limit is too small.
function test_depositTransaction_smallGasLimit_reverts() external {
vm.expectRevert("OptimismPortal: gas limit too small");
optimismPortal2.depositTransaction({ _to: address(1), _value: 0, _gasLimit: 0, _isCreation: false, _data: hex"" });
}
/// @dev Tests that `depositTransaction` succeeds for small,
/// but sufficient, gas limits.
function testFuzz_depositTransaction_smallGasLimit_succeeds(bytes memory _data, bool _shouldFail) external {
uint64 gasLimit = optimismPortal2.minimumGasLimit(uint64(_data.length));
if (_shouldFail) {
gasLimit = uint64(bound(gasLimit, 0, gasLimit - 1));
vm.expectRevert("OptimismPortal: gas limit too small");
}
optimismPortal2.depositTransaction({
_to: address(0x40),
_value: 0,
_gasLimit: gasLimit,
_isCreation: false,
_data: _data
});
}
/// @dev Tests that `minimumGasLimit` succeeds for small calldata sizes.
/// The gas limit should be 21k for 0 calldata and increase linearly
/// for larger calldata sizes.
function test_minimumGasLimit_succeeds() external {
assertEq(optimismPortal2.minimumGasLimit(0), 21_000);
assertTrue(optimismPortal2.minimumGasLimit(2) > optimismPortal2.minimumGasLimit(1));
assertTrue(optimismPortal2.minimumGasLimit(3) > optimismPortal2.minimumGasLimit(2));
}
/// @dev Tests that `depositTransaction` succeeds for an EOA.
function testFuzz_depositTransaction_eoa_succeeds(
address _to,
uint64 _gasLimit,
uint256 _value,
uint256 _mint,
bool _isCreation,
bytes memory _data
)
external
{
_gasLimit = uint64(
bound(
_gasLimit,
optimismPortal2.minimumGasLimit(uint64(_data.length)),
systemConfig.resourceConfig().maxResourceLimit
)
);
if (_isCreation) _to = address(0);
// EOA emulation
vm.expectEmit(address(optimismPortal2));
emitTransactionDeposited({
_from: depositor,
_to: _to,
_value: _value,
_mint: _mint,
_gasLimit: _gasLimit,
_isCreation: _isCreation,
_data: _data
});
vm.deal(depositor, _mint);
vm.prank(depositor, depositor);
optimismPortal2.depositTransaction{ value: _mint }({
_to: _to,
_value: _value,
_gasLimit: _gasLimit,
_isCreation: _isCreation,
_data: _data
});
assertEq(address(optimismPortal2).balance, _mint);
}
/// @dev Tests that `depositTransaction` succeeds for a contract.
function testFuzz_depositTransaction_contract_succeeds(
address _to,
uint64 _gasLimit,
uint256 _value,
uint256 _mint,
bool _isCreation,
bytes memory _data
)
external
{
_gasLimit = uint64(
bound(
_gasLimit,
optimismPortal2.minimumGasLimit(uint64(_data.length)),
systemConfig.resourceConfig().maxResourceLimit
)
);
if (_isCreation) _to = address(0);
vm.expectEmit(address(optimismPortal2));
emitTransactionDeposited({
_from: AddressAliasHelper.applyL1ToL2Alias(address(this)),
_to: _to,
_value: _value,
_mint: _mint,
_gasLimit: _gasLimit,
_isCreation: _isCreation,
_data: _data
});
vm.deal(address(this), _mint);
vm.prank(address(this));
optimismPortal2.depositTransaction{ value: _mint }({
_to: _to,
_value: _value,
_gasLimit: _gasLimit,
_isCreation: _isCreation,
_data: _data
});
assertEq(address(optimismPortal2).balance, _mint);
}
}
contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest {
// Reusable default values for a test withdrawal
Types.WithdrawalTransaction _defaultTx;
FaultDisputeGame game;
uint256 _proposedGameIndex;
uint256 _proposedBlockNumber;
bytes32 _stateRoot;
bytes32 _storageRoot;
bytes32 _outputRoot;
bytes32 _withdrawalHash;
bytes[] _withdrawalProof;
Types.OutputRootProof internal _outputRootProof;
// Use a constructor to set the storage vars above, so as to minimize the number of ffi calls.
constructor() {
super.enableFaultProofs();
super.setUp();
_defaultTx = Types.WithdrawalTransaction({
nonce: 0,
sender: alice,
target: bob,
value: 100,
gasLimit: 100_000,
data: hex""
});
// Get withdrawal proof data we can use for testing.
(_stateRoot, _storageRoot, _outputRoot, _withdrawalHash, _withdrawalProof) =
ffi.getProveWithdrawalTransactionInputs(_defaultTx);
// Setup a dummy output root proof for reuse.
_outputRootProof = Types.OutputRootProof({
version: bytes32(uint256(0)),
stateRoot: _stateRoot,
messagePasserStorageRoot: _storageRoot,
latestBlockhash: bytes32(uint256(0))
});
}
/// @dev Setup the system for a ready-to-use state.
function setUp() public override {
_proposedBlockNumber = 0xFF;
game = FaultDisputeGame(
address(
disputeGameFactory.create(
optimismPortal2.respectedGameType(), Claim.wrap(_outputRoot), abi.encode(_proposedBlockNumber)
)
)
);
_proposedGameIndex = disputeGameFactory.gameCount() - 1;
// Warp beyond the chess clocks and finalize the game.
vm.warp(block.timestamp + game.gameDuration().raw() / 2 + 1 seconds);
// Fund the portal so that we can withdraw ETH.
vm.deal(address(optimismPortal2), 0xFFFFFFFF);
}
/// @dev Asserts that the reentrant call will revert.
function callPortalAndExpectRevert() external payable {
vm.expectRevert("OptimismPortal: can only trigger one withdrawal per transaction");
// Arguments here don't matter, as the require check is the first thing that happens.
// We assume that this has already been proven.
optimismPortal2.finalizeWithdrawalTransaction(_defaultTx);
// Assert that the withdrawal was not finalized.
assertFalse(optimismPortal2.finalizedWithdrawals(Hashing.hashWithdrawal(_defaultTx)));
}
/// @dev Tests that `deleteProvenWithdrawal` reverts when called by a non-SAURON.
function testFuzz_deleteProvenWithdrawal_onlySauron_reverts(address _act, bytes32 _wdHash) external {
vm.assume(_act != address(optimismPortal2.sauron()));
vm.expectRevert("OptimismPortal: only sauron can delete proven withdrawals");
optimismPortal2.deleteProvenWithdrawal(_wdHash);
}
/// @dev Tests that the SAURON role can delete any proven withdrawal.
function test_deleteProvenWithdrawal_sauron_succeeds() external {
vm.expectEmit(true, true, true, true);
emit WithdrawalProven(_withdrawalHash, alice, bob);
optimismPortal2.proveWithdrawalTransaction({
_tx: _defaultTx,
_disputeGameIndex: _proposedGameIndex,
_outputRootProof: _outputRootProof,
_withdrawalProof: _withdrawalProof
});
// Ensure the withdrawal has been proven.
(, uint64 timestamp) = optimismPortal2.provenWithdrawals(_withdrawalHash);
assertEq(timestamp, block.timestamp);
// Delete the proven withdrawal.
vm.prank(optimismPortal2.sauron());
optimismPortal2.deleteProvenWithdrawal(_withdrawalHash);
// Ensure the withdrawal has been deleted
(, timestamp) = optimismPortal2.provenWithdrawals(_withdrawalHash);
assertEq(timestamp, 0);
}
/// @dev Tests that `deleteProvenWithdrawal` reverts when called by a non-SAURON.
function testFuzz_blacklist_onlySauron_reverts(address _act) external {
vm.assume(_act != address(optimismPortal2.sauron()));
vm.expectRevert("OptimismPortal: only sauron can blacklist dispute games");
optimismPortal2.blacklistDisputeGame(IDisputeGame(address(0xdead)));
}
/// @dev Tests that the SAURON role can blacklist any dispute game.
function testFuzz_blacklist_sauron_succeeds(address _addr) external {
vm.prank(optimismPortal2.sauron());
optimismPortal2.blacklistDisputeGame(IDisputeGame(_addr));
assertTrue(optimismPortal2.disputeGameBlacklist(IDisputeGame(_addr)));
}
/// @dev Tests that `setRespectedGameType` reverts when called by a non-SAURON.
function testFuzz_setRespectedGameType_onlySauron_reverts(address _act, GameType _ty) external {
vm.assume(_act != address(optimismPortal2.sauron()));
vm.prank(_act);
vm.expectRevert("OptimismPortal: only sauron can set the respected game type");
optimismPortal2.setRespectedGameType(_ty);
}
/// @dev Tests that the SAURON role can set the respected game type to anything they want.
function testFuzz_setRespectedGameType_sauron_succeeds(GameType _ty) external {
vm.prank(optimismPortal2.sauron());
optimismPortal2.setRespectedGameType(_ty);
assertEq(optimismPortal2.respectedGameType().raw(), _ty.raw());
}
/// @dev Tests that `proveWithdrawalTransaction` reverts when paused.
function test_proveWithdrawalTransaction_paused_reverts() external {
vm.prank(optimismPortal2.GUARDIAN());
superchainConfig.pause("identifier");
vm.expectRevert("OptimismPortal: paused");
optimismPortal2.proveWithdrawalTransaction({
_tx: _defaultTx,
_disputeGameIndex: _proposedGameIndex,
_outputRootProof: _outputRootProof,
_withdrawalProof: _withdrawalProof
});
}
/// @dev Tests that `proveWithdrawalTransaction` reverts when the target is the portal contract.
function test_proveWithdrawalTransaction_onSelfCall_reverts() external {
_defaultTx.target = address(optimismPortal2);
vm.expectRevert("OptimismPortal: you cannot send messages to the portal contract");
optimismPortal2.proveWithdrawalTransaction({
_tx: _defaultTx,
_disputeGameIndex: _proposedGameIndex,
_outputRootProof: _outputRootProof,
_withdrawalProof: _withdrawalProof
});
}
/// @dev Tests that `proveWithdrawalTransaction` reverts when the outputRootProof does not match the output root
function test_proveWithdrawalTransaction_onInvalidOutputRootProof_reverts() external {
// Modify the version to invalidate the withdrawal proof.
_outputRootProof.version = bytes32(uint256(1));
vm.expectRevert("OptimismPortal: invalid output root proof");
optimismPortal2.proveWithdrawalTransaction({
_tx: _defaultTx,
_disputeGameIndex: _proposedGameIndex,
_outputRootProof: _outputRootProof,
_withdrawalProof: _withdrawalProof
});
}
/// @dev Tests that `proveWithdrawalTransaction` reverts when the withdrawal is missing.
function test_proveWithdrawalTransaction_onInvalidWithdrawalProof_reverts() external {
// modify the default test values to invalidate the proof.
_defaultTx.data = hex"abcd";
vm.expectRevert("MerkleTrie: path remainder must share all nibbles with key");
optimismPortal2.proveWithdrawalTransaction({
_tx: _defaultTx,
_disputeGameIndex: _proposedGameIndex,
_outputRootProof: _outputRootProof,
_withdrawalProof: _withdrawalProof
});
}
/// @dev Tests that `proveWithdrawalTransaction` reverts when the withdrawal has already
/// been proven.
function test_proveWithdrawalTransaction_replayProve_reverts() external {
vm.expectEmit(true, true, true, true);
emit WithdrawalProven(_withdrawalHash, alice, bob);
optimismPortal2.proveWithdrawalTransaction({
_tx: _defaultTx,
_disputeGameIndex: _proposedGameIndex,
_outputRootProof: _outputRootProof,
_withdrawalProof: _withdrawalProof
});
vm.expectRevert("OptimismPortal: withdrawal hash has already been proven, and dispute game is not invalid");
optimismPortal2.proveWithdrawalTransaction({
_tx: _defaultTx,
_disputeGameIndex: _proposedGameIndex,
_outputRootProof: _outputRootProof,
_withdrawalProof: _withdrawalProof
});
}
/// @dev Tests that `proveWithdrawalTransaction` reverts if the dispute game being proven against is not of the
/// respected game type.
function test_proveWithdrawalTransaction_badGameType_reverts() external {
vm.mockCall(
address(disputeGameFactory),
abi.encodeCall(disputeGameFactory.gameAtIndex, (_proposedGameIndex)),
abi.encode(GameType.wrap(0xFF), Timestamp.wrap(uint64(block.timestamp)), IDisputeGame(address(game)))
);
vm.expectRevert("OptimismPortal: invalid game type");
optimismPortal2.proveWithdrawalTransaction({
_tx: _defaultTx,
_disputeGameIndex: _proposedGameIndex,
_outputRootProof: _outputRootProof,
_withdrawalProof: _withdrawalProof
});
}
/// @dev Tests that `proveWithdrawalTransaction` can be re-executed if the dispute game proven against has been
/// blacklisted.
function test_proveWithdrawalTransaction_replayProveBlacklisted_suceeds() external {
vm.expectEmit(true, true, true, true);
emit WithdrawalProven(_withdrawalHash, alice, bob);
optimismPortal2.proveWithdrawalTransaction({
_tx: _defaultTx,
_disputeGameIndex: _proposedGameIndex,
_outputRootProof: _outputRootProof,
_withdrawalProof: _withdrawalProof
});
// Blacklist the dispute dispute game.
vm.prank(optimismPortal2.sauron());
optimismPortal2.blacklistDisputeGame(IDisputeGame(address(game)));
vm.expectEmit(true, true, true, true);
emit WithdrawalProven(_withdrawalHash, alice, bob);
optimismPortal2.proveWithdrawalTransaction({
_tx: _defaultTx,
_disputeGameIndex: _proposedGameIndex,
_outputRootProof: _outputRootProof,
_withdrawalProof: _withdrawalProof
});
}
/// @dev Tests that `proveWithdrawalTransaction` can be re-executed if the dispute game proven against has resolved
/// against the favor of the root claim.
function test_proveWithdrawalTransaction_replayProveBadProposal_suceeds() external {
vm.expectEmit(true, true, true, true);
emit WithdrawalProven(_withdrawalHash, alice, bob);
optimismPortal2.proveWithdrawalTransaction({
_tx: _defaultTx,
_disputeGameIndex: _proposedGameIndex,
_outputRootProof: _outputRootProof,
_withdrawalProof: _withdrawalProof
});
vm.mockCall(address(game), abi.encodeCall(game.status, ()), abi.encode(GameStatus.CHALLENGER_WINS));
vm.expectEmit(true, true, true, true);
emit WithdrawalProven(_withdrawalHash, alice, bob);
optimismPortal2.proveWithdrawalTransaction({
_tx: _defaultTx,
_disputeGameIndex: _proposedGameIndex,
_outputRootProof: _outputRootProof,
_withdrawalProof: _withdrawalProof
});
}
/// @dev Tests that `proveWithdrawalTransaction` can be re-executed if the dispute game proven against is no longer
/// of the respected game type.
function test_proveWithdrawalTransaction_replayRespectedGameTypeChanged_suceeds() external {
// Prove the withdrawal against a game with the current respected game type.
vm.expectEmit(true, true, true, true);
emit WithdrawalProven(_withdrawalHash, alice, bob);
optimismPortal2.proveWithdrawalTransaction({
_tx: _defaultTx,
_disputeGameIndex: _proposedGameIndex,
_outputRootProof: _outputRootProof,
_withdrawalProof: _withdrawalProof
});
// Update the respected game type to 0xbeef.
vm.prank(optimismPortal2.sauron());
optimismPortal2.setRespectedGameType(GameType.wrap(0xbeef));
// Create a new game and mock the game type as 0xbeef in the factory.
IDisputeGame newGame =
disputeGameFactory.create(GameType.wrap(0), Claim.wrap(_outputRoot), abi.encode(_proposedBlockNumber + 1));
vm.mockCall(
address(disputeGameFactory),
abi.encodeCall(disputeGameFactory.gameAtIndex, (_proposedGameIndex + 1)),
abi.encode(GameType.wrap(0xbeef), Timestamp.wrap(uint64(block.timestamp)), IDisputeGame(address(newGame)))
);
// Re-proving should be successful against the new game.
vm.expectEmit(true, true, true, true);
emit WithdrawalProven(_withdrawalHash, alice, bob);
optimismPortal2.proveWithdrawalTransaction({
_tx: _defaultTx,
_disputeGameIndex: _proposedGameIndex + 1,
_outputRootProof: _outputRootProof,
_withdrawalProof: _withdrawalProof
});
}
/// @dev Tests that `proveWithdrawalTransaction` succeeds.
function test_proveWithdrawalTransaction_validWithdrawalProof_succeeds() external {
vm.expectEmit(true, true, true, true);
emit WithdrawalProven(_withdrawalHash, alice, bob);
optimismPortal2.proveWithdrawalTransaction({
_tx: _defaultTx,
_disputeGameIndex: _proposedGameIndex,
_outputRootProof: _outputRootProof,
_withdrawalProof: _withdrawalProof
});
}
/// @dev Tests that `finalizeWithdrawalTransaction` succeeds.
function test_finalizeWithdrawalTransaction_provenWithdrawalHash_succeeds() external {
uint256 bobBalanceBefore = address(bob).balance;
vm.expectEmit(true, true, true, true);
emit WithdrawalProven(_withdrawalHash, alice, bob);
optimismPortal2.proveWithdrawalTransaction({
_tx: _defaultTx,
_disputeGameIndex: _proposedGameIndex,
_outputRootProof: _outputRootProof,
_withdrawalProof: _withdrawalProof
});
// Warp and resolve the dispute game.
game.resolveClaim(0);
game.resolve();
vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1 seconds);
vm.expectEmit(true, true, false, true);
emit WithdrawalFinalized(_withdrawalHash, true);
optimismPortal2.finalizeWithdrawalTransaction(_defaultTx);
assert(address(bob).balance == bobBalanceBefore + 100);
}
/// @dev Tests that `finalizeWithdrawalTransaction` reverts if the contract is paused.
function test_finalizeWithdrawalTransaction_paused_reverts() external {
vm.prank(optimismPortal2.GUARDIAN());
superchainConfig.pause("identifier");
vm.expectRevert("OptimismPortal: paused");
optimismPortal2.finalizeWithdrawalTransaction(_defaultTx);
}
/// @dev Tests that `finalizeWithdrawalTransaction` reverts if the withdrawal has not been
function test_finalizeWithdrawalTransaction_ifWithdrawalNotProven_reverts() external {
uint256 bobBalanceBefore = address(bob).balance;
vm.expectRevert("OptimismPortal: withdrawal has not been proven yet");
optimismPortal2.finalizeWithdrawalTransaction(_defaultTx);
assert(address(bob).balance == bobBalanceBefore);
}
/// @dev Tests that `finalizeWithdrawalTransaction` reverts if the withdrawal has not been
/// proven long enough ago.
function test_finalizeWithdrawalTransaction_ifWithdrawalProofNotOldEnough_reverts() external {
uint256 bobBalanceBefore = address(bob).balance;
vm.expectEmit(true, true, true, true);
emit WithdrawalProven(_withdrawalHash, alice, bob);
optimismPortal2.proveWithdrawalTransaction({
_tx: _defaultTx,
_disputeGameIndex: _proposedGameIndex,
_outputRootProof: _outputRootProof,
_withdrawalProof: _withdrawalProof
});
vm.expectRevert("OptimismPortal: proven withdrawal has not matured yet");
optimismPortal2.finalizeWithdrawalTransaction(_defaultTx);
assert(address(bob).balance == bobBalanceBefore);
}
/// @dev Tests that `finalizeWithdrawalTransaction` reverts if the provenWithdrawal's timestamp
/// is less than the dispute game's creation timestamp.
function test_finalizeWithdrawalTransaction_timestampLessThanGameCreation_reverts() external {
uint256 bobBalanceBefore = address(bob).balance;
// Prove our withdrawal
vm.expectEmit(true, true, true, true);
emit WithdrawalProven(_withdrawalHash, alice, bob);
optimismPortal2.proveWithdrawalTransaction({
_tx: _defaultTx,
_disputeGameIndex: _proposedGameIndex,
_outputRootProof: _outputRootProof,
_withdrawalProof: _withdrawalProof
});
// Warp to after the finalization period
vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1);
// Mock a createdAt change in the dispute game.
vm.mockCall(address(game), abi.encodeWithSignature("createdAt()"), abi.encode(block.timestamp + 1));
// Attempt to finalize the withdrawal
vm.expectRevert("OptimismPortal: withdrawal timestamp less than dispute game creation timestamp");
optimismPortal2.finalizeWithdrawalTransaction(_defaultTx);
// Ensure that bob's balance has remained the same
assertEq(bobBalanceBefore, address(bob).balance);
}
/// @dev Tests that `finalizeWithdrawalTransaction` reverts if the dispute game has not resolved in favor of the
/// root claim.
function test_finalizeWithdrawalTransaction_ifDisputeGameNotResolved_reverts() external {
uint256 bobBalanceBefore = address(bob).balance;
// Prove our withdrawal
vm.expectEmit(true, true, true, true);
emit WithdrawalProven(_withdrawalHash, alice, bob);
optimismPortal2.proveWithdrawalTransaction({
_tx: _defaultTx,
_disputeGameIndex: _proposedGameIndex,
_outputRootProof: _outputRootProof,
_withdrawalProof: _withdrawalProof
});
// Warp to after the finalization period
vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1);
// Attempt to finalize the withdrawal
vm.expectRevert("OptimismPortal: output proposal has not been finalized yet");
optimismPortal2.finalizeWithdrawalTransaction(_defaultTx);
// Ensure that bob's balance has remained the same
assertEq(bobBalanceBefore, address(bob).balance);
}
/// @dev Tests that `finalizeWithdrawalTransaction` reverts if the target reverts.
function test_finalizeWithdrawalTransaction_targetFails_fails() external {
uint256 bobBalanceBefore = address(bob).balance;
vm.etch(bob, hex"fe"); // Contract with just the invalid opcode.
vm.expectEmit(true, true, true, true);
emit WithdrawalProven(_withdrawalHash, alice, bob);
optimismPortal2.proveWithdrawalTransaction({
_tx: _defaultTx,
_disputeGameIndex: _proposedGameIndex,
_outputRootProof: _outputRootProof,
_withdrawalProof: _withdrawalProof
});
// Resolve the dispute game.
game.resolveClaim(0);
game.resolve();
vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1);
vm.expectEmit(true, true, true, true);
emit WithdrawalFinalized(_withdrawalHash, false);
optimismPortal2.finalizeWithdrawalTransaction(_defaultTx);
assert(address(bob).balance == bobBalanceBefore);
}
/// @dev Tests that `finalizeWithdrawalTransaction` reverts if the withdrawal has already been
/// finalized.
function test_finalizeWithdrawalTransaction_onReplay_reverts() external {
vm.expectEmit(true, true, true, true);
emit WithdrawalProven(_withdrawalHash, alice, bob);
optimismPortal2.proveWithdrawalTransaction({
_tx: _defaultTx,
_disputeGameIndex: _proposedGameIndex,
_outputRootProof: _outputRootProof,
_withdrawalProof: _withdrawalProof
});
// Resolve the dispute game.
game.resolveClaim(0);
game.resolve();
vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1);
vm.expectEmit(true, true, true, true);
emit WithdrawalFinalized(_withdrawalHash, true);
optimismPortal2.finalizeWithdrawalTransaction(_defaultTx);
vm.expectRevert("OptimismPortal: withdrawal has already been finalized");
optimismPortal2.finalizeWithdrawalTransaction(_defaultTx);
}
/// @dev Tests that `finalizeWithdrawalTransaction` reverts if the withdrawal transaction
/// does not have enough gas to execute.
function test_finalizeWithdrawalTransaction_onInsufficientGas_reverts() external {
// This number was identified through trial and error.
uint256 gasLimit = 150_000;
Types.WithdrawalTransaction memory insufficientGasTx = Types.WithdrawalTransaction({
nonce: 0,
sender: alice,
target: bob,
value: 100,
gasLimit: gasLimit,
data: hex""
});
// Get updated proof inputs.
(bytes32 stateRoot, bytes32 storageRoot,,, bytes[] memory withdrawalProof) =
ffi.getProveWithdrawalTransactionInputs(insufficientGasTx);
Types.OutputRootProof memory outputRootProof = Types.OutputRootProof({
version: bytes32(0),
stateRoot: stateRoot,
messagePasserStorageRoot: storageRoot,
latestBlockhash: bytes32(0)
});
vm.mockCall(
address(game), abi.encodeCall(game.rootClaim, ()), abi.encode(Hashing.hashOutputRootProof(outputRootProof))
);
optimismPortal2.proveWithdrawalTransaction({
_tx: insufficientGasTx,
_disputeGameIndex: _proposedGameIndex,
_outputRootProof: outputRootProof,
_withdrawalProof: withdrawalProof
});
// Resolve the dispute game.
game.resolveClaim(0);
game.resolve();
vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1);
vm.expectRevert("SafeCall: Not enough gas");
optimismPortal2.finalizeWithdrawalTransaction{ gas: gasLimit }(insufficientGasTx);
}
/// @dev Tests that `finalizeWithdrawalTransaction` reverts if a sub-call attempts to finalize
/// another withdrawal.
function test_finalizeWithdrawalTransaction_onReentrancy_reverts() external {
uint256 bobBalanceBefore = address(bob).balance;
// Copy and modify the default test values to attempt a reentrant call by first calling to
// this contract's callPortalAndExpectRevert() function above.
Types.WithdrawalTransaction memory _testTx = _defaultTx;
_testTx.target = address(this);
_testTx.data = abi.encodeWithSelector(this.callPortalAndExpectRevert.selector);
// Get modified proof inputs.
(
bytes32 stateRoot,
bytes32 storageRoot,
bytes32 outputRoot,
bytes32 withdrawalHash,
bytes[] memory withdrawalProof
) = ffi.getProveWithdrawalTransactionInputs(_testTx);
Types.OutputRootProof memory outputRootProof = Types.OutputRootProof({
version: bytes32(0),
stateRoot: stateRoot,
messagePasserStorageRoot: storageRoot,
latestBlockhash: bytes32(0)
});
// Return a mock output root from the game.
vm.mockCall(address(game), abi.encodeCall(game.rootClaim, ()), abi.encode(outputRoot));
vm.expectEmit(true, true, true, true);
emit WithdrawalProven(withdrawalHash, alice, address(this));
optimismPortal2.proveWithdrawalTransaction(_testTx, _proposedGameIndex, outputRootProof, withdrawalProof);
// Resolve the dispute game.
game.resolveClaim(0);
game.resolve();
vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1);
vm.expectCall(address(this), _testTx.data);
vm.expectEmit(true, true, true, true);
emit WithdrawalFinalized(withdrawalHash, true);
optimismPortal2.finalizeWithdrawalTransaction(_testTx);
// Ensure that bob's balance was not changed by the reentrant call.
assert(address(bob).balance == bobBalanceBefore);
}
/// @dev Tests that `finalizeWithdrawalTransaction` succeeds.
function testDiff_finalizeWithdrawalTransaction_succeeds(
address _sender,
address _target,
uint256 _value,
uint256 _gasLimit,
bytes memory _data
)
external
{
vm.assume(
_target != address(optimismPortal2) // Cannot call the optimism portal or a contract
&& _target.code.length == 0 // No accounts with code
&& _target != CONSOLE // The console has no code but behaves like a contract
&& uint160(_target) > 9 // No precompiles (or zero address)
);
// Total ETH supply is currently about 120M ETH.
uint256 value = bound(_value, 0, 200_000_000 ether);
vm.deal(address(optimismPortal2), value);
uint256 gasLimit = bound(_gasLimit, 0, 50_000_000);
uint256 nonce = l2ToL1MessagePasser.messageNonce();
// Get a withdrawal transaction and mock proof from the differential testing script.
Types.WithdrawalTransaction memory _tx = Types.WithdrawalTransaction({
nonce: nonce,
sender: _sender,
target: _target,
value: value,
gasLimit: gasLimit,
data: _data
});
(
bytes32 stateRoot,
bytes32 storageRoot,
bytes32 outputRoot,
bytes32 withdrawalHash,
bytes[] memory withdrawalProof
) = ffi.getProveWithdrawalTransactionInputs(_tx);
// Create the output root proof
Types.OutputRootProof memory proof = Types.OutputRootProof({
version: bytes32(uint256(0)),
stateRoot: stateRoot,
messagePasserStorageRoot: storageRoot,
latestBlockhash: bytes32(uint256(0))
});
// Ensure the values returned from ffi are correct
assertEq(outputRoot, Hashing.hashOutputRootProof(proof));
assertEq(withdrawalHash, Hashing.hashWithdrawal(_tx));
// Setup the dispute game to return the output root
vm.mockCall(address(game), abi.encodeCall(game.rootClaim, ()), abi.encode(outputRoot));
// Prove the withdrawal transaction
optimismPortal2.proveWithdrawalTransaction(_tx, _proposedGameIndex, proof, withdrawalProof);
(IDisputeGame _game,) = optimismPortal2.provenWithdrawals(withdrawalHash);
assertTrue(_game.rootClaim().raw() != bytes32(0));
// Resolve the dispute game
game.resolveClaim(0);
game.resolve();
// Warp past the finalization period
vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1);
// Finalize the withdrawal transaction
vm.expectCallMinGas(_tx.target, _tx.value, uint64(_tx.gasLimit), _tx.data);
optimismPortal2.finalizeWithdrawalTransaction(_tx);
assertTrue(optimismPortal2.finalizedWithdrawals(withdrawalHash));
}
/// @dev Tests that `finalizeWithdrawalTransaction` reverts if the withdrawal's dispute game has been blacklisted.
function test_finalizeWithdrawalTransaction_blacklisted_reverts() external {
vm.expectEmit(true, true, true, true);
emit WithdrawalProven(_withdrawalHash, alice, bob);
optimismPortal2.proveWithdrawalTransaction({
_tx: _defaultTx,
_disputeGameIndex: _proposedGameIndex,
_outputRootProof: _outputRootProof,
_withdrawalProof: _withdrawalProof
});
// Resolve the dispute game.
game.resolveClaim(0);
game.resolve();
vm.prank(optimismPortal2.sauron());
optimismPortal2.blacklistDisputeGame(IDisputeGame(address(game)));
vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1);
vm.expectRevert("OptimismPortal: dispute game has been blacklisted");
optimismPortal2.finalizeWithdrawalTransaction(_defaultTx);
}
/// @dev Tests that `finalizeWithdrawalTransaction` reverts if the withdrawal's dispute game is still in the air
/// gap.
function test_finalizeWithdrawalTransaction_gameInAirGap_reverts() external {
vm.expectEmit(true, true, true, true);
emit WithdrawalProven(_withdrawalHash, alice, bob);
optimismPortal2.proveWithdrawalTransaction({
_tx: _defaultTx,
_disputeGameIndex: _proposedGameIndex,
_outputRootProof: _outputRootProof,
_withdrawalProof: _withdrawalProof
});
// Warp past the finalization period.
vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1);
// Resolve the dispute game.
game.resolveClaim(0);
game.resolve();
// Attempt to finalize the withdrawal directly after the game resolves. This should fail.
vm.expectRevert("OptimismPortal: output proposal in air-gap");
optimismPortal2.finalizeWithdrawalTransaction(_defaultTx);
// Finalize the withdrawal transaction. This should succeed.
vm.warp(block.timestamp + optimismPortal2.disputeGameFinalityDelaySeconds() + 1);
optimismPortal2.finalizeWithdrawalTransaction(_defaultTx);
assertTrue(optimismPortal2.finalizedWithdrawals(_withdrawalHash));
}
/// @dev Tests that `finalizeWithdrawalTransaction` reverts if the respected game type has changed since the
/// withdrawal was proven.
function test_finalizeWithdrawalTransaction_respectedTypeChangedSinceProving_reverts() external {
vm.expectEmit(true, true, true, true);
emit WithdrawalProven(_withdrawalHash, alice, bob);
optimismPortal2.proveWithdrawalTransaction({
_tx: _defaultTx,
_disputeGameIndex: _proposedGameIndex,
_outputRootProof: _outputRootProof,
_withdrawalProof: _withdrawalProof
});
// Warp past the finalization period.
vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1);
// Resolve the dispute game.
game.resolveClaim(0);
game.resolve();
// Change the respected game type in the portal.
vm.prank(optimismPortal2.sauron());
optimismPortal2.setRespectedGameType(GameType.wrap(0xFF));
vm.expectRevert("OptimismPortal: invalid game type");
optimismPortal2.finalizeWithdrawalTransaction(_defaultTx);
}
/// @dev Tests an e2e prove -> finalize path, checking the edges of each delay for correctness.
function test_finalizeWithdrawalTransaction_delayEdges_succeeds() external {
// Prove the withdrawal transaction.
vm.expectEmit(true, true, true, true);
emit WithdrawalProven(_withdrawalHash, alice, bob);
optimismPortal2.proveWithdrawalTransaction({
_tx: _defaultTx,
_disputeGameIndex: _proposedGameIndex,
_outputRootProof: _outputRootProof,
_withdrawalProof: _withdrawalProof
});
// Attempt to finalize the withdrawal transaction 1 second before the proof has matured. This should fail.
vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds());
vm.expectRevert("OptimismPortal: proven withdrawal has not matured yet");
optimismPortal2.finalizeWithdrawalTransaction(_defaultTx);
// Warp 1 second in the future, past the proof maturity delay, and attempt to finalize the withdrawal.
// This should also fail, since the dispute game has not resolved yet.
vm.warp(block.timestamp + 1 seconds);
vm.expectRevert("OptimismPortal: output proposal has not been finalized yet");
optimismPortal2.finalizeWithdrawalTransaction(_defaultTx);
// Finalize the dispute game and attempt to finalize the withdrawal again. This should also fail, since the
// air gap dispute game delay has not elapsed.
game.resolveClaim(0);
game.resolve();
vm.warp(block.timestamp + optimismPortal2.disputeGameFinalityDelaySeconds());
vm.expectRevert("OptimismPortal: output proposal in air-gap");
optimismPortal2.finalizeWithdrawalTransaction(_defaultTx);
// Warp 1 second in the future, past the air gap dispute game delay, and attempt to finalize the withdrawal.
// This should succeed.
vm.warp(block.timestamp + 1 seconds);
optimismPortal2.finalizeWithdrawalTransaction(_defaultTx);
assertTrue(optimismPortal2.finalizedWithdrawals(_withdrawalHash));
}
}
contract OptimismPortal2_Upgradeable_Test is CommonTest {
function setUp() public override {
super.enableFaultProofs();
super.setUp();
}
/// @dev Tests that the proxy is initialized correctly.
function test_params_initValuesOnProxy_succeeds() external {
(uint128 prevBaseFee, uint64 prevBoughtGas, uint64 prevBlockNum) = optimismPortal2.params();
ResourceMetering.ResourceConfig memory rcfg = systemConfig.resourceConfig();
assertEq(prevBaseFee, rcfg.minimumBaseFee);
assertEq(prevBoughtGas, 0);
assertEq(prevBlockNum, block.number);
}
/// @dev Tests that the proxy can be upgraded.
function test_upgradeToAndCall_upgrading_succeeds() external {
// Check an unused slot before upgrading.
bytes32 slot21Before = vm.load(address(optimismPortal2), bytes32(uint256(21)));
assertEq(bytes32(0), slot21Before);
NextImpl nextImpl = new NextImpl();
vm.startPrank(EIP1967Helper.getAdmin(address(optimismPortal2)));
// The value passed to the initialize must be larger than the last value
// that initialize was called with.
Proxy(payable(address(optimismPortal2))).upgradeToAndCall(
address(nextImpl), abi.encodeWithSelector(NextImpl.initialize.selector, 2)
);
assertEq(Proxy(payable(address(optimismPortal2))).implementation(), address(nextImpl));
// Verify that the NextImpl contract initialized its values according as expected
bytes32 slot21After = vm.load(address(optimismPortal2), bytes32(uint256(21)));
bytes32 slot21Expected = NextImpl(address(optimismPortal2)).slot21Init();
assertEq(slot21Expected, slot21After);
}
}
/// @title OptimismPortal2_ResourceFuzz_Test
/// @dev Test various values of the resource metering config to ensure that deposits cannot be
/// broken by changing the config.
contract OptimismPortal2_ResourceFuzz_Test is CommonTest {
/// @dev The max gas limit observed throughout this test. Setting this too high can cause
/// the test to take too long to run.
uint256 constant MAX_GAS_LIMIT = 30_000_000;
function setUp() public override {
super.enableFaultProofs();
super.setUp();
}
/// @dev Test that various values of the resource metering config will not break deposits.
function testFuzz_systemConfigDeposit_succeeds(
uint32 _maxResourceLimit,
uint8 _elasticityMultiplier,
uint8 _baseFeeMaxChangeDenominator,
uint32 _minimumBaseFee,
uint32 _systemTxMaxGas,
uint128 _maximumBaseFee,
uint64 _gasLimit,
uint64 _prevBoughtGas,
uint128 _prevBaseFee,
uint8 _blockDiff
)
external
{
// Get the set system gas limit
uint64 gasLimit = systemConfig.gasLimit();
// Bound resource config
_maxResourceLimit = uint32(bound(_maxResourceLimit, 21000, MAX_GAS_LIMIT / 8));
_gasLimit = uint64(bound(_gasLimit, 21000, _maxResourceLimit));
_prevBaseFee = uint128(bound(_prevBaseFee, 0, 3 gwei));
// Prevent values that would cause reverts
vm.assume(gasLimit >= _gasLimit);
vm.assume(_minimumBaseFee < _maximumBaseFee);
vm.assume(_baseFeeMaxChangeDenominator > 1);
vm.assume(uint256(_maxResourceLimit) + uint256(_systemTxMaxGas) <= gasLimit);
vm.assume(_elasticityMultiplier > 0);
vm.assume(((_maxResourceLimit / _elasticityMultiplier) * _elasticityMultiplier) == _maxResourceLimit);
_prevBoughtGas = uint64(bound(_prevBoughtGas, 0, _maxResourceLimit - _gasLimit));
_blockDiff = uint8(bound(_blockDiff, 0, 3));
// Pick a pseudorandom block number
vm.roll(uint256(keccak256(abi.encode(_blockDiff))) % uint256(type(uint16).max) + uint256(_blockDiff));
// Create a resource config to mock the call to the system config with
ResourceMetering.ResourceConfig memory rcfg = ResourceMetering.ResourceConfig({
maxResourceLimit: _maxResourceLimit,
elasticityMultiplier: _elasticityMultiplier,
baseFeeMaxChangeDenominator: _baseFeeMaxChangeDenominator,
minimumBaseFee: _minimumBaseFee,
systemTxMaxGas: _systemTxMaxGas,
maximumBaseFee: _maximumBaseFee
});
vm.mockCall(
address(systemConfig), abi.encodeWithSelector(systemConfig.resourceConfig.selector), abi.encode(rcfg)
);
// Set the resource params
uint256 _prevBlockNum = block.number - _blockDiff;
vm.store(
address(optimismPortal2),
bytes32(uint256(1)),
bytes32((_prevBlockNum << 192) | (uint256(_prevBoughtGas) << 128) | _prevBaseFee)
);
// Ensure that the storage setting is correct
(uint128 prevBaseFee, uint64 prevBoughtGas, uint64 prevBlockNum) = optimismPortal2.params();
assertEq(prevBaseFee, _prevBaseFee);
assertEq(prevBoughtGas, _prevBoughtGas);
assertEq(prevBlockNum, _prevBlockNum);
// Do a deposit, should not revert
optimismPortal2.depositTransaction{ gas: MAX_GAS_LIMIT }({
_to: address(0x20),
_value: 0x40,
_gasLimit: _gasLimit,
_isCreation: false,
_data: hex""
});
}
}
......@@ -32,6 +32,7 @@ contract Specification_Test is CommonTest {
CHALLENGER,
SYSTEMCONFIGOWNER,
GUARDIAN,
SAURON,
MESSENGER,
L1PROXYADMINOWNER,
GOVERNANCETOKENOWNER,
......@@ -235,6 +236,7 @@ contract Specification_Test is CommonTest {
});
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("finalizedWithdrawals(bytes32)") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("guardian()") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("sauron()") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("initialize(address,address,address)") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("l2Sender()") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("minimumGasLimit(uint64)") });
......@@ -248,10 +250,12 @@ contract Specification_Test is CommonTest {
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("disputeGameFactory()") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("disputeGameBlacklist(address)") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("respectedGameType()") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("blacklistDisputeGame(address)") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("deleteProvenWithdrawal(bytes32)") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("setRespectedGameType(uint32)") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("blacklistDisputeGame(address)"), _auth: Role.SAURON });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("deleteProvenWithdrawal(bytes32)"), _auth: Role.SAURON });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("setRespectedGameType(uint32)"), _auth: Role.SAURON });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("checkWithdrawal(bytes32)") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("proofMaturityDelaySeconds()") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("disputeGameFinalityDelaySeconds()") });
// ProtocolVersions
_addSpec({ _name: "ProtocolVersions", _sel: _getSel("RECOMMENDED_SLOT()") });
......
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { StdUtils } from "forge-std/Test.sol";
import { Vm } from "forge-std/Vm.sol";
import { OptimismPortal2 } from "src/L1/OptimismPortal2.sol";
import { AddressAliasHelper } from "src/vendor/AddressAliasHelper.sol";
import { SystemConfig } from "src/L1/SystemConfig.sol";
import { ResourceMetering } from "src/L1/ResourceMetering.sol";
import { Constants } from "src/libraries/Constants.sol";
import { CommonTest } from "test/setup/CommonTest.sol";
import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol";
import { Types } from "src/libraries/Types.sol";
import { FaultDisputeGame } from "src/dispute/FaultDisputeGame.sol";
import "src/libraries/DisputeTypes.sol";
contract OptimismPortal2_Depositor is StdUtils, ResourceMetering {
Vm internal vm;
OptimismPortal2 internal portal;
bool public failedToComplete;
constructor(Vm _vm, OptimismPortal2 _portal) {
vm = _vm;
portal = _portal;
initialize();
}
function initialize() internal initializer {
__ResourceMetering_init();
}
function resourceConfig() public pure returns (ResourceMetering.ResourceConfig memory) {
return _resourceConfig();
}
function _resourceConfig() internal pure override returns (ResourceMetering.ResourceConfig memory) {
ResourceMetering.ResourceConfig memory rcfg = Constants.DEFAULT_RESOURCE_CONFIG();
return rcfg;
}
// A test intended to identify any unexpected halting conditions
function depositTransactionCompletes(
address _to,
uint256 _value,
uint64 _gasLimit,
bool _isCreation,
bytes memory _data
)
public
payable
{
vm.assume((!_isCreation || _to == address(0)) && _data.length <= 120_000);
uint256 preDepositvalue = bound(_value, 0, type(uint128).max);
// Give the depositor some ether
vm.deal(address(this), preDepositvalue);
// cache the contract's eth balance
uint256 preDepositBalance = address(this).balance;
uint256 value = bound(preDepositvalue, 0, preDepositBalance);
(, uint64 cachedPrevBoughtGas,) = ResourceMetering(address(portal)).params();
ResourceMetering.ResourceConfig memory rcfg = resourceConfig();
uint256 maxResourceLimit = uint64(rcfg.maxResourceLimit);
uint64 gasLimit = uint64(
bound(_gasLimit, portal.minimumGasLimit(uint64(_data.length)), maxResourceLimit - cachedPrevBoughtGas)
);
try portal.depositTransaction{ value: value }(_to, value, gasLimit, _isCreation, _data) {
// Do nothing; Call succeeded
} catch {
failedToComplete = true;
}
}
}
contract OptimismPortal2_Invariant_Harness is CommonTest {
// Reusable default values for a test withdrawal
Types.WithdrawalTransaction _defaultTx;
uint256 _proposedGameIndex;
uint256 _proposedBlockNumber;
bytes32 _stateRoot;
bytes32 _storageRoot;
bytes32 _outputRoot;
bytes32 _withdrawalHash;
bytes[] _withdrawalProof;
Types.OutputRootProof internal _outputRootProof;
function setUp() public virtual override {
super.enableFaultProofs();
super.setUp();
_defaultTx = Types.WithdrawalTransaction({
nonce: 0,
sender: alice,
target: bob,
value: 100,
gasLimit: 100_000,
data: hex""
});
// Get withdrawal proof data we can use for testing.
(_stateRoot, _storageRoot, _outputRoot, _withdrawalHash, _withdrawalProof) =
ffi.getProveWithdrawalTransactionInputs(_defaultTx);
// Setup a dummy output root proof for reuse.
_outputRootProof = Types.OutputRootProof({
version: bytes32(uint256(0)),
stateRoot: _stateRoot,
messagePasserStorageRoot: _storageRoot,
latestBlockhash: bytes32(uint256(0))
});
// Create a dispute game with the output root we've proposed.
_proposedBlockNumber = 0xFF;
FaultDisputeGame game = FaultDisputeGame(
address(
disputeGameFactory.create(
optimismPortal2.respectedGameType(), Claim.wrap(_outputRoot), abi.encode(_proposedBlockNumber)
)
)
);
_proposedGameIndex = disputeGameFactory.gameCount() - 1;
// Warp beyond the finalization period for the dispute game and resolve it.
vm.warp(block.timestamp + game.gameDuration().raw() + 1 seconds);
game.resolveClaim(0);
game.resolve();
// Fund the portal so that we can withdraw ETH.
vm.deal(address(optimismPortal2), 0xFFFFFFFF);
}
}
contract OptimismPortal2_Deposit_Invariant is CommonTest {
OptimismPortal2_Depositor internal actor;
function setUp() public override {
super.setUp();
// Create a deposit actor.
actor = new OptimismPortal2_Depositor(vm, optimismPortal2);
targetContract(address(actor));
bytes4[] memory selectors = new bytes4[](1);
selectors[0] = actor.depositTransactionCompletes.selector;
FuzzSelector memory selector = FuzzSelector({ addr: address(actor), selectors: selectors });
targetSelector(selector);
}
/// @custom:invariant Deposits of any value should always succeed unless
/// `_to` = `address(0)` or `_isCreation` = `true`.
///
/// All deposits, barring creation transactions and transactions
/// sent to `address(0)`, should always succeed.
function invariant_deposit_completes() external {
assertEq(actor.failedToComplete(), false);
}
}
contract OptimismPortal2_CannotTimeTravel is OptimismPortal2_Invariant_Harness {
function setUp() public override {
super.setUp();
// Prove the withdrawal transaction
optimismPortal2.proveWithdrawalTransaction(_defaultTx, _proposedGameIndex, _outputRootProof, _withdrawalProof);
// Set the target contract to the portal proxy
targetContract(address(optimismPortal2));
// Exclude the proxy admin from the senders so that the proxy cannot be upgraded
excludeSender(EIP1967Helper.getAdmin(address(optimismPortal2)));
}
/// @custom:invariant `finalizeWithdrawalTransaction` should revert if the proof maturity period has not elapsed.
///
/// A withdrawal that has been proven should not be able to be finalized
/// until after the proof maturity period has elapsed.
function invariant_cannotFinalizeBeforePeriodHasPassed() external {
vm.expectRevert("OptimismPortal: proven withdrawal has not matured yet");
optimismPortal2.finalizeWithdrawalTransaction(_defaultTx);
}
}
contract OptimismPortal2_CannotFinalizeTwice is OptimismPortal2_Invariant_Harness {
function setUp() public override {
super.setUp();
// Prove the withdrawal transaction
optimismPortal2.proveWithdrawalTransaction(_defaultTx, _proposedGameIndex, _outputRootProof, _withdrawalProof);
// Warp past the proof maturity period.
vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1);
// Finalize the withdrawal transaction.
optimismPortal2.finalizeWithdrawalTransaction(_defaultTx);
// Set the target contract to the portal proxy
targetContract(address(optimismPortal2));
// Exclude the proxy admin from the senders so that the proxy cannot be upgraded
excludeSender(EIP1967Helper.getAdmin(address(optimismPortal2)));
}
/// @custom:invariant `finalizeWithdrawalTransaction` should revert if the withdrawal has already been finalized.
///
/// Ensures that there is no chain of calls that can be made that allows a withdrawal to be
/// finalized twice.
function invariant_cannotFinalizeTwice() external {
vm.expectRevert("OptimismPortal: withdrawal has already been finalized");
optimismPortal2.finalizeWithdrawalTransaction(_defaultTx);
}
}
contract OptimismPortal_CanAlwaysFinalizeAfterWindow is OptimismPortal2_Invariant_Harness {
function setUp() public override {
super.setUp();
// Prove the withdrawal transaction
optimismPortal2.proveWithdrawalTransaction(_defaultTx, _proposedGameIndex, _outputRootProof, _withdrawalProof);
// Warp past the proof maturity period.
vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1);
// Set the target contract to the portal proxy
targetContract(address(optimismPortal2));
// Exclude the proxy admin from the senders so that the proxy cannot be upgraded
excludeSender(EIP1967Helper.getAdmin(address(optimismPortal2)));
}
/// @custom:invariant A withdrawal should **always** be able to be finalized `PROOF_MATURITY_DELAY_SECONDS` after
/// it was successfully proven, if the game has resolved and passed the air-gap.
///
/// This invariant asserts that there is no chain of calls that can be made that will prevent a
/// withdrawal from being finalized exactly `PROOF_MATURITY_DELAY_SECONDS` after it was
/// successfully proven and the game has resolved and passed the air-gap.
function invariant_canAlwaysFinalize() external {
uint256 bobBalanceBefore = address(bob).balance;
optimismPortal2.finalizeWithdrawalTransaction(_defaultTx);
assertEq(address(bob).balance, bobBalanceBefore + _defaultTx.value);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment