How to use this
This is the list the detector suite implements, written for a person rather than a parser. Read it as a pre-flight check: go through your contract once per section, and you will catch most of what an automated audit would flag before you run one.
The sections are ordered by how much money each class has cost the industry, roughly. If you only have time for two, do money movement and who is in charge — the overwhelming majority of losses come from those two, not from anything exotic.
1. Money movement
Reentrancy
The check: does any function write to state after making an external call?
Why: an external call gives the callee control. If your balance update has not happened yet, they can call back in and spend the same balance twice.
The fix: checks, then effects, then interactions. Update state before you call out, and add a reentrancy guard on anything that moves value. Full treatment in the reentrancy guide.
Unchecked low-level calls
The check: is the boolean returned by call, send or delegatecall ignored?
Why: low-level calls do not revert on failure, they return false. Ignore it and your code proceeds as though a transfer succeeded when it did not.
The fix: capture the result and require it, or use a wrapper that reverts.
Missing withdraw path
The check: can every asset the contract can receive also leave it?
Why: a payable contract with no withdraw function, or one whose withdraw sends to a wrong hardcoded address, locks funds permanently. This is not exploitable by anyone — it is just gone.
2. Who is in charge
Missing access control
The check: does every privileged function — mint, pause, withdraw, upgrade, set fees, transfer ownership — have a modifier restricting who can call it?
Why: a mint function without onlyOwner is not a vulnerability so much as a public faucet. It is embarrassingly common and it is found within minutes of deployment.
tx.origin authorization
The check: is tx.origin used in any authorization comparison?
Why: tx.origin is whoever signed the transaction, not whoever called you. Any contract the owner interacts with can call your function while tx.origin is still the owner.
The fix: msg.sender, always, for authorization.
Centralization
The check: enumerate everything the owner can do. Mint unlimited supply? Pause transfers? Blacklist an address? Change fees to 100%? Upgrade the implementation?
Why: these are not bugs — they are the contract working as written. They are also the single most useful thing in a report for anyone deciding whether to trust the contract, and the reason centralization gets its own finding class rather than being filed under "informational".
Missing zero-address validation
The check: are address parameters validated before being stored, especially in ownership transfer and recipient setters?
Why: transferring ownership to the zero address permanently removes the ability to administer the contract. Setting a fee recipient to zero burns the fees.
3. Arithmetic
Unchecked arithmetic
The check: on Solidity below 0.8, is SafeMath used everywhere? On 0.8 and above, what is inside each unchecked block?
Why: before 0.8 arithmetic wraps silently — subtract one from zero and you get the largest possible number, which is how balance checks get bypassed. From 0.8 the compiler reverts on overflow, unless someone wrapped the maths in unchecked for gas and never revisited it.
Precision and ordering
The check: does any expression divide before it multiplies?
Why: integer division truncates. (a / b) * c loses precision that (a * c) / b keeps. In fee and reward maths this is where the rounding errors that drain a pool over thousands of transactions come from.
4. External calls
Arbitrary delegatecall
The check: can any caller influence the target of a delegatecall?
Why: delegatecall runs someone else's code against your storage and your balance. A user-controlled target is total compromise, not partial.
Unprotected selfdestruct
The check: is selfdestruct present, and is it behind access control?
Why: an unguarded selfdestruct lets anyone delete the contract and sweep its balance.
Trust in return data
The check: does the contract believe values returned by contracts it does not control — prices, balances, exchange rates?
Why: an oracle that can be moved in one block can be moved by a flash loan. This is the boundary where automated analysis genuinely stops and human review starts.
5. Randomness and time
Weak randomness
The check: is any random value derived from block.timestamp, blockhash, block.difficulty / prevrandao, or block.number?
Why: all of these are visible to, or influenced by, whoever builds the block. If there is money in the outcome, the outcome is chosen, not drawn.
The fix: a commit-reveal scheme, or a verifiable randomness oracle.
Timestamp dependence
The check: does logic depend on block.timestamp at a resolution finer than roughly a minute?
Why: block producers have some latitude over timestamps. Day-scale deadlines are fine; second-scale auctions are not.
6. Availability
Unbounded loops
The check: does any loop iterate over an array that users can grow?
Why: once the array is long enough that the loop exceeds the block gas limit, the function can never be called again. Correct code that has become uncallable is still a denial of service.
The fix: pagination, or a pull-payment pattern where each user claims individually.
Failed transfer blocking a queue
The check: can one recipient's failure stop everyone else being paid?
Why: a loop that pushes payments halts on the first recipient whose receive() reverts — deliberately, if they want to grief you. Let users pull instead.
7. Hygiene
Floating pragma
The check: is the pragma pinned (0.8.24) or floating (^0.8.0)?
Why: a floating pragma means the bytecode you tested and the bytecode you deployed may have come from different compilers, with different bugs.
Shadowed variables
The check: does any local or parameter share a name with a state variable or inherited member?
Why: the shadowing declaration wins, so an assignment intended for storage silently writes to a local that is discarded at the end of the call.
Missing events
The check: does every sensitive state change emit an event?
Why: events are how monitoring, indexers and your own incident response see what happened. A silent ownership transfer is one nobody notices until it matters.
8. Gas
Not security, but real money, and kept as a separate finding class so it never inflates the severity counts:
- Array length in the loop condition — re-read from storage every iteration; cache it in a local.
- Post-increment in loops —
++iis marginally cheaper thani++. - Require strings — custom errors cost less deployed and less to revert.
- Public functions never called internally —
externalavoids copying arguments into memory. - Storage packing — order struct fields so they share slots.
What no checklist covers
Work through all twenty and you will have eliminated the bugs that have historically caused the most losses. You will not have answered whether your protocol's design is sound. No checklist catches an incentive that pays attackers, a governance process that can be captured cheaply, or a liquidation curve that fails in a fast market.
That is the boundary between what an automated audit does completely and what a human expert is for. Run the checklist first, mechanically, then spend expert time on the questions only experts can answer — the case for doing both is on the audit companies page.