github.com/ethereum-optimism/optimism@v1.7.2/packages/contracts-bedrock/test/invariants/OptimismPortal2.t.sol (about) 1 // SPDX-License-Identifier: MIT 2 pragma solidity 0.8.15; 3 4 import { StdUtils } from "forge-std/Test.sol"; 5 import { Vm } from "forge-std/Vm.sol"; 6 7 import { OptimismPortal2 } from "src/L1/OptimismPortal2.sol"; 8 import { AddressAliasHelper } from "src/vendor/AddressAliasHelper.sol"; 9 import { SystemConfig } from "src/L1/SystemConfig.sol"; 10 import { ResourceMetering } from "src/L1/ResourceMetering.sol"; 11 import { Constants } from "src/libraries/Constants.sol"; 12 13 import { CommonTest } from "test/setup/CommonTest.sol"; 14 import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; 15 import { Types } from "src/libraries/Types.sol"; 16 17 import { FaultDisputeGame } from "src/dispute/FaultDisputeGame.sol"; 18 import "src/libraries/DisputeTypes.sol"; 19 20 contract OptimismPortal2_Depositor is StdUtils, ResourceMetering { 21 Vm internal vm; 22 OptimismPortal2 internal portal; 23 bool public failedToComplete; 24 25 constructor(Vm _vm, OptimismPortal2 _portal) { 26 vm = _vm; 27 portal = _portal; 28 initialize(); 29 } 30 31 function initialize() internal initializer { 32 __ResourceMetering_init(); 33 } 34 35 function resourceConfig() public pure returns (ResourceMetering.ResourceConfig memory) { 36 return _resourceConfig(); 37 } 38 39 function _resourceConfig() internal pure override returns (ResourceMetering.ResourceConfig memory) { 40 ResourceMetering.ResourceConfig memory rcfg = Constants.DEFAULT_RESOURCE_CONFIG(); 41 return rcfg; 42 } 43 44 // A test intended to identify any unexpected halting conditions 45 function depositTransactionCompletes( 46 address _to, 47 uint256 _value, 48 uint64 _gasLimit, 49 bool _isCreation, 50 bytes memory _data 51 ) 52 public 53 payable 54 { 55 vm.assume((!_isCreation || _to == address(0)) && _data.length <= 120_000); 56 57 uint256 preDepositvalue = bound(_value, 0, type(uint128).max); 58 // Give the depositor some ether 59 vm.deal(address(this), preDepositvalue); 60 // cache the contract's eth balance 61 uint256 preDepositBalance = address(this).balance; 62 uint256 value = bound(preDepositvalue, 0, preDepositBalance); 63 64 (, uint64 cachedPrevBoughtGas,) = ResourceMetering(address(portal)).params(); 65 ResourceMetering.ResourceConfig memory rcfg = resourceConfig(); 66 uint256 maxResourceLimit = uint64(rcfg.maxResourceLimit); 67 uint64 gasLimit = uint64( 68 bound(_gasLimit, portal.minimumGasLimit(uint64(_data.length)), maxResourceLimit - cachedPrevBoughtGas) 69 ); 70 71 try portal.depositTransaction{ value: value }(_to, value, gasLimit, _isCreation, _data) { 72 // Do nothing; Call succeeded 73 } catch { 74 failedToComplete = true; 75 } 76 } 77 } 78 79 contract OptimismPortal2_Invariant_Harness is CommonTest { 80 // Reusable default values for a test withdrawal 81 Types.WithdrawalTransaction _defaultTx; 82 83 uint256 _proposedGameIndex; 84 uint256 _proposedBlockNumber; 85 bytes32 _stateRoot; 86 bytes32 _storageRoot; 87 bytes32 _outputRoot; 88 bytes32 _withdrawalHash; 89 bytes[] _withdrawalProof; 90 Types.OutputRootProof internal _outputRootProof; 91 92 function setUp() public virtual override { 93 super.enableFaultProofs(); 94 super.setUp(); 95 96 _defaultTx = Types.WithdrawalTransaction({ 97 nonce: 0, 98 sender: alice, 99 target: bob, 100 value: 100, 101 gasLimit: 100_000, 102 data: hex"" 103 }); 104 // Get withdrawal proof data we can use for testing. 105 (_stateRoot, _storageRoot, _outputRoot, _withdrawalHash, _withdrawalProof) = 106 ffi.getProveWithdrawalTransactionInputs(_defaultTx); 107 108 // Setup a dummy output root proof for reuse. 109 _outputRootProof = Types.OutputRootProof({ 110 version: bytes32(uint256(0)), 111 stateRoot: _stateRoot, 112 messagePasserStorageRoot: _storageRoot, 113 latestBlockhash: bytes32(uint256(0)) 114 }); 115 116 // Create a dispute game with the output root we've proposed. 117 _proposedBlockNumber = 0xFF; 118 FaultDisputeGame game = FaultDisputeGame( 119 payable( 120 address( 121 disputeGameFactory.create( 122 optimismPortal2.respectedGameType(), Claim.wrap(_outputRoot), abi.encode(_proposedBlockNumber) 123 ) 124 ) 125 ) 126 ); 127 _proposedGameIndex = disputeGameFactory.gameCount() - 1; 128 129 // Warp beyond the finalization period for the dispute game and resolve it. 130 vm.warp(block.timestamp + game.gameDuration().raw() + 1 seconds); 131 game.resolveClaim(0); 132 game.resolve(); 133 134 // Fund the portal so that we can withdraw ETH. 135 vm.deal(address(optimismPortal2), 0xFFFFFFFF); 136 } 137 } 138 139 contract OptimismPortal2_Deposit_Invariant is CommonTest { 140 OptimismPortal2_Depositor internal actor; 141 142 function setUp() public override { 143 super.setUp(); 144 // Create a deposit actor. 145 actor = new OptimismPortal2_Depositor(vm, optimismPortal2); 146 147 targetContract(address(actor)); 148 149 bytes4[] memory selectors = new bytes4[](1); 150 selectors[0] = actor.depositTransactionCompletes.selector; 151 FuzzSelector memory selector = FuzzSelector({ addr: address(actor), selectors: selectors }); 152 targetSelector(selector); 153 } 154 155 /// @custom:invariant Deposits of any value should always succeed unless 156 /// `_to` = `address(0)` or `_isCreation` = `true`. 157 /// 158 /// All deposits, barring creation transactions and transactions 159 /// sent to `address(0)`, should always succeed. 160 function invariant_deposit_completes() external { 161 assertEq(actor.failedToComplete(), false); 162 } 163 } 164 165 contract OptimismPortal2_CannotTimeTravel is OptimismPortal2_Invariant_Harness { 166 function setUp() public override { 167 super.setUp(); 168 169 // Prove the withdrawal transaction 170 optimismPortal2.proveWithdrawalTransaction(_defaultTx, _proposedGameIndex, _outputRootProof, _withdrawalProof); 171 172 // Set the target contract to the portal proxy 173 targetContract(address(optimismPortal2)); 174 // Exclude the proxy admin from the senders so that the proxy cannot be upgraded 175 excludeSender(EIP1967Helper.getAdmin(address(optimismPortal2))); 176 } 177 178 /// @custom:invariant `finalizeWithdrawalTransaction` should revert if the proof maturity period has not elapsed. 179 /// 180 /// A withdrawal that has been proven should not be able to be finalized 181 /// until after the proof maturity period has elapsed. 182 function invariant_cannotFinalizeBeforePeriodHasPassed() external { 183 vm.expectRevert("OptimismPortal: proven withdrawal has not matured yet"); 184 optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); 185 } 186 } 187 188 contract OptimismPortal2_CannotFinalizeTwice is OptimismPortal2_Invariant_Harness { 189 function setUp() public override { 190 super.setUp(); 191 192 // Prove the withdrawal transaction 193 optimismPortal2.proveWithdrawalTransaction(_defaultTx, _proposedGameIndex, _outputRootProof, _withdrawalProof); 194 195 // Warp past the proof maturity period. 196 vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); 197 198 // Finalize the withdrawal transaction. 199 optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); 200 201 // Set the target contract to the portal proxy 202 targetContract(address(optimismPortal2)); 203 // Exclude the proxy admin from the senders so that the proxy cannot be upgraded 204 excludeSender(EIP1967Helper.getAdmin(address(optimismPortal2))); 205 } 206 207 /// @custom:invariant `finalizeWithdrawalTransaction` should revert if the withdrawal has already been finalized. 208 /// 209 /// Ensures that there is no chain of calls that can be made that allows a withdrawal to be 210 /// finalized twice. 211 function invariant_cannotFinalizeTwice() external { 212 vm.expectRevert("OptimismPortal: withdrawal has already been finalized"); 213 optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); 214 } 215 } 216 217 contract OptimismPortal_CanAlwaysFinalizeAfterWindow is OptimismPortal2_Invariant_Harness { 218 function setUp() public override { 219 super.setUp(); 220 221 // Prove the withdrawal transaction 222 optimismPortal2.proveWithdrawalTransaction(_defaultTx, _proposedGameIndex, _outputRootProof, _withdrawalProof); 223 224 // Warp past the proof maturity period. 225 vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); 226 227 // Set the target contract to the portal proxy 228 targetContract(address(optimismPortal2)); 229 // Exclude the proxy admin from the senders so that the proxy cannot be upgraded 230 excludeSender(EIP1967Helper.getAdmin(address(optimismPortal2))); 231 } 232 233 /// @custom:invariant A withdrawal should **always** be able to be finalized `PROOF_MATURITY_DELAY_SECONDS` after 234 /// it was successfully proven, if the game has resolved and passed the air-gap. 235 /// 236 /// This invariant asserts that there is no chain of calls that can be made that will prevent a 237 /// withdrawal from being finalized exactly `PROOF_MATURITY_DELAY_SECONDS` after it was 238 /// successfully proven and the game has resolved and passed the air-gap. 239 function invariant_canAlwaysFinalize() external { 240 uint256 bobBalanceBefore = address(bob).balance; 241 242 optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); 243 244 assertEq(address(bob).balance, bobBalanceBefore + _defaultTx.value); 245 } 246 }