On October 6, a smart contract that had been functionally inert for roughly four years performed its final meaningful action: it surrendered 200 WETH to an address with no relationship to its owner. There was no flash loan. No oracle skew. No reentrancy loop, no governance capture, no cross-chain message forgery. The attacker invoked a single function β a function that, in any competently written contract, reverts with an authorization error. It did not revert. It paid out.
That is the entire exploit. It fits in a tweet. It should not have been possible. And it was, because one modifier β auth, inherited from the ds-auth framework that has guarded MakerDAO's core contracts since 2017 β was missing from a third-party keeper's withdrawal path. The attacker did not break the code. The attacker read it, found the seam, and pulled.
The victim was not MakerDAO. The victim was a third-party liquidation bot: an independent operator that had, during the DeFi Summer of 2020, participated in four ETH-A collateral auctions and won. Each auction netted 50 WETH. Four auctions, 200 WETH. The bot won the collateral but never claimed it. The deal() call β the settlement primitive that releases a winning bidder's collateral from the auction contract β was never executed. The funds sat, unclaimed, inside a MakerDAO Flipper contract for years. Then someone settled the auction, routed the collateral through the bot's own internal ledger, and walked out the front door because the front door had no lock.
Context: What a MakerDAO Auction Actually Is
To understand why this happened, you have to understand what a MakerDAO collateral auction is at the contract level β not the economics, the plumbing. Most coverage of DeFi liquidations treats auctions as an abstraction: "the protocol sells collateral to repay debt." That abstraction hides the actual state machine, and the actual state machine is where this entire event lives.
MakerDAO's auction system at the time of the 2020 auctions was built from a small set of interlocking contracts, each with a narrow responsibility. The design philosophy was Unix-like: one contract, one job, composed through a shared internal accounting layer.
The Vat is the core accounting engine. It holds the canonical ledger of every vault, every collateral type, every debt position, and every internal balance. Critically, the Vat does not hold ERC-20 tokens. It holds accounting units β internal balances denominated in a normalized form. When you deposit collateral into a Maker vault, the ERC-20 token moves into an adapter contract, and the Vat records that you are credited with an equivalent internal balance. The token and the accounting entry live in different places. This separation is deliberate. It is also the reason the exploit was able to move value through contracts that never touched a token until the final step.
The Cat (Collateral Auction Thug) is the liquidation trigger. When a vault falls below its liquidation ratio, anyone can call Cat.bite(), which cancels the vault's debt against the Vat and initiates an auction for the seized collateral.
The Flipper runs the auction itself. It is a simple English auction: bidders submit bids in DAI-denominated internal balance, the auction runs for a fixed duration plus a bid-extension window, and the highest bidder at close wins. When the auction ends, the winner is entitled to the collateral β but only after calling deal().
And this is the hinge of the entire event. deal() is a public, permissionless function. Anyone can call it on a concluded auction. It is not restricted to the winner. It is not restricted to the protocol. It is not restricted to the seller. The auction contract simply checks: is the auction finished? If yes, transfer the collateral to the winning bidder's account and mark the auction as dealt. The protocol's designers made deal() permissionless for a defensible reason: they did not want a winning bidder's failure to call deal() to permanently lock collateral in the auction contract. A permissionless settlement function is a liveness guarantee. If the winner goes offline, anyone can finish the job on the winner's behalf.
That design decision β permissionless settlement β is correct. It is also the first domino in this event. Because the auction from 2020 was never dealt, the collateral remained "owned" by the winner in the Flipper's internal state but had not yet been moved. It was a claim, not a balance. And a claim, unlike a balance, can be settled by anyone.
The Keeper Economy and the Auctions of 2020
The DeFi Summer of 2020 was a gold rush for liquidation keepers. As leverage stacked across lending markets and collateral ratios compressed, liquidations became a competitive sport. Keepers β bots that monitor vaults and race to call bite() when a position becomes unhealthy β formed an entire sub-industry. Some were professionally operated by trading firms. Some were solo operators running a script on a VPS. Some were forks of the reference implementation, makerdao/auction-keeper, modified to run faster or cheaper.
Auction numbers 1457 through 1460 fall in this period. Four ETH-A auctions. The winning bidder β our third-party bot β paid for the collateral in DAI and was credited, internally, with the right to claim 50 WETH per auction. That is 200 WETH total, at a time when ETH traded in a range that made the sum materially interesting but not spectacular. The operator likely had many such auctions running in parallel. Claiming each one β calling deal() β was a bookkeeping step, not a profit event. The profit was already locked in at bid time.
And here is where human operational reality collides with on-chain permanence. The bot's architecture included an upgradeable proxy: a contract that holds funds and delegates logic to a separate implementation contract that can be swapped out by an admin. This is a standard pattern. It is also a pattern that quietly transfers risk. When a keeper migrates logic β from v1 to v2, from a slow implementation to a faster one β the proxy keeps its balance and its permissions. If the new implementation forgets to carry forward an authorization check that the old one had, the proxy does not complain. It simply starts accepting instructions from anyone who can reach the functions the new implementation exposes.
Our operator appears to have migrated, stopped operating, or simply lost track. The four auctions were never dealt. The 200 WETH claim sat in the Flipper, attached to a proxy that had gone dark. For years. Until October 6.
Core: The Attack Path in Three Calls
The exploit is elegant in the way that a key left under a doormat is elegant. It required no capital, no leverage, no market position, no front-running. It required three calls, executed in sequence, using only public functions on public contracts.
Step one: settle the auction. The attacker called deal() on auctions 1457 through 1460. This is permissionless. The Flipper released the collateral β 200 WETH worth β to the winning bidder's account inside the Vat. At this instant, the funds moved from "unclaimed auction claim" to "internal balance credited to the keeper's account." Nothing external happened yet. The value was still inside the Vat, denominated in internal units, belonging to the proxy's account.
Step two: route through the ledger. The attacker then used Vat.flux β the internal transfer primitive β to move the collateral balance within the Vat from the keeper's account to an account the attacker controlled. Vat.flux is the ledger's bookkeeping operation. It moves value between internal accounts without touching any token. It is fast, cheap, and, when authorization is correct, restricted to authorized parties.
Step three: exit to the real world. With the collateral now credited to the attacker's internal account, the attacker called GemJoin.exit on the ETH adapter. GemJoin.exit is the bridge from internal accounting to actual ERC-20 tokens. It burns the internal balance and releases the underlying WETH. The attacker then unwrapped the WETH to ETH.
The three-step path β deal() β Vat.flux β GemJoin.exit β is, functionally, the correct and intended way to move collateral out of a settled MakerDAO auction. It is the same path a legitimate keeper would use. The attacker did not invent a novel exploit. The attacker used the protocol's own plumbing exactly as documented. The only thing that made it theft rather than housekeeping was that the first two steps were performed on someone else's account, and the third step was performed by a contract that should never have let a stranger reach it.
The Missing Modifier
Here is the core defect. The proxy contract exposed a withdraw function. In a correct implementation, that function would carry the auth modifier from ds-auth β the same authorization framework that protects Vat.flux, Cat.bite, and the rest of MakerDAO's sensitive surface. The auth modifier checks a simple thing: is the caller on the list of authorized addresses? If not, revert.
In this proxy, the withdraw function had no such modifier. It was open. Any address could call it, specify a target, and instruct the contract to release its balance.
This is a low-level bug. It is not a subtle reentrancy, not a curve mispricing, not an unchecked arithmetic overflow in a hard-to-reach branch. It is the absence of a single line of code β the absence of a guard on a function whose entire purpose is to move money. The code doesn't lie: a function that moves funds and lacks an access check is not a design decision, it is an omission. And an omission in an upgradeable proxy is not a static risk. It is a persistent one, because the proxy keeps its balance across every implementation swap.
I have seen this pattern before. In 2018, during the post-ICO correction, I spent 400 hours auditing EtherDelta's source. The vulnerability I found there was different in mechanics β an integer overflow in the trading engine β but identical in character: a boundary that the developers assumed was enforced by context rather than by code. They assumed that because only the trading engine could reach the vulnerable path, the path was safe. But assumptions about reachability are not access control. They are hopes. EtherDelta's overflow and this proxy's missing modifier are the same class of error: the developer confused "this is hard to reach" with "this is guarded." It is never the same thing.
The Upgradeable Proxy as a Risk Amplifier
The upgradeable proxy pattern deserves its own treatment here, because it is the reason the risk was not bounded to a single deployment.
A proxy contract holds state β balances, permissions, configuration. An implementation contract holds logic. The proxy uses delegatecall to execute the implementation's code in the proxy's own storage context. This means the implementation can read and write the proxy's balance and its authorization list. When the admin points the proxy at a new implementation, the new code inherits everything the proxy holds.
The benefits are real: you can fix bugs, add features, and improve gas efficiency without migrating state. The costs are also real, and they are usually underweighted. The first cost is that the attack surface is not frozen. Every implementation swap is a chance to introduce a defect that operates on the same funds the old implementation guarded. The second cost is that the proxy's balance is a permanent honeypot. A dormant proxy with a non-zero balance and a slightly wrong implementation is a standing invitation.
In this case, the operator's proxy held 200 WETH of claimable collateral β or, more precisely, held the internal account to which 200 WETH of claimable collateral was attached. The implementation it delegated to lacked the auth guard on withdraw. The two facts combined into an open vault.
The bottleneck isn't the exploit. It's the infrastructure. The exploit took one afternoon. The infrastructure that made it possible β a forgotten proxy, an unclaimed auction, a missing modifier β accumulated over four years of neglect. Security failures are rarely single points in time. They are slow accretions that a single action finally releases.
The Reference Implementation Was Right There
The most frustrating part of this event, from an auditor's perspective, is that the correct pattern was publicly available and widely used. MakerDAO publishes makerdao/auction-keeper, the reference implementation for running a keeper. It handles the full lifecycle: bid, wait for auction close, call deal(), and settle. It does not leave claims dangling. It does not expose a withdraw without a guard.
The broader keeper ecosystem offers further templates. Gelato and Keep3r both provide execution frameworks with explicit permissioning models. A keeper built on those rails inherits their access-control assumptions. A keeper built from scratch inherits nothing but its author's discipline.
This bot was built from scratch. It was not audited. It did not follow the reference. It did not proactively call deal(), which would have emptied the claim before anyone else could settle it. It kept a live proxy with a balance and a lax implementation. Every one of those decisions was survivable in isolation. Together they were fatal.
I want to be precise about the attribution here, because it matters. MakerDAO's core contracts behaved exactly as designed. deal() being permissionless is not a bug; it is a liveness feature. Vat.flux is correctly authorized. GemJoin.exit is correctly authorized. The protocol did not fail. A third-party operator failed to secure its own infrastructure, and the protocol's design β correctly β did not protect it from that failure. There is no protocol-level fix that would have prevented this without breaking the liveness guarantee that deal() exists to provide.
The Economics of the Un-Dealt Auction
There is a deeper economic point buried here, and it is worth pulling out.
A liquidation keeper's business model is a spread: pay DAI for collateral, claim the collateral, sell it, keep the difference. The claim step β deal() β is where the keeper converts a right into an asset. In a healthy operation, that step is automated and immediate, because the value is real only once the collateral is in hand. A keeper that wins an auction and does not deal is a keeper that has, functionally, parked an asset in a contract it does not control the settlement of.
This is not a rare pathology. During the 2020 rush, many operators ran crude scripts. When the market cooled, the scripts were abandoned. Abandoned scripts leave abandoned claims. The 200 WETH here is a symptom of a systemic pattern: the DeFi keeper economy created a long tail of unclaimed, under-guarded assets that no one is monitoring.
In early 2022, I built a predictive model of under-collateralization across three lending platforms and forecast a 30% drop in TVL within six weeks. That model worked because systemic fragility is quantifiable if you look at the right signals. Dormant claims are the same kind of signal. They are not on any dashboard. They do not appear in TVL. They are the dark matter of DeFi β invisible mass that nevertheless exerts gravity on security outcomes.
Why Formal Verification Would Have Caught It
This is a case study in what formal verification is for.
The defect is a missing authorization guard. A formal specification of the contract would state a property like: "No state-changing function that transfers value may execute unless the caller is in the authorized set." That property is decidable. A model checker or a symbolic execution tool would have flagged withdraw immediately, because the function transfers value and has no caller restriction. This is not a property that requires cleverness to express. It requires only that someone write it down and check it.
In 2026, I led the security audit of a modular consensus layer involving five external teams. I enforced a review process that rejected 20% of initial designs for lacking formal verification. That decision delayed the launch by two weeks and, in my assessment, prevented a cross-chain bridge exploit. The teams that resisted the requirement argued that formal verification was expensive overhead for well-understood code. This proxy is the counterexample. The code was well-understood. The understanding was wrong. Formal verification does not care whether you understand your code; it cares whether the code satisfies a stated property. That is a different and more reliable question.
Contrarian: The Blind Spot Is Not the Exploit
The instinctive reading of this event is that it is a minor curiosity β 200 WETH, a third-party bot, a forgotten auction. Small numbers, low stakes, no protocol damage. The instinct is wrong, and the wrongness is the point.
The real risk is not this exploit. It is the population of contracts that share its preconditions. This bot is one proxy. The 2020 DeFi Summer produced thousands of keepers, many built from the same crude templates, many using upgradeable proxies, many abandoned. If a meaningful fraction of them share the same missing-modifier pattern, then there is a latent attack surface whose size no one has measured. The 200 WETH is a sample, not the population. And the attacker who found this sample demonstrated a specific, transferable skill: the ability to identify dormant MakerDAO auctions and route value out through under-guarded proxies. That skill does not expire when this one proxy is drained.
The second blind spot is the assumption that dormancy equals safety. In most software, an idle service is a low-risk service. In DeFi, the opposite is often true. A live, actively operated contract is monitored, patched, and watched by its owner. A dormant contract is unwatched. Its permissions drift out of alignment with its purpose. Its implementation may be swapped by an admin who has moved on. Its balance sits in place. Dormancy is not a reduction of risk; it is a transfer of risk from the owner (who stopped caring) to whoever is paying attention. Resilience isn't measured in the uptime of active systems. It's audited in the winter, when the operators have left and the contracts are still holding funds.
The third blind spot is governance. The proxy in this event had an admin β someone with the right to swap the implementation. That admin is, in effect, a multi-sig or a single key, depending on the operator. When I reverse-engineered the custodial cold-storage architectures behind the 2024 spot Bitcoin ETFs, I found that their multi-signature schemes deviated from decentralization ideals in ways that created single points of failure. The same critique applies here at a smaller scale. "Code is law" is a slogan that assumes the code is fixed. It is not fixed when an admin can replace it. The upgrade right always sits with someone β a key, a multi-sig, a governance vote β and that someone is the real authority, not the code. This bot's withdraw function was governed by nobody, which is worse than being governed by a single key, because at least a single key can be revoked. An absent guard cannot be revoked. It can only be exploited.
The fourth blind spot is the most uncomfortable. The funds moved through Tornado Cash. That routing is not incidental. It is a signal about the attacker's operational maturity. Tornado Cash is a standard tool for actors who want to obscure the origin of capital β and it is disproportionately favored by advanced persistent threat groups, including those associated with state-level operations. The confidence here is moderate, not high: anyone can use Tornado Cash, and using it does not prove affiliation. But the combination of a sophisticated understanding of MakerDAO's 2020 auction internals and a privacy-conscious exit path suggests an attacker who does this for a living. That is a different threat model than an opportunistic script kiddie, and it implies the attacker may be scanning for more contracts with the same profile.
Takeaway: The Vulnerability Forecast
The forward-looking question is not who lost 200 WETH. It is who is still holding an un-dealt auction behind an under-guarded proxy, right now, as you read this.
My assessment is that this event is a leading indicator, not a one-off. The conditions that produced it β abandoned keepers, unclaimed claims, upgradeable proxies with drifted authorization, permissionless settlement primitives β are all still present in the ecosystem. The attacker has demonstrated a method. The method is cheap to replicate. The targets are cheap to find: scan for proxies with non-zero balances, diff their implementation against known-good patterns, check for missing auth modifiers on value-transfer functions. This is a scriptable audit. Someone will script it.
The bottleneck isn't the difficulty of finding these contracts. It's the infrastructure that let them accumulate in the first place. Nobody owns the long tail. The operators have left. The protocol, correctly, does not intervene. And so the dark matter keeps drifting, unmeasured, until it collides with someone who is paying attention.
The lesson for builders is not subtle. If you operate a keeper, call deal() immediately and keep your proxies drained. If you write a proxy, put an auth modifier on every function that moves value, and verify it formally. If you run a protocol, accept that permissionless settlement is a liveness feature and that its cost is a permanent tail of unclaimed claims. And if you are a security researcher, start building the registry of dormant contracts β because the next 200 WETH, and the next 200,000, is already sitting there, waiting for a key that someone forgot to install.