Write Foundry invariant tests with handler contracts, ghost variables, and actor rotation. Use when a vault, AMM, or any value-moving Solidity contract only has unit tests.
Scanned 9/2/2026
Install to Claude Code
npx -y skills add nuroctane/nur-cli --skill writing-foundry-invariant-handlers --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Writing Foundry Invariant Handlers?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/nuroctane-writing-foundry-invariant-handlers)More formats (shields.io, HTML) on the badges page.
---
name: writing-foundry-invariant-handlers
description: "Write Foundry invariant tests with handler contracts, ghost variables, and actor rotation. Use when a vault, AMM, or any value-moving Solidity contract only has unit tests."
---
# Writing Foundry invariant handlers
Unit tests show one path. Handler-based `invariant_*` tests show whether conservation laws survive **random sequences**. This is the researcher skill that actually finds economic bugs.
Authorized code only. Fail the audit if value-moving contracts have no invariants.
Full snippet lives in `skills/security/SCA/auditing-foundry-smart-contract-security/references/api-reference.md`.
## When this is the playbook
- User says invariant, handler, ghost variable, stateful fuzz, "forge invariant".
- Coverage of deposit/withdraw/borrow/liquidate is only happy-path unit tests.
- Static analysis is clean but accounting could still desync.
## Steps
### 1. Write the laws first (English, then `assert`)
Examples:
- Conservation: `address(vault).balance == ghostDeposits - ghostWithdraws` (adjust for fees).
- Solvency: `totalAssets() >= totalShares implied assets`.
- Access: only role X can `setFeed` / `upgradeTo`.
- No unexpected mint: `totalSupply` only changes in `deposit`/`withdraw`.
If you cannot state the law, you cannot test it. Stop and read the protocol.
### 2. Handler, not raw `targetContract(vault)`
The fuzzer calling `vault` directly will spam reverts and miss realistic sequences. Wrap it:
- `bound` every amount into a live range.
- Rotate actors with `useActor(seed)` + `makeAddr`.
- Track **ghosts** for sums the contract does not expose.
- Early-return (don't revert) on empty-balance withdraws so `fail_on_revert` can later be `true`.
- `targetContract(address(handler))` in `setUp`.
See the VaultHandler pattern in the Foundry skill api-reference.
### 3. Cover the adversarial extras
Add handler actions for the ways real attackers compose calls:
- donate tokens / ETH directly to the vault (ERC-4626 inflation).
- `deal` weird balances, then deposit 1 wei.
- set a stale oracle if the protocol allows it in-test.
- call view functions that other protocols might use mid-reentrancy (read-only reentrancy).
Do **not** add a "drain" action that broadcasts. Stay in `forge test`.
### 4. Tune and read failures
```toml
[invariant]
runs = 256
depth = 128
fail_on_revert = false # true once the handler constrains inputs
[fuzz]
runs = 10000
```
When an invariant breaks: shrink the call sequence (`-vvvv`), turn it into a **unit test** that replays those calls, then fix or file.
### 5. Pair with revert tests
Every `onlyOwner` / `require` gets `test_RevertWhen_*` + `vm.prank(attacker)` + `vm.expectRevert`. Invariants do not replace access-control unit tests.
## Expected output
- `test/handlers/*Handler.sol` and `test/*Invariant.t.sol`
- `forge test --match-test invariant_ -vvv` passing (or a minimized failing sequence for the report)
- A one-line statement of each conservation law
Rounding / first-depositor / Balancer-class paid bugs are handler work. After a hit, map `$` via `hunting-x-linked-bounties` so the report cites a real analog, not a vibe.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!