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 ...@@ -16,6 +16,7 @@ This directory contains documentation for all defined invariant tests within `co
- [InvariantTest.sol](./InvariantTest.sol.md) - [InvariantTest.sol](./InvariantTest.sol.md)
- [L2OutputOracle](./L2OutputOracle.md) - [L2OutputOracle](./L2OutputOracle.md)
- [OptimismPortal](./OptimismPortal.md) - [OptimismPortal](./OptimismPortal.md)
- [OptimismPortal2](./OptimismPortal2.md)
- [ResourceMetering](./ResourceMetering.md) - [ResourceMetering](./ResourceMetering.md)
- [SafeCall](./SafeCall.md) - [SafeCall](./SafeCall.md)
- [SystemConfig](./SystemConfig.md) - [SystemConfig](./SystemConfig.md)
......
...@@ -32,8 +32,8 @@ ...@@ -32,8 +32,8 @@
"sourceCodeHash": "0xdc27421279afb6c3b26fc8c589c5d213695f666c74d2c2c41cb7df719d172f37" "sourceCodeHash": "0xdc27421279afb6c3b26fc8c589c5d213695f666c74d2c2c41cb7df719d172f37"
}, },
"src/L1/OptimismPortal2.sol": { "src/L1/OptimismPortal2.sol": {
"initCodeHash": "0x48b052d78aafe26222a58fb01fee937402e43479c611739567551445ac235986", "initCodeHash": "0xbbf753c1df3e4eabdd910124948afc5bfda7e219ece0f3a16804ea2129d512ed",
"sourceCodeHash": "0xfb5b8bfa0ae30cd5969376d26aff9c9e2c44e5c1ebecdc95a83efad0eaaf0e85" "sourceCodeHash": "0x78457c09e7e80b1e950f794999dcd5f8426dde850793fbaf9aba9718d29d9141"
}, },
"src/L1/ProtocolVersions.sol": { "src/L1/ProtocolVersions.sol": {
"initCodeHash": "0x72cd467e8bcf019c02675d72ab762e088bcc9cc0f1a4e9f587fa4589f7fdd1b8", "initCodeHash": "0x72cd467e8bcf019c02675d72ab762e088bcc9cc0f1a4e9f587fa4589f7fdd1b8",
......
...@@ -154,6 +154,19 @@ ...@@ -154,6 +154,19 @@
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
}, },
{
"inputs": [],
"name": "disputeGameFinalityDelaySeconds",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{ {
"inputs": [], "inputs": [],
"name": "donateETH", "name": "donateETH",
...@@ -322,13 +335,26 @@ ...@@ -322,13 +335,26 @@
"outputs": [ "outputs": [
{ {
"internalType": "bool", "internalType": "bool",
"name": "paused_", "name": "",
"type": "bool" "type": "bool"
} }
], ],
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
}, },
{
"inputs": [],
"name": "proofMaturityDelaySeconds",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{ {
"inputs": [ "inputs": [
{ {
...@@ -448,6 +474,19 @@ ...@@ -448,6 +474,19 @@
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
}, },
{
"inputs": [],
"name": "sauron",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "pure",
"type": "function"
},
{ {
"inputs": [ "inputs": [
{ {
......
...@@ -183,10 +183,27 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { ...@@ -183,10 +183,27 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver {
return superchainConfig.guardian(); 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. /// @notice Getter for the current paused status.
/// @return paused_ Whether or not the contract is paused. function paused() public view returns (bool) {
function paused() public view returns (bool paused_) { return superchainConfig.paused();
paused_ = 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. /// @notice Computes the minimum gas limit for a deposit.
...@@ -266,9 +283,10 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { ...@@ -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 // 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 // resolves against the root claim, or the dispute game is blacklisted, we allow
// re-proving the withdrawal against a new proposal. // re-proving the withdrawal against a new proposal.
IDisputeGame game = provenWithdrawal.disputeGameProxy;
require( require(
provenWithdrawal.timestamp == 0 || gameProxy.status() == GameStatus.CHALLENGER_WINS 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" "OptimismPortal: withdrawal hash has already been proven, and dispute game is not invalid"
); );
...@@ -424,9 +442,8 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { ...@@ -424,9 +442,8 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver {
respectedGameType = _gameType; respectedGameType = _gameType;
} }
/// @notice Checks if a withdrawal can be finalized. NOTE: Decision was made to have this /// @notice Checks if a withdrawal can be finalized. This function will revert if the withdrawal cannot be
/// function revert rather than returning a boolean so that was more obvious why the /// finalized, and otherwise has no side-effects.
/// function failed.
/// @param _withdrawalHash Hash of the withdrawal to check. /// @param _withdrawalHash Hash of the withdrawal to check.
function checkWithdrawal(bytes32 _withdrawalHash) public view { function checkWithdrawal(bytes32 _withdrawalHash) public view {
ProvenWithdrawal memory provenWithdrawal = provenWithdrawals[_withdrawalHash]; ProvenWithdrawal memory provenWithdrawal = provenWithdrawals[_withdrawalHash];
...@@ -445,7 +462,7 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { ...@@ -445,7 +462,7 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver {
// safety against weird bugs in the proving step. // safety against weird bugs in the proving step.
require( require(
provenWithdrawal.timestamp > disputeGameProxy.createdAt().raw(), 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. // A proven withdrawal must wait at least `PROOF_MATURITY_DELAY_SECONDS` before finalizing.
...@@ -462,6 +479,11 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { ...@@ -462,6 +479,11 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver {
"OptimismPortal: output proposal has not been finalized yet" "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 // 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 // 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. // intervention in the event that a dispute game is resolved incorrectly.
......
This diff is collapsed.
...@@ -32,6 +32,7 @@ contract Specification_Test is CommonTest { ...@@ -32,6 +32,7 @@ contract Specification_Test is CommonTest {
CHALLENGER, CHALLENGER,
SYSTEMCONFIGOWNER, SYSTEMCONFIGOWNER,
GUARDIAN, GUARDIAN,
SAURON,
MESSENGER, MESSENGER,
L1PROXYADMINOWNER, L1PROXYADMINOWNER,
GOVERNANCETOKENOWNER, GOVERNANCETOKENOWNER,
...@@ -235,6 +236,7 @@ contract Specification_Test is CommonTest { ...@@ -235,6 +236,7 @@ contract Specification_Test is CommonTest {
}); });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("finalizedWithdrawals(bytes32)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("finalizedWithdrawals(bytes32)") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("guardian()") }); _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("initialize(address,address,address)") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("l2Sender()") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("l2Sender()") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("minimumGasLimit(uint64)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("minimumGasLimit(uint64)") });
...@@ -248,10 +250,12 @@ contract Specification_Test is CommonTest { ...@@ -248,10 +250,12 @@ contract Specification_Test is CommonTest {
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("disputeGameFactory()") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("disputeGameFactory()") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("disputeGameBlacklist(address)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("disputeGameBlacklist(address)") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("respectedGameType()") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("respectedGameType()") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("blacklistDisputeGame(address)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("blacklistDisputeGame(address)"), _auth: Role.SAURON });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("deleteProvenWithdrawal(bytes32)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("deleteProvenWithdrawal(bytes32)"), _auth: Role.SAURON });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("setRespectedGameType(uint32)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("setRespectedGameType(uint32)"), _auth: Role.SAURON });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("checkWithdrawal(bytes32)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("checkWithdrawal(bytes32)") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("proofMaturityDelaySeconds()") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("disputeGameFinalityDelaySeconds()") });
// ProtocolVersions // ProtocolVersions
_addSpec({ _name: "ProtocolVersions", _sel: _getSel("RECOMMENDED_SLOT()") }); _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