At a glance
- The bug
- A wallet asks you to sign a message you cannot read, so you approve what it means rather than what it says
- The weapon
- A token approval, signed or sent, that grants a stranger the right to move your tokens whenever they like
- The theft
- Once the approval exists, the attacker calls transferFrom at leisure. No further click from you is needed
- The defence for users
- Prefer wallets that show human-readable data, approve exact amounts not unlimited, verify the spender and domain, and revoke old approvals
- The defence for builders
- Present typed, readable EIP-712 data, scope approvals tightly, and never ask for an infinite allowance out of convenience
The signature you cannot read
The strongest wallets on the safest hardware still have one weak joint: the moment a human decides whether to sign. If the thing on screen is an opaque hash, a row of hex, the decision is not really informed. This is blind signing, and it is the mechanism behind most drained wallets that were never technically "hacked" at all. Nothing was broken. The owner was persuaded to sign, and the signature did exactly what it said, which was not what they thought.
EIP-712 exists partly to fix this: it lets a wallet display structured, labelled fields instead of a hash, so you can see that you are about to grant a spender the right to move your tokens. The attack, then, is to get you to sign anyway, either by presenting a legitimate-looking site that requests a malicious approval, or by exploiting a wallet that still shows only a hash.
The approval drainer
Almost every token drainer relies on the same primitive: the approval. To let a marketplace or a swap move your tokens, you grant it an allowance, and from then on it can call transferFrom up to that limit without asking again. That is convenient and, pointed at an attacker, it is fatal.
// You are asked to approve a "spender" for an unlimited amount.
function approve(address spender, uint256 amount) external returns (bool);
// With that allowance set, the spender needs nothing more from you:
function transferFrom(address from, address to, uint256 amount) external returns (bool);
// you attacker everything
The modern version does not even need an on-chain approval first. EIP-2612 permit and Permit2 let you grant an allowance with a single off-chain signature. You sign a message; the attacker submits it and sweeps in the same breath. For an NFT the equivalent is setApprovalForAll, one signature that hands over every token in a collection.
// EIP-2612: the fields you are actually authorising.
struct Permit {
address owner; // you
address spender; // should be a protocol you trust; here, the attacker
uint256 value; // should be an exact amount; here, type(uint256).max
uint256 nonce;
uint256 deadline;
}
// Sign this and the spender can drain 'value' with no transaction from you.
What actually protects you
For anyone holding real value, the habits matter more than any single tool.
- Read the fields, not the vibe. Use a wallet that renders EIP-712 data. Before signing an approval or permit, check the spender address and the amount. A spender you do not recognise, or a value of "unlimited", is the whole attack.
- Approve exact amounts. The convenience of an infinite allowance is also its danger. Approve what a transaction needs, not the maximum.
- Verify the domain. A permit is bound to a token and a contract. A good wallet shows the domain; a mismatch between the site you think you are on and the contract you are signing for is a red flag.
- Revoke. Old allowances never expire on their own. Review and revoke them periodically, and keep long-term holdings in a separate wallet that never signs on unfamiliar sites.
- Clear-sign on hardware. A hardware wallet that decodes the transaction on its own screen removes the "trust the website's summary" step entirely.
If you build the site, you own the framing. Present decoded, human-readable EIP-712 data so users can see what they authorise. Request scoped allowances tied to the exact operation rather than type(uint256).max for convenience, and prefer short permit deadlines. Every unlimited approval your interface normalises is one an attacker's clone can imitate without looking unusual.
The short version. Most drained wallets were not broken into; the owner signed something they could not read. The payload is almost always an approval or a permit that hands a stranger the right to move your tokens, after which no further click is needed. Sign only data you can read, grant exact amounts to spenders you recognise, verify the domain, revoke what you no longer use, and keep serious holdings behind a wallet that never signs on the open web.
References & further reading
- Ethereum community, Token approvals. How allowances and
transferFromwork, and why they persist. - Ethereum Improvement Proposals, EIP-2612: permit. Granting an allowance with an off-chain signature.
- Ethereum Improvement Proposals, EIP-712. Typed data, so a wallet can show what is being signed.
- MetaMask, Revoking token approvals. Finding and removing allowances you no longer want.