Skip to content
Security practice6 min read

Why audited code still gets exploited

Nobody touched the Curve pools for two years. The Vyper compiler bug that drained them was found anyway. Your attack surface moves even when your code doesn't.

By Alex Cipher — ongoing smart contract security review for small GameFi and DeFi teams.

In July 2021 the Vyper team shipped compiler version 0.2.15. Plenty of protocols compiled with it, deployed, got audited, and moved on. The contracts sat there working exactly as intended for two years.

On 30 July 2023 someone worked out that the reentrancy lock in that compiler version, and in 0.2.16 and 0.3.0, didn't actually work. The lock state wasn't stored where it needed to be, which left cross-function reentrancy wide open in contracts whose authors had explicitly written a guard against it. Curve pools got drained, and so did Alchemix, JPEG'd, Metronome, deBridge and Ellipsis. Reported totals vary between roughly $50 and $70 million depending on who is counting.

Nobody touched that code for two years. Not a single line changed between "safe" and "drained".

An audit measures you against a moving reference

This is the part that gets glossed over when people talk about being audited as though it were a state you achieve. An audit is a measurement: this code, at this commit, against everything the reviewer knew on the day they read it.

Both halves of that can go stale. The code side is obvious, and it is the one everybody plans for. Ship a change, and the report describes something you are no longer running. The other half is quieter. Even if you never touch the code again, the thing it was measured against keeps expanding. New techniques get found, written up, and turned into tooling that anyone can point at your address.

A clean report is not a certificate. It is a receipt for a point in time.

The industry outside crypto settled this argument decades ago

Bruce Schneier wrote the line everyone eventually reaches for, in the preface to Secrets & Lies back in 2000:

Security is not a product; it itself is a process.

He was talking about firewalls and enterprise networks, not Solidity. The reasoning transfers cleanly anyway. You cannot buy a fixed amount of security once and hold it, because the other side of the equation is a set of people actively working to expand what is possible. Traditional software security absorbed this and built continuous practice around it: monitoring, patch cycles, dependency scanning, re-testing. Nobody seriously argues that a penetration test in March means you are secure in November.

Crypto arrived at the same conclusion later and is still halfway through adopting it. The serious protocols have moved from treating security as a launch expense to treating it as an operating one. The rest are still buying a report and filing it.

Whole categories of attack arrive after you deploy

The Vyper bug is a clean example because the cause sat below the code entirely. But it is not a freak case. Read-only reentrancy is a good second one: it was first written up as a class in 2022, and it works across protocol boundaries. An attacker reenters through a view function that reports a price, and something built on top of that price reads a number that is briefly nonsense.

Think about what that meant for everyone already deployed. If you had integrated a pool and read its price in the obvious way, your contract did not become buggy in 2022. It was always exploitable. It just was not known to be, by you or by whoever audited you, because the technique had not been published yet.

Every one of those write-ups converts a set of already-deployed, already-audited, never-touched contracts from safe into vulnerable overnight. Not because anyone shipped anything. Because the map got redrawn.

Crypto makes this worse than it is anywhere else

Three things stack up here in a way they do not in ordinary software.

  • Your code is public and permanent. Anyone can read your bytecode, at leisure, forever. There is no obscurity to lean on and often no ability to patch quietly.
  • The attacker is paid immediately and in full. Most software vulnerabilities need a chain of steps to monetise. Yours pays out in one transaction, which funds a great deal of patient research into your specific contracts.
  • Your dependencies are somebody else's live code. You inherit the assumptions of every protocol you integrate, and those assumptions can be invalidated by a technique found against them, not against you.

What actually helps

Not another audit of the same commit. What closes this gap is someone who already holds a model of your system re-reading what is deployed against what has become known since the last look, and doing that on a schedule rather than after an incident.

That is the second half of what ongoing review is for. When you are shipping, I read the diff before it goes live. When you are not, I re-read the live contracts against the techniques that have appeared since. Same retainer either way, because from where I sit they are the same job: keeping the review current against a target that moves whether or not you do.

The context is what makes it work. Someone who mapped your trust boundaries and value flows last quarter can look at a new technique and tell you in an afternoon whether it reaches you. Someone starting cold has to rebuild that understanding before they can answer, which is why the answer usually arrives after the exploit rather than before.

What this is not

It is not a replacement for a full audit. Freeze the code for a launch or a major version and get it audited properly, by me or by anyone else. Ongoing review is what runs in the long gaps between those milestones, which is where most of your protocol's life actually happens.

It also is not a guarantee. Nothing is. Nobody knew about the Vyper lock for two years, and no reviewer would have caught it in 2021. What continuous review buys you is that the gap between a technique becoming public and someone checking whether it applies to you is measured in weeks rather than in however long it takes an attacker to get to your address.

The honest version

If your contracts are stable and you are wondering why you would pay monthly for a review of code that is not changing: this is the reason. You are not paying for someone to watch your commits. You are paying for the distance between what was known when you were audited and what is known now to stay small.

The onboarding review is $300 to $600 once, and ongoing review starts at $300 a month. Full numbers are on the pricing page. If you have recently been through an incident, this one is more relevant.

Otherwise: tell me what you have deployed. Telegram, email, or a call, whichever you'd actually use.

Shipping changes after an incident?

Send me your repo, your chain, and what you are about to deploy. You get a number and a start date back, usually the same day.

No obligation, and no sales sequence. Ongoing Review runs for a 3-month minimum, then month to month with 30 days' notice.