# How to Win Smart Contract Audit Contests
Public audit contests are the fastest, most brutal feedback loop in security. You read a codebase the whole world is also reading, you have a few days, and you only get paid for unique, valid, high-severity findings. This guide gives you a workflow that turns scattered effort into consistent placements, from your first contest to your first leaderboard finish.
Set Expectations Before You Start
Most newcomers quit after one or two contests because they expected to win money immediately. The reward distribution is heavily skewed: a single High can outweigh ten Lows, and duplicates split the pot. Your real goal in the first ten contests is calibration: learning which findings are valid, which are noise, and how judges weigh severity. Treat early contests as paid practice.
Severity, in most contest frameworks, is a function of impact times likelihood:
- High: direct loss or locking of funds, or a clear protocol-breaking state, with a plausible path.
- Medium: loss only under specific conditions, or broken core functionality without direct theft.
- Low / Informational: code quality, no real fund or logic impact. These rarely pay.
If a bug cannot end with "and therefore funds are lost, stuck, or stolen", it is probably not a High.
Phase 1: Scope Fast (First Two Hours)
Do not start reading line one of the largest file. Build a map first.
cloc or solidity-metrics show you where the complexity is.# quick complexity map
cloc src/ --by-file
# count external and public functions, the attack surface
grep -rEn 'function .*\b(external|public)\b' src/ | wc -l
By the end of phase one you should be able to say, in two sentences, how the protocol makes and moves money. If you cannot, keep mapping.
Phase 2: Hunt High-Severity Bugs
Generality beats line-by-line reading. Walk the codebase through the lens of recurring bug classes, because the same mistakes recur across protocols.
Bug classes that score
- Accounting and rounding. Division before multiplication, fee math that rounds in the wrong direction, share-price manipulation in vaults (the classic first-depositor inflation attack on ERC-4626).
- Access control gaps. A privileged function missing a modifier, an initializer callable twice, a role check that compares the wrong address.
- Reentrancy, including read-only reentrancy. State read mid-callback before it is updated. Cross-function and cross-contract variants are where the real money is, not the textbook single-function case.
- Oracle and price manipulation. Spot price from a single AMM pool, no TWAP, no staleness check on a price feed.
- Unchecked external calls and return values. A token that returns
falseinstead of reverting, a low-levelcallwhose success flag is ignored.
- Slippage and deadline. Swaps with no minimum-out or a hardcoded zero, leaving users open to sandwiching.
Consider this realistic vault pattern. The bug is subtle and high-severity:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Vault {
mapping(address => uint256) public shares;
uint256 public totalShares;
IERC20 public immutable asset;
constructor(IERC20 _asset) {
asset = _asset;
}
function deposit(uint256 amount) external returns (uint256 minted) {
uint256 totalAssets = asset.balanceOf(address(this));
// when totalShares == 0, shares are minted 1:1
minted = totalShares == 0
? amount
: (amount * totalShares) / totalAssets;
shares[msg.sender] += minted;
totalShares += minted;
asset.transferFrom(msg.sender, address(this), amount);
}
}
The finding: an attacker deposits 1 wei, receives 1 share, then transfers a large amount of the asset directly to the vault. Now totalAssets is huge while totalShares is 1. The next honest depositor computes minted = (amount * 1) / totalAssets, which rounds down to zero shares for a real deposit, donating their funds to the attacker. The fix is to mint dead shares at construction or to track totalAssets internally rather than reading balanceOf.
How to actually find these
- For every external function, ask: what happens if I call this with zero, with max uint, twice in the same transaction, or out of the intended order?
- Follow each
transferandcall. Confirm the return value is checked and the state is updated before the external call.
- Write a property and try to break it with a fuzzer. Foundry invariant tests are excellent for accounting bugs:
function invariant_totalSharesBackedByAssets() public view {
// total redeemable value should never exceed assets held
assertLe(vault.totalShares(), asset.balanceOf(address(vault)) + 1);
}
A passing proof-of-concept test transforms a "maybe" into a High. Judges reward findings they can run.
Phase 3: Write Reports Judges Reward
A correct bug with a weak writeup gets downgraded or marked a duplicate of a better-written one. Structure every finding the same way:
Write for a tired reader. State the impact in the first line, never bury it. Avoid speculation: if you cannot show the loss, say so honestly and downgrade yourself rather than risk an "invalid" mark that hurts your accuracy score.
Time Management Across the Contest Window
A week-long contest is a marathon with a sprint at the end.
- Day 1: scope and mapping only. No findings yet.
- Days 2 to 4: deep dives along the money paths, one subsystem at a time. Log every suspicion in a scratch file, even half-formed ones.
- Day 5: build proof-of-concept tests for your strongest leads. Drop the weak ones.
- Final day: polish writeups, double-check severities, submit. Do not chase a new lead in the last hours; finish what you can prove.
Protect your attention. Two hours of focused review on one subsystem beats a full day of skimming everything.
Leveling Up From Beginner
- Read judged reports. After each contest, study the Highs that placed. You will see the same patterns again.
- Keep a personal bug-class checklist and run every contract through it. Your edge is pattern recognition, and a checklist makes it systematic.
- Specialize. Pick a niche (vaults, lending, bridges, AMMs) and go deep. Specialists out-find generalists in their domain.
- Build proof-of-concept muscle. Speed at writing Foundry tests is a direct multiplier on how many findings you can confirm before the deadline.
Practice This Hands-On
The fastest way to internalize these bug classes is to exploit them deliberately in a safe environment, then fix them. On app.solingo-blockchain.xyz you can work through audit-style exercises, write proof-of-concept exploits, and check your severity reasoning against worked solutions. Start with the accounting and access-control tracks, then move to reentrancy and oracle manipulation. Read code, break it, write the report, repeat.