github.com/ethereum-optimism/optimism@v1.7.2/packages/contracts-bedrock/test/invariants/ResourceMetering.t.sol (about) 1 // SPDX-License-Identifier: MIT 2 pragma solidity 0.8.15; 3 4 import { Test } from "forge-std/Test.sol"; 5 6 import { StdUtils } from "forge-std/StdUtils.sol"; 7 import { StdInvariant } from "forge-std/StdInvariant.sol"; 8 9 import { Arithmetic } from "src/libraries/Arithmetic.sol"; 10 import { ResourceMetering } from "src/L1/ResourceMetering.sol"; 11 import { Constants } from "src/libraries/Constants.sol"; 12 import { InvariantTest } from "test/invariants/InvariantTest.sol"; 13 14 contract ResourceMetering_User is StdUtils, ResourceMetering { 15 bool public failedMaxGasPerBlock; 16 bool public failedRaiseBaseFee; 17 bool public failedLowerBaseFee; 18 bool public failedNeverBelowMinBaseFee; 19 bool public failedMaxRaiseBaseFeePerBlock; 20 bool public failedMaxLowerBaseFeePerBlock; 21 22 // Used as a special flag for the purpose of identifying unchecked math errors specifically 23 // in the test contracts, not the target contracts themselves. 24 bool public underflow; 25 26 constructor() { 27 initialize(); 28 } 29 30 function initialize() internal initializer { 31 __ResourceMetering_init(); 32 } 33 34 function resourceConfig() public pure returns (ResourceMetering.ResourceConfig memory) { 35 return _resourceConfig(); 36 } 37 38 function _resourceConfig() internal pure override returns (ResourceMetering.ResourceConfig memory) { 39 ResourceMetering.ResourceConfig memory rcfg = Constants.DEFAULT_RESOURCE_CONFIG(); 40 return rcfg; 41 } 42 43 /// @notice Takes the necessary parameters to allow us to burn arbitrary amounts of gas to test 44 /// the underlying resource metering/gas market logic 45 function burn(uint256 _gasToBurn, bool _raiseBaseFee) public { 46 // Part 1: we cache the current param values and do some basic checks on them. 47 uint256 cachedPrevBaseFee = uint256(params.prevBaseFee); 48 uint256 cachedPrevBoughtGas = uint256(params.prevBoughtGas); 49 uint256 cachedPrevBlockNum = uint256(params.prevBlockNum); 50 51 ResourceMetering.ResourceConfig memory rcfg = resourceConfig(); 52 uint256 targetResourceLimit = uint256(rcfg.maxResourceLimit) / uint256(rcfg.elasticityMultiplier); 53 54 // check that the last block's base fee hasn't dropped below the minimum 55 if (cachedPrevBaseFee < uint256(rcfg.minimumBaseFee)) { 56 failedNeverBelowMinBaseFee = true; 57 } 58 // check that the last block didn't consume more than the max amount of gas 59 if (cachedPrevBoughtGas > uint256(rcfg.maxResourceLimit)) { 60 failedMaxGasPerBlock = true; 61 } 62 63 // Part2: we perform the gas burn 64 65 // force the gasToBurn into the correct range based on whether we intend to 66 // raise or lower the baseFee after this block, respectively 67 uint256 gasToBurn; 68 if (_raiseBaseFee) { 69 gasToBurn = bound(_gasToBurn, uint256(targetResourceLimit), uint256(rcfg.maxResourceLimit)); 70 } else { 71 gasToBurn = bound(_gasToBurn, 0, targetResourceLimit); 72 } 73 74 _burnInternal(uint64(gasToBurn)); 75 76 // Part 3: we run checks and modify our invariant flags based on the updated params values 77 78 // Calculate the maximum allowed baseFee change (per block) 79 uint256 maxBaseFeeChange = cachedPrevBaseFee / uint256(rcfg.baseFeeMaxChangeDenominator); 80 81 // If the last block used more than the target amount of gas (and there were no 82 // empty blocks in between), ensure this block's baseFee increased, but not by 83 // more than the max amount per block 84 if ( 85 (cachedPrevBoughtGas > uint256(targetResourceLimit)) 86 && (uint256(params.prevBlockNum) - cachedPrevBlockNum == 1) 87 ) { 88 failedRaiseBaseFee = failedRaiseBaseFee || (params.prevBaseFee <= cachedPrevBaseFee); 89 failedMaxRaiseBaseFeePerBlock = 90 failedMaxRaiseBaseFeePerBlock || ((uint256(params.prevBaseFee) - cachedPrevBaseFee) < maxBaseFeeChange); 91 } 92 93 // If the last block used less than the target amount of gas, (or was empty), 94 // ensure that: this block's baseFee was decreased, but not by more than the max amount 95 if ( 96 (cachedPrevBoughtGas < uint256(targetResourceLimit)) 97 || (uint256(params.prevBlockNum) - cachedPrevBlockNum > 1) 98 ) { 99 // Invariant: baseFee should decrease 100 failedLowerBaseFee = failedLowerBaseFee || (uint256(params.prevBaseFee) > cachedPrevBaseFee); 101 102 if (params.prevBlockNum - cachedPrevBlockNum == 1) { 103 // No empty blocks 104 // Invariant: baseFee should not have decreased by more than the maximum amount 105 failedMaxLowerBaseFeePerBlock = failedMaxLowerBaseFeePerBlock 106 || ((cachedPrevBaseFee - uint256(params.prevBaseFee)) <= maxBaseFeeChange); 107 } else if (params.prevBlockNum - cachedPrevBlockNum > 1) { 108 // We have at least one empty block 109 // Update the maxBaseFeeChange to account for multiple blocks having passed 110 unchecked { 111 maxBaseFeeChange = uint256( 112 int256(cachedPrevBaseFee) 113 - Arithmetic.clamp( 114 Arithmetic.cdexp( 115 int256(cachedPrevBaseFee), 116 int256(uint256(rcfg.baseFeeMaxChangeDenominator)), 117 int256(uint256(params.prevBlockNum) - cachedPrevBlockNum) 118 ), 119 int256(uint256(rcfg.minimumBaseFee)), 120 int256(uint256(rcfg.maximumBaseFee)) 121 ) 122 ); 123 } 124 125 // Detect an underflow in the previous calculation. 126 // Without using unchecked above, and detecting the underflow here, fuzzer would 127 // otherwise ignore the revert. 128 underflow = underflow || maxBaseFeeChange > cachedPrevBaseFee; 129 130 // Invariant: baseFee should not have decreased by more than the maximum amount 131 failedMaxLowerBaseFeePerBlock = failedMaxLowerBaseFeePerBlock 132 || ((cachedPrevBaseFee - uint256(params.prevBaseFee)) <= maxBaseFeeChange); 133 } 134 } 135 } 136 137 function _burnInternal(uint64 _gasToBurn) private metered(_gasToBurn) { } 138 } 139 140 contract ResourceMetering_Invariant is StdInvariant, InvariantTest { 141 ResourceMetering_User internal actor; 142 143 function setUp() public override { 144 super.setUp(); 145 // Create a actor. 146 actor = new ResourceMetering_User(); 147 148 targetContract(address(actor)); 149 150 bytes4[] memory selectors = new bytes4[](1); 151 selectors[0] = actor.burn.selector; 152 FuzzSelector memory selector = FuzzSelector({ addr: address(actor), selectors: selectors }); 153 targetSelector(selector); 154 } 155 156 /// @custom:invariant The base fee should increase if the last block used more 157 /// than the target amount of gas. 158 /// 159 /// If the last block used more than the target amount of gas 160 /// (and there were no empty blocks in between), ensure this 161 /// block's baseFee increased, but not by more than the max amount 162 /// per block. 163 function invariant_high_usage_raise_baseFee() external { 164 assertFalse(actor.failedRaiseBaseFee()); 165 } 166 167 /// @custom:invariant The base fee should decrease if the last block used less 168 /// than the target amount of gas. 169 /// 170 /// If the previous block used less than the target amount of gas, 171 /// the base fee should decrease, but not more than the max amount. 172 function invariant_low_usage_lower_baseFee() external { 173 assertFalse(actor.failedLowerBaseFee()); 174 } 175 176 /// @custom:invariant A block's base fee should never be below `MINIMUM_BASE_FEE`. 177 /// 178 /// This test asserts that a block's base fee can never drop 179 /// below the `MINIMUM_BASE_FEE` threshold. 180 function invariant_never_below_min_baseFee() external { 181 assertFalse(actor.failedNeverBelowMinBaseFee()); 182 } 183 184 /// @custom:invariant A block can never consume more than `MAX_RESOURCE_LIMIT` gas. 185 /// 186 /// This test asserts that a block can never consume more than 187 /// the `MAX_RESOURCE_LIMIT` gas threshold. 188 function invariant_never_above_max_gas_limit() external { 189 assertFalse(actor.failedMaxGasPerBlock()); 190 } 191 192 /// @custom:invariant The base fee can never be raised more than the max base fee change. 193 /// 194 /// After a block consumes more gas than the target gas, the base fee 195 /// cannot be raised more than the maximum amount allowed. The max base 196 /// fee change (per-block) is derived as follows: 197 /// `prevBaseFee / BASE_FEE_MAX_CHANGE_DENOMINATOR` 198 function invariant_never_exceed_max_increase() external { 199 assertFalse(actor.failedMaxRaiseBaseFeePerBlock()); 200 } 201 202 /// @custom:invariant The base fee can never be lowered more than the max base fee change. 203 /// 204 /// After a block consumes less than the target gas, the base fee cannot 205 /// be lowered more than the maximum amount allowed. The max base fee 206 /// change (per-block) is derived as follows: 207 /// `prevBaseFee / BASE_FEE_MAX_CHANGE_DENOMINATOR` 208 function invariant_never_exceed_max_decrease() external { 209 assertFalse(actor.failedMaxLowerBaseFeePerBlock()); 210 } 211 212 /// @custom:invariant The `maxBaseFeeChange` calculation over multiple blocks can never 213 /// underflow. 214 /// 215 /// When calculating the `maxBaseFeeChange` after multiple empty blocks, 216 /// the calculation should never be allowed to underflow. 217 function invariant_never_underflow() external { 218 assertFalse(actor.underflow()); 219 } 220 }