github.com/ethereum-optimism/optimism@v1.7.2/packages/contracts-bedrock/test/L1/ResourceMetering.t.sol (about)

     1  // SPDX-License-Identifier: MIT
     2  pragma solidity 0.8.15;
     3  
     4  // Testing utilities
     5  import { Test } from "forge-std/Test.sol";
     6  
     7  // Libraries
     8  import { Constants } from "src/libraries/Constants.sol";
     9  
    10  // Target contract dependencies
    11  import { Proxy } from "src/universal/Proxy.sol";
    12  
    13  // Target contract
    14  import { ResourceMetering } from "src/L1/ResourceMetering.sol";
    15  
    16  contract MeterUser is ResourceMetering {
    17      ResourceMetering.ResourceConfig public innerConfig;
    18  
    19      constructor() {
    20          initialize();
    21          innerConfig = Constants.DEFAULT_RESOURCE_CONFIG();
    22      }
    23  
    24      function initialize() public initializer {
    25          __ResourceMetering_init();
    26      }
    27  
    28      function resourceConfig() public view returns (ResourceMetering.ResourceConfig memory) {
    29          return _resourceConfig();
    30      }
    31  
    32      function _resourceConfig() internal view override returns (ResourceMetering.ResourceConfig memory) {
    33          return innerConfig;
    34      }
    35  
    36      function use(uint64 _amount) public metered(_amount) { }
    37  
    38      function set(uint128 _prevBaseFee, uint64 _prevBoughtGas, uint64 _prevBlockNum) public {
    39          params = ResourceMetering.ResourceParams({
    40              prevBaseFee: _prevBaseFee,
    41              prevBoughtGas: _prevBoughtGas,
    42              prevBlockNum: _prevBlockNum
    43          });
    44      }
    45  
    46      function setParams(ResourceMetering.ResourceConfig memory newConfig) public {
    47          innerConfig = newConfig;
    48      }
    49  }
    50  
    51  /// @title ResourceMetering_Test
    52  /// @dev Tests are based on the default config values.
    53  ///      It is expected that these config values are used in production.
    54  contract ResourceMetering_Test is Test {
    55      MeterUser internal meter;
    56      uint64 initialBlockNum;
    57  
    58      /// @dev Sets up the test contract.
    59      function setUp() public {
    60          meter = new MeterUser();
    61          initialBlockNum = uint64(block.number);
    62      }
    63  
    64      /// @dev Tests that the initial resource params are set correctly.
    65      function test_meter_initialResourceParams_succeeds() external {
    66          (uint128 prevBaseFee, uint64 prevBoughtGas, uint64 prevBlockNum) = meter.params();
    67          ResourceMetering.ResourceConfig memory rcfg = meter.resourceConfig();
    68  
    69          assertEq(prevBaseFee, rcfg.minimumBaseFee);
    70          assertEq(prevBoughtGas, 0);
    71          assertEq(prevBlockNum, initialBlockNum);
    72      }
    73  
    74      /// @dev Tests that reinitializing the resource params are set correctly.
    75      function test_meter_reinitializedResourceParams_succeeds() external {
    76          (uint128 prevBaseFee, uint64 prevBoughtGas, uint64 prevBlockNum) = meter.params();
    77  
    78          // Reset the initialized slot to enable reinitialization.
    79          vm.store(address(meter), bytes32(uint256(0)), bytes32(uint256(0)));
    80          meter.initialize();
    81  
    82          (uint128 postBaseFee, uint64 postBoughtGas, uint64 postBlockNum) = meter.params();
    83          assertEq(prevBaseFee, postBaseFee);
    84          assertEq(prevBoughtGas, postBoughtGas);
    85          assertEq(prevBlockNum, postBlockNum);
    86      }
    87  
    88      /// @dev Tests that updating the resource params to the same values works correctly.
    89      function test_meter_updateParamsNoChange_succeeds() external {
    90          meter.use(0); // equivalent to just updating the base fee and block number
    91          (uint128 prevBaseFee, uint64 prevBoughtGas, uint64 prevBlockNum) = meter.params();
    92          meter.use(0);
    93          (uint128 postBaseFee, uint64 postBoughtGas, uint64 postBlockNum) = meter.params();
    94  
    95          assertEq(postBaseFee, prevBaseFee);
    96          assertEq(postBoughtGas, prevBoughtGas);
    97          assertEq(postBlockNum, prevBlockNum);
    98      }
    99  
   100      /// @dev Tests that updating the initial block number sets the meter params correctly.
   101      function test_meter_updateOneEmptyBlock_succeeds() external {
   102          vm.roll(initialBlockNum + 1);
   103          meter.use(0);
   104          (uint128 prevBaseFee, uint64 prevBoughtGas, uint64 prevBlockNum) = meter.params();
   105  
   106          assertEq(prevBaseFee, 1 gwei);
   107          assertEq(prevBoughtGas, 0);
   108          assertEq(prevBlockNum, initialBlockNum + 1);
   109      }
   110  
   111      /// @dev Tests that updating the initial block number sets the meter params correctly.
   112      function test_meter_updateTwoEmptyBlocks_succeeds() external {
   113          vm.roll(initialBlockNum + 2);
   114          meter.use(0);
   115          (uint128 prevBaseFee, uint64 prevBoughtGas, uint64 prevBlockNum) = meter.params();
   116  
   117          assertEq(prevBaseFee, 1 gwei);
   118          assertEq(prevBoughtGas, 0);
   119          assertEq(prevBlockNum, initialBlockNum + 2);
   120      }
   121  
   122      /// @dev Tests that updating the initial block number sets the meter params correctly.
   123      function test_meter_updateTenEmptyBlocks_succeeds() external {
   124          vm.roll(initialBlockNum + 10);
   125          meter.use(0);
   126          (uint128 prevBaseFee, uint64 prevBoughtGas, uint64 prevBlockNum) = meter.params();
   127  
   128          assertEq(prevBaseFee, 1 gwei);
   129          assertEq(prevBoughtGas, 0);
   130          assertEq(prevBlockNum, initialBlockNum + 10);
   131      }
   132  
   133      /// @dev Tests that updating the gas delta sets the meter params correctly.
   134      function test_meter_updateNoGasDelta_succeeds() external {
   135          ResourceMetering.ResourceConfig memory rcfg = meter.resourceConfig();
   136          uint256 target = uint256(rcfg.maxResourceLimit) / uint256(rcfg.elasticityMultiplier);
   137          meter.use(uint64(target));
   138          (uint128 prevBaseFee, uint64 prevBoughtGas, uint64 prevBlockNum) = meter.params();
   139  
   140          assertEq(prevBaseFee, 1000000000);
   141          assertEq(prevBoughtGas, target);
   142          assertEq(prevBlockNum, initialBlockNum);
   143      }
   144  
   145      /// @dev Tests that the meter params are set correctly for the maximum gas delta.
   146      function test_meter_useMax_succeeds() external {
   147          ResourceMetering.ResourceConfig memory rcfg = meter.resourceConfig();
   148          uint64 target = uint64(rcfg.maxResourceLimit) / uint64(rcfg.elasticityMultiplier);
   149          uint64 elasticityMultiplier = uint64(rcfg.elasticityMultiplier);
   150  
   151          meter.use(target * elasticityMultiplier);
   152  
   153          (, uint64 prevBoughtGas,) = meter.params();
   154          assertEq(prevBoughtGas, target * elasticityMultiplier);
   155  
   156          vm.roll(initialBlockNum + 1);
   157          meter.use(0);
   158          (uint128 postBaseFee,,) = meter.params();
   159          assertEq(postBaseFee, 2125000000);
   160      }
   161  
   162      /// @dev Tests that the metered modifier reverts if the baseFeeMaxChangeDenominator is set to 1.
   163      ///      Since the metered modifier internally calls solmate's powWad function, it will revert
   164      ///      with the error string "UNDEFINED" since the first parameter will be computed as 0.
   165      function test_meter_denominatorEq1_reverts() external {
   166          ResourceMetering.ResourceConfig memory rcfg = meter.resourceConfig();
   167          uint64 target = uint64(rcfg.maxResourceLimit) / uint64(rcfg.elasticityMultiplier);
   168          uint64 elasticityMultiplier = uint64(rcfg.elasticityMultiplier);
   169          rcfg.baseFeeMaxChangeDenominator = 1;
   170          meter.setParams(rcfg);
   171          meter.use(target * elasticityMultiplier);
   172  
   173          (, uint64 prevBoughtGas,) = meter.params();
   174          assertEq(prevBoughtGas, target * elasticityMultiplier);
   175  
   176          vm.roll(initialBlockNum + 2);
   177  
   178          vm.expectRevert("UNDEFINED");
   179          meter.use(0);
   180      }
   181  
   182      /// @dev Tests that the metered modifier reverts if the value is greater than allowed.
   183      function test_meter_useMoreThanMax_reverts() external {
   184          ResourceMetering.ResourceConfig memory rcfg = meter.resourceConfig();
   185          uint64 target = uint64(rcfg.maxResourceLimit) / uint64(rcfg.elasticityMultiplier);
   186          uint64 elasticityMultiplier = uint64(rcfg.elasticityMultiplier);
   187  
   188          vm.expectRevert("ResourceMetering: cannot buy more gas than available gas limit");
   189          meter.use(target * elasticityMultiplier + 1);
   190      }
   191  
   192      /// @dev Tests that resource metering can handle large gaps between deposits.
   193      function testFuzz_meter_largeBlockDiff_succeeds(uint64 _amount, uint256 _blockDiff) external {
   194          // This test fails if the following line is commented out.
   195          // At 12 seconds per block, this number is effectively unreachable.
   196          vm.assume(_blockDiff < 433576281058164217753225238677900874458691);
   197  
   198          ResourceMetering.ResourceConfig memory rcfg = meter.resourceConfig();
   199          uint64 target = uint64(rcfg.maxResourceLimit) / uint64(rcfg.elasticityMultiplier);
   200          uint64 elasticityMultiplier = uint64(rcfg.elasticityMultiplier);
   201  
   202          vm.assume(_amount < target * elasticityMultiplier);
   203          vm.roll(initialBlockNum + _blockDiff);
   204          meter.use(_amount);
   205      }
   206  }
   207  
   208  /// @title CustomMeterUser
   209  /// @notice A simple wrapper around `ResourceMetering` that allows the initial
   210  ///         params to be set in the constructor.
   211  contract CustomMeterUser is ResourceMetering {
   212      uint256 public startGas;
   213      uint256 public endGas;
   214  
   215      constructor(uint128 _prevBaseFee, uint64 _prevBoughtGas, uint64 _prevBlockNum) {
   216          params = ResourceMetering.ResourceParams({
   217              prevBaseFee: _prevBaseFee,
   218              prevBoughtGas: _prevBoughtGas,
   219              prevBlockNum: _prevBlockNum
   220          });
   221      }
   222  
   223      function _resourceConfig() internal pure override returns (ResourceMetering.ResourceConfig memory) {
   224          return Constants.DEFAULT_RESOURCE_CONFIG();
   225      }
   226  
   227      function use(uint64 _amount) public returns (uint256) {
   228          uint256 initialGas = gasleft();
   229          _metered(_amount, initialGas);
   230          return initialGas - gasleft();
   231      }
   232  }
   233  
   234  /// @title ArtifactResourceMetering_Test
   235  /// @notice A table test that sets the state of the ResourceParams and then requests
   236  ///         various amounts of gas. This test ensures that a wide range of values
   237  ///         can safely be used with the `ResourceMetering` contract.
   238  ///         It also writes a CSV file to disk that includes useful information
   239  ///         about how much gas is used and how expensive it is in USD terms to
   240  ///         purchase the deposit gas.
   241  contract ArtifactResourceMetering_Test is Test {
   242      uint128 internal minimumBaseFee;
   243      uint128 internal maximumBaseFee;
   244      uint64 internal maxResourceLimit;
   245      uint64 internal targetResourceLimit;
   246  
   247      string internal outfile;
   248  
   249      // keccak256(abi.encodeWithSignature("Error(string)", "ResourceMetering: cannot buy more gas than available gas
   250      // limit"))
   251      bytes32 internal cannotBuyMoreGas = 0x84edc668cfd5e050b8999f43ff87a1faaa93e5f935b20bc1dd4d3ff157ccf429;
   252      // keccak256(abi.encodeWithSignature("Panic(uint256)", 0x11))
   253      bytes32 internal overflowErr = 0x1ca389f2c8264faa4377de9ce8e14d6263ef29c68044a9272d405761bab2db27;
   254      // keccak256(hex"")
   255      bytes32 internal emptyReturnData = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470;
   256  
   257      /// @dev Sets up the tests with constants from the ResourceMetering contract.
   258      function setUp() public {
   259          vm.roll(1_000_000);
   260  
   261          MeterUser base = new MeterUser();
   262          ResourceMetering.ResourceConfig memory rcfg = base.resourceConfig();
   263          minimumBaseFee = uint128(rcfg.minimumBaseFee);
   264          maximumBaseFee = rcfg.maximumBaseFee;
   265          maxResourceLimit = uint64(rcfg.maxResourceLimit);
   266          targetResourceLimit = uint64(rcfg.maxResourceLimit) / uint64(rcfg.elasticityMultiplier);
   267  
   268          outfile = string.concat(vm.projectRoot(), "/.resource-metering.csv");
   269          try vm.removeFile(outfile) { } catch { }
   270      }
   271  
   272      /// @dev Generates a CSV file. No more than the L1 block gas limit should
   273      ///      be supplied to the `meter` function to avoid long execution time.
   274      function test_meter_generateArtifact_succeeds() external {
   275          vm.writeLine(
   276              outfile,
   277              "prevBaseFee,prevBoughtGas,prevBlockNumDiff,l1BaseFee,requestedGas,gasConsumed,ethPrice,usdCost,success"
   278          );
   279  
   280          // prevBaseFee value in ResourceParams
   281          uint128[] memory prevBaseFees = new uint128[](5);
   282          prevBaseFees[0] = minimumBaseFee;
   283          prevBaseFees[1] = maximumBaseFee;
   284          prevBaseFees[2] = uint128(50 gwei);
   285          prevBaseFees[3] = uint128(100 gwei);
   286          prevBaseFees[4] = uint128(200 gwei);
   287  
   288          // prevBoughtGas value in ResourceParams
   289          uint64[] memory prevBoughtGases = new uint64[](1);
   290          prevBoughtGases[0] = uint64(0);
   291  
   292          // prevBlockNum diff, simulates blocks with no deposits when non zero
   293          uint64[] memory prevBlockNumDiffs = new uint64[](2);
   294          prevBlockNumDiffs[0] = 0;
   295          prevBlockNumDiffs[1] = 1;
   296  
   297          // The amount of L2 gas that a user requests
   298          uint64[] memory requestedGases = new uint64[](3);
   299          requestedGases[0] = maxResourceLimit;
   300          requestedGases[1] = targetResourceLimit;
   301          requestedGases[2] = uint64(100_000);
   302  
   303          // The L1 base fee
   304          uint256[] memory l1BaseFees = new uint256[](4);
   305          l1BaseFees[0] = 1 gwei;
   306          l1BaseFees[1] = 50 gwei;
   307          l1BaseFees[2] = 75 gwei;
   308          l1BaseFees[3] = 100 gwei;
   309  
   310          // USD price of 1 ether
   311          uint256[] memory ethPrices = new uint256[](2);
   312          ethPrices[0] = 1600;
   313          ethPrices[1] = 3200;
   314  
   315          // Iterate over all of the test values and run a test
   316          for (uint256 i; i < prevBaseFees.length; i++) {
   317              for (uint256 j; j < prevBoughtGases.length; j++) {
   318                  for (uint256 k; k < prevBlockNumDiffs.length; k++) {
   319                      for (uint256 l; l < requestedGases.length; l++) {
   320                          for (uint256 m; m < l1BaseFees.length; m++) {
   321                              for (uint256 n; n < ethPrices.length; n++) {
   322                                  uint256 snapshotId = vm.snapshot();
   323  
   324                                  uint128 prevBaseFee = prevBaseFees[i];
   325                                  uint64 prevBoughtGas = prevBoughtGases[j];
   326                                  uint64 prevBlockNumDiff = prevBlockNumDiffs[k];
   327                                  uint64 requestedGas = requestedGases[l];
   328                                  uint256 l1BaseFee = l1BaseFees[m];
   329                                  uint256 ethPrice = ethPrices[n];
   330                                  string memory result = "success";
   331  
   332                                  vm.fee(l1BaseFee);
   333  
   334                                  CustomMeterUser meter = new CustomMeterUser({
   335                                      _prevBaseFee: prevBaseFee,
   336                                      _prevBoughtGas: prevBoughtGas,
   337                                      _prevBlockNum: uint64(block.number)
   338                                  });
   339  
   340                                  vm.roll(block.number + prevBlockNumDiff);
   341  
   342                                  // Call the metering code and catch the various
   343                                  // types of errors.
   344                                  uint256 gasConsumed = 0;
   345                                  try meter.use{ gas: 30_000_000 }(requestedGas) returns (uint256 _gasConsumed) {
   346                                      gasConsumed = _gasConsumed;
   347                                  } catch (bytes memory err) {
   348                                      bytes32 hash = keccak256(err);
   349                                      if (hash == cannotBuyMoreGas) {
   350                                          result = "ResourceMetering: cannot buy more gas than available gas limit";
   351                                      } else if (hash == overflowErr) {
   352                                          result = "arithmetic overflow/underflow";
   353                                      } else if (hash == emptyReturnData) {
   354                                          result = "out of gas";
   355                                      } else {
   356                                          result = "UNKNOWN ERROR";
   357                                      }
   358                                  }
   359  
   360                                  // Compute the USD cost of the gas used
   361                                  uint256 usdCost = (gasConsumed * l1BaseFee * ethPrice) / 1 ether;
   362  
   363                                  vm.writeLine(
   364                                      outfile,
   365                                      string.concat(
   366                                          vm.toString(prevBaseFee),
   367                                          ",",
   368                                          vm.toString(prevBoughtGas),
   369                                          ",",
   370                                          vm.toString(prevBlockNumDiff),
   371                                          ",",
   372                                          vm.toString(l1BaseFee),
   373                                          ",",
   374                                          vm.toString(requestedGas),
   375                                          ",",
   376                                          vm.toString(gasConsumed),
   377                                          ",",
   378                                          "$",
   379                                          vm.toString(ethPrice),
   380                                          ",",
   381                                          "$",
   382                                          vm.toString(usdCost),
   383                                          ",",
   384                                          result
   385                                      )
   386                                  );
   387  
   388                                  assertTrue(vm.revertTo(snapshotId));
   389                              }
   390                          }
   391                      }
   392                  }
   393              }
   394          }
   395      }
   396  }