All writeups
Audit PrepBest PracticesSmart Contract Security

How to Prepare Your Contracts for an Audit

June 26, 20269 min read

Most of the friction in an audit isn't the code. It's everything around the code that nobody got around to writing down. I've started engagements where the first two days were spent just figuring out what the protocol was supposed to do, and I've started others where I was finding real bugs by the afternoon of day one. The difference was never how clever the team was. It was how prepared they were.

So here's the honest version of how to get ready, written from the chair I actually sit in. I've described how I actually audit a contract elsewhere, and this is the mirror image of that. Everything below is something that makes my job faster, which directly means you get more of my attention spent on the things that matter and less of it spent reconstructing context you already had in your head.

Freeze the scope and give me a commit hash

This is the one people resist most, and it's the most important. Before the audit starts, stop changing the code. Pick a commit, give me the hash, and that's the thing I review.

I understand the urge to keep improving while you wait. You found something, you fixed it, why not push it. The problem is that an audit is a snapshot of a specific state of the code, and if that state moves under me, the whole thing degrades. I trace a function from its inputs all the way back to where the values come from. That tracing takes hours and it assumes the ground isn't shifting. If contract B changes while I'm three contracts deep into how it's called, I either have to start that thread over or I report on code that no longer exists. Both are a waste of the money you're paying me.

Freeze it. If something genuinely critical comes up mid-review, we talk about it and handle it deliberately, not by you quietly pushing to the branch I'm reading.

Write down what it's supposed to do

I read documentation before I read code, on purpose, and I won't relax about this one. Almost every serious bug is a gap between what the code does and what it was meant to do. I can only find that gap if you've told me the second half. If I only have the code, I learn what it does, and then I start unconsciously assuming that whatever it does is what you intended. That's the exact trap that lets bugs walk straight past an auditor.

You don't need a polished whitepaper. You need a short, honest spec of intended behavior. What is this protocol for. Who are the actors. What's supposed to happen on the happy path, and what's explicitly not allowed. Where does value enter and leave the system. If a number is a running balance, say so. If a function is only ever meant to be called by your keeper bot, say so.

When the docs are good, my questions to you drop sharply, and every question I don't have to ask is time I spend reading your code instead of waiting on a Telegram reply. When the docs are bad or missing, I'm reverse-engineering your intent from the implementation, which is slower and, worse, biased by the implementation itself.

NatSpec and tests that mean something

Comment the code at the interface level. NatSpec on external and public functions, the kind that says what a function is for and what it assumes, not the kind that restates the function name.

/// @notice Accrues protocol fees to the referrer's running balance.
/// @dev Adds to the existing pending amount; must never overwrite it.
/// @param referrer Address that earns the fee.
/// @param amount Fee earned on this single trade.
function _creditReferrer(address referrer, uint256 amount) internal {
    pendingReferrerFees[referrer] += amount;
}

That comment is doing real work. It tells me the variable is a tally and that overwriting it would be a bug. If you'd written that and the code underneath said = instead of +=, the contradiction jumps out. That's not a hypothetical, by the way. A bare assignment into something that should have accumulated is exactly the referrer-fee bug I found in Pear Protocol, and a one-line comment about intent is the kind of thing that surfaces it faster.

And actually write it. NatSpec feels like a chore when the code reads fine to you, but it earns its keep twice over. It lets me understand your contract faster, so more of the engagement goes to hunting bugs instead of decoding what each function is supposed to do. And it shows me how you think about your own system, the same window your README and your tests give me. When the comment and the code tell the same story, I move fast. When they don't, that gap is usually where the bug is.

Now the tests, because this is where I want to change how you think. I read your test suite almost like documentation, and not for the reason you'd guess. A test suite is a written record of what you thought about. Every test is a scenario you cared enough to check. Which means the gaps in your tests are a map of what you didn't think about, and the cases you never wrote a test for are usually the cases you never reasoned through in the code either.

So when I see thorough happy-path tests and nothing covering a weird input, an empty array, a reentrant call, a malicious caller, that's not a clean bill of health. That's an arrow pointing me at where to dig. Auditors read the gaps. Knowing that, the move is obvious: write the adversarial tests yourself. Test the failure paths, the access-control reverts, the boundary values. Every one you write is a bug you might catch before I do, and a place I no longer have to flag as untested.

Run the free tools yourself first

Before you hand me anything, run the static analyzers. Slither and Aderyn are free, they're fast, and they catch the mechanical stuff in minutes.

slither .
aderyn .

Do this, read the output, and fix the obvious things. Unchecked return values, shadowed variables, the well-known reentrancy patterns, the missing zero-address checks. None of that needs a human, and you genuinely do not want to pay an auditor's rate for findings a script produces for free. I'm going to run these tools anyway on my first pass. If you've already cleared that layer, the noise I'm wading through is lower and my human attention starts where the tools give up, which is the business logic, the intent, the stuff no analyzer can reason about. That's what you're actually paying for.

To be clear, passing Slither does not mean your code is safe. It means you didn't make me spend the first day on lint.

Tell me what you're worried about

Give me a known-issues list, and be specific. If you already know an external dependency is sketchy, that a function is gas-heavy, that you punted on a particular edge case, write it down. You're not exposing weakness by doing this. You're focusing the review.

Better still, tell me the part you're personally nervous about. Every team has a contract or a function that they touch carefully because something about it never sat right. That instinct is almost always worth something. When a founder says "the withdrawal accounting in this one vault makes me a little uneasy," I go there first and I go there hard. You know your system better than I do on day one. Point me.

This also keeps us from spending budget on things you've consciously accepted. If a centralization risk is a deliberate tradeoff for your launch phase, say so, and I won't write three paragraphs telling you the owner has too much power as if it's news.

Document roles and the deployment plan

Access control is where a surprising share of real findings live, and it's invisible if I don't know who's supposed to be able to do what. Give me the roles. Who's the owner, who's the admin, what can each privileged address do, which functions are gated and which are deliberately open. The bug is usually the one function that looks exactly like its twenty neighbors but is missing the modifier they all have. I find that by checking every privileged path against the roles you intended, so I need to know what you intended.

That goes for inputs too. The way a player got to hand-pick rare NFTs in the AI Arena audit was a function trusting a value the caller fully controlled. If your docs say which inputs are trusted and which are user-supplied, I can check that boundary directly instead of inferring it.

Then the deployment and config plan. Constructor arguments, initializer values, what gets set at deploy time, the order of operations, any privileged setup that has to happen before the system is safe to use. Plenty of protocols are fine in the code and broken in how they get configured on mainnet. If the audit only covers the code and not the launch, you've left a real gap.

Be honest about timeline

What slows audits down is consistent: no docs, unclear intent, no tests, and code that's still moving. What speeds them up is the opposite of those four things, and you control all of them before the engagement even starts.

On cost and timing, set expectations that match reality. A small scope is roughly a week, a mid-sized system a couple of weeks, a larger one more, and the pricing bands on the audits page lay out the ranges. But those assume a frozen, documented, tested codebase. Hand me a moving target with no spec and the same scope takes longer and produces fewer findings, because I burned the early days building the context you could have handed me on day one.

None of this prep is exotic. It's the same discipline that makes the audit itself work: write down what should happen, show your work in the tests, and don't make the careful reader reconstruct what you already knew. Do that and the audit stops being a translation exercise and starts being what you actually paid for, which is someone spending real attention on whether your code does what you think it does. If you're getting close to that point, here's how I work.

Shipping something that needs this checked?

Get an independent smart contract audit before mainnet — manual review, Foundry testing, and fix verification.

Request an Audit