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  }