Smart Contract Audit Mistakes: 27 Ways to Miss Bugs
Quick answer. The biggest smart contract audit mistakes are trusting the developer, reviewing contracts in isolation, skipping external calls, comments, tests and PoCs, and ignoring divisions, weird ERC-20 tokens and front-running. Flip each one and you find more bugs.
Most auditing advice tells you how to find vulnerabilities. Studying smart contract audit mistakes, the ways you can miss them, is just as useful.
Ways to miss bugs are easy to come up with. Flip each one, and you get a habit that uncovers more vulnerabilities.
Below are 27 ways auditors miss vulnerabilities, each with its flip: what to do instead.
Calls, Collaboration and Mindset
1. Not examining every external call for reentrancy
External calls are probably the most dangerous thing you can see in a smart contract.
An arbitrary external call can manipulate a system in many ways, and reentrancy is just one.
It is still, in our experience, one of the most common exploits we see come up in audits.
When you see an external call, a red flag should go off. Do not only check that function, or only the external and public functions in that contract.
Examine every function in the system. Also consider dependencies that may be built on top of it later.
The Curve read-only reentrancy was exactly that: an issue in protocols built on top of Curve's accounting. See reentrancy attacks explained for the full class.
2. Doing the audit in isolation
Plenty of solo auditors do excellent work, so it can be done. In our experience, though, collaboration makes an audit ten times better in two ways.
First, on a large codebase you can branch out and learn the code like a hive mind.
Then you share context from different parts of the protocol and build attack ideas together.
Second, others filter your false positives. You may be convinced of a finding until a colleague points out a validation that makes it revert.
The result is a more accurate report, where every issue is valid and worth remediating.
3. Trusting that the developer got it right
This one is core to auditing. It is easy to slip into validating the code rather than searching for ways to invalidate it.
If you assume the developers are good and thought of every edge case, you will nod along line by line. Instead, get into the attacker's mindset.
Be skeptical of every line, convinced there are critical bugs in every function. If you believe the issues are there and want to find them, you will.
4. Not reading the comments
It is common to spend an hour on a function that makes no sense, then read its docstring and have everything click.
Read the comments, the README and all the available documentation. It massively speeds up context gaining, so you can focus on the interesting parts.
5. Not writing tests
Most audit reports probably include no written tests, but we have found them a huge value add.
When a codebase has poor coverage, writing tests that fill it out and cover interesting edge cases is a great way to find bugs the developers never considered.
6. Only thinking about the audit while looking at the code
On larger audits there is a phase where you are just learning the codebase. Once it is indexed in your brain, you can work through attack vectors anywhere.
In the shower, at the gym or buying groceries, you can trace a function call and the state it changes in your head.
Interesting cases will pop into your head while you are out. Write them down immediately, because you may never unearth that thought again.
Assumptions About the System
7. Not checking whether external calls can DoS
Another danger of external calls: can the call force the transaction to fail, or spend a huge amount of gas? Either can be dangerous for the protocol.
8. Assuming contracts receiving ether can receive and withdraw it
This classic comes up more often than it should. A contract is expected to receive ether, perhaps when unwrapping wrapped ether, but has no receive or fallback function.
It is one of the easiest critical vulnerabilities to find, and being unable to receive ether can be catastrophic for most systems. Check that the ether can also be withdrawn.
Check the chain as well. Some wrapped native tokens, such as Wrapped Fantom and Wrapped AVAX, send the native token with .transfer on withdrawal.
That forwards only 2,300 gas. On those chains, validate that the contract's receive logic and any transfer it makes stay under 2,300 gas.
9. Assuming parallel data structures stay in sync
When two state variables overlap and track more or less the same thing, there is often a knob that pushes them out of sync.
These are great candidates for fuzz testing. If fuzzing is not possible, examine every edge case that could make one disagree with the other.
10. Not looking for gas optimizations
If you only want the most medium, high and critical findings, skipping gas work might actually help.
But gas optimizations show the scrutiny given to the audit. Depending on the chain, they can save the team more than the audit cost over time.
11. Not making notes or in-code audit tags
On a first pass through a codebase, you may have 17 ideas while reading a single function.
Comment each one in the code as you go, stay focused on your current thread, and come back to the tags later.
12. Not diagramming the system
People usually overlook this. Break the system down into its functions, its contracts and how they interact.
A diagram gives you a higher-level understanding and often reveals attack vectors against the protocol as a whole. It also lets collaborators see how you view the system.
13. Assuming purported invariants always hold
Codebases often carry comments like "we always expect this to be zero here" or "all fees should have been charged by now."
Test every documented invariant. If one can be negated, that almost always leads to a critical vulnerability.
14. Not asking the developers questions
There is some truth to this one. Working through code yourself, without the developers' view of how it works, gives a purer analysis of whether it actually works.
But when something is poorly documented or makes no sense, ask. Asking what an obtusely named variable represents can save hours. Otherwise, work through the code yourself or with peers.
15. Considering each contract in isolation
Reentrancy and other manipulation often happen across contracts, as part of the whole system. Reviewing contracts one by one misses an entire order of magnitude of vulnerabilities.
You would never consider only the exploits that cross a single line. Treat contracts as sets of functions, and the system as a set of contracts.
Spend real time on how the contracts interact. That is where many of the most interesting vulnerabilities live.
16. Not looking at test coverage
This is a form of trusting the developers: assuming everything is well tested. Many protocols have poor coverage.
Check it, and filling out that coverage often surfaces real vulnerabilities.
17. Not writing PoCs
Skipping PoCs is how false positives reach a report. It also means you never learn the system by interacting with it and exploiting it.
Writing a PoC usually teaches you a lot about the protocol. Sometimes a suspected critical turns out invalid, yet debugging the PoC uncovers a different critical.
Write PoCs for your high and critical findings. Our guide to writing a smart contract PoC shows how.
Incentives and Habits
18. Accepting payment regardless of the findings
This goes back to the attacker's mindset. If you are paid a fixed amount up front, you are incentivized to validate the contracts and finish fast.
Most auditors will not consciously think that way, but many are paid that way. Look at the incentives a payment structure creates.
Paying auditors per vulnerability is one way to build this in. It makes finding a critical fun, and it aligns incentives with the protocol, which wants every bug found.
That structure trains your brain to hyperfocus on finding as many vulnerabilities as possible.
19. Having a developer's mindset, not an attacker's
This rides on the last one. A developer's mindset validates the code; an attacker's mindset looks for how to invalidate it.
20. Doing multiple audits at the same time
Some auditors say parallel audits help them, since an idea from one codebase can apply to another. In our experience it is always best to focus fully on one audit at a time.
21. Not sleeping or eating well
Everyone knows it, but it cannot be stressed enough. Get optimal sleep and keep your diet as clean as possible.
A foggy brain can let a vulnerability into the wild that costs seven, eight or nine figures. Taking care of yourself is part of the job.
Code Patterns to Check
22. Not examining divisions for precision loss or divide by zero
A division symbol should set off an alarm. It can cause critical precision loss in a calculation.
It can also cause a divide by zero revert. That gives an attacker a knob: a way to make a transaction revert, which can be a building block in a larger critical exploit.
23. Not examining subtractions for underflow reverts
Like a divide by zero, an underflow revert gives an attacker a knob to twist. Stacked with other knobs, it can add up to a critical exploit.
24. Ignoring special-case ERC-20 tokens
The weird-erc20 repository lists many token behaviors that break assumptions. Among them:
- ERC-777 tokens, whose transfer callbacks allow reentrancy.
- Tokens with missing return values, including popular ones like USDT and BNB.
- Tether Gold, which declares a boolean return value but returns false even on success.
- Fee-on-transfer tokens, including burn-on-transfer tokens.
- Rebasing tokens, whose balances can change outside transfers.
- Upgradable tokens and flash-mintable tokens.
- Tokens with blocklists, and tokens an admin can pause.
- Zero value cases, such as tokens that revert on zero value transfers.
- Tokens with multiple addresses, for example a second contract that delegatecalls into the token.
There are even more cases than these, and each can yield findings. Our weird ERC-20 tokens checklist walks through them.
25. Ignoring front-running and back-running
Front-running is a huge class of vulnerabilities. The attacker knows something is about to happen and places a transaction just before it, and possibly another just after.
Centralized upgrades and protocol changes are common targets. An attacker front-runs a change to the contract's state, gains from it, then back-runs it to realize the benefit.
Take an optimizing vault. A transaction claims rewards from a third-party contract and injects them back into the vault, increasing the vault's value in one step.
An attacker front-runs it to get a bump in their vault tokens' value, then back-runs it with a withdrawal and walks away with the delta.
See front-running in smart contracts for more patterns.
26. Not considering incentives or tokenomics
This class sits above the code, but it matters just as much.
If a group has an obvious incentive to pull a 51% attack on governance, that is a high risk. It belongs in the audit report.
27. Not surrounding yourself with others in security
Staying alone is a reliable way to miss vulnerabilities.
Connect with like-minded people working on Solidity security and smart contract hacking. Join communities that grow your skills, and take part in practice audits with other auditors.
Audit Habits Checklist
- Check every external call for reentrancy across the whole system, DoS by revert and gas use.
- Run attack ideas and findings past collaborators.
- Assume the developers got it wrong, and audit like an attacker.
- Read every comment and all documentation.
- Write tests, check coverage and write PoCs for high and critical findings.
- Keep thinking about the code away from the desk, and write ideas down at once.
- Confirm ether receivers have
receiveorfallback, a withdrawal path and a 2,300 gas budget where needed. - Try to push parallel data structures out of sync.
- Report gas optimizations.
- Leave audit tags and diagram the system.
- Try to break every documented invariant.
- Ask developers about anything undocumented or unclear.
- Review contracts as one system.
- Choose incentives that reward findings, and do one audit at a time.
- Sleep and eat well.
- Check every division and subtraction for precision loss and reverts.
- Test against weird ERC-20 tokens.
- Look for front-running and back-running around every state change.
- Consider incentives and tokenomics.
- Learn alongside other security researchers.
For the full process these habits fit into, read how to audit a smart contract.
How Guardian Helps
Guardian's smart contract audit puts two independent teams on the same scope, one focused on expert manual review, and each cross-checks the other.
An invariant suite fuzzes the protocol's safety properties underneath both. To audit with us, see careers at Guardian.
Frequently Asked Questions
What is the most common way to miss a vulnerability in an audit?
Not examining every external call. External calls are probably the most dangerous thing in a smart contract, and reentrancy remains one of the most common exploits. Check every function in the system, plus future dependencies.
Should auditors ask developers questions?
Yes, when something is poorly documented or makes no sense. Otherwise, work through the code yourself first. Your analysis is purer without the developers' view of how it should work.
Why write PoCs during an audit?
PoCs keep false positives out of the report and teach you how the system really behaves. Debugging a PoC for one suspected critical sometimes uncovers a different, real critical.
Why do division and subtraction matter so much?
A division can lose precision or revert on divide by zero, and a subtraction can revert on underflow. Each gives an attacker a knob, a building block for a larger critical exploit.
Sources
Get an audit that does not miss
Guardian puts two independent teams and an invariant suite on every scope, so one reviewer's blind spot is not yours.