An audit buys you a fixed number of hours with experienced researchers. How many of those hours go into hunting the bugs that matter depends a lot on you.
Every hour spent getting your project to compile, guessing what a function is supposed to do or re-reading code that changed overnight is an hour nobody spent on your liquidation logic. Same price, less audit.
Here's what we ask teams to sort out before kickoff. There's a README template at the end you can copy straight into your repo.
| # | Step | When | If you skip it |
|---|---|---|---|
| 1 | Freeze the code | Right before kickoff | Report covers code you won't deploy |
| 2 | Be precise about scope | When asking for a quote | Integrations and deploy scripts go unreviewed |
| 3 | One command to build and test | Weeks before | Day one is spent on tech support |
| 4 | Write down how it should work | Weeks before | Auditors reverse-engineer your intent |
| 5 | Test what would hurt | During development | Slower proofs of concept, fewer bugs found |
| 6 | Run the free tools | Before testnet | You pay seniors to write up scanner output |
| 7 | Deploy on testnet | Before freezing | Config bugs show up on mainnet |
| 8 | Developer on call | During the audit | Edge cases get one look instead of five |
| 9 | Time for fixes and fix review | After the report | Unreviewed fixes go to mainnet |
1. Freeze the code
You'll do this last, right before kickoff, but it matters more than anything else on the list, so it goes first.
Pick a commit hash, send it to us, and stop changing anything in scope until the audit is done. If something really has to change, tell us that day, not on the final report call.
Auditing a codebase that's still moving is like proofreading a book while the author rewrites chapter three. And the report only covers the commit we reviewed. Anything you change after that is unaudited code, whatever the badge on your website says.
Finish the features first, too. “We'll add the withdrawal queue after the audit” means you're also buying a second audit.
2. Be precise about scope
Tell us exactly what's in and what's out:
- the contracts in scope, with line counts if you have them (it helps us quote accurately)
- what's out, like mocks, test helpers, third-party dependencies and anything already audited
- every external integration: oracles, DEXs, bridges, lending markets, odd tokens. A lot of the worst exploits happen where one protocol meets another.
- every chain you'll deploy on. Chains differ in ways that matter.
block.numberon Arbitrum, for example, doesn't mean what it means on Ethereum mainnet.
Think twice before leaving deploy and upgrade scripts out. Nomad's $190M bridge hack started with an upgrade that initialized a trusted root to zero.
3. One command to build, one to test
A fresh clone should compile and run the full test suite without manual steps:
forge test, cargo test, go test ./...,
daml test, whatever your stack uses.
A few things that often go missing:
- pinned dependency versions, so what we build is exactly what you'll deploy
- pinned compiler version and optimizer settings
- for fork tests, the RPC variables listed in an
.env.exampleand a pinned fork block number, so the tests don't change every time mainnet does
If “how do I run the tests?” is the first message in the audit channel, you're paying senior-researcher rates for tech support.
4. Write down how it's supposed to work
Auditors find bugs by comparing what the code does with what it's supposed to do. If the second part only lives in your team's heads, we have to reverse-engineer it. We're good at reading code. We're less good at reading minds.
It doesn't need to be long. Cover:
- Overview: what the protocol does, who uses it and how money moves through it
- Roles: who can call what (admins, keepers, users, other contracts) and how much you trust each of them
- Invariants: what must always be true
- Assumptions: token decimals, whether fee-on-transfer or rebasing tokens are supported, oracle staleness limits, how upgrades work
- Known issues: anything you already know about and accept, so we can spend the time elsewhere
Invariants are the part teams skip most often, and they're the most useful part for us. Good ones are short, specific to your protocol and testable:
| Invariant | What breaking it looks like |
|---|---|
| The vault's shares are never worth more than the assets it actually holds. | Inflation and donation attacks, insolvency |
| A healthy position can't be liquidated. | Users lose collateral they shouldn't |
| The contract always holds enough reward tokens to pay what stakers have earned. | Last stakers out can't claim |
| Nobody can set fees above 10%, not even the owner. | A compromised key drains users through fees |
If you can't write down what must always be true about your protocol, don't worry. An attacker will be happy to show you what isn't.
While you're at it, comments that explain why a line exists save us hours. Comments
that say // increment i don't.
5. Test what would hurt
Coverage numbers matter less than what's actually covered. Focus on:
- unit tests for core functions, including edge cases: zero amounts, max values, the first depositor, the last withdrawer
- integration tests for full user flows: deposit, borrow, liquidate, withdraw
- fuzz and invariant tests for the math, if you can. Turn the invariants from step 4 into invariant tests and you've done a good chunk of our job for us.
Good tests also make our proofs of concept quicker to write, which leaves more time for finding the next bug.
6. Run the free tools first
Run the free static analyzers for your stack, plus whichever AI scanner you like. Read the output and fix what's real.
| Stack | Free tools to run first |
|---|---|
| Solidity | Slither, Aderyn |
| Rust | cargo audit, Clippy |
| Go | gosec, govulncheck |
| Any | An AI scanner of your choice |
Every issue a free tool could have caught is an issue you're paying senior researchers to write up. Let the tools catch the obvious stuff, so the researchers can spend their time on business logic, economics and integrations, which is where tools struggle. (We wrote more about what AI tools do and don't catch.)
7. Deploy on testnet
Testnet is where you find out your deploy script passes 100, meaning 1%, to a
contract that reads it as 100%. Much better to learn that there than from Crypto Twitter.
It's also where you get real usage to fine-tune parameters before you freeze the code.
8. Have a developer on call
Questions come up in every audit. Is this intentional? What happens if the keeper goes offline for a day? Can this value ever be zero?
Name one technical person who knows the code, set up a shared channel (Telegram, Slack, Discord, whatever you use) and answer fast. A question answered in ten minutes keeps the researcher in the flow. A question answered two days later means they've moved on, and that weird edge case got one look instead of five.
9. Leave time for fixes and fix review
When the report arrives, you still need to:
- YouFix the findings, criticals and highs first.
- UsReview the fixes, to confirm they work and didn't break anything else.
- YouDeploy exactly the commit that was reviewed.
Keep the fixes easy to review: one commit per fix, the finding ID in the commit message, and no refactors mixed in. A 3,000-line “audit fixes” diff is basically a new audit.
Planning mainnet for the morning after the report lands is a bold strategy. It rarely pays off.
A README template you can steal
Drop this in your repo, fill it in, and most of the prep above is done:
# Audit README
## Scope
- Commit: <hash>
- In scope: src/Vault.sol, src/Strategy.sol, src/libraries/FeeMath.sol,
script/Deploy.s.sol (~1,300 nSLOC)
- Out of scope: src/mocks/, test/, lib/ (third-party dependencies)
- Chains: Ethereum mainnet, Arbitrum
## Overview
What the protocol does, who uses it and how funds move through it (2-3 paragraphs).
## Roles
| Role | Can do | Trust level |
|--------|--------------------------|-------------------------------------|
| Owner | set fees, pause, upgrade | Fully trusted (3/5 multisig) |
| Keeper | call harvest() | Semi-trusted, can't move user funds |
| User | deposit, withdraw | Untrusted |
## Upgrades
UUPS proxy. Upgrades go through the owner multisig and a 48-hour timelock.
## Invariants
- Vault shares are never worth more than the assets the vault holds
- Nobody can set fees above 10%, not even the owner
## External integrations and assumptions
- Chainlink ETH/USD feed; the staleness limit matches the feed's heartbeat on each chain
- On Arbitrum, the Chainlink L2 sequencer uptime feed is checked before prices are used
- Standard ERC20s with 6-18 decimals only; no fee-on-transfer or rebasing tokens
## Known issues
- Owner can set fees anywhere up to 10%. Accepted: owner is a 3/5 multisig.
## Previous audits
- <auditor, date, commit, link to report>
## Build and test
`forge install && forge test`
## Contact
- <name, Telegram or Slack handle>
Quick checklist
- Commit hash frozen and shared
- Clear in-scope and out-of-scope list, including integrations, chains and deploy scripts
- Builds and passes tests from a fresh clone, no manual steps
- Overview, roles, invariants and assumptions written down
- Unit, integration and (ideally) fuzz and invariant tests for the important paths
- Free static analyzers and AI scanners run, real issues fixed
- Deployed and used on testnet
- Technical contact and shared channel ready
- Time planned for fixes and fix review before mainnet
None of this is complicated, and most of it is pretty boring. But when researchers don't have to fight your build, guess your intent or chase a moving target, all of their experience goes into finding bugs before somebody else finds them for you.
Not sure you're ready? Send us your scope and we'll tell you what's missing. And if you haven't picked an auditor yet, start with the questions to ask before you sign.