Root Cause
The keeper proxy deployed by '79th Vault' included a drain() function (selector 0x8804d1de) that lacked MakerDAO's standard ds-auth (auth) modifier. The keeper had successfully bid on ETH-A collateral in four auctions during the March 2020 Black Thursday crash but the operator never called deal() to settle the auctions and retrieve the collateral. For over six years, 200 WETH sat accessible to any caller.
A secondary contributing factor was that the keeper's delegation was never revoked after the auction interactions completed — an open permission path that lingered unnoticed.
// Simplified vulnerable pattern (ds-auth omitted)
contract KeeperProxy {
// MakerDAO standard: protected functions use `auth`
// modifier auth { require(wards[msg.sender] == 1, "not-authorized"); _; }
// ❌ Missing auth modifier
function drain(address dst, uint256 wad) public /* no auth */ {
GemLike(weth).transfer(dst, wad);
}
}
// Correct pattern:
function drain(address dst, uint256 wad) public auth {
GemLike(weth).transfer(dst, wad);
}
CertiK attributed the flaw to implementation 0x68399ed8aa33C5b43F863EE6782de492006A5546. The attacker funded their wallet via Tornado Cash before executing.
Attack Steps
| Step | Action | Detail |
|---|---|---|
| 1 | Fund attacker wallet | New address funded through Tornado Cash |
| 2 | Identify exposed function | Locate drain() selector 0x8804d1de with no auth guard |
| 3 | Call drain() | Transfer all 200 WETH to attacker address in a single transaction |
| 4 | Launder proceeds | Convert WETH → ETH, route through Tornado Cash |
Time to exploit: Single transaction; no flash loan or multi-block manipulation required.
Impact
- Total loss:
200 WETH ($538,000–$543,000 at time of exploit) - Chain: Ethereum mainnet
- Core MakerDAO contracts: Unaffected — this was an independent keeper proxy, not MakerDAO core
- Root cause age: The vulnerable contract was deployed circa March 2020; the flaw persisted for over six years
- Operator position: 79th Vault has described the event as a private-key compromise, but on-chain evidence points to the unprotected function
Lessons for Auditors
- Audit all functions, including "utility" ones: Admin utilities like
drain(),sweep(), orrescue()are high-value targets. If a function can move tokens, it must have access control regardless of how rarely it is expected to be called. - Revoke delegations after use: The missing permission-revoke after delegation contributed to the exposure. Implement a cleanup checklist: after any privileged auction interaction, revoke the delegation immediately.
- Dead code / dormant contracts are still attack surface: A contract that hasn't been touched in years is not safe — it is merely undiscovered. Implement token balance monitoring and automated alerts for dormant contracts holding significant value.
- MakerDAO ds-auth pattern: When building on MakerDAO's keeper infrastructure, every public/external function that moves funds must have
modifier auth. Static analysis tools (Slither'smissing-zero-checkand custom detectors) should flag missingauthon value-transfer functions. - Execute or revoke auction wins: Any keeper that wins an auction should immediately call
deal()to settle, or transfer the winning bid rights to a monitored address. Leaving settled auctions uncollected is an operational risk.