Unit tests verify that code works correctly with expected inputs. Security testing verifies that code works correctly with adversarial inputs — and that's a fundamentally different challenge. Fuzz testing, invariant testing, and formal verification find bugs that unit tests never will. Here's how to add each to your workflow.
The Testing Pyramid for Smart Contracts
- Unit Tests — Verify individual functions with known inputs/outputs
- Integration Tests — Verify contract interactions work correctly
- Fuzz Tests — Throw random inputs at functions to find edge cases
- Invariant Tests — Verify properties that should ALWAYS be true
- Formal Verification — Mathematically prove correctness properties
Fuzz Testing with Foundry
Foundry's fuzzer generates random inputs and runs your test function thousands of times, looking for inputs that cause failure.
// Foundry fuzz test — runs with random inputs
function testFuzz_depositAndWithdraw(uint256 amount) public {
amount = bound(amount, 1, 1000 ether); // Constrain to valid range
deal(address(token), alice, amount);
vm.startPrank(alice);
token.approve(address(vault), amount);
vault.deposit(amount);
uint256 shares = vault.balanceOf(alice);
vault.withdraw(shares);
// User should get back at least their deposit (minus fees)
assertGe(token.balanceOf(alice), amount * 99 / 100);
vm.stopPrank();
}
Invariant Testing
Invariant tests define properties that must always hold, then let the fuzzer call random sequences of functions trying to break them.
// This property must ALWAYS be true:
function invariant_vaultSolvency() public {
assertGe(
token.balanceOf(address(vault)),
vault.totalAssets(),
"Vault is insolvent!"
);
}
function invariant_totalSharesMatchIndividual() public {
uint256 sum;
for (uint i = 0; i < actors.length; i++) {
sum += vault.balanceOf(actors[i]);
}
assertEq(vault.totalSupply(), sum);
}
What Each Level Catches
| Testing Level | Catches | Misses |
|---|---|---|
| Unit tests | Basic logic errors, simple regressions | Edge cases, state-dependent bugs |
| Fuzz tests | Boundary errors, overflow, input validation | Multi-step exploits, economic attacks |
| Invariant tests | State corruption, accounting bugs, solvency | External integration bugs |
| Formal verification | Mathematical proof of correctness | Requires specification (what IS correct?) |
Minimum Security Testing Checklist
- ✅ Unit tests for all functions (happy path + error cases)
- ✅ Fuzz tests for any function that takes numeric inputs
- ✅ Invariant tests for core accounting (solvency, supply matching)
- ✅ Test with extreme values: 0, 1, type(uint256).max
- ✅ Test as both EOA and contract callers
- ✅ Test reentrancy scenarios with malicious receiver contracts
Testing catches bugs before they reach production. Auditing catches the ones testing missed. Get both with Vultbase.