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.
The Permit-drainer family is the single most dangerous class of crypto signature scams in 2024-2026. They bypass every "approval review" tool that only looks at transactions.

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's Permit, PermitSingle, PermitBatch or PermitTransferFrom, this is an approval-equivalent. Treat it accordingly.
  • Check the value / amount field. 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.
The fundamental property hasn't changed: a signature you didn't fully understand is still a signature. Guard exists so the "I didn't understand" path stops being a one-click route to losing funds.