The attacker never touched the contracts. Using an unauthorised API key they injected a script into BadgerDAO's own website, which quietly added unlimited spend approvals for their address to transactions users believed were routine. Every user who lost money was on the correct domain, using a real wallet, clicking a button the site put there.
Then they waited. The script ran for about three weeks, harvesting approvals from close to 200 accounts. On 2 December 2021 they drained all of them in under ten hours.
Two decision points, and the second is the one people miss. Users verified where the request came from and never verified what it asked for. And nobody lost money on the day they made the mistake, so nobody had any reason to look.
This is the material that does not exist in beginner education anywhere else, and it is the reason personal wallet compromises went from a minor share of stolen value to a dominant one.
The shift is simple to state. Attackers stopped trying to steal keys. Stealing a key is hard. Getting you to sign something is easy, and the result is identical: the chain executes the theft as an authorised action, and it is irreversible because it was never unauthorised.
The eight types
| Request | What it does | Danger |
|---|---|---|
transfer | Moves a specific amount, once | Low, but check the destination |
approve | Lets a contract move your tokens up to a limit | High if the limit is unlimited |
increaseAllowance | Raises a permission that already exists | High |
setApprovalForAll | Grants control of an entire token collection | Critical |
permit and Permit2 | Off-chain signature granting an allowance. No gas, no transaction, invisible in your history | Critical |
eth_sign and raw hex | Signing data you cannot read | Never |
| EIP-7702 delegation | Points your account at contract code, and persists until replaced | Critical |
| Plain message, SIWE | Proves you control an address | Low, if it really is a login |
Read the table as two groups rather than eight rows.
One-time actions move a known amount once. transfer is the whole group.
Standing authority is everything else. It grants a capability that persists after you close the tab, and it is redeemable at a time the attacker chooses. This is the distinction that matters. A drainer does not need to empty your wallet while you are watching.
Why the free one is the worst one
Approvals used to cost gas. That was accidentally protective: a transaction appeared in your history, you paid for it, and it was visible afterwards.
EIP-2612 permit and Permit2 removed the cost. You sign a structured message off-chain. No gas. No transaction. Nothing in your history to review later. The attacker holds a valid signed authorisation and submits it whenever they like.
Users read "no gas fee required" as reassurance. It is the opposite. The absence of a transaction means the absence of a record, and the absence of a record means you will not find it in an audit of your own history. Cost is not a proxy for risk, and here it is inverted.
EIP-712 is the standard that makes these messages structured rather than opaque, which is what lets a good wallet render "Approve USDC spending by 0x..., limit 500" instead of a wall of hex. That is a real improvement. It is also why the next lesson exists: when the rendering fails, the protection fails with it, and the request still looks signable.
Unlimited by default
Most interfaces request an unlimited allowance. The reason given is convenience: approve once, trade many times, pay gas once.
The trade is that you have granted a contract permission to move every unit of that token you will ever hold in that address, for as long as the approval stands. If the contract is upgradeable, if its admin key is compromised, or if it was never what it claimed, the exposure is not the amount you traded. It is the balance.
Approving the amount you actually intend to spend costs more gas across many trades. It is usually the right call anyway, and lesson 5 covers how to audit and revoke what you already granted.
My hardware wallet protects me from this.
It does not. A hardware wallet protects the key. Every request in this table is a legitimate operation performed with your key, correctly, at your instruction. The device will sign it because you told it to. Hardware protects you from key extraction and from nothing in this lesson.
setApprovalForAll, specifically
This one deserves separate attention because the name understates it and the interface rarely clarifies.
It does not approve a token. It approves an operator for a collection. Listing an item for sale legitimately requires it, which is exactly why the request looks routine, and why fake listing flows are built around it. If you grant it to a hostile contract, every item in that collection is transferable at the attacker's discretion, including items you acquire afterwards.
When you cannot classify it
There is one correct answer and it is short.
If you cannot say which row of the table a request belongs to, do not sign it. Not "sign carefully." Not "sign a small amount first." An unclassifiable request is an unverifiable request, and no legitimate process is destroyed by a five minute delay while you work out what it is.
Signature requests divide into things that move a known amount once, and things that grant standing authority redeemable later. Almost every large individual loss in the last three years came from the second group, signed by someone who thought they were doing the first. The free ones are the dangerous ones, unlimited is the default and rarely necessary, and hardware does not save you. The Signature Range at the end of this course tests exactly this, and it requires zero false negatives on the hostile requests.