RedDuck
Copied

Audit Readiness vs a Security Audit: The Honest Boundary_

Written by

Ilya Kubariev

Posted: Aug 10, 2026
12 min reading
6

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.

  1. A written specification. What the system does, what has to stay true, and what you've decided is out of scope.

  2. A threat model. Who the adversaries are, what they control, and which assumptions kill you if they turn out to be false.

  3. A privileged-role matrix. Every onlyOwner and onlyAgent function, who holds the key, and the worst thing that role can do.

  4. A test suite with invariants in it, not only unit tests.

  5. A clean static-analysis run. Slither, Aderyn, compiler warnings, with a written reason for every suppression.

  6. NatSpec across the external surface, describing intent rather than restating the signature.

  7. 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 bandWhat it costsWhat it finds
0 → 70%Tests you would write anywayReal functional bugs, constantly
70 → 90%Meaningful but productive effortError paths, revert conditions, access control gaps — genuinely valuable
90 → 98%Disproportionate: contorted setups, mocks, harnessesOccasionally a real bug; mostly confirmation
98 → 100%Often the most expensive week of the sprintAlmost 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 require that 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:

solidity
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:

solidity
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":

solidity
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:

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

_Our Cases

Since 2020, RedDuck has been building Web3 products end to end – smart contracts, indexers, backends and the interfaces people actually trade in. Two of our public projects show the range: Midas, a platform issuing regulated real-world assets onchain, and Injex, the first liquidity aggregator in the Injective ecosystem.

Midas — tokenized investment products dashboard
EVM
RWATokenization

Midas: Tokenized Treasuries & Yield Strategies

Midas (midas.app) came to us to put regulated, institution-grade yield products onchain: mTBILL, a tokenized short-duration US Treasury product, and mBASIS, a market-neutral basis-trading strategy — issued as ERC-20 tokens that an investor can hold, move and redeem without ever leaving DeFi.

The hard part is not the token — it is everything around it. Minting and redemption are priced against the fund's net asset value, which has to reach the contracts through an oracle; access is permissioned, so the compliance perimeter lives in the contract rather than in a form; and redemptions have to feel instant while the underlying assets settle on a banking timeline. We built that stack end to end and took it through multiple external audits.

Today Midas runs a growing family of yield tokens that stay fully composable across dozens of EVM networks — supplied as collateral and routed through other protocols like any ERC-20, still redeemable at NAV on demand — with nine-figure TVL and, in an EU first, tokenized treasuries open to retail investors.

Explore more
Injex DEX aggregator landing page
Injective L1
DeFiDEX aggregator

Injex: The First Liquidity Aggregator on Injective

Liquidity on Injective sits in a dozen separate venues, so getting a fair price used to mean opening every DEX in turn and comparing by hand. Injex removes that step: it is the first aggregator in the ecosystem, and it quotes the whole market at once.

Under the swap card there is a routing layer that reads reserves across DojoSwap, Helix, Astroport, Neptune, White Whale and Hydro, splits an order across whichever pools give the best combined rate, and settles it in a single transaction — later extended with limit orders, staking and on-chain analytics.

The interface stays deliberately plain: one card, the best price, minimal slippage. Newcomers can use it like an ordinary exchange, while traders who care about execution can still see the full route.

Explore more