Commit 47f9eb1c authored by Maurelian's avatar Maurelian

feat(ctb): Remove echidna

parent 102c475c
......@@ -7,7 +7,6 @@ ignore:
- "**/*.t.sol"
- "op-bindings/bindings/*.go"
- "packages/contracts-bedrock/contracts/vendor/WETH9.sol"
- "packages/contracts-bedrock/contracts/echidna"
- "packages/contracts-bedrock/contracts/cannon" # tested through Go tests
coverage:
status:
......
......@@ -80,17 +80,6 @@ pnpm build
pnpm test
```
#### Running Echidna tests
You must have [Echidna](https://github.com/crytic/echidna) installed.
Contracts targetted for Echidna testing are located in `./contracts/echidna`
Each target contract is tested with a separate pnpm command, for example:
```shell
pnpm echidna:aliasing
```
### Deployment
The smart contracts are deployed using `foundry` with a `hardhat-deploy` compatibility layer. When the contracts are deployed,
......
pragma solidity 0.8.15;
import { AddressAliasHelper } from "../vendor/AddressAliasHelper.sol";
contract EchidnaFuzzAddressAliasing {
bool internal failedRoundtrip;
/**
* @notice Takes an address to be aliased with AddressAliasHelper and then unaliased
* and updates the test contract's state indicating if the round trip encoding
* failed.
*/
function testRoundTrip(address addr) public {
// Alias our address
address aliasedAddr = AddressAliasHelper.applyL1ToL2Alias(addr);
// Unalias our address
address undoneAliasAddr = AddressAliasHelper.undoL1ToL2Alias(aliasedAddr);
// If our round trip aliasing did not return the original result, set our state.
if (addr != undoneAliasAddr) {
failedRoundtrip = true;
}
}
/**
* @custom:invariant Address aliases are always able to be undone.
*
* Asserts that an address that has been aliased with `applyL1ToL2Alias` can always
* be unaliased with `undoL1ToL2Alias`.
*/
function echidna_round_trip_aliasing() public view returns (bool) {
// ASSERTION: The round trip aliasing done in testRoundTrip(...) should never fail.
return !failedRoundtrip;
}
}
pragma solidity 0.8.15;
import { Burn } from "../libraries/Burn.sol";
import { StdUtils } from "forge-std/Test.sol";
contract EchidnaFuzzBurnEth is StdUtils {
bool internal failedEthBurn;
/**
* @notice Takes an integer amount of eth to burn through the Burn library and
* updates the contract state if an incorrect amount of eth moved from the contract
*/
function testBurn(uint256 _value) public {
// cache the contract's eth balance
uint256 preBurnBalance = address(this).balance;
uint256 value = bound(_value, 0, preBurnBalance);
// execute a burn of _value eth
Burn.eth(value);
// check that exactly value eth was transfered from the contract
unchecked {
if (address(this).balance != preBurnBalance - value) {
failedEthBurn = true;
}
}
}
/**
* @custom:invariant `eth(uint256)` always burns the exact amount of eth passed.
*
* Asserts that when `Burn.eth(uint256)` is called, it always burns the exact amount
* of ETH passed to the function.
*/
function echidna_burn_eth() public view returns (bool) {
// ASSERTION: The amount burned should always match the amount passed exactly
return !failedEthBurn;
}
}
contract EchidnaFuzzBurnGas is StdUtils {
bool internal failedGasBurn;
/**
* @notice Takes an integer amount of gas to burn through the Burn library and
* updates the contract state if at least that amount of gas was not burned
* by the library
*/
function testGas(uint256 _value) public {
// cap the value to the max resource limit
uint256 MAX_RESOURCE_LIMIT = 8_000_000;
uint256 value = bound(_value, 0, MAX_RESOURCE_LIMIT);
// cache the contract's current remaining gas
uint256 preBurnGas = gasleft();
// execute the gas burn
Burn.gas(value);
// cache the remaining gas post burn
uint256 postBurnGas = gasleft();
// check that at least value gas was burnt (and that there was no underflow)
unchecked {
if (postBurnGas - preBurnGas > value || preBurnGas - value > preBurnGas) {
failedGasBurn = true;
}
}
}
/**
* @custom:invariant `gas(uint256)` always burns at least the amount of gas passed.
*
* Asserts that when `Burn.gas(uint256)` is called, it always burns at least the amount
* of gas passed to the function.
*/
function echidna_burn_gas() public view returns (bool) {
// ASSERTION: The amount of gas burned should be strictly greater than the
// the amount passed as _value (minimum _value + whatever minor overhead to
// the value after the call)
return !failedGasBurn;
}
}
pragma solidity 0.8.15;
import { Encoding } from "../libraries/Encoding.sol";
contract EchidnaFuzzEncoding {
bool internal failedRoundtripAToB;
bool internal failedRoundtripBToA;
/**
* @notice Takes a pair of integers to be encoded into a versioned nonce with the
* Encoding library and then decoded and updates the test contract's state
* indicating if the round trip encoding failed.
*/
function testRoundTripAToB(uint240 _nonce, uint16 _version) public {
// Encode the nonce and version
uint256 encodedVersionedNonce = Encoding.encodeVersionedNonce(_nonce, _version);
// Decode the nonce and version
uint240 decodedNonce;
uint16 decodedVersion;
(decodedNonce, decodedVersion) = Encoding.decodeVersionedNonce(encodedVersionedNonce);
// If our round trip encoding did not return the original result, set our state.
if ((decodedNonce != _nonce) || (decodedVersion != _version)) {
failedRoundtripAToB = true;
}
}
/**
* @notice Takes an integer representing a packed version and nonce and attempts
* to decode them using the Encoding library before re-encoding and updates
* the test contract's state indicating if the round trip encoding failed.
*/
function testRoundTripBToA(uint256 _versionedNonce) public {
// Decode the nonce and version
uint240 decodedNonce;
uint16 decodedVersion;
(decodedNonce, decodedVersion) = Encoding.decodeVersionedNonce(_versionedNonce);
// Encode the nonce and version
uint256 encodedVersionedNonce = Encoding.encodeVersionedNonce(decodedNonce, decodedVersion);
// If our round trip encoding did not return the original result, set our state.
if (encodedVersionedNonce != _versionedNonce) {
failedRoundtripBToA = true;
}
}
/**
* @custom:invariant `testRoundTripAToB` never fails.
*
* Asserts that a raw versioned nonce can be encoded / decoded to reach the same raw value.
*/
function echidna_round_trip_encoding_AToB() public view returns (bool) {
// ASSERTION: The round trip encoding done in testRoundTripAToB(...)
return !failedRoundtripAToB;
}
/**
* @custom:invariant `testRoundTripBToA` never fails.
*
* Asserts that an encoded versioned nonce can always be decoded / re-encoded to reach
* the same encoded value.
*/
function echidna_round_trip_encoding_BToA() public view returns (bool) {
// ASSERTION: The round trip encoding done in testRoundTripBToA should never
// fail.
return !failedRoundtripBToA;
}
}
pragma solidity 0.8.15;
import { Hashing } from "../libraries/Hashing.sol";
import { Encoding } from "../libraries/Encoding.sol";
contract EchidnaFuzzHashing {
bool internal failedCrossDomainHashHighVersion;
bool internal failedCrossDomainHashV0;
bool internal failedCrossDomainHashV1;
/**
* @notice Takes the necessary parameters to perform a cross domain hash with a randomly
* generated version. Only schema versions 0 and 1 are supported and all others should revert.
*/
function testHashCrossDomainMessageHighVersion(
uint16 _version,
uint240 _nonce,
address _sender,
address _target,
uint256 _value,
uint256 _gasLimit,
bytes memory _data
) public {
// generate the versioned nonce
uint256 encodedNonce = Encoding.encodeVersionedNonce(_nonce, _version);
// hash the cross domain message. we don't need to store the result since the function
// validates and should revert if an invalid version (>1) is encoded
Hashing.hashCrossDomainMessage(encodedNonce, _sender, _target, _value, _gasLimit, _data);
// check that execution never makes it this far for an invalid version
if (_version > 1) {
failedCrossDomainHashHighVersion = true;
}
}
/**
* @notice Takes the necessary parameters to perform a cross domain hash using the v0 schema
* and compares the output of a call to the unversioned function to the v0 function directly
*/
function testHashCrossDomainMessageV0(
uint240 _nonce,
address _sender,
address _target,
uint256 _value,
uint256 _gasLimit,
bytes memory _data
) public {
// generate the versioned nonce with the version set to 0
uint256 encodedNonce = Encoding.encodeVersionedNonce(_nonce, 0);
// hash the cross domain message using the unversioned and versioned functions for
// comparison
bytes32 sampleHash1 = Hashing.hashCrossDomainMessage(
encodedNonce,
_sender,
_target,
_value,
_gasLimit,
_data
);
bytes32 sampleHash2 = Hashing.hashCrossDomainMessageV0(
_target,
_sender,
_data,
encodedNonce
);
// check that the output of both functions matches
if (sampleHash1 != sampleHash2) {
failedCrossDomainHashV0 = true;
}
}
/**
* @notice Takes the necessary parameters to perform a cross domain hash using the v1 schema
* and compares the output of a call to the unversioned function to the v1 function directly
*/
function testHashCrossDomainMessageV1(
uint240 _nonce,
address _sender,
address _target,
uint256 _value,
uint256 _gasLimit,
bytes memory _data
) public {
// generate the versioned nonce with the version set to 1
uint256 encodedNonce = Encoding.encodeVersionedNonce(_nonce, 1);
// hash the cross domain message using the unversioned and versioned functions for
// comparison
bytes32 sampleHash1 = Hashing.hashCrossDomainMessage(
encodedNonce,
_sender,
_target,
_value,
_gasLimit,
_data
);
bytes32 sampleHash2 = Hashing.hashCrossDomainMessageV1(
encodedNonce,
_sender,
_target,
_value,
_gasLimit,
_data
);
// check that the output of both functions matches
if (sampleHash1 != sampleHash2) {
failedCrossDomainHashV1 = true;
}
}
/**
* @custom:invariant `hashCrossDomainMessage` reverts if `version` is > `1`.
*
* The `hashCrossDomainMessage` function should always revert if the `version` passed is > `1`.
*/
function echidna_hash_xdomain_msg_high_version() public view returns (bool) {
// ASSERTION: A call to hashCrossDomainMessage will never succeed for a version > 1
return !failedCrossDomainHashHighVersion;
}
/**
* @custom:invariant `version` = `0`: `hashCrossDomainMessage` and `hashCrossDomainMessageV0`
* are equivalent.
*
* If the version passed is 0, `hashCrossDomainMessage` and `hashCrossDomainMessageV0` should be
* equivalent.
*/
function echidna_hash_xdomain_msg_0() public view returns (bool) {
// ASSERTION: A call to hashCrossDomainMessage and hashCrossDomainMessageV0
// should always match when the version passed is 0
return !failedCrossDomainHashV0;
}
/**
* @custom:invariant `version` = `1`: `hashCrossDomainMessage` and `hashCrossDomainMessageV1`
* are equivalent.
*
* If the version passed is 1, `hashCrossDomainMessage` and `hashCrossDomainMessageV1` should be
* equivalent.
*/
function echidna_hash_xdomain_msg_1() public view returns (bool) {
// ASSERTION: A call to hashCrossDomainMessage and hashCrossDomainMessageV1
// should always match when the version passed is 1
return !failedCrossDomainHashV1;
}
}
pragma solidity 0.8.15;
import { OptimismPortal } from "../L1/OptimismPortal.sol";
import { L2OutputOracle } from "../L1/L2OutputOracle.sol";
import { AddressAliasHelper } from "../vendor/AddressAliasHelper.sol";
import { SystemConfig } from "../L1/SystemConfig.sol";
import { ResourceMetering } from "../L1/ResourceMetering.sol";
import { Constants } from "../libraries/Constants.sol";
contract EchidnaFuzzOptimismPortal {
OptimismPortal internal portal;
bool internal failedToComplete;
constructor() {
ResourceMetering.ResourceConfig memory rcfg = Constants.DEFAULT_RESOURCE_CONFIG();
SystemConfig systemConfig = new SystemConfig({
_owner: address(1),
_overhead: 0,
_scalar: 10000,
_batcherHash: bytes32(0),
_gasLimit: 30_000_000,
_unsafeBlockSigner: address(0),
_config: rcfg
});
portal = new OptimismPortal({
_l2Oracle: L2OutputOracle(address(0)),
_guardian: address(0),
_paused: false,
_config: systemConfig
});
}
// A test intended to identify any unexpected halting conditions
function testDepositTransactionCompletes(
address _to,
uint256 _mint,
uint256 _value,
uint64 _gasLimit,
bool _isCreation,
bytes memory _data
) public payable {
failedToComplete = true;
require(!_isCreation || _to == address(0), "EchidnaFuzzOptimismPortal: invalid test case.");
portal.depositTransaction{ value: _mint }(_to, _value, _gasLimit, _isCreation, _data);
failedToComplete = false;
}
/**
* @custom:invariant Deposits of any value should always succeed unless
* `_to` = `address(0)` or `_isCreation` = `true`.
*
* All deposits, barring creation transactions and transactions sent to `address(0)`,
* should always succeed.
*/
function echidna_deposit_completes() public view returns (bool) {
return !failedToComplete;
}
}
pragma solidity 0.8.15;
import { ResourceMetering } from "../L1/ResourceMetering.sol";
import { Arithmetic } from "../libraries/Arithmetic.sol";
import { StdUtils } from "forge-std/Test.sol";
import { Constants } from "../libraries/Constants.sol";
contract EchidnaFuzzResourceMetering is ResourceMetering, StdUtils {
bool internal failedMaxGasPerBlock;
bool internal failedRaiseBaseFee;
bool internal failedLowerBaseFee;
bool internal failedNeverBelowMinBaseFee;
bool internal failedMaxRaiseBaseFeePerBlock;
bool internal failedMaxLowerBaseFeePerBlock;
// Used as a special flag for the purpose of identifying unchecked math errors specifically
// in the test contracts, not the target contracts themselves.
bool internal underflow;
constructor() {
initialize();
}
function initialize() internal initializer {
__ResourceMetering_init();
}
function resourceConfig() public pure returns (ResourceMetering.ResourceConfig memory) {
return _resourceConfig();
}
function _resourceConfig()
internal
pure
override
returns (ResourceMetering.ResourceConfig memory)
{
ResourceMetering.ResourceConfig memory rcfg = Constants.DEFAULT_RESOURCE_CONFIG();
return rcfg;
}
/**
* @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);
ResourceMetering.ResourceConfig memory rcfg = resourceConfig();
uint256 targetResourceLimit = uint256(rcfg.maxResourceLimit) /
uint256(rcfg.elasticityMultiplier);
// check that the last block's base fee hasn't dropped below the minimum
if (cachedPrevBaseFee < uint256(rcfg.minimumBaseFee)) {
failedNeverBelowMinBaseFee = true;
}
// check that the last block didn't consume more than the max amount of gas
if (cachedPrevBoughtGas > uint256(rcfg.maxResourceLimit)) {
failedMaxGasPerBlock = true;
}
// Part2: we perform the gas burn
// force the gasToBurn into the correct range based on whether we intend to
// raise or lower the baseFee after this block, respectively
uint256 gasToBurn;
if (_raiseBaseFee) {
gasToBurn = bound(
_gasToBurn,
uint256(targetResourceLimit),
uint256(rcfg.maxResourceLimit)
);
} else {
gasToBurn = bound(_gasToBurn, 0, targetResourceLimit);
}
_burnInternal(uint64(gasToBurn));
// Part 3: we run checks and modify our invariant flags based on the updated params values
// Calculate the maximum allowed baseFee change (per block)
uint256 maxBaseFeeChange = cachedPrevBaseFee / uint256(rcfg.baseFeeMaxChangeDenominator);
// 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
if (
(cachedPrevBoughtGas > uint256(targetResourceLimit)) &&
(uint256(params.prevBlockNum) - cachedPrevBlockNum == 1)
) {
failedRaiseBaseFee = failedRaiseBaseFee || (params.prevBaseFee <= cachedPrevBaseFee);
failedMaxRaiseBaseFeePerBlock =
failedMaxRaiseBaseFeePerBlock ||
((uint256(params.prevBaseFee) - cachedPrevBaseFee) < maxBaseFeeChange);
}
// If the last block used less than the target amount of gas, (or was empty),
// ensure that: this block's baseFee was decreased, but not by more than the max amount
if (
(cachedPrevBoughtGas < uint256(targetResourceLimit)) ||
(uint256(params.prevBlockNum) - cachedPrevBlockNum > 1)
) {
// Invariant: baseFee should decrease
failedLowerBaseFee =
failedLowerBaseFee ||
(uint256(params.prevBaseFee) > cachedPrevBaseFee);
if (params.prevBlockNum - cachedPrevBlockNum == 1) {
// No empty blocks
// Invariant: baseFee should not have decreased by more than the maximum amount
failedMaxLowerBaseFeePerBlock =
failedMaxLowerBaseFeePerBlock ||
((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),
int256(uint256(rcfg.baseFeeMaxChangeDenominator)),
int256(uint256(params.prevBlockNum) - cachedPrevBlockNum)
),
int256(uint256(rcfg.minimumBaseFee)),
int256(uint256(rcfg.maximumBaseFee))
)
);
}
// 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
failedMaxLowerBaseFeePerBlock =
failedMaxLowerBaseFeePerBlock ||
((cachedPrevBaseFee - uint256(params.prevBaseFee)) <= maxBaseFeeChange);
}
}
}
function _burnInternal(uint64 _gasToBurn) private metered(_gasToBurn) {}
/**
* @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.
*/
function echidna_high_usage_raise_baseFee() public view returns (bool) {
return !failedRaiseBaseFee;
}
/**
* @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.
*/
function echidna_low_usage_lower_baseFee() public view returns (bool) {
return !failedLowerBaseFee;
}
/**
* @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.
*/
function echidna_never_below_min_baseFee() public view returns (bool) {
return !failedNeverBelowMinBaseFee;
}
/**
* @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.
*/
function echidna_never_above_max_gas_limit() public view returns (bool) {
return !failedMaxGasPerBlock;
}
/**
* @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`
*/
function echidna_never_exceed_max_increase() public view returns (bool) {
return !failedMaxRaiseBaseFeePerBlock;
}
/**
* @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`
*/
function echidna_never_exceed_max_decrease() public view returns (bool) {
return !failedMaxLowerBaseFeePerBlock;
}
/**
* @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.
*/
function echidna_underflow() public view returns (bool) {
return !underflow;
}
}
cryticArgs: ["--hardhat-ignore-compile"]
format: text
# Set the timeout to 3 minutes to keep CI from getting too long.
# The tool also adds 2 or 3 minutes before/after the actual timeout window.
timeout: 180
# Prevent calls to the (non-existent) fallback function of EchidnaFuzzResourceMetering
filterFunctions: [EchidnaFuzzResourceMetering.*fallback*()]
......@@ -18,7 +18,6 @@ build_info = true
build_info_path = 'artifacts/build-info'
ffi = true
fuzz_runs = 16
no_match_contract = 'EchidnaFuzz'
# PNPM symlinks all node_modules from the monorepo root
allow_paths = ["../../node_modules", "./**"]
......@@ -32,6 +31,3 @@ fs_permissions = [
[profile.ci]
fuzz_runs = 512
[profile.echidna]
bytecode_hash = 'ipfs'
# `AddressAliasing` Invariants
## Address aliases are always able to be undone.
**Test:** [`FuzzAddressAliasing.sol#L32`](../contracts/echidna/FuzzAddressAliasing.sol#L32)
Asserts that an address that has been aliased with `applyL1ToL2Alias` can always be unaliased with `undoL1ToL2Alias`.
# `Burn` Invariants
## `eth(uint256)` always burns the exact amount of eth passed.
**Test:** [`FuzzBurn.sol#L35`](../contracts/echidna/FuzzBurn.sol#L35)
Asserts that when `Burn.eth(uint256)` is called, it always burns the exact amount of ETH passed to the function.
## `gas(uint256)` always burns at least the amount of gas passed.
**Test:** [`FuzzBurn.sol#L77`](../contracts/echidna/FuzzBurn.sol#L77)
Asserts that when `Burn.gas(uint256)` is called, it always burns at least the amount of gas passed to the function.
......@@ -10,15 +10,3 @@ Asserts that a raw versioned nonce can be encoded / decoded to reach the same ra
**Test:** [`Encoding.t.sol#L87`](../contracts/test/invariants/Encoding.t.sol#L87)
Asserts that an encoded versioned nonce can always be decoded / re-encoded to reach the same encoded value.
## `testRoundTripAToB` never fails.
**Test:** [`FuzzEncoding.sol#L56`](../contracts/echidna/FuzzEncoding.sol#L56)
Asserts that a raw versioned nonce can be encoded / decoded to reach the same raw value.
## `testRoundTripBToA` never fails.
**Test:** [`FuzzEncoding.sol#L67`](../contracts/echidna/FuzzEncoding.sol#L67)
Asserts that an encoded versioned nonce can always be decoded / re-encoded to reach the same encoded value.
......@@ -16,21 +16,3 @@ If the version passed is 0, `hashCrossDomainMessage` and `hashCrossDomainMessage
**Test:** [`Hashing.t.sol#L166`](../contracts/test/invariants/Hashing.t.sol#L166)
If the version passed is 1, `hashCrossDomainMessage` and `hashCrossDomainMessageV1` should be equivalent.
## `hashCrossDomainMessage` reverts if `version` is > `1`.
**Test:** [`FuzzHashing.sol#L120`](../contracts/echidna/FuzzHashing.sol#L120)
The `hashCrossDomainMessage` function should always revert if the `version` passed is > `1`.
## `version` = `0`: `hashCrossDomainMessage` and `hashCrossDomainMessageV0` are equivalent.
**Test:** [`FuzzHashing.sol#L132`](../contracts/echidna/FuzzHashing.sol#L132)
If the version passed is 0, `hashCrossDomainMessage` and `hashCrossDomainMessageV0` should be equivalent.
## `version` = `1`: `hashCrossDomainMessage` and `hashCrossDomainMessageV1` are equivalent.
**Test:** [`FuzzHashing.sol#L145`](../contracts/echidna/FuzzHashing.sol#L145)
If the version passed is 1, `hashCrossDomainMessage` and `hashCrossDomainMessageV1` should be equivalent.
......@@ -22,9 +22,3 @@ Ensures that there is no chain of calls that can be made that allows a withdrawa
**Test:** [`OptimismPortal.t.sol#L260`](../contracts/test/invariants/OptimismPortal.t.sol#L260)
This invariant asserts that there is no chain of calls that can be made that will prevent a withdrawal from being finalized exactly `FINALIZATION_PERIOD_SECONDS` after it was successfully proven.
## Deposits of any value should always succeed unless `_to` = `address(0)` or `_isCreation` = `true`.
**Test:** [`FuzzOptimismPortal.sol#L57`](../contracts/echidna/FuzzOptimismPortal.sol#L57)
All deposits, barring creation transactions and transactions sent to `address(0)`, should always succeed.
......@@ -7,10 +7,8 @@ This directory contains documentation for all defined invariant tests within `co
## Table of Contents
- [AddressAliasHelper](./AddressAliasHelper.md)
- [AddressAliasing](./AddressAliasing.md)
- [Burn.Eth](./Burn.Eth.md)
- [Burn.Gas](./Burn.Gas.md)
- [Burn](./Burn.md)
- [CrossDomainMessenger](./CrossDomainMessenger.md)
- [Encoding](./Encoding.md)
- [Hashing](./Hashing.md)
......@@ -47,20 +45,3 @@ function invariant_<shortDescription>() external {
// ...
}
```
### Echidna Invariants
All `echidna` invariant tests must exist within the `contracts/echidna` folder, and the file name should be
`Fuzz<ContractName>.sol`, where `<ContractName>` is the name of the contract that is being tested.
All property tests within `echidna` invariant files should follow the convention:
```solidity
/**
* @custom:invariant <title>
*
* <longDescription>
*/
function echidna_<shortDescription>() external view returns (bool) {
// ...
}
```
......@@ -40,45 +40,3 @@ After a block consumes less than the target gas, the base fee cannot be lowered
**Test:** [`ResourceMetering.t.sol#L244`](../contracts/test/invariants/ResourceMetering.t.sol#L244)
When calculating the `maxBaseFeeChange` after multiple empty blocks, the calculation should never be allowed to underflow.
## The base fee should increase if the last block used more than the target amount of gas
**Test:** [`FuzzResourceMetering.sol#L158`](../contracts/echidna/FuzzResourceMetering.sol#L158)
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.
## The base fee should decrease if the last block used less than the target amount of gas
**Test:** [`FuzzResourceMetering.sol#L169`](../contracts/echidna/FuzzResourceMetering.sol#L169)
If the previous block used less than the target amount of gas, the base fee should decrease, but not more than the max amount.
## A block's base fee should never be below `MINIMUM_BASE_FEE`
**Test:** [`FuzzResourceMetering.sol#L179`](../contracts/echidna/FuzzResourceMetering.sol#L179)
This test asserts that a block's base fee can never drop below the `MINIMUM_BASE_FEE` threshold.
## A block can never consume more than `MAX_RESOURCE_LIMIT` gas.
**Test:** [`FuzzResourceMetering.sol#L189`](../contracts/echidna/FuzzResourceMetering.sol#L189)
This test asserts that a block can never consume more than the `MAX_RESOURCE_LIMIT` gas threshold.
## The base fee can never be raised more than the max base fee change.
**Test:** [`FuzzResourceMetering.sol#L200`](../contracts/echidna/FuzzResourceMetering.sol#L200)
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`
## The base fee can never be lowered more than the max base fee change.
**Test:** [`FuzzResourceMetering.sol#L211`](../contracts/echidna/FuzzResourceMetering.sol#L211)
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`
## The `maxBaseFeeChange` calculation over multiple blocks can never underflow.
**Test:** [`FuzzResourceMetering.sol#L222`](../contracts/echidna/FuzzResourceMetering.sol#L222)
When calculating the `maxBaseFeeChange` after multiple empty blocks, the calculation should never be allowed to underflow.
......@@ -10,7 +10,6 @@
],
"scripts": {
"bindings": "cd ../../op-bindings && make",
"build:with-metadata": "FOUNDRY_PROFILE=echidna pnpm build:forge",
"build": "npx nx build:contracts",
"prebuild:contracts": "./scripts/verify-foundry-install.sh",
"build:contracts": "pnpm build:forge",
......@@ -35,14 +34,7 @@
"lint:ts:fix": "eslint --fix .",
"lint:contracts:fix": "pnpm solhint --fix 'contracts/**/!(DisputeTypes|RLPReader).sol' && pnpm prettier --write 'contracts/**/!(DisputeTypes|RLPReader).sol'",
"lint:fix": "pnpm lint:contracts:fix && pnpm lint:ts:fix",
"lint": "pnpm lint:fix && pnpm lint:check",
"echidna:aliasing": "echidna-test --contract EchidnaFuzzAddressAliasing --config ./echidna.yaml .",
"echidna:burn:gas": "echidna-test --contract EchidnaFuzzBurnGas --config ./echidna.yaml .",
"echidna:burn:eth": "echidna-test --contract EchidnaFuzzBurnEth --config ./echidna.yaml .",
"echidna:encoding": "echidna-test --contract EchidnaFuzzEncoding --config ./echidna.yaml .",
"echidna:portal": "echidna-test --contract EchidnaFuzzOptimismPortal --config ./echidna.yaml .",
"echidna:hashing": "echidna-test --contract EchidnaFuzzHashing --config ./echidna.yaml .",
"echidna:metering": "echidna-test --contract EchidnaFuzzResourceMetering --config ./echidna.yaml ."
"lint": "pnpm lint:fix && pnpm lint:check"
},
"dependencies": {
"@eth-optimism/core-utils": "^0.12.1",
......
......@@ -8,9 +8,7 @@ const BASE_INVARIANTS_DIR = path.join(
'test',
'invariants'
)
const BASE_ECHIDNA_DIR = path.join(__dirname, '..', 'contracts', 'echidna')
const BASE_DOCS_DIR = path.join(__dirname, '..', 'invariant-docs')
const BASE_ECHIDNA_GH_URL = '../contracts/echidna/'
const BASE_INVARIANT_GH_URL = '../contracts/test/invariants/'
const NATSPEC_INV = '@custom:invariant'
const BLOCK_COMMENT_PREFIX_REGEX = /\*(\/)?/
......@@ -20,7 +18,6 @@ const BLOCK_COMMENT_HEADER_REGEX = /\*\s(.)+/
type Contract = {
name: string
fileName: string
isEchidna: boolean
docs: InvariantDoc[]
}
......@@ -52,11 +49,8 @@ const docGen = (dir: string): void => {
const lines = fileContents.split('\n').map((line: string) => line.trim())
// Create an object to store all invariant test docs for the current contract
const isEchidna = fileName.startsWith('Fuzz')
const name = isEchidna
? fileName.replace('Fuzz', '').replace('.sol', '')
: fileName.replace('.t.sol', '')
const contract: Contract = { name, fileName, isEchidna, docs: [] }
const name = fileName.replace('.t.sol', '')
const contract: Contract = { name, fileName, docs: [] }
let currentDoc: InvariantDoc
......@@ -179,20 +173,12 @@ const renderContractDoc = (contract: Contract, header: boolean): string => {
const docs = contract.docs
.map((doc: InvariantDoc) => {
const line = `${contract.fileName}#L${doc.lineNo}`
return `## ${doc.header}\n**Test:** [\`${line}\`](${getGithubBase(
contract
)}${line})\n\n${doc.desc}`
return `## ${doc.header}\n**Test:** [\`${line}\`](${BASE_INVARIANT_GH_URL}${line})\n\n${doc.desc}`
})
.join('\n\n')
return `${_header}\n${docs}`
}
/**
* Get the base URL for the test contract
*/
const getGithubBase = ({ isEchidna }: Contract): string =>
isEchidna ? BASE_ECHIDNA_GH_URL : BASE_INVARIANT_GH_URL
// Generate the docs
// Forge
......@@ -202,9 +188,5 @@ docGen(BASE_INVARIANTS_DIR)
// New line
console.log()
// Echidna
console.log('Generating docs for echidna invariants...')
docGen(BASE_ECHIDNA_DIR)
// Generate an updated table of contents
tocGen()
......@@ -10,6 +10,6 @@
"hardhat_ignore_compile": false,
"disable_color": false,
"exclude_dependencies": true,
"filter_paths": "contracts/test,contracts/vendor,contracts/echidna,node_modules,contracts/cannon/MIPS.sol",
"filter_paths": "contracts/test,contracts/vendor,node_modules,contracts/cannon/MIPS.sol",
"foundry_out_directory": "artifacts"
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment