How We Found Real Vulnerabilities with Fuzzing
By Antonio · Security researcherHow We Found Real Vulnerabilities with Fuzzing
Over the past two years, Recon has found critical vulnerabilities in some of DeFi's most important protocols using invariant testing and fuzzing. In this post, we'll walk through real findings and the properties that caught them.
Finding #1: Insolvency in a Major Lending Protocol
Severity: Critical Impact: Complete protocol insolvency
The protocol had a vault system where users deposit assets and receive shares. The core invariant should be simple:
totalAssets >= totalShares * pricePerShare
Our fuzzer found a sequence where:
- User A deposits a large amount
- Rewards are distributed
- User B deposits a small amount
- User A withdraws
- The protocol now owes more than it has
The root cause was a rounding direction error in the share calculation during step 3. When rewards had just been distributed, the price per share increased, but new deposits were rounding in the wrong direction, effectively giving new depositors slightly more shares than they deserved.
The property that caught it:
function invariant_solvency() public returns (bool) {
return vault.totalAssets() >= vault.convertToAssets(vault.totalSupply());
}
Finding #2: Rounding-Based Cap Bypass
Severity: Medium Impact: Bypass of protocol-enforced limits
A protocol had a deposit cap to limit total exposure. The cap was checked against the total deposited amount. However, the conversion between shares and assets introduced small rounding errors.
Our fuzzer discovered that by making many small deposits, a user could accumulate slightly more actual exposure than the cap allowed. While each individual rounding error was tiny (1 wei), across thousands of operations, the cumulative bypass was significant.
The property that caught it:
function invariant_capEnforced() public returns (bool) {
return vault.totalAssets() <= vault.depositCap();
}
Finding #3: Permanent DoS Through Overflow
Severity: High Impact: Permanent denial of service
In a staking contract, a cumulative reward tracker used uint128 for storage. Our fuzzer found that after a specific sequence of stake/unstake operations with large values, the reward tracker could overflow, permanently bricking the contract.
The interesting aspect: the individual operations all used reasonable values. It was the specific sequence and timing that triggered the overflow. This is exactly the kind of bug that unit tests miss.
The property that caught it:
function invariant_canAlwaysUnstake() public returns (bool) {
// Try unstaking for each actor - should never revert
for (uint i = 0; i < actors.length; i++) {
uint balance = staking.balanceOf(actors[i]);
if (balance > 0) {
try staking.unstake(balance) {} catch {
return false;
}
}
}
return true;
}
Writing Effective Properties
The properties that catch real bugs tend to be:
- Simple and fundamental: "The protocol is solvent" catches more bugs than complex properties about specific edge cases.
- Focused on accounting: Most DeFi bugs are accounting bugs. Track that assets = liabilities.
- Tested against withdrawal: The ultimate test is whether all users can withdraw their fair share.
- State-aware: Check properties that span multiple transactions, not just single operations.
Tools We Use
At Recon, we use our Chimera framework to write tests compatible with Echidna, Medusa, and Foundry simultaneously. This lets us leverage each fuzzer's strengths:
- Echidna for thorough corpus-based exploration
- Medusa for fast parallel fuzzing
- Foundry for quick iteration during development
We run these in the cloud using Recon Pro for maximum computational power, often running campaigns of millions of test sequences.
Conclusion
Fuzzing is not theoretical. It finds real bugs that manual review and unit testing miss. If you're building a DeFi protocol, invariant testing should be a core part of your security process, not an afterthought.
Ready to find bugs before attackers do? Request an audit with Recon.