Invariant Testing Explained in Detail
Invariant testing checks properties that should always remain true. The test runner generates sequences of calls, not only single inputs, then checks whether the property still holds.
Common smart contract invariants include solvency, conservation of assets, total supply consistency, access control boundaries, and limits on protocol state.
Smart contract example
A lending vault might claim that it can always cover user withdrawals. An invariant can express that directly:
function invariant_vaultSolvent() public view {
assertGe(vault.totalAssets(), vault.totalLiabilities());
}
A stronger test runs deposits, withdrawals, borrows, repayments, liquidations, and oracle updates in many orders, closer to how users and markets interact with the system.
Invariant Testing in Auditing
Many protocol bugs only appear after a sequence of valid actions. A single deposit test may pass. A deposit, partial withdrawal, price update, liquidation, and second withdrawal may break accounting.
Invariant testing fits DeFi well because protocol safety often depends on relationships between balances, shares, debt, collateral, and prices. It can expose issues behind flash loan attacks, oracle manipulation, and accounting bugs.
Red flags in code
-
No clear statement of what must always be true.
-
Accounting spread across contracts without system-level tests.
-
Invariants that only check one contract in isolation.
-
Handlers that never call risky functions.
-
Ghost variables that are updated differently from the real protocol.
-
Invariants that pass because inputs are over-constrained.
How to test or review it
-
Write the invariant in plain English first.
-
Use handlers to model realistic actors and call sequences.
-
Track off-chain expectations with ghost variables when the contract does not expose enough state.
-
Check call coverage. If the fuzzer never reaches liquidations, admin transitions, oracle updates, or withdrawal paths, the result is weak.
-
Reduce failing traces to the smallest sequence that explains the bug.
-
Use fuzz testing first when the risk is input-specific rather than sequence-specific.