October 16, 2026
|
Exploit Postmortem
MakerDAO Keeper Exploit

MakerDAO Keeper's $543K Loss and How Olympix Would Have Prevented It

‍Date: October 6, 2026

Chain: Ethereum

Loss: 200 ETH, about $543K at the time of the exploit

Class: missing access control on a function that hands out Vat permissions

In March 2020, during MakerDAO's Black Thursday liquidations, an automated bidder won four lots of ETH collateral for nothing. It bid zero DAI on each 50 ETH lot, won all four, and then never came back to collect them. For six and a half years, 200 ETH sat inside a MakerDAO auction contract, credited to that keeper.

On October 6, 2026, someone else collected it. A wallet funded with 0.1 ETH from Tornado Cash deployed an attack contract at 06:12:11 UTC, and one minute later a single transaction settled the four 2020 auctions, pulled the collateral out of MakerDAO, and delivered 200 ETH, worth about $543,000, to the attacker. MakerDAO's core contracts did exactly what they were built to do the whole time. The keeper's own code handed the money over, through one function that skipped the permission check every other sensitive function in the contract enforced.

Zero Bids on Black Thursday

On March 12, 2020, the price of ETH fell roughly 50% within 24 hours, and MakerDAO's liquidation system was flooded with undercollateralized vaults. MakerDAO sold the seized collateral through auction contracts called Flippers, and third-party bots called keepers did the bidding. Network congestion combined with low DAI liquidity meant some keepers won large lots of ETH by bidding zero. CoinDesk later reported that about $8M in collateral left the system this way.

A keeper that wins a Flipper auction still has to call deal() to settle it. Settling moves the collateral into the winner's account in the Vat, MakerDAO's core accounting contract, and from there it can be withdrawn through a GemJoin adapter. This keeper, an upgradeable proxy at 0x9c05a05893Ada984FC20D0DA0c046De5Cc0e8273, won auctions 1457 through 1460 from the ETH-A Flipper and never called deal() on any of them. MakerDAO has since rebranded as Sky and retired the Flipper, but the contract kept its records, and the 200 ETH stayed exactly where the auctions left it.

Every Function Guarded Except One

Anyone can call deal() on a Flipper auction, so settling the four lots was never the obstacle. Settling them only credits the collateral to the keeper's own Vat account, and moving anything out of that account takes the keeper's permission. In the Vat, that permission is called hope. When an account calls hope on another address, it gives that address full control over its collateral and internal DAI.

According to CertiK's analysis, every privileged function in the keeper's implementation, including its own wrappers for hope, flux, and move, runs a ds-auth check first and reverts for any caller who isn't authorized. ds-auth is the same access-control library MakerDAO's own contracts use, and the keeper applied it consistently, with one exception.

Function 0x8804d1de accepts any address as an adapter. It asks the adapter for a couple of details, checks with the Vat whether the keeper has already delegated to it, calls Vat.hope on it if it hasn't, and then calls the adapter's join() function. At no point does it check who is calling. The researcher who decompiled the unverified implementation for the DeFiHackLabs proof of concept found no owner lookup or caller comparison anywhere before the function's body runs, and the function never calls nope to take the permission back afterward.

That made the one function in the contract that gives the keeper's Vat permissions to an outside address also the one function anyone could call, and it finished by handing execution to code the caller wrote while that permission was live. The implementation's source was never verified on Etherscan, so its code couldn't be read there, and the flaw sat in the live implementation next to 200 ETH that needed exactly this permission to move.

Sixty Seconds From Deployment to Drain

At 05:57:23 UTC, a new wallet received 0.1 ETH from Tornado Cash, its only funding. At 06:12:11 UTC, the attacker deployed an attack contract, and at 06:13:11 UTC the exploit transaction ran. Inside it, the attack contract created an adapter module preloaded with the addresses of the ETH-A Flipper, the Vat, the ETH-A GemJoin and WETH, a payout address, and the four auction IDs.

The attack contract called the keeper's function 0x8804d1de with the module's address. The keeper called Vat.hope on the module, giving it control of the keeper's Vat account, and then called the module's join(). Inside that callback, the module called deal() on auctions 1457, 1458, 1459, and 1460, which credited 200 ETH of collateral to the keeper's Vat account. It then used its new permission to flux all 200 ETH to itself and exited it through GemJoin as 200 WETH, which the attack contract unwrapped and sent to the attacker's wallet, all within the same transaction. At 06:19:35 UTC, the first 10 ETH deposit reached the Tornado Cash router, and the rest followed in 10 ETH lots.

Every MakerDAO contract in that sequence behaved correctly. The Flipper settled auctions for their rightful winner, the Vat honored a hope call that really did come from the keeper, and GemJoin released collateral that was really in the account. The attacker never needed the keeper's authorization, because function 0x8804d1de did the authorizing on the attacker's behalf.

A Permission That Outlived the Attack

The collateral came out of the keeper's own Vat account, and CertiK identifies the keeper's owner as the same address that operated it in March 2020. Function 0x8804d1de never revokes the delegation it grants, so CertiK lists the attacker's module as still holding control over the keeper's Vat account after the transaction ended. The delegation outlived the attack the same way the collateral outlived Black Thursday.

How Olympix Finds the One Function That Doesn't Match

Olympix reads a contract as an attacker would, as a set of states, transitions and entry points, and it doesn't need verified source on a block explorer to do that, because it works on the code a team actually ships. In this keeper, the states that matter are who controls the keeper's Vat account, and every entry point that can change that is a path worth mapping. The engine reads signals to decide which of those paths to attack first, and Olympix's static analysis surfaces the one here: a public function that calls Vat.hope on an address the caller supplies, with no ds-auth check in front of it, in a contract where every neighboring function that touches the same permission is guarded.

The keeper writes its own invariant into the code surrounding that function. Every other function that moves or delegates its Vat balance sits behind ds-auth, so the rule is plain: only an authorized caller should ever be able to grant or use control over the keeper's Vat account. BugPoCer reads that rule off the guarded functions and then tries to break it. It runs Counterexample-Guided Invariant Refutation, working outward from the keeper as a whole and executing every path into the Vat's permission system, and the path through 0x8804d1de breaks the rule with a single call from an address the keeper has never seen.

The result is a counterexample you can run. A fresh address passes in an adapter it controls, the keeper grants the adapter its Vat permissions and calls back into it, and the adapter settles the four open auctions, moves the 200 ETH of collateral to itself, and exits it through GemJoin. BugPoCer returns that sequence with the minimal conditions that trigger it and a generated Forge test that replays it, which is the October 6 attack produced in development rather than on mainnet.

Olympix has shown this class on a real exploit before. When the ycdeal3 RWAVault lost $387K in April 2026 to a withdraw function that never checked who was calling, BugPoCer flagged both the withdraw and redeem paths as High-severity missing access control and produced a working proof of concept for each. The keeper's function carries the same defect in a different setting, a sensitive action anyone can reach because one function skipped the check its neighbors enforce.

Six and a Half Years Later

The 200 ETH in this keeper survived Black Thursday, a protocol rebrand, and the retirement of the auction contract holding it. It couldn't survive one function, written alongside properly guarded ones, that gave the keeper's Vat permissions to whoever asked. The rule that function broke was already written into the rest of the contract, which made it a rule an engine could infer and test long before 2026. To see how Olympix maps the permission paths in your own contracts and hands back a runnable counterexample for every one that breaks, book a technical walkthrough.

On-Chain References

Protocol: Third-party MakerDAO ETH-A liquidation keeper

Chain: Ethereum

Date: October 6, 2026, 06:13:11 UTC

Loss: 200 ETH (about $542,982 at the time, per CertiK)

Attacker EOA: 0x01EB...A5FA

Attack contract: 0xEc99...1dcF

Adapter module (still holds Vat delegation): 0xf09a...C170

Keeper proxy: 0x9c05...8273

Keeper implementation (source unverified): 0x6839...5546

Keeper owner: 0xaDC3...F073

ETH-A Flipper: 0xd8a0...5531

Vat: 0x35D1...492B

ETH-A GemJoin: 0x2F0b...042A

Funding tx: 0xd4c0...8ac6

Attack contract deployment tx: 0x4a58...1167

Exploit tx: 0xbb69...a88c

Block: 26131471

First Tornado Cash deposit: 0xb8ec...efeb

Sources

CertiK: MakerDAO Legacy Auction Keeper Incident Analysis

DeFiHackLabs PR #1295: PoC for the MakerDAO flip-keeper unprotected drain selector

CoinDesk: Mempool Manipulation Enabled Theft of $8M in MakerDAO Collateral on Black Thursday

‍

More from Olympix:

No items found.

Ready to Shift Security Assurance In-House? Talk to Our Security Experts Today.