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