How to Prepare for a Smart Contract Audit
Quick answer. To prepare for a smart contract audit, document the system, run static analysis, review every external call and comment your assumptions. Then hold a kickoff call, freeze the code, reach full coverage and hand over your tests.
Knowing how to prepare for a smart contract audit can save you tens of thousands of dollars. An audit can cost that much or more, and take weeks or even months.
Get it wrong and you may need follow-up audits or a new auditor, adding more cost and more weeks.
This guide covers the 13 most common mistakes teams make going into an audit, and how to turn each into an advantage. It then adds five design habits that make the review faster, cheaper and more secure.
Many teams keep making these mistakes even after their first audit. Expect to find a few of your own here.
Get the Code Ready
1. Write real documentation
Time is the scarcest thing in an audit. The most time-consuming part is understanding the codebase, the context gaining phase.
That phase is the main thing standing between auditors and the critical bugs in your code. Good documentation shrinks it. Document:
- The high-level goals of the system.
- The goals of each contract.
- The code path each goal follows through the contracts.
- Anything users should explicitly not be able to do.
- Every formula, written out and briefly explained.
Auditors then understand what the protocol is trying to do much faster. They also get a measuring stick for whether the code does what the documentation says.
2. Run static analysis first
Run a static analyzer such as Slither and resolve the notable issues it raises. This clears surface-level issues before the audit starts.
The cleaner the code going in, the deeper the audit can go in its allotted time.
It also means fewer code changes after the audit. Every change from a finding is more code to review, more time and more money.
More changed code also means a higher risk of an exploit reaching the live application. Aim to need as few post-audit changes as possible.
3. Examine every external call
A 2022 study of 181 real-world DeFi incidents found price oracle attacks the most frequent type (15%).
Next came permissionless interactions (10.5%): a protocol calling an attacker's contract that matches the expected interface but does harm.
Review every external call in the system. Pay special attention to calls that can go to an arbitrary address. That address can:
- Revert, to deny service.
- Return nonsensical results.
- Grief you on gas.
- Re-enter the system.
Since oracle manipulation leads the list, check your oracle integrations against the oracle's documentation. Do financial analysis on whether a manipulation attack would be profitable.
For the full set of risks, see external call attacks.
4. Use checks-effects-interactions without compromise
Checks-effects-interactions describes how to structure the body of every function:
- Checks: the
requirestatements come first. - Effects: then the state updates.
- Interactions: only after every state update, the external calls to other contracts.
Applying it wherever possible avoids many reentrancy attacks. That protects your system, and other systems that build on top of your contracts. More in reentrancy attacks explained.
5. Call out your assumptions
Leave a comment on every invariant and assumption in the code. This is a road map for auditors.
It gives them pieces of context, and things to try to break. They can fuzz or even formally verify that each invariant actually holds.
If an assumption you believed is false, you want to know as soon as possible. Notes in the code are the best way to tell auditors what to test.
Work Well With the Auditors
6. Hold a kickoff call
Context gaining is the biggest time sink in any audit. Auditors must understand the protocol before they can point out deep bugs.
A complete, recorded kickoff call with the audit team cuts straight through that phase.
Walk through how each contract interacts with the others, what the system's goals are, and how the contracts accomplish each goal.
7. Get an initial third-party review
Before the in-depth audit, get a quick high-level review from an independent auditor, or even another web3 developer.
A third perspective may show you things you want to change first. Like static analysis, it clears surface-level issues out of the way.
The audit then works like a tighter sieve, catching more granular, nuanced and interesting bugs. The resulting security is far higher.
8. Flag the areas you are unsure of
Document anywhere you are not fully confident. For example:
- A function you never fully worked through and proved to yourself.
- A question about an integration with an underlying protocol, or with an oracle.
- An open question about whether a certain financial attack is economically feasible.
Auditors can then focus there, and either answer your questions or invalidate your concerns. It also flags the weaker, less verified parts of the codebase.
9. Keep a line of communication open
Every audit team has its own style. Still, leave your door open so auditors can bring you attack ideas, questions and suggestions during the audit.
Time is probably the biggest bottleneck on how much security an audit delivers. Your context combined with the auditors' knowledge and attack creativity is a huge efficiency win.
10. Freeze the code
This mistake is far too common. The most vulnerable code tends to be the most recently written.
High profile exploits often come from an update added later, from lines added during remediation, or from code written just before the audit with no internal review.
Freeze the codebase and have your team review it internally first: at least a few days, ideally a week or longer for a large codebase.
The audit can then surface the truly interesting vulnerabilities your team would never have found.
11. Engage auditors early
For smaller projects, bringing auditors in at the end works fine. For a large, complex protocol with heavy math or many moving parts, bring them in early.
Security then shapes the design while it is still being worked out. Early code is enough for auditors to comment on.
For example, auditors might suggest keeping all funds in one contract with one choke point. That restricts the paths an attacker can use to reach the funds, and can lower future review costs.
Early engagement has other benefits:
- The audit team gets a head start on context, nearly matching your developers' understanding.
- They can jump back in, see what changed and point out bugs quickly.
- Booking is easier when an ongoing engagement is agreed, so you can plan the audit and launch sooner.
12. Reach 100% code coverage
This is another painfully common mistake. Thorough tests with full coverage are very low hanging fruit.
With coverage handled, the audit team can focus on building a fuzzing suite or even moving toward formal verification. Strive for 100% coverage.
13. Hand over the test suite
Your test suite is hidden documentation, and extremely valuable to auditors. It shows how the protocol expects to be used.
It also shows which areas are under-tested and need attention. Auditors save time by copying test setups, or starting a fuzzing harness from them.
Give auditors the full repository: tests, scripts, documentation and everything else. Keep your private keys; give them everything else.
Five Habits for a Cheaper Audit
These five habits come from years of smart contract security work. Teams that build them in arrive at the audit most of the way to launch, and the review costs less.
1. Be meticulous about external calls
External calls are involved in a great many exploits. Before handing off code, grep for every external call and step through each one. Think about:
- Reentrancy. Can the callee re-enter?
- Denial of service. If the call reverts, is that bad?
- Gas griefing. Can it burn a lot of the caller's gas, or push the transaction past the block gas limit so it can never execute?
- Return data. Are you parsing it? Could unexpected values cause harm or a denial of service?
Focus on calls to untrusted addresses, and split them into separate transactions where you can.
A common pattern almost always leads to gas griefing or DoS. One user's transaction sends ether to an arbitrary fee recipient the user does not control.
The recipient can revert, or spend a ton of gas and make the user pay for it.
Instead, record each recipient's accrued fees in a mapping. Let the recipient pull them from the contract in a separate transaction.
2. Avoid inheritance where you can
From a security researcher's view, inheritance adds a whole extra dimension of attack surface.
A common case: a contract inherits from OpenZeppelin's ERC-4626 and overrides some functions for its own features. It forgets to override another one, such as mint, redeem or withdraw.
Reviewers must track which functions are overridden, which storage variables live in the parent, and what is still callable that should not be.
With many parents, it is hard even to know where a super call goes. You end up reasoning about C3 linearization, the rule Solidity uses to pick which parent runs.
Inheritance makes the system less readable and creates blind spots in other files. In our opinion, avoid it if you can.
3. Write less code
This is the closest thing to a magic pill. If a relatively simple protocol is nearing 2,000 lines of Solidity, go back to the drawing board.
Most simple systems can be implemented in around 1,000 to 1,500 lines. A 3,000-line staking app that pays rewards in your token needs another try.
Write succinct, readable code and architect it well. You spend far less on security reviews and lower the chance of a hack.
4. Limit external functions to the necessary ones
Be explicit about what users need to do through the front end. Those actions are your external functions, and nothing more.
Every state-changing external function adds quadratically to the attack surface. Each must be considered alongside every other state transition function, in every combination.
Ideally, an application has around three external functions that change state. Remove superfluous ones and bottleneck actions down to a core set.
This removes attack paths and many of the tools auditors use to make interesting vulnerabilities happen.
5. Move extra logic offchain
If logic is not needed for state that must live on the blockchain, or for mission-critical validation, move it offchain.
Take a liquidity protocol where each position holds an amount of liquidity. Users cannot easily work with raw liquidity amounts.
So the team lets users pass a percentage, like 100% or 75%, and the contract converts it into a liquidity amount before removing it.
That conversion is superfluous onchain. It adds lines to the hardest code to secure, and every test and invariant suite must account for it.
Leave ease-of-use logic to the UI. Or put it in separate periphery contracts, so the core stays stripped down and can be highly secured.
Audit Preparation Checklist
- Document goals, code paths, forbidden actions and formulas.
- Run static analysis and resolve notable issues.
- Review every external call, especially to arbitrary addresses, and your oracle integrations.
- Apply checks-effects-interactions everywhere.
- Comment every invariant and assumption.
- Book a recorded kickoff call.
- Get a quick third-party review.
- Flag the code you are unsure of.
- Stay reachable during the audit.
- Freeze the code and review it internally first.
- Bring auditors in early on large, complex systems.
- Reach 100% code coverage and hand over the full repository.
- Use pull payments instead of pushing ether to untrusted addresses.
- Minimize inheritance, code size and state-changing external functions.
- Move ease-of-use logic to the UI or periphery contracts.
To see the review from the auditor's side, read how to audit a smart contract.
How Guardian Helps
Guardian's audits start with a design review that resolves architectural risk before code hardens. An invariant suite and senior manual review follow.
Vanguard adds two independent audit teams and six months of review for changes after production.
Frequently Asked Questions
What documentation should you give auditors?
Document the system's high-level goals, each contract's goals, the code path for each goal, anything users must not be able to do, and every formula. Hand over the full repository, including tests and scripts.
Why is a code freeze important before an audit?
The most vulnerable code tends to be the most recently written. Freeze the codebase and review it internally for at least a few days, ideally a week or more.
When should you engage auditors?
Small projects can bring auditors in at the end. Large, complex protocols benefit from early engagement: security shapes the design, auditors gain context sooner, and booking and launch planning get easier.
How can you make a smart contract audit cheaper?
Write less code, avoid inheritance, limit state-changing external functions, and move ease-of-use logic offchain or into periphery contracts. Run static analysis first, so fewer post-audit fixes need review.
Sources
Arrive at your audit ready to launch
Guardian reviews your design early and audits the code with two teams and an invariant suite.