You can potentially use the afterInvariant
function of the Invariant Testing (i.e., Stateful Fuzz Testing).
Here's the minimal example showcasing the invariant testing of the Counter contract (i.e., the contract that is created by default after executing forge init
command):
src/Counter.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Counter {
uint256 public number;
function setNumber(uint256 newNumber) public {
number = newNumber;
}
function increment() public {
number++;
}
}
test/Handler.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test, console} from "forge-std/Test.sol";
import {Counter} from "../src/Counter.sol";
contract Handler is Test {
Counter public counter;
bool public isIncrementTested;
bool public isSetNumberTested;
uint256 public numberOfTestRunsForIncrement;
uint256 public numberOfTestRunsForSetNumber;
constructor() {
counter = new Counter();
}
function increment() public {
uint256 numberBeforeIncrement = counter.number();
if (numberBeforeIncrement < type(uint256).max) {
counter.increment();
assertEq(counter.number(), numberBeforeIncrement + 1);
}
isIncrementTested = true;
numberOfTestRunsForIncrement++;
}
function setNumber(uint256 x) public {
counter.setNumber(x);
assertEq(counter.number(), x);
isSetNumberTested = true;
numberOfTestRunsForSetNumber++;
}
}
test/Invariants.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test, console} from "forge-std/Test.sol";
import {StdInvariant} from "forge-std/StdInvariant.sol";
import {Handler} from "./Handler.t.sol";
// import {Counter} from "../src/Counter.sol";
contract InvariantsTest is StdInvariant, Test {
Handler public handler;
function setUp() public {
handler = new Handler();
targetContract(address(handler));
}
function invariant_test() public pure {}
function afterInvariant() public view {
console.log(handler.isIncrementTested(), handler.isSetNumberTested());
console.log(handler.numberOfTestRunsForIncrement(), handler.numberOfTestRunsForSetNumber());
assertEq(handler.isIncrementTested(), true);
assertEq(handler.isSetNumberTested(), true);
assertEq(handler.numberOfTestRunsForIncrement() + handler.numberOfTestRunsForSetNumber(), 128);
}
}
Add the configuration for invariant testing in your foundry.toml file:
[invariant]
runs = 128
depth = 128
fail_on_revert = true
Explanation
In the Handler contract:
increment()
and setNumber()
functions are defined, that are basically testing the corresponding functions of the Counter contract by passing in random inputs as arguments.
And, isIncrementTested
, isSetNumberTested
, numberOfTestRunsForIncrement
and numberOfTestRunsForSetNumber
are the handler ghost variables that are basically tracking the test calls.
In the InvariantsTest contract:
invariant_test()
function is supposed to execute and test all the functions defined in the Handler contract (i.e., the target contract of the invariant testing) with random inputs as arguments.
afterInvariant()
function would be executed after each invariant test (as in this case there is only one inviant test i.e., invariant_test()
). As per the docs:
afterInvariant()
function is called at the end of each invariant run
(if declared), allowing post campaign processing. This function can be
used for logging campaign metrics (e.g. how many times a selector was
called) and post fuzz campaign testing (e.g. close out all positions
and assert all funds are able to exit the system).