Standard library for writing ripfuzz harnesses.
Clone the repository into your project and add a remapping:
git clone https://github.com/pyk/ripfuzz-std lib/ripfuzz-std# remappings.txt
ripfuzz/=lib/ripfuzz-std/src/
Note
A ripfuzz fetch command to install pinned releases is planned:
ripfuzz fetch https://github.com/pyk/ripfuzz-std/archive/v2.0.0.tar.gzripfuzz has three use cases:
| Command | Use case |
|---|---|
ripfuzz test |
Find broken invariants |
ripfuzz max |
Find the maximum value |
ripfuzz exec |
Execute a script once |
Invariant testing checks that properties of your protocol hold after every
sequence of state-changing calls. Ripfuzz deploys your test contract, runs
setup() once, generates sequences of handler calls, and runs every
invariant_* function after each call. A broken invariant is reported with the
shortest sequence that reproduces it.
End-to-end example:
examples/ExampleInvariantTest.sol, run
by make test.
A invariant test contract inherits InvariantTest and has three kinds of
functions:
- Setup:
setup()runs once before fuzzing. Deploy the target protocol and prepare state. - Handlers: external or public functions the fuzzer calls with random inputs to mutate state.
- Invariant functions: functions prefixed with
invariant_that take no arguments. Ripfuzz runs them after every handler call.
Declare each invariant as a handle with createInvariant, then assert with the
check family: ensure, eq, neq, gt, gte, lt, lte. A failed check
reports the invariant by id by reverting with BrokenInvariantError.
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;
import {InvariantTest} from "ripfuzz/std.sol";
contract Counter {
// [*] State ==============================================================
uint256 public count;
address public owner;
// [*] Constructor ========================================================
constructor() {
owner = msg.sender;
}
// [*] Handlers ===========================================================
function add(uint256 x) external {
count += x;
}
}
contract ExampleInvariantTest is InvariantTest {
// [*] Invariants =========================================================
Invariant internal invOwner = createInvariant("INV-01", "owner never changes");
Invariant internal invSmall = createInvariant("INV-02", "count must stay small");
// [*] State ==============================================================
Counter internal counter;
// [*] Setup ==============================================================
function setup() external {
addActor("user");
rvm.prank(getActor(0));
counter = new Counter();
}
// [*] Handlers ===========================================================
function add(uint256 actorId, uint256 x) external useActor(actorId) {
x = bound(x, 1, 10);
counter.add(x);
}
// [*] Invariant functions ================================================
function invariant_OwnerNeverChanges() external {
eq(counter.owner(), getActor(0), invOwner);
}
function invariant_CountStaysSmall() external {
lt(counter.count(), 100, invSmall);
}
}ripfuzz test examples/ExampleInvariantTest.solRipfuzz compiles the harness, deploys it, runs setup(), then fuzzes. The
example reports one broken invariant: fourteen add calls push count to
100:
fuzzing started: 2 threads, 2000 runs, max 20 calls, 2 invariants, 120s timeout
found broken invariant INV-02
fuzzing finished: 1 broken invariant, 2000 runs, 0s
shrinking started: 1 broken invariant, 2 threads, 10000 runs
broken invariant INV-02 minimized from 14 calls to 14
shrinking finished: 1 broken invariant, 2s
Each broken invariant is saved under .ripfuzz/traces with the full call
sequence and the failing check values:
Logs:
INV-02: count must stay small
a: 100
b: 100
Broken invariants are deduplicated by invariant id, so each id is reported once
with its shortest reproduction. Invariant functions that use handle-based
checks cannot be view because the checks call the ripfuzz VM.
Max mode answers "how large can this value get?". It generates sequences of
handler calls, measures a value() function after every call, and reports the
highest value found with the shortest sequence that produced it. Use it to
validate limits and caps: the reported maximum shows whether a bound is tight
or unreachable.
A max harness inherits Harness, defines handlers and one value() function,
and must not declare invariant_* functions:
Example: examples/ExampleMax.sol, run by
make max.
import {Harness} from "ripfuzz/std.sol";
contract ExampleMax is Harness {
function setup() external {
// Deploy the target and register actors.
}
function accrue(uint256 actorId, uint256 x) external useActor(actorId) {
x = bound(x, 1, 10);
accumulator.accrue(x);
}
function value() external view returns (uint256) {
// ...
}
}The value function takes no arguments, returns exactly one uint256, and
must be view or pure. Read harness state with plain Solidity: calling
cheatcodes inside value is not supported.
Run with:
ripfuzz max examples/ExampleMax.solThe campaign climbs toward the maximum and reports each improvement:
initial value 0 measured for examples/ExampleMax.sol:ExampleMax
found best sequence: value 100, 11 calls
found best sequence: value 199, 20 calls
shrinking finished: 20 calls, 10001 attempts, 3s
For the example accumulator, twenty accrue calls of at most 10 each reach
200, so the campaign converges to value 200 with a 20-call sequence: the
bound is tight. The best sequence is saved under .ripfuzz/traces for replay.
Scripts run a deterministic flow once with ripfuzz exec: replay a position
against a fork, verify a strategy, or reproduce a sequence found by test or
max. A script inherits Script, optionally defines setup(), and must
define exec():
Example: examples/ExampleScript.sol, run by
make exec.
import {Script} from "ripfuzz/std.sol";
contract ExampleScript is Script {
function exec() external {
address alice = rvm.addr(1);
rvm.label(alice, "alice");
rvm.deal(alice, 100 ether);
log("alice: ", alice);
log("done");
}
}Run with:
ripfuzz exec examples/ExampleScript.solThe log output prints to the terminal and the execution trace is saved under
.ripfuzz/traces:
script examples/ExampleScript.sol:ExampleScript deployed at 0xD1c9...cd22
alice: 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf
done
execution trace for examples/ExampleScript.sol:ExampleScript saved
Harnesses, invariant tests, and scripts access ripfuzz cheatcodes through
rvm. Full interface: src/RVM.sol.
| # | Group | Cheatcodes |
|---|---|---|
| 1 | Block | warp, roll, fee, coinbase, prevrandao, chainId |
| 2 | Account | deal, etch, setNonce, getNonce, store, load |
| 3 | Prank | prank, startPrank, stopPrank |
| 4 | Label | label, getLabel |
| 5 | Conversion | toString, parse* |
| 6 | Code / wallet | getCode, addr, sign, ffi |
| 7 | Environment | getEnv |
| 8 | Fork | fork |
rvm.deal(user, 100 ether);
rvm.prank(user);
rvm.warp(block.timestamp + 1 days);
rvm.fork(rpcUrl, 25_708_159);Notes:
prankapplies to one call.startPrankpersists across calls untilstopPrank.getEnvreverts when the key is missing. The two-argument form returns the default instead. Values from a project.envfile are supported.forkswitches chain state for all subsequent calls. Per-chain RPC URLs come from the environment or a project.envfile.- Reverting with
BrokenInvariantErrorreports a broken invariant and reverts the call. Prefer the check helpers onInvariantTestover reverting it directly.
Scripts and harnesses can call foundry-style deal helpers without any extra
import. The ether form wraps the rvm.deal cheatcode. The token form probes
the balanceOf mapping slot with rvm.store and rvm.load, then writes the
new balance to storage. Both Solidity and Vyper mapping layouts are supported.
deal(user, 100 ether);
deal(token, user, 1_000_000e18);End-to-end example: examples/ExampleDeal.sol, run
by make exec.
Notes:
- The token
dealsets the balance, it does not add to it. totalSupplyis not updated.- Only plain
mapping(address => uint256)balances are supported. Tokens that derive balances (rebasing, fee-on-transfer, packed structs) revert withDeal: balance slot not found. - Balance slots are probed once per token and cached on the calling script or
harness, so repeated deals on the same token skip the probe. Use
Deal.findBalanceSlotwith theDeal.dealslot form to share a slot across contracts.
A few cheatcodes are available directly on scripts and harnesses without the
rvm prefix: deal, fork, and label.
fork(rpcUrl, blockNumber);
label(user, "alice");MIT