Even audited protocols can still be exploited. That's not a knock on auditors. It's just what audit tools are structurally unable to see. If you're a developer shipping smart contracts, understanding exactly what each layer of the security stack checks, and what it cannot reliably detect, matters more than knowing which tool has the fanciest dashboard.
This article walks through the security stack in the order a contract actually moves through it, starting with automated pattern matching and ending with mathematical proof, then spends real time on the gap that no tool in that stack closes. This piece extends the consensus and cryptographic architecture pillar. Audit status itself is a claim worth verifying, not assuming, which ties directly into our companion piece on whitepaper claim analysis.
The Security Stack At A Glance
Layer | How it works | Best at catching | The gap |
Static analysis (Slither) | Reads code without running it, matches known-bad patterns | Reentrancy, uninitialized variables, missing access checks | Anything with valid syntax but wrong intent, novel bugs with no matching pattern |
Symbolic execution (Mythril, Halmos) | Runs code with symbolic inputs, uses an SMT solver to check reachability | Input-dependent and ordering-dependent vulnerabilities | Path explosion, complex contracts can exceed the solver's time budget before full coverage |
Fuzzing (Echidna) | Generates random transaction sequences to break defined invariants | Sequence-dependent edge cases humans and solvers overlook | Only as good as the invariants you write; weak properties give false confidence |
Formal verification | Mathematically proves a stated property holds for all possible inputs | A single, precisely defined property, with actual certainty | High specification overhead, struggles with complex economic logic |
The First Line Of Defense Is Static Analysis
Static analysis is pattern matching for code. The tool never runs your contract; it reads the code and flags anything that matches a known-bad shape.
Slither, the most widely used open source static analyzer for Solidity, converts your contract into an intermediate representation called SlithIR and uses a compiler technique called Static Single Assignment (SSA) form to make it easier to trace how data and permissions move through the code. From that representation, Slither runs a large library of detectors, checks for known patterns like reentrancy, uninitialized state variables, and functions that should be permission-gated but aren't.
The value for developers is speed. Slither runs in seconds per contract; it slots into CI so every commit gets checked automatically, and it catches the kind of "embarrassing" bug that never should have reached a human auditor's desk in the first place.
The gap is exactly what you'd expect from pattern matching. It only catches what it's already been taught to recognize. A logic error with perfectly valid syntax, one where the code does exactly what was written but not what was intended, won't trip a single detector. That's precisely the category of bug behind the 2022 Nomad Bridge exploit, where a routine contract upgrade initialized a trusted root value to zero, which happened to be indistinguishable from an untrusted root under the existing validation logic.
Every message was accepted as proven by default. Static analysis had nothing to flag, because nothing was syntactically wrong. Over $190 million drained out over a few hours, and because the exploit required copying and resubmitting a public transaction rather than any real technical skill, hundreds of separate addresses joined in before it was over.
Symbolic Execution Explores Every Path
Where static analysis reads code, symbolic execution runs it, just not with real numbers.
Symbolic execution tools, such as Mythril (an open-source EVM bytecode analyzer) and modern bounded symbolic execution tools like Halmos, execute a contract using symbolic variables instead of concrete input values. As Trail of Bits engineers explain in an interview with Cyfrin's engineering blog, the process is essentially converting your code into math.
Every execution path becomes a set of boolean expressions, and those expressions get handed to an SMT solver (a program like Z3 that checks whether a set of logical constraints can be satisfied) to determine whether a genuinely dangerous state, say, an unauthorized ETH transfer, is actually reachable from some valid input.
This is meaningfully stronger than static analysis for one specific class of problem, vulnerabilities that only exist for certain input values or certain transaction orderings, exactly the kind of condition-dependent bug a pattern matcher can't reason about.
The gap is called path explosion. Every additional conditional branch roughly doubles the number of paths the solver has to consider. On a real DeFi protocol with dozens of interacting functions, the total path count grows exponentially. The solver hits a timeout before it finishes exploring everything, which means unexplored paths simply don't get checked, not because they're safe, but because the tool ran out of time. The Trail of Bits team's own framing is worth repeating directly: symbolic execution is not a silver bullet, and even when it runs to completion, it only proves the one specific property you asked it to check, nothing broader.
Fuzzing Breaks The System Instead Of Reading It
Property-based fuzzing takes a different approach entirely. Instead of checking for known bug patterns or mathematically exploring paths, tools like Echidna generate large volumes of random transaction sequences and try to violate invariants, properties you've explicitly defined that must always hold. A simple example: total supply must always equal the sum of every account balance.
This catches a category of bug that both static analysis and symbolic execution tend to miss: sequence-dependent edge cases that only emerge after a specific, unusual chain of transactions. Fuzzing doesn't need to understand your business logic in the abstract; it just needs to keep trying combinations until something breaks.
The gap is baked into the method. A fuzzer only finds what you tell it to look for. If your invariants are weak, incomplete, or simply wrong about what your protocol is supposed to guarantee, the fuzzer will run cleanly and report no problems, and that clean report tells you nothing except that your stated properties weren't broken. It creates a specific kind of false confidence; teams sometimes treat a passing fuzz suite as proof of safety when it only proves the properties they thought to write down.
Formal Verification Offers Mathematical Certainty, Within A Narrow Scope
Formal verification (FV) is often pitched as the strongest tool available, and mechanically that's true. Rather than sampling execution paths or generating random inputs, FV uses a mathematical model to prove or disprove that a stated property holds across every possible input, not most inputs, all of them.
For a specific, well-defined claim, this function can never be re-entered while a prior call from the same function is still executing, for instance, a formal proof is categorically stronger evidence than a clean scan or a clean fuzz run. That's not statistical confidence. That's a proof.
The overhead is the real cost. Writing formal specifications, in a language like Certora's CVL, requires expertise most Solidity developers don't have and most teams don't budget time for. And just like symbolic execution, formal verification tools can struggle badly with the kind of complex, interdependent economic logic that real DeFi protocols run on; proving a narrow technical property is tractable, proving that an entire lending market's economic model behaves safely under every market condition generally is not.
What The Tools Cannot See, On Any Layer
This is where the security stack, as a stack of tools, runs out of road. None of the four layers above are built to catch these next categories, not because the tools are immature, but because these problems aren't code-shaped.
- 1. Business logic errors. A tool can flag reentrancy as dangerous. It cannot know your lending protocol should liquidate only at a 1.5x ratio. That number is a business decision, not a security pattern. Logic errors like this, code that runs as written but breaks intended design, drive much of DeFi's losses. They stay invisible to generic detectors by nature.
- 2. Economic exploits and oracle manipulation. Flash loan attacks manipulate price feeds across multiple protocols in one transaction. Each contract may function exactly as designed. The vulnerability lives in the interaction between protocols, not in any single contract. This is an economic failure, not a code defect. No single-contract scanner models multi-protocol interaction.
- 3. Governance and centralization risk. No analyzer flags a multisig with too few signers, because it's a deliberate choice, not a bug. The same applies when flash-loaned voting power can pass a malicious proposal in one block. Researchers call this a human-shaped hole: a gap created by a decision, not a missed pattern.
- 4. Integration and composability gaps. Most tools analyze contracts in isolation. When Protocol A calls Protocol B, and B's behavior shifts through an upgrade or dependency change, that failure sits outside any single-contract scan. The 2023 Curve/Vyper compiler bug shows this exactly: a miscompilation below the audited Solidity layer put dozens of pools at risk simultaneously.
- 5. Library misuse. Even trusted libraries get misused. A USENIX Security study of OpenZeppelin usage scanned 35,882 contracts across three EVM chains. It found 47,431 functions with potentially insecure code, out of 2.75 million analyzed. Most were missing zero-address checks, the guard that stops transfers from burning assets or bricking permissions. One widely cited "36.3 percent" adoption figure couldn't be confirmed in the paper, so it's omitted here. What is confirmed: even a trusted library fails if a developer half-implements it, something no scanner checks by default.
Why Audit Status Alone Is Not A Safety Signal
An audit report existing is not the same as a protocol being safe, and the distinction matters more than most teams treat it. A comprehensive walkthrough of the audit process from a leading contest-based audit firm makes the point directly: an audit is a scoped, point-in-time review. If the code changes after the audit ends, if a dependency updates, or if deployment parameters shift, none of that gets re-checked automatically.
The same source notes that access control failures topped real-world incident data for the second consecutive year ahead of oracle manipulation and reentrancy, which tells you something important: the top categories are well understood and still causing losses, because understanding a category in the abstract and catching every instance of it in a specific, live codebase are different problems.
It's also worth reading an audit's findings carefully rather than just checking whether one exists. A finding marked "acknowledged" means the team saw the issue and chose not to fix it, often for a genuine business reason, not that it was resolved. Distinguishing between issues that are fixable with a code change and issues that reflect a deliberate governance or centralization tradeoff is exactly the kind of check we walk through in more depth in our companion piece on verifying project claims.
Residual Risk Never Reaches Zero
Security researchers emphasize a core concept every layer of this stack manages: residual risk, the probability that a vulnerability exists somewhere the analysis hasn't reached yet. Longer automated analysis lowers residual risk, but it never eliminates it. Automated tooling does not replace a manual audit, it merely prepares a codebase for one, because many categories of bugs cannot be detected generically at all.
That's the right mental model for the whole stack, not just one tool in it. A realistic modern workflow layers these tools by strength rather than picking one, static analysis on every commit for speed, fuzzing and symbolic execution before every release for path and sequence coverage, a full manual audit before mainnet deployment for the business logic and economic reasoning no automated tool can do, and bug bounties plus ongoing monitoring after launch, because residual risk doesn't stop existing just because the code shipped.
The Real Question An Audit Answers
None of this is an argument against tooling; every layer catches real, costly bugs that a human reviewer working alone would eventually miss from fatigue if nothing else. It's an argument for treating a green scan or a clean audit report as a snapshot, not a guarantee. The honest question to ask before deploying anything meaningful isn't did this pass its tools, it's
- Does the deployed bytecode actually match what was reviewed, and
- Does the reviewed code actually do what the system was designed to do?
The tools get you most of the way to answering the first half. The second half is still, and probably always will be, a human judgment call.
Related Articles
- Consensus, Custody and Cryptographic Architecture, A Technical Reference
- Evaluating Cryptographic Claims in Whitepapers Against Verifiable On-Chain Evidence
- Cross-Chain Bridge Security, A Technical Post-Mortem of Major Exploits and Design Lessons
- Oracle Design and Data Integrity, How Price and Asset Feeds Are Secured, and How They Fail











