How to Audit a Smart Contract: A Complete System
Quick answer. To audit a smart contract, prepare first, then list the system's jobs to be done and walk each through the code. Tag ideas with @audit, list attack knobs, break invariants, prove findings with PoCs and review the fixes.
Most people learning how to audit a smart contract start the same way.
You open the repository, the README is the default Hardhat template, and there are a dozen contracts with no obvious starting point.
So you meander through the code and hope something jumps out.
That will not produce a quality audit.
This guide lays out a repeatable system instead: mindset, preparation, context gaining, audit tags, attack knobs, invariants, proofs of concept and the wrap-up.
Start With the Right Mindset
The most important part of an audit begins before you open the codebase. It is the belief you bring into it.
Believe you can find the bugs
You can find valid, high impact vulnerabilities in whatever codebase you are reviewing.
Your first audit is the hardest mainly because you have not yet built the confidence to say so.
When thoughts like "I don't have enough experience" or "I'm not smart enough to comment on this code" creep in, ignore them and push forward.
After a few audits, opening a new codebase gets easier because you trust your own track record.
Believe the bugs exist
The second belief is that the developers made mistakes. Virtually no codebase goes through a real audit with zero findings.
To prove it to yourself, read past Code4rena contest reports. Count how many findings each one has, and how many different wardens reported unique issues.
Some recent audit reports list 50, 70 or even 100 unique findings for a single codebase.
If you believe the findings are there and believe you can uncover them, you will find them.
Prepare Before the Code Arrives
Preparation is mostly logistics. Get these right before you have the code.
1. Budget enough time
You need time to do due diligence on every part of the codebase. A figure of 200 lines of Solidity per hour is often quoted.
In our view that pace may suit a contest, where missing findings is acceptable.
In an audit, plan for something closer to 100 lines per hour, or even 50 for code that is central to how the protocol works.
2. Be well rested
It sounds obvious. You cannot evaluate code against every attack vector without the mental capacity that sleep and a good diet give you.
3. Research similar protocols
If you are auditing a DEX, read previous DEX audits first. List the common attack vectors, the common exploits, the incentives and what could go wrong.
Doing this up front gives you context going in.
It saves your hours in the code for evaluating real code paths and inventing attack vectors specific to this protocol.
4. Decide who you work with
Settle how many people are on the audit and how you will communicate before you start.
In our experience, working with at least one other auditor gives a tenfold increase in the quality of findings.
When you combine mental models of the codebase, you come up with far more creative attack vectors than you would have time to reach alone.
5. Set up your tooling
- A code editor you can navigate fast. You should be able to command-click into any function or contract and keep your train of thought, instead of hunting for the file and searching it.
- A shared findings document for everyone on the audit.
- A shared test repository that everyone makes pull requests to, so the team builds a bank of tests and proofs of concept as the audit goes.
Begin With a High-Level Pass
Your number one goal at the start is to gain context on the system as fast as possible.
Once you understand the whole system, possible bugs start to jump out as you read.
With context, you can return to any line or question and see straight away whether it is a valid issue and how severe it could be.
Read what documentation exists
Read the README first if it is useful. If it is only the generated Hardhat README, scan the codebase for large block comments that give context or gotchas.
Skim them; you do not need to read every one line by line yet. Many are specific to the code in their file, but some carry global context.
List the jobs to be done
Use the research you already did to enumerate the jobs to be done: everything a user is supposed to be able to do once the system is deployed.
Write them on a notepad or in the contracts themselves.
For a simple DEX, users can:
- Swap one token for another directly.
- Swap along a path of tokens when two tokens have no direct liquidity.
- Provide liquidity and collect fees on it.
- Withdraw liquidity.
There is probably a fee collection mechanism behind all of this too.
Walk each job through the code
Go down the list and find where each interaction begins. For an exchange that is often a router contract, so start with its external and public functions.
Then click through the path a user's transaction takes. Expand each function until you understand its high-level logic, and check the job off when you are done.
On this pass, do not read line by line or try to memorize the code. Ask three things:
- What is each function's purpose?
- How is storage being changed, at a high level, to make the job happen?
- How do the contracts interact with each other to achieve it?
This breaks the code into small, digestible pieces and gives you a road map of the logic you need to understand.
It also follows the protocol the way the developers designed it: main functionality first, not some niche utility function.
Run the test suite and coverage
Run the tests early. Are they all passing, or are some failing? The state of the test suite tells you a lot about overall code quality.
A coverage checker shows the blind spots in the tests. Note them down, give them a closer look later, and consider filling those tests in yourself.
Generate a call graph
For a complex system, a call graph from a tool such as Surya breaks the contracts down into a picture of how they call each other.
It helps you build a mental model quickly.
Leave Audit Tags, Not Rabbit Holes
Time is limited. A contest does not last forever, and nobody can spend a year on one contract.
You will be many times more efficient at spotting vulnerabilities once you have context, so focus fully on gaining it first.
That means leaving rabbit holes and first ideas by the wayside, without losing them. This is what audit tags are for.
You might notice a possible reentrancy, an edge case, a small gas optimization or simply wonder "why is this like this?"
Leave a comment in the code marked @audit, such as // @audit what if this position has zero liquidity?, then carry on with the thread you were following.
Later, grep the codebase for @audit to find every tag and address it. The key is that you do not explore the edge case immediately.
You note it and come back, so you are not pulled in seven directions at once.
Dig Into the Meat of the Audit
After the high-level pass, you have general context on the jobs to be done and audit tags scattered through the code.
Now go back and pull on those threads.
Read each tagged area at a finer grain, line by line, with its edge case or question in mind.
In our experience many of the most common critical vulnerabilities come down to plain typos in the code, and only that level of attention catches them.
Stick to the thread you are on. If you spot another possible vector along the way, such as a gas griefing attack, tag it and put it in the backlog.
Finish the current tag, then investigate the new ones.
Resolving tags creates new tags, and you resolve those in turn. Each round leaves you with far more context than you had before.
Build a List of Attack Knobs
Eventually you run out of leads: no threads left to pull, no tags left in the code.
The next step is to list the knobs an attacker has over the protocol.
A knob is any interesting behavior in the code that could be used to cause a bug or vulnerability.
What storage can an attacker manipulate? What can they make happen?
Generic knobs
A few apply to every Solidity codebase:
- External calls to an arbitrary address.
- Divisions that can round down to zero or lose precision.
- Divisions that can revert with a divide by zero.
- Subtractions that can underflow and revert.
Protocol-specific knobs
The best knobs are usually unique to the codebase and depend on its context.
In one review involving the Uniswap V3 Staker contract, anybody could unstake for another user once the incentive key had ended.
That knob led to a high impact finding: a malicious actor could unstake on behalf of the LP staker contract and steal rewards from other stakers.
Another kind of knob: an attacker who can stop someone from transferring an NFT, perhaps by setting up a specific callback contract.
Tag it in the code and add it to your running list.
Use the list
Once you have full context, go back through each function at a granular level and evaluate the effect of every knob on it.
Can any knob be twisted into a critical vulnerability?
The knobs are your building blocks.
Often a high impact finding only appears when you combine two or more of them into an attack that applies to this protocol alone.
Break Invariants on Purpose
During the high-level pass you will have met invariants: properties that ought to hold for every transaction with the protocol. For example:
- If I deposit into the vault, I should only ever be able to withdraw exactly what I put in.
- Two back-to-back transactions should never leave me with more out than I put in.
The usual instinct is to validate invariants, by formal verification or by walking through and deciding they hold.
Do the opposite. Use divergent thinking to list every way each invariant could be broken.
Take the vault invariant: deposit tokens, withdraw in the same block, and get a different amount out. Ways to break it include:
- Fee-on-transfer tokens.
- An attacker sandwiching another user's deposit, front-running and back-running it to end up with a different amount.
- An attacker sandwiching the vault's harvest mechanism.
Now combine the list of ways to break each invariant with your list of knobs.
Can one knob, or several together, make one of those breaks happen? Would it actually affect the protocol?
Repeat the Cycle and Collaborate
The meat of an audit is a continuous loop.
Learn more about the code, come up with new and bespoke attack vectors, then re-examine the code with them in mind.
Tag what you notice, stay on your current thread and return to the tags later.
Repeat until you have explored every code path and every edge case you can think of.
Collaborate with the other auditors throughout.
It dramatically increases how fast you cover ground, and speed matters most while you are still gaining context.
Get the context downloaded into your head as fast as possible.
Once you have a strong mental model, you can step away from the computer and keep testing attack vectors subconsciously.
You might read something on Twitter and realize it applies to the contract. Bounce those ideas off your fellow auditors to cover far more attack types together.
Write Tests and PoCs as You Go
Writing tests and proofs of concept during the audit does two things:
- It proves out attack vectors. When you are not sure an idea is valid, a PoC settles it.
- It checks your mental model. A test shows that the contracts work the way you think, or shows exactly where your misconceptions are.
Write them while you are in the meat of the audit, not at the end. Our guide to writing a smart contract PoC covers the craft in detail.
Wrap Up the Audit
Tie up loose ends
Go through the code and resolve every remaining audit tag, including the ones you put off until the last minute. Get every thought down on paper.
Validate every finding
Work through each finding in the findings document until you understand exactly how it works, ideally with a PoC for each. That includes findings other auditors made.
Understanding every finding deepens your understanding of the protocol. It also confirms each one is valid at the severity it is listed at.
When every auditor understands and agrees with each finding, the report has no vapor findings. See smart contract audit severity levels for how to rank them.
Write the report
Each finding needs:
- A clear, concise description with the exact relevant line numbers.
- A pointer to a PoC where possible, well commented, so any reader can follow it and run the test to see the vulnerability for themselves.
- A recommended fix. It gives the team a path forward, and it often makes the problem clearer. Sometimes the solution explains the crux of a vulnerability better than the description does.
Stay through the fixes
Delivering the report does not end the audit. Teams often change the code after an audit and are exploited through those changes.
Verify that every finding in the report was actually resolved, and that the new additions did not open new vulnerabilities.
Ideally, follow the team all the way to the deployed version and confirm it matches what the audit prescribed.
Smart Contract Audit Checklist
- Go in believing you can find bugs, and that bugs are there to find.
- Budget roughly 100 lines per hour, and 50 for central code.
- Rest well and research similar protocols and their past audits.
- Line up at least one collaborator, a shared findings doc and a shared test repo.
- Read the README and block comments, then list the jobs to be done.
- Walk each job through the code at a high level; run tests, coverage and a call graph.
- Tag every idea with
@auditinstead of chasing it. - Pull on each tag line by line, staying on one thread at a time.
- Build a running list of attack knobs, generic and protocol-specific.
- List invariants, list ways to break them, and match those to your knobs.
- Write tests and PoCs during the audit.
- Resolve every tag, validate every finding, and write clear findings with fixes.
- Review the fixes and follow the code to deployment.
For the opposite view, the habits that make auditors miss bugs, read smart contract audit mistakes.
How Guardian Helps
Guardian's smart contract audit runs in three passes: a design review, an invariant suite built around the protocol's safety properties, and a manual audit by senior researchers.
Vanguard, Guardian's flagship audit, puts two independent teams on the same codebase.
If you want to audit this way full time, see careers at Guardian.
Frequently Asked Questions
How many lines of code can you audit per hour?
A figure of 200 lines of Solidity per hour is often quoted, but that pace suits contests. For a thorough audit, plan for closer to 100 lines per hour, and 50 for code central to the protocol.
What is an audit tag?
An audit tag is a comment marked @audit that you leave in the code when an idea comes up. You keep gaining context instead of chasing it, then grep for the tags later.
What are attack knobs in a smart contract audit?
Attack knobs are behaviors an attacker controls that could cause a bug, such as arbitrary external calls, risky divisions and subtractions, or protocol-specific powers. Combining knobs produces bespoke attacks.
Is the audit over when the report is delivered?
No. Teams often change code after an audit and get exploited through those changes. Verify every fix, check new code, and ideally follow the team to the deployed version.
Sources
Put this system to work on your protocol
Guardian's researchers bring this process, two teams and an invariant suite to every smart contract audit.