The basics
On Ethereum and EVM chains, there are two ways for a smart contract to spend your ERC-20 tokens: you call transfer yourself, or you approve the contract to call transferFrom on your behalf. The classic approval pattern is on-chain: you send a transaction calling approve(spender, amount) on the token contract, and from then on the spender has that allowance.
That works, but it costs gas. Worse, it requires two transactions for any "swap one token for another" interaction (approve, then swap). UX teams hated this for years. So in 2017, Uniswap V2 shipped permit: a way to grant the same allowance via an off-chain signature instead of a transaction. EIP-2612 standardised it. Today the equivalent for any token (even ones that don't natively support Permit) is Permit2 by Uniswap Labs.
What you actually sign
The signature is a piece of structured data per EIP-712 — a way to make typed JSON-like payloads cryptographically signable. The payload has three parts:
Domain separator
Says which contract the signature is for. Includes the contract name, version, chain ID and the contract's address. This is what stops the same signature from being valid on a different chain or a different contract.
Primary type
The name of the data type being signed — for EIP-2612 it's Permit, for Permit2 it's PermitSingle or PermitBatch.
Message
The actual data. For a Permit it looks like this:
{
"owner": "0xYou…",
"spender": "0xContract…",
"value": "1000000000000000000", ← amount, in token units
"nonce": 12,
"deadline": 1748000000
}When you sign, you produce a 65-byte ECDSA signature. Anyone holding the signature can submit a call to permit(owner, spender, value, deadline, v, r, s) on the token contract. That call atomically sets allowance(owner, spender) = value — exactly as if you had called approve on-chain.
Why this is dangerous
- It's not a transaction. No gas, no Etherscan entry, no wallet history row. A user reviewing their wallet a week later won't see anything.
- It's portable. The signature is a piece of data, not a chain event. The attacker can hold it for days before submitting.
- It looks like a login. Many wallets render typed-data signatures with very little context. You see a request to "sign a message" and you sign.
- It's unlimited by default in most drainers. Permit doesn't enforce a cap. Drainers set
value = 2^256 - 1, identical to an unlimited on-chain approval but invisible.
Permit2 makes it more general
Uniswap's Permit2 contract sits in front of any ERC-20, even ones without native Permit. You approve Permit2 once (on-chain), and then every subsequent permission to specific spenders is off-chain. The structure is similar:
PermitSingle {
details {
token: 0xUSDC…
amount: 1000000000 ← u160, max 2^160 - 1
expiration: 1748000000
nonce: 0
}
spender: 0xRouter…
sigDeadline: 1748000000
}Note that Permit2 limits the amount to u160 instead of u256. That's still ~1.46 × 1048 — vastly more than the supply of any real token. The "unlimited Permit2" pattern still works; UTXO Guard flags anything above 2160... wait — Permit2's amount IS bounded to u160. So the "unlimited" comparison threshold is the same value the Permit2 contract treats as "infinite": 2^160 - 1.
How to read what you're signing
- Open the wallet's "details" section before signing — most wallets hide the typed-data fields by default.
- Look at the
primaryType: if it'sPermit,PermitSingle,PermitBatchorPermitTransferFrom, this is an approval-equivalent. Treat it accordingly. - Check the
value/amountfield. If it's a long string of f's or extremely large compared to your balance, it's unlimited. - Check the
spender. Is it a contract you recognise? Etherscan it before signing. - Check the
deadline. Far-future deadlines are normal but extreme far-future ones (years away) are a red flag.
What UTXO Guard does about it
Guard's content script intercepts eth_signTypedData_v3 and eth_signTypedData_v4 before they reach the wallet popup. It parses the payload using the SafeSign kernel and surfaces these signals:
PERMIT_UNLIMITED critical value ≥ 2^160 → drainer signature PERMIT medium bounded permit, but still off-chain approval TYPED_DATA medium other EIP-712 signature RAW_SIGN critical eth_sign — signs arbitrary hash, never use
For high or critical, Guard slides a banner down from the top of the page before the wallet popup gets focus. You see "Unlimited Permit signature — spender 0xDEAD…beef can drain this token at any time" in plain language. The decision still happens in your wallet — Guard never blocks.
If you've already signed one
Permit signatures with bounded deadline expire automatically once the deadline passes. If you signed something recently and you're not sure if it's been submitted:
- Check your wallet for a pending allowance to the spender address. If it shows up, revoke it via revoke.cash or your wallet's revoker — that creates a new approval(spender, 0) on-chain.
- If you're holding a high balance of the affected token, move the balance to a fresh wallet. A signed permit can't drain tokens you don't hold.
- If you signed a Permit2 PermitSingle, you can call increaseNonce on the Permit2 contract to invalidate outstanding signatures for that token+spender pair.