TL;DR
At 06:13:11 UTC on October 6, 2026, one Ethereum transaction took 200 ETH, $542,982 at the time per CertiK, out of a third-party auction keeper that had taken part in MakerDAO's ETH-A liquidations. The keeper won four 50 ETH lots with zero bids during the Black Thursday crash of March 2020 and never settled them. One function in its unverified implementation, selector 0x8804d1de, skipped the ds-auth check that every other privileged function runs. It accepted any address as an "adapter", called Vat.hope() on it, then called back into it. The attacker's adapter used that delegation to settle auctions 1457 to 1460 on the retired ETH-A Flipper, move the 200 ETH of collateral to itself and exit it as WETH. MakerDAO's core contracts behaved as designed. The attacker was funded with 0.1 ETH from Tornado Cash sixteen minutes before the exploit, and all 200 ETH went back into Tornado in twenty 10 ETH deposits within sixteen minutes of it. Value nobody claims is guarded only by the oldest code that can still claim it.
What Happened to the Keeper?
A keeper is a bot that takes part in a protocol's maintenance markets, here MakerDAO's collateral auctions. This one was an upgradeable proxy at 0x9c05...8273, created on February 7, 2020, five weeks before the crash. Blockscout lists its implementation at 0x6839...5546, and CertiK's address table records that implementation's source as not verified.
On Black Thursday the keeper won four ETH-A lots, auctions 1457, 1458, 1459 and 1460, each worth 50 ETH, each for a zero bid. Winning a MakerDAO Flipper auction does not move the collateral. The winner, or anyone, has to call deal() afterwards. Nobody did. Blockscout's record of direct calls to the keeper shows the owner address 0xaDC3...F073 working it on March 12, 2020, and then nothing from the owner after that.
So for six and a half years, 200 ETH of won collateral sat inside MakerDAO's Vat, credited to a retired auction house and claimable to a keeper whose owner had stopped calling it.
Defimon Alerts, quoted by Crypto Times, put the scope plainly: "MakerDAO core (Vat/Flipper/GemJoin) behaved as designed; the flaw is the third-party keeper's unprotected exit function." Protos notes the contract was not part of the current Sky ecosystem.
What Was Black Thursday, and Why Were There Zero-Bid Lots at All?
On March 12 and 13, 2020, ETH fell roughly 50%, and the liquidations that followed "effectively swamped" MakerDAO's liquidators, according to the MakerMan post-mortem summarized by Cointelegraph. Network congestion kept most bidders out, and a single bidder won nearly 62,843 ETH for virtually zero DAI across 1,461 auctions. The system ended up undercollateralized by an estimated 6.65 million DAI.
That night is the named precedent here, and this incident is its long tail. The 2020 failure was economic: auctions cleared at zero because nobody else could get a bid in. The 2026 failure is an access-control bug sitting on top of that leftover value. The four lots in this keeper were won at zero, never collected, and then forgotten by everyone except, eventually, an attacker.
What Does Vat.hope() Actually Grant?
The Vat is MakerDAO's core accounting contract. It tracks every account's collateral balance (gem) and internal DAI. One line in the dss source defines delegation:
function hope(address usr) external { can[msg.sender][usr] = 1; }After hope(usr), the Vat's wish() check returns true for usr on every action against the caller's account, including flux(), which moves collateral between accounts. The grant has no scope, no amount and no expiry. It ends only when the account calls nope(usr).
A keeper needs this kind of delegation to work with auction houses and adapters. That is why the keeper's own hope, flux and move wrappers sit behind ds-auth, the owner-and-authority check MakerDAO-era contracts inherit from the ds-auth library. CertiK found all of them revert with ds-auth-unauthorized for a stranger. One function did not.
The other half of the bug is in the Flipper. deal(id) has no caller restriction. Once an auction is finished, anyone can call it, and it flux-es the lot to the winning bidder, bids[id].guy. Settling someone else's auction is normal and harmless, because the collateral lands in the winner's account. It only becomes theft if the caller already controls the winner's account.
How Did the Exploit Work?
The attacker needed control of the keeper's Vat account for the length of one transaction. Function 0x8804d1de handed it over.
CertiK's reconstruction of the function: it takes any address as an adapter, calls drip() and vat() on it, checks Vat.can() to see whether it has already delegated to that address, calls Vat.hope() on it if not, and then calls the adapter's join(). That last call hands execution to attacker-written code while the fresh delegation is live. The function never calls nope, so the delegation outlived the transaction. CertiK lists the attacker's adapter, 0xf09a...C170, as still holding Vat delegation over the keeper.
CertiK calls the root cause a missing access control. In MITRE's catalogue it is CWE-862, missing authorization, here on the one function that both grants delegation and passes control to the grantee.
The Attack, Step by Step
| # | Time (UTC, Oct 6) | Step | Source |
|---|---|---|---|
| 1 | 05:57:23 | Attacker EOA 0x01EB...A5FA receives 0.1 ETH from Tornado Cash, its only funding | CertiK, funding tx |
| 2 | 06:12:11 | The EOA deploys the attack contract, 0xEc99...1dcF | CertiK |
| 3 | 06:13:11 | Exploit tx: the attack contract creates adapter 0xf09a...C170 and calls keeper function 0x8804d1de with it | CertiK |
| 4 | same tx | Keeper calls Vat.hope(adapter), then adapter.join() | CertiK |
| 5 | same tx | Inside join(), the adapter calls deal() on ETH-A Flipper auctions 1457 to 1460, crediting 200 ETH of collateral to the keeper's Vat account | CertiK, Crypto Times |
| 6 | same tx | The adapter flux()es all 200 ETH to itself and exits it through the ETH-A GemJoin as 200 WETH | Blockscout token transfers |
| 7 | same tx | WETH goes to the attack contract, is unwrapped, and 200 ETH lands in the attacker EOA | CertiK, Blockscout |
| 8 | 06:19:35 to 06:28:35 | Twenty 10 ETH deposits into the Tornado Cash router | CertiK, Blockscout, attacker txs |
The token-transfer log for the exploit shows exactly three WETH movements of 200.0 each: GemJoin to the adapter, adapter to the attack contract, and the attack contract's unwrap.
How Much Was Taken, and Which Figure Should You Trust?
200 ETH. Every source agrees on the quantity, and the on-chain transfer log shows exactly 200.0 WETH leaving the ETH-A GemJoin.
The dollar figure differs by the ETH price each source used. CertiK says $542,982 at the time of the exploit. Defimon Alerts and Crypto Times say about $538,000. Protos rounds to "over $500,000". All three describe the same 200 ETH at slightly different ETH prices. We use CertiK's figure because it is pinned to the time of the exploit.
One internal inconsistency is worth fixing for anyone tracing funds. CertiK's key-transactions table calls the 06:19:35 deposit the "first of nine 10 ETH deposits", while its fund-flow section says 20 deposits of 10 ETH. The arithmetic favors CertiK's second count: nine deposits of 10 ETH would leave 110 ETH unaccounted for, and twenty account for all 200. The attacker's transaction list on Blockscout shows the 10 ETH deposit calls to the Tornado Cash router still running at 06:28:35 UTC, fifteen minutes after the exploit.
The Economics: 0.1 ETH In, 200 ETH Out
The economics are lopsided. The attacker put in 0.1 ETH from Tornado Cash, and Etherscan records a fee of about 0.0005 ETH for the exploit transaction. The return was 200 ETH, roughly 2,000 times the funding.
The preparation was short. Funding at 05:57, the attack contract deployed at 06:12, the drain at 06:13, and the first Tornado deposit six minutes later. Sixteen minutes from first funding to drain.
What the attacker needed was knowledge: that this keeper existed, that its implementation had an unguarded path to hope, and that the Flipper still held unsettled lots won by it. The last part is public by design. bids is a public mapping on the Flipper, and deal() deletes an auction's entry once it settles, so any finished auction that still has a winner (guy) on record is an unsettled lot. A loop over auction IDs finds every one of them. How this attacker actually found the keeper is not public. Since the implementation source was never verified, the unguarded function most likely came from reading the bytecode, which is our inference rather than anything the attacker or CertiK has said. Either way, the capital required was 0.1 ETH.
Why Was 200 ETH Still Sitting There?
The value was stranded by design. Flipper auctions settle on request, and a keeper that never asked never received. Anyone could have called deal() on those four auctions at any point, but it sends the lot to the winner, so a stranger who settled them would have paid gas to move 200 ETH into someone else's account. Nobody had a reason to. The collateral stayed in the Flipper's Vat account, with the keeper recorded as guy on four finished auctions, through everything that followed, including the protocol's rebrand to Sky.
Retiring a contract from a protocol's active path does not retire the obligations it still holds. The ETH-A Flipper stopped running auctions, but deal() still worked for anything left inside it, as it should. The keeper's owner stopped running it, but its proxy kept every function it had in March 2020, including the one without ds-auth.
The keeper was an upgradeable proxy, so its admin could have swapped the implementation, or settled the lots, at any point in those six years. Nothing on the record says anyone tried.
Would an Audit Have Caught It?
Yes, if the review covered every external function of the keeper implementation. The bug is a pattern reviewers look for first: one function that grants delegation and makes an external call, sitting next to sibling functions that all carry ds-auth, while it carries nothing. A reviewer reading the delegation path asks who may call it, and the answer here is anyone.
We have found no public audit report for this keeper, and its implementation was never verified on-chain, so there is no record of whether it was reviewed in 2020.
The harder failure was the absence of a review after the keeper was retired. Its code was the same on October 5, 2026 as in March 2020, and nobody on the defending side was reading it. Our smart contract audits map each contract's trust boundaries and upgrade mechanisms, then review every function line by line, which is exactly where a delegation grant without an access check shows up. Scope is agreed with your team before review begins, so a keeper you retired in 2020 with value still owed to it can sit in that scope next to the contract you ship next month.
Could Monitoring Have Changed the Outcome?
No. Delegation, settlement, extraction and exit all landed in one transaction, block 26,131,471, so there was no on-chain moment between setup and loss for an alert to act on. The standing condition beforehand was 200 ETH of finished, unsettled auctions on a retired Flipper, which had been the normal state of those four lots since 2020.
The signals came during and after:
- A Tornado-funded EOA with 0.1 ETH deployed a contract fifteen minutes after funding.
- A keeper whose owner had been silent since March 2020 granted Vat delegation, by
hope, to a contract created in the same transaction. - Four ETH-A Flipper auctions from 2020 settled in one call, followed by a 200 WETH exit through the ETH-A GemJoin.
- Twenty 10 ETH Tornado deposits over nine minutes.
Every one of these arrived after the keeper's claim had already been spent. By the time any alert could fire, the 200 ETH sat in the attacker's own EOA, and there was no contract left to pause or patch.
Legacy Contracts Are Live Attack Surface
Every protocol older than a few years has a population of contracts like this one: keepers, liquidator bots, migration helpers, old vault adapters, deprecated routers. Most hold nothing. Some hold standing claims, unsettled auctions, unclaimed rewards, approvals, or delegations, and those claims are only as safe as the least-protected function in the oldest contract that can exercise them.
An audit certifies the code you showed it, on the day you showed it. This keeper's code never changed between 2020 and 2026, and neither did the value it could claim. Nobody re-read it after the owner went quiet.
The same shape runs through the delegated-permission incidents on this blog. A Safe module is a delegation, as the SquidRouterModule exploit showed; a standing ERC-20 approval is a delegation, as the Jared honeypot showed. Vat.hope is the most complete delegation in MakerDAO's design. Granting one from an unauthenticated function, then calling the grantee, is the same failure with fewer steps.
What Should Operators Do Now?
-
Inventory every contract you have ever deployed that can still claim value. For each one, list what it is owed or can pull: unsettled auctions, unclaimed rewards, Vat
gemordai, approvals it holds, delegations it holds. A retired keeper with a claim is live attack surface. -
Settle or sweep stranded value, then revoke. If a dormant contract is owed funds, collect them with the owner key and move them somewhere monitored. Then call
nopeon every Vat delegation, and revoke approvals the contract no longer uses. -
Audit every path to a delegation grant. Any function that calls
hope,approve,enableModuleor an equivalent must carry the same access control as the most privileged function in the contract. Grep for these calls in your oldest code first. -
Never call into an address you just delegated to. Grant-then-callback hands attacker code a live permission. If a callback is unavoidable, revoke the delegation in the same function before returning, and restrict which addresses can be passed in.
-
Use the proxy admin while you still hold it. An upgradeable keeper can be patched or emptied. Check that the admin key still exists and is controlled, and use it before a stranger finds the gap.
What We Still Do Not Know
- Who operated the keeper. CertiK identifies the owner address,
0xaDC3...F073, and nothing public ties it to a named team. - Whether the owner knew about the four unsettled lots, or about the unguarded function.
- Whether any other 2020 keeper with unsettled Flipper lots carries the same function. CertiK's analysis covers this keeper only, and we have not seen a public census of the unsettled lots still recorded on the retired Flippers.
No statement from the keeper's owner or from Sky had been published as of October 12, 2026.
Frequently Asked Questions
Was MakerDAO or Sky hacked?
No. The Vat, Flipper and GemJoin all behaved as designed. Defimon Alerts said the flaw was the third-party keeper's unprotected function, and Protos noted the contract was not part of the current Sky ecosystem. The 200 ETH belonged to the keeper's unsettled 2020 auction wins.
How much was stolen in the MakerDAO keeper exploit?
200 ETH, which CertiK valued at $542,982 at the time of the October 6, 2026 exploit. Other outlets give about $538,000 for the same 200 ETH at a slightly different price.
Can the stolen ETH be recovered?
Not yet, and the odds are poor. The attacker deposited all 200 ETH into Tornado Cash in twenty 10 ETH deposits within sixteen minutes of the exploit.
Are other old MakerDAO keepers at risk?
Yes, any that hold unsettled auction wins or other claims and expose an unauthenticated path to Vat delegation. Any operator of a 2020-era keeper should check its Vat balances and delegations now.
Sources / References
- CertiK: MakerDAO Legacy Auction Keeper Incident Analysis
- Etherscan: exploit transaction 0xbb6940f7...
- Etherscan: attacker funding transaction 0xd4c0b18a...
- Blockscout API: exploit transaction token transfers
- Blockscout API: attacker EOA transactions
- Blockscout API: keeper proxy address
- Blockscout API: keeper proxy creation transaction
- Blockscout API: direct calls to the keeper proxy
- The Crypto Times: Dormant MakerDAO Keeper Drained of $538K, Core Remains Intact
- Protos: Black Thursday revisited: MakerDAO keeper exploited for $500K
- Cointelegraph: Maker Debt Crisis Post-Mortem Recommends New Safeguards
- makerdao/dss: vat.sol
- makerdao/dss: flip.sol
- MITRE CWE-862: Missing Authorization



