Skip to content

10 June 2026

Why audits catch what unit tests miss

auditscardanoprocess

In this article we discuss why audits are needed and why unit tests are just not enough.

What this looks like in practice

Unit tests verify expected behavior. They confirm the system does what you designed it to do, under conditions you anticipated. They pass or fail against assertions you wrote. This means that unit tests only catch failures you anticipated upon.

In many scenarios and especially Web3, relying only on unit tests is especially dangerous, the code might do what it is expected but the code may not be safe, this is where funds get drained. Immutability and adversial incentives make the missed cases catastrophic rather than annoying.

Audits examine actual state, they inspect what is really there, configurations, permissions, data, dependencies, drift etc. without being bound to your expectations. This lets them surface:
Unknown unknowns, problems you never wrote a test for because you didn’t know they were possible.
Blind spot coverage, untested paths, edge cases and code no test exercises.
Assumptions baked into the tests themselves, if your mental model is wrong, your tests inherit that wrongness and pass anyway.
Emergent and systematic issues, interactions between components, config drift, accumulated tech debt, security gaps that only appear when you look at the whole rather than isolated units.
State over time, test run at a moment, audits catch what changed after deployment for example manual edits, expired certificates, permission creep.
Adversarial angles, auditors ask themselves how could this be abused instead of does this feature work.

What does unit tests structurally miss in smart contract/Web3 context:

Economic & incentives attacks, critical questions are not asked.
Cross contract & composability risk, your contract in isolation is correct but what when it is called or called by, reentrancy, malicious token callbacks and unexpected interactions with other protocols only appear when contracts compose. Unit tests mock those away.
State drift & upgrade hazards, proxy patterns, storage-slot collissions, uninitialized implementations and delegated context confusion. Unit tests on a single logic contract won’t catch that your upgrade corrupts storage layouts or leaves an implementation contract self-destructive.
Access control & privilege, tests confirm the owner can do thing. They rarely confirm that no one else can, accross every function and every path. Missing modifiers, unprotected initializers and forgotten admin functions are classic audit findings that green test suites hide.
Wrong assumptions baked in, if you misunderstood roundings, decimals, integer division truncation, or how tokens fee on transfer works, your tests encode the same misunderstanding and pass.
Chain-specific realities, MEV/frontrunning, block-timestamp manipulaton, gras-griefing, reorg behavior and transaction ordering. These are properties of a live adversial environment not of a single function.

On Cardano eUTxO specifically (Plutus/Aiken etc.): unit-testing a validator doesn’t catch datum/redeemer mismatches under real UTxO contention, double satifaction vulnerabilities, concurrency limits from single-UTxO contention or incorrect assumptions about what other inputs/outputs a transaction may carry. Property-based tests and formal reasoning about the transaction as a whole catch these issues; where isolated unit tests don’t.

Unit tests are necessary but only prove that your code meets the specification. In an immutable, adversial environment, you also need integration/fork testing, invariant and property-based fuzzing, static analysis and an independent audit.