Every protocol landing page has the badge. Audited by somebody, usually with a logo attached. It tells you less than people assume.
An audit is a fixed number of expert hours pointed at a codebase on a particular Tuesday. What those hours turn up depends almost entirely on the state the code was in when they started. Hand two firms the same budget and the same seniority, and you'll still get two reports that don't resemble each other, because one team showed up with a spec, a set of invariants and a tagged commit, and the other showed up mid-refactor with a README.
Readiness is what separates those two teams. It isn't a cheaper audit and it doesn't stand in for one. Below is where that line sits, and then the argument that burns a week of engineering time on most projects: whether chasing 100% branch coverage is worth it.
Short version
A security audit is an external, time-boxed, adversarial review by people who didn't write the code. The value comes from them being unfamiliar and hostile. An auditor asks what happens if the caller is malicious and the oracle is stale and the token takes a fee on transfer. Your own team can't ask that question honestly, because they already know what the code was meant to do.
Audit readiness is everything you do first so those hours land somewhere useful: spec, invariants, tests, static analysis cleared, NatSpec, threat model, a frozen branch.
The line falls here. Readiness kills the findable bugs, the ones any competent reviewer or half-decent tool would flag. The audit is for everything else: economic logic, cross-contract interactions, assumptions that are wrong rather than missing. Neither one hands you a guarantee, and neither one is measured by a coverage percentage.
Three things an audit can't be
These constraints don't go away, and any firm worth hiring will say so before you sign.
Time. Two weeks is somewhere between 80 and 160 expert hours, against a system your team spent thousands of hours building. Auditors triage. What they triage toward is set by what you hand them on day one.
Scope. The review covers a commit. Anything merged after the freeze is unreviewed, and a fair share of post-audit incidents start with someone pointing out it was only one line.
Intent. An auditor can show that your code does what it says. Whether what it says matches what your economics need is a question your specification answers. If you haven't written one, the auditor reconstructs it from the code, and code that implements a bad idea correctly looks completely fine.
The loss data lines up with this. Immunefi's 2026 ecosystem report puts 89.1% of 2025 DeFi losses on protocol logic exploits, meaning flaws particular to how one application was designed. The categories everyone used to worry about have mostly collapsed. Bridge exploits went from 73% of losses in 2022 to 3% in 2025. Flash-loan attacks, 54% down to under 1%. Total exploit losses fell 74%, from $2.62B in 2022 to roughly $680.3M in 2025, and the median loss per attack dropped from $6M to $1.5M.
So the industry solved the bug classes that tools and checklists catch. What's left over is design error, and design error is the thing a prepared audit reaches and an unprepared one runs out of time before getting to.
What readiness actually means
Seven artifacts. None of them exotic.
A written specification. What the system does, what has to stay true, and what you've decided is out of scope.
A threat model. Who the adversaries are, what they control, and which assumptions kill you if they turn out to be false.
A privileged-role matrix. Every
onlyOwnerandonlyAgentfunction, who holds the key, and the worst thing that role can do.A test suite with invariants in it, not only unit tests.
A clean static-analysis run. Slither, Aderyn, compiler warnings, with a written reason for every suppression.
NatSpec across the external surface, describing intent rather than restating the signature.
A frozen commit, plus an agreed code-freeze policy for the length of the engagement.
Readiness guides from OpenZeppelin, Quantstamp and the rest land on roughly this list, usually with 95%+ test coverage attached, all automated findings triaged, and the code frozen during review. That coverage number is where teams start arguing with each other.
What chasing 100% branch coverage costs
Branch coverage asks whether both outcomes of every conditional got exercised. Solidity makes the denominator bigger than people expect: every require, every modifier, every short-circuiting &&, every ternary, and every branch inside library code you inherited without reading.
The curve isn't linear. Roughly what it looks like across the codebases we've taken to audit:
| Coverage band | What it costs | What it finds |
|---|---|---|
| 0 → 70% | Tests you would write anyway | Real functional bugs, constantly |
| 70 → 90% | Meaningful but productive effort | Error paths, revert conditions, access control gaps — genuinely valuable |
| 90 → 98% | Disproportionate: contorted setups, mocks, harnesses | Occasionally a real bug; mostly confirmation |
| 98 → 100% | Often the most expensive week of the sprint | Almost always nothing — you are testing defensive code that cannot trigger |
That top band costs so much because of what's left in it. The last branches are usually unreachable by construction: a require guarding against a state the rest of the system won't produce, an else on an enum with two values, an overflow check the compiler inserted that your arithmetic can't trigger. Reaching them means building a harness that corrupts internal state on purpose, and then owning that harness for the life of the project. You end up maintaining a careful model of a system that cannot exist.
There's a running cost as well, which nobody prices in at the moment the target gets set. Tests written to hit branches tend to assert on structure instead of behaviour, so they break every time someone refactors. CI gets slower, and slow CI teaches a team to merge without waiting for it. And under a hard gate, the cheapest way to go green is a test that calls the branch and asserts nothing whatsoever. Coverage reads 100%. Nothing was checked. Goodhart's law, with a badge in the README.
What high coverage does buy you
It's a useful number that gets sold as a different, better number. High branch coverage reliably catches:
Dead code on its way to mainnet. If a branch can't be covered, either it shouldn't be there or you've misunderstood your own system, and both are better learned before an auditor points them out.
Revert paths and setters nobody tested. A
requirethat never fired in any test is a dependable source of backwards error semantics, and untested initialisers are where deployment bugs live.Refactor regressions, because a suite that touches everything makes noise when behaviour shifts.
Auditor hours. Every stupid bug your suite kills is an hour someone spends on your liquidation math instead.
The last one is the case I'd make to a CTO. Not that the code is safe. That you aren't paying senior audit rates for someone to find an unchecked setter.
Where coverage lets you down
Two pieces of evidence, both worth forwarding to whoever set the target.
Coverage and defect detection are only loosely related. Inozemtseva and Holmes measured this properly in their ICSE 2014 paper, an ACM Distinguished Paper, across five large open-source projects with over a thousand test methods each. Control for the number of test cases and the correlation between coverage and effectiveness comes out low to moderate, and stronger coverage criteria did not provide greater insight. What coverage mostly measures is how many tests you wrote.
The bugs that actually get exploited are largely invisible to tooling anyway. The ICSE 2023 paper Demystifying Exploitable Bugs in Smart Contracts went through 516 real vulnerabilities from 2021 and 2022 and found over 80% were beyond the reach of existing automated tools. They're functional bugs, and catching them requires knowing what the protocol was supposed to do. Something like one in five is machine-auditable.
The failure mode is easier to show than to describe. A lending protocol liquidates a position once its health factor falls below the threshold:
uint256 constant LIQUIDATION_THRESHOLD = 1e18;
function isLiquidatable(uint256 healthFactor) public pure returns (bool) {
return healthFactor <= LIQUIDATION_THRESHOLD; // BUG: should be <
}Two tests, and between them they cover it completely:
function test_healthyPositionIsSafe() public {
assertFalse(engine.isLiquidatable(1.1e18)); // false branch
}
function test_unhealthyPositionIsLiquidatable() public {
assertTrue(engine.isLiquidatable(0.9e18)); // true branch
}100% branch coverage. 100% line coverage. The bug ships. Neither test ever passes in healthFactor == LIQUIDATION_THRESHOLD, the one value that tells < apart from <=, and it's precisely the value an attacker will construct, because a position sitting exactly on the threshold is now liquidatable while your documentation promises it isn't. The coverage report confirmed the line executed. It had no opinion about the assertion.
Three things that do work: invariants, fuzzing, mutation testing
Invariants. Write down what must always hold, then let a fuzzer try to break it across random call sequences. Not "withdraw works". Closer to "the vault can never owe more than it's holding":
function invariant_vaultRemainsSolvent() public view {
assertGe(asset.balanceOf(address(vault)), vault.totalAssets());
}Invariants capture intent, which is the layer coverage can't see, and they age well. A decent one outlives three rewrites of the function it constrains.
Fuzzing. Foundry, Echidna, Medusa. They generate the inputs nobody on your team would think to type, including the boundary in the example above. A fuzzer reaches that == case in seconds. A human writing table-driven tests reaches it once they already suspect something is wrong.
Mutation testing, which is the only direct answer to "is this suite any good?". It breaks your code in small ways on purpose and checks whether the tests notice. Every mutant that survives marks a spot where coverage was lying to you. For Solidity, slither-mutate runs against a Foundry suite:
slither-mutate ./src --test-cmd="forge test"Trail of Bits' version ranks mutants by severity, from swapping whole statements for reverts down to flipping operators, which is exactly the < to <= change above. Run it on that example and the mutant survives. That's the report you wanted, and coverage was never going to produce it.
Given the choice, take 85% coverage with a 95% mutation score over 100% coverage with a 60% one. If CI is going to enforce a single number, that's the number.
The bar we set before an external audit
The seven artifacts are the floor. Four more things go on top before code leaves for an external firm, and on security-led engagements we build toward them from the first sprint:
An invariant suite covering solvency, accounting identities and access control.
Fuzzing wired into CI, rather than running on one engineer's laptop.
Branch coverage above 90% on anything that moves value, with a written explanation for each branch that isn't covered.
A mutation run, with surviving mutants triaged instead of quietly ignored.
Note that the coverage line says 90%, and that the explanations are the part doing the work. A team that can point at eleven uncovered branches and say why each is unreachable understands its system. A team that reached 100% by writing eleven tests against a state-corrupting harness understands its harness.
Budgeting for readiness
This isn't a phase you bolt on in the final fortnight. It's something like 15–25% of engineering effort spread across the build, and it's cheapest starting at architecture. Retrofit invariants onto a finished protocol and you'll usually discover the accounting model makes several of them impossible to state, which is itself a finding, arriving at the worst possible moment.
The order that works:
Architecture. Threat model and role matrix before any contracts exist. It's the only point where access-control structure is cheap to change.
Implementation. Invariants in the same PR as the feature they constrain. Coverage as a signal, never a gate.
Pre-audit. Fuzz campaign, mutation run, static analysis, spec finalised, freeze.
Audit. Adversarial review of a system that has already answered the cheap questions.
After launch. Monitoring and incident response, since the threat model keeps moving once you're live.
Teams that work in that order get shorter audits and more interesting findings. Teams that start at step four get a report full of things their own CI should have caught, billed at senior auditor rates.
FAQ
Is audit readiness the same as a pre-audit? No. A pre-audit is a lighter external review by another firm. Readiness is your own engineering work: specification, invariants, threat modeling, test infrastructure. Done properly, it often removes the need for a pre-audit at all.
We hit 100% coverage and a 95% mutation score. Do we still need an audit? Yes. Both of those measure whether the code does what you think it should. An audit is where someone checks whether what you think is right, and that's where 89.1% of last year's DeFi losses came from.
Is 100% branch coverage ever the right target? On something small, self-contained and valuable, yes: a vault's accounting core, a transfer path, a signature verifier. At that size the remaining branches are usually reachable for real. As a company-wide CI gate across a whole protocol, it mostly produces tests that assert nothing.
Does an audit make a protocol safe? It lowers risk inside a defined scope at a point in time. It isn't a warranty, no serious firm claims otherwise, and the moment you change code after the report you're outside the scope that was reviewed.
Which single change improves audit outcomes most? Writing the specification. Auditors who know what the system is meant to do come back with design flaws. Auditors reverse-engineering intent out of Solidity come back with style notes.
How long does audit preparation take? Four to eight weeks is typical when security wasn't involved during the build, and the specification is usually what holds it up. With security there from architecture onward, preparation is mostly freezing and packaging what already exists.
If you want help getting there
We do this as part of building: threat modeling and role architecture at design time, invariant and fuzzing infrastructure alongside features, mutation testing to prove the suite is real, and coordination with audit firms once the code is genuinely ready for them. It doesn't lengthen the runway to launch. It changes what comes back in the report.
→ DeFi security & audit readiness at RedDuck
If security is arriving after the architecture is settled, smart contract engineering and protocol design are where the fixable decisions still live.
Related reading: ERC-3643 vs ERC-1400: what a complete RWA system actually contains, on compliance-heavy codebases where the privileged-role matrix is most of the security story.

