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 }