FuzzResourceMetering.sol 8.51 KB
Newer Older
1 2 3
pragma solidity 0.8.15;

import { ResourceMetering } from "../L1/ResourceMetering.sol";
4 5
import { Arithmetic } from "../libraries/Arithmetic.sol";
import { StdUtils } from "forge-std/Test.sol";
6

7
contract EchidnaFuzzResourceMetering is ResourceMetering, StdUtils {
8 9 10 11 12 13
    bool internal failedMaxGasPerBlock;
    bool internal failedRaiseBaseFee;
    bool internal failedLowerBaseFee;
    bool internal failedNeverBelowMinBaseFee;
    bool internal failedMaxRaiseBaseFeePerBlock;
    bool internal failedMaxLowerBaseFeePerBlock;
14

15 16
    // Used as a special flag for the purpose of identifying unchecked math errors specifically
    // in the test contracts, not the target contracts themselves.
17
    bool internal underflow;
18

19 20 21 22
    constructor() {
        initialize();
    }

23
    function initialize() internal initializer {
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
        __ResourceMetering_init();
    }

    /**
     * @notice Takes the necessary parameters to allow us to burn arbitrary amounts of gas to test
     *         the underlying resource metering/gas market logic
     */
    function testBurn(uint256 _gasToBurn, bool _raiseBaseFee) public {
        // Part 1: we cache the current param values and do some basic checks on them.
        uint256 cachedPrevBaseFee = uint256(params.prevBaseFee);
        uint256 cachedPrevBoughtGas = uint256(params.prevBoughtGas);
        uint256 cachedPrevBlockNum = uint256(params.prevBlockNum);

        // check that the last block's base fee hasn't dropped below the minimum
        if (cachedPrevBaseFee < uint256(MINIMUM_BASE_FEE)) {
39
            failedNeverBelowMinBaseFee = true;
40 41 42 43 44 45 46 47 48
        }
        // check that the last block didn't consume more than the max amount of gas
        if (cachedPrevBoughtGas > uint256(MAX_RESOURCE_LIMIT)) {
            failedMaxGasPerBlock = true;
        }

        // Part2: we perform the gas burn

        // force the gasToBurn into the correct range based on whether we intend to
49
        // raise or lower the baseFee after this block, respectively
50 51
        uint256 gasToBurn;
        if (_raiseBaseFee) {
52 53 54 55 56
            gasToBurn = bound(
                _gasToBurn,
                uint256(TARGET_RESOURCE_LIMIT),
                uint256(MAX_RESOURCE_LIMIT)
            );
57
        } else {
58
            gasToBurn = bound(_gasToBurn, 0, uint256(TARGET_RESOURCE_LIMIT));
59 60 61 62 63 64
        }

        _burnInternal(uint64(gasToBurn));

        // Part 3: we run checks and modify our invariant flags based on the updated params values

65
        // Calculate the maximum allowed baseFee change (per block)
66
        uint256 maxBaseFeeChange = cachedPrevBaseFee / uint256(BASE_FEE_MAX_CHANGE_DENOMINATOR);
67 68

        // If the last block used more than the target amount of gas (and there were no
69
        // empty blocks in between), ensure this block's baseFee increased, but not by
70 71 72 73 74
        // more than the max amount per block
        if (
            (cachedPrevBoughtGas > uint256(TARGET_RESOURCE_LIMIT)) &&
            (uint256(params.prevBlockNum) - cachedPrevBlockNum == 1)
        ) {
75 76 77 78
            failedRaiseBaseFee = failedRaiseBaseFee || (params.prevBaseFee <= cachedPrevBaseFee);
            failedMaxRaiseBaseFeePerBlock =
                failedMaxRaiseBaseFeePerBlock ||
                ((uint256(params.prevBaseFee) - cachedPrevBaseFee) < maxBaseFeeChange);
79
        }
80

81
        // If the last block used less than the target amount of gas, (or was empty),
82
        // ensure that: this block's baseFee was decreased, but not by more than the max amount
83 84 85 86
        if (
            (cachedPrevBoughtGas < uint256(TARGET_RESOURCE_LIMIT)) ||
            (uint256(params.prevBlockNum) - cachedPrevBlockNum > 1)
        ) {
87
            // Invariant: baseFee should decrease
88 89
            failedLowerBaseFee =
                failedLowerBaseFee ||
90
                (uint256(params.prevBaseFee) > cachedPrevBaseFee);
91

92
            if (params.prevBlockNum - cachedPrevBlockNum == 1) {
93 94
                // No empty blocks
                // Invariant: baseFee should not have decreased by more than the maximum amount
95 96
                failedMaxLowerBaseFeePerBlock =
                    failedMaxLowerBaseFeePerBlock ||
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
                    ((cachedPrevBaseFee - uint256(params.prevBaseFee)) <= maxBaseFeeChange);
            } else if (params.prevBlockNum - cachedPrevBlockNum > 1) {
                // We have at least one empty block
                // Update the maxBaseFeeChange to account for multiple blocks having passed
                unchecked {
                    maxBaseFeeChange = uint256(
                        int256(cachedPrevBaseFee) -
                            Arithmetic.clamp(
                                Arithmetic.cdexp(
                                    int256(cachedPrevBaseFee),
                                    BASE_FEE_MAX_CHANGE_DENOMINATOR,
                                    int256(uint256(params.prevBlockNum) - cachedPrevBlockNum)
                                ),
                                MINIMUM_BASE_FEE,
                                MAXIMUM_BASE_FEE
                            )
                    );
                }

                // Detect an underflow in the previous calculation.
                // Without using unchecked above, and detecting the underflow here, echidna would
                // otherwise ignore the revert.
                underflow = underflow || maxBaseFeeChange > cachedPrevBaseFee;

                // Invariant: baseFee should not have decreased by more than the maximum amount
122 123
                failedMaxLowerBaseFeePerBlock =
                    failedMaxLowerBaseFeePerBlock ||
124
                    ((cachedPrevBaseFee - uint256(params.prevBaseFee)) <= maxBaseFeeChange);
125 126 127 128 129 130
            }
        }
    }

    function _burnInternal(uint64 _gasToBurn) private metered(_gasToBurn) {}

131 132 133 134 135 136 137 138
    /**
     * @custom:invariant The base fee should increase if the last block used more
     * than the target amount of gas
     *
     * If the last block used more than the target amount of gas (and there were no
     * empty blocks in between), ensure this block's baseFee increased, but not by
     * more than the max amount per block.
     */
139 140
    function echidna_high_usage_raise_baseFee() public view returns (bool) {
        return !failedRaiseBaseFee;
141 142
    }

143 144 145 146 147 148 149
    /**
     * @custom:invariant The base fee should decrease if the last block used less
     * than the target amount of gas
     *
     * If the previous block used less than the target amount of gas, the base fee should decrease,
     * but not more than the max amount.
     */
150 151
    function echidna_low_usage_lower_baseFee() public view returns (bool) {
        return !failedLowerBaseFee;
152 153
    }

154 155 156 157 158 159
    /**
     * @custom:invariant A block's base fee should never be below `MINIMUM_BASE_FEE`
     *
     * This test asserts that a block's base fee can never drop below the
     * `MINIMUM_BASE_FEE` threshold.
     */
160 161
    function echidna_never_below_min_baseFee() public view returns (bool) {
        return !failedNeverBelowMinBaseFee;
162 163
    }

164 165 166 167 168 169
    /**
     * @custom:invariant A block can never consume more than `MAX_RESOURCE_LIMIT` gas.
     *
     * This test asserts that a block can never consume more than the `MAX_RESOURCE_LIMIT`
     * gas threshold.
     */
170 171 172 173
    function echidna_never_above_max_gas_limit() public view returns (bool) {
        return !failedMaxGasPerBlock;
    }

174 175 176 177 178 179 180
    /**
     * @custom:invariant The base fee can never be raised more than the max base fee change.
     *
     * After a block consumes more gas than the target gas, the base fee cannot be raised
     * more than the maximum amount allowed. The max base fee change (per-block) is derived
     * as follows: `prevBaseFee / BASE_FEE_MAX_CHANGE_DENOMINATOR`
     */
181
    function echidna_never_exceed_max_increase() public view returns (bool) {
182
        return !failedMaxRaiseBaseFeePerBlock;
183 184
    }

185 186 187 188 189 190 191
    /**
     * @custom:invariant The base fee can never be lowered more than the max base fee change.
     *
     * After a block consumes less than the target gas, the base fee cannot be lowered more
     * than the maximum amount allowed. The max base fee change (per-block) is derived as
     *follows: `prevBaseFee / BASE_FEE_MAX_CHANGE_DENOMINATOR`
     */
192
    function echidna_never_exceed_max_decrease() public view returns (bool) {
193
        return !failedMaxLowerBaseFeePerBlock;
194
    }
195

196 197 198 199 200 201 202
    /**
     * @custom:invariant The `maxBaseFeeChange` calculation over multiple blocks can never
     * underflow.
     *
     * When calculating the `maxBaseFeeChange` after multiple empty blocks, the calculation
     * should never be allowed to underflow.
     */
203 204 205
    function echidna_underflow() public view returns (bool) {
        return !underflow;
    }
206
}