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.

#StepWhenIf you skip it
1Freeze the codeRight before kickoffReport covers code you won't deploy
2Be precise about scopeWhen asking for a quoteIntegrations and deploy scripts go unreviewed
3One command to build and testWeeks beforeDay one is spent on tech support
4Write down how it should workWeeks beforeAuditors reverse-engineer your intent
5Test what would hurtDuring developmentSlower proofs of concept, fewer bugs found
6Run the free toolsBefore testnetYou pay seniors to write up scanner output
7Deploy on testnetBefore freezingConfig bugs show up on mainnet
8Developer on callDuring the auditEdge cases get one look instead of five
9Time for fixes and fix reviewAfter the reportUnreviewed 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.number on 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.example and 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:

InvariantWhat 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.

StackFree tools to run first
SoliditySlither, Aderyn
Rustcargo audit, Clippy
Gogosec, govulncheck
AnyAn 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:

  1. YouFix the findings, criticals and highs first.
  2. UsReview the fixes, to confirm they work and didn't break anything else.
  3. 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.