How to Revoke Token Approvals and Permit2 Signatures
Most of the drained wallets I look at weren't hacked in the movie sense. Nobody broke any cryptography. The owner gave a contract permission to move their tokens, sometimes years ago, and someone eventually used it.
That permission is a token approval. And over the last few years a second kind has become common, one that most people don't even know they've given: Permit2 allowances and signed permits. Both can be cleaned up, but the way you do it is different, and there's one case where cleaning them up does nothing at all.
Let me go through it the way I'd do it on my own wallets.
The three kinds of permission you've probably granted
1. Classic ERC-20 approvals. When you swap a token for the first time, the dApp asks you to approve(spender, amount). Lots of interfaces default to an unlimited amount (2^256 - 1) so you never have to approve again. That approval sits on the token contract forever, until you change it.
2. NFT operator approvals. setApprovalForAll(operator, true) gives one address control over every NFT you hold in that collection, now and in the future. Marketplaces need it to list your stuff. Phishing sites love it for the same reason.
3. Permit2. Permit2 is a contract Uniswap deployed that many apps now use as a shared approval layer. You approve Permit2 once per token with a normal ERC-20 approval, and then individual apps get access through Permit2, either as a stored allowance (the AllowanceTransfer part) or through a one-time signature (the SignatureTransfer part).
The Permit2 detail that matters for security: a lot of it happens through signatures you sign off-chain. No transaction, no gas, nothing in your wallet history. MetaMask's own guide on signature phishing points out that attackers can sit on a signed Permit2 permit and use it later, when you've already forgotten about it.
Step 1: find what you've approved
Use a dedicated tool. Don't try to work it out from your transaction history.
- revoke.cash lists ERC-20 approvals, NFT approvals and Permit2 allowances per chain. It also explains each type well in its approvals guide.
- Etherscan has a Token Approval Checker under its tools menu that does the same for Ethereum.
Connect in read-only mode or just paste your address. You don't need to connect a wallet just to look.
When you go through the list, sort by risk:
- Unlimited approvals to anything you don't use anymore. Dead protocols, old bridges, random farms from 2021. If that contract has a bug or an upgradeable admin key gets compromised, your tokens are exposed.
setApprovalForAllon valuable collections, especially to marketplaces you don't use.- Approvals to unverified contracts or EOAs. An approval to a plain wallet address is almost never legitimate. That's a red flag.
- Permit2 allowances with long expirations.
Step 2: revoke the classic approvals
Revoking an ERC-20 approval is just another approval with amount zero:
IERC20(token).approve(spender, 0);
For NFTs:
IERC721(collection).setApprovalForAll(operator, false);
revoke.cash and Etherscan wrap these into a button, but under the hood that's all it is. Each revoke is a transaction and costs gas. On mainnet that adds up, which is why I'd rather revoke the dangerous ten than the harmless hundred.
Step 3: deal with Permit2
Permit2 needs a bit more thought, because there are three layers.
Layer 1: the ERC-20 approval to Permit2 itself. Permit2 can only move a token if you approved Permit2 on that token. Revoke that approval and every Permit2 path for that token is cut off: stored allowances and signed permits alike. If you suspect you signed something bad and don't know what, this is the hammer.
Layer 2: stored Permit2 allowances. These live inside the Permit2 contract, per token and per spender. Each has an amount and an expiration. You can set one to zero, or use lockdown, which takes a list of token/spender pairs and zeroes all of them in one transaction:
// AllowanceTransfer.sol
function lockdown(TokenSpenderPair[] calldata approvals) external;
Layer 3: permits you signed but nobody used yet. This is the scary one, because there's nothing on-chain to see. For AllowanceTransfer permits, Permit2 has invalidateNonces(token, spender, newNonce), which bumps your nonce so every older signature for that pair becomes invalid. The contract only lets you move the nonce forward a limited amount per call (up to 2^16), to stop griefing. For SignatureTransfer permits, there's invalidateUnorderedNonces(wordPos, mask), which marks specific nonces as used.
In practice, if you don't know exactly which signature you gave, doing layer 1 for the tokens you care about is faster and safer than hunting down nonces.
What about EIP-2612 permits?
Some tokens, USDC for example, support permit natively (EIP-2612). You sign a message, someone submits it, and it becomes a normal approval. A signed-but-unused permit stays valid until its deadline, and only stops working once the token's nonce for your address moves past it.
Once it's been used it's just a regular approval, so you revoke it like one. If you think someone has an unused permit from you, the reliable defence is to move the tokens to another address before they use it. MetaMask gives the same advice for signatures you can't revoke.
The case where revoking does nothing
Here's what people get wrong. If your private key or seed phrase leaked, approvals are irrelevant. The attacker doesn't need your permission to move tokens. They are you. They'll just call transfer directly.
Worse, every revoke you send costs ETH, and if there's a sweeper bot on the wallet, that ETH disappears before you can use it. I've seen people top up gas five or six times trying to revoke approvals on a wallet the attacker fully controls. Every time, the ETH was gone in the same block.
How to tell which situation you're in:
- Approval/permit exploit: tokens left your wallet through
transferFromcalled by some contract or address. Your ETH is untouched. New ETH you send stays put. - Key compromise: ETH leaves the moment it arrives, tokens move with plain
transfercalls from your own address, or your address suddenly has code on it. That last one is an EIP-7702 delegation, which I explain in my guide on how to check and remove a malicious 7702 delegation.
If it's a key compromise, stop revoking. You need a rescue: move the remaining assets out in one atomic Flashbots bundle where the gas money and the transfers land in the same block. I wrote the full process in how to rescue tokens from a compromised wallet with a sweeper bot.
Habits that keep you out of this mess
- Approve exact amounts for one-off interactions. It costs you one extra transaction next time. That's cheap insurance.
- Read what you sign. If a site asks for a signature and the wallet shows
PermitSingle,PermitBatchorPermitTransferFrom, you're signing spending rights. A "login" or "claim" should never need that. - Use a separate hot wallet for minting, airdrops and new protocols. Keep long-term holdings in a wallet that never connects to anything new.
- Do an approval audit every few months. Twenty minutes on revoke.cash is enough.
- Use a hardware wallet for the cold wallet. It doesn't stop you from signing a bad permit, but it does stop malware from signing one without you.
Quick reference
- ERC-20 approval:
approve(spender, 0) - NFT operator:
setApprovalForAll(operator, false) - All Permit2 access for a token: revoke the ERC-20 approval to the Permit2 contract
- Permit2 stored allowances:
lockdown([...pairs]) - Unused Permit2 allowance signatures:
invalidateNonces(token, spender, newNonce) - Unused Permit2 transfer signatures:
invalidateUnorderedNonces(wordPos, mask) - Leaked key: don't revoke, rescue
If you already lost the race and the wallet is being swept, the MEVDAO Token Rescue Bot does the bundled rescue for you. It's part of the subscription along with the sniping tools. For anything weird, message me on Telegram.
I send tips like these every week on the MEV Secrets email list. You can join right below.