DEV Community

ohmygod
ohmygod

Posted on

The Phantom Approval: How ERC-2612 Permit Signatures Are Being Weaponized to Drain DeFi Wallets Without On-Chain Traces

In Q1 2026, over $47 million was stolen through permit-based phishing — attacks where victims sign an off-chain message and lose their entire token balance without ever submitting a transaction. No approve() call appears on-chain before the drain. No Etherscan alert fires. The tokens simply vanish.

This is the Phantom Approval attack: weaponizing ERC-2612's permit() function to bypass every traditional approval monitoring defense.

How ERC-2612 Permit Works (And Why It's Dangerous)

ERC-2612 added gasless approvals to ERC-20 tokens. Instead of calling approve() on-chain, a user signs an EIP-712 typed message off-chain. Anyone can then submit that signature to the token contract's permit() function to set the approval.

// The permit function — anyone can call this with a valid signature
function permit(
    address owner,
    address spender,
    uint256 value,
    uint256 deadline,
    uint8 v, bytes32 r, bytes32 s
) external {
    require(block.timestamp <= deadline, "ERC2612: expired deadline");

    bytes32 structHash = keccak256(abi.encode(
        PERMIT_TYPEHASH,
        owner,
        spender,
        value,    // Usually type(uint256).max
        _useNonce(owner),
        deadline  // Usually type(uint256).max
    ));

    bytes32 hash = _hashTypedDataV4(structHash);
    address signer = ECDSA.recover(hash, v, r, s);
    require(signer == owner, "ERC2612: invalid signature");

    _approve(owner, spender, value);
}
Enter fullscreen mode Exit fullscreen mode

The critical insight: the approval happens entirely off-chain until the attacker decides to use it. Traditional monitoring tools watch for Approval events. With permit, there's no event until the attacker calls permit() + transferFrom() in the same transaction — by which point the funds are already gone.

The Kill Chain: 5 Steps to a Phantom Drain

Step 1: Social Engineering the Signature

The attacker creates a convincing context for signing. Common vectors in 2026:

  • Fake NFT minting sites that request "gasless approval"
  • Compromised DeFi frontends (the Neutrl DNS hijack pattern)
  • Phishing DMs claiming "claim your airdrop" with a signing request
  • Malicious WalletConnect sessions on lookalike domains

Step 2: The Permit Payload

The victim sees a signing request for what appears to be a harmless message. But the EIP-712 typed data contains:

{
  "types": {
    "Permit": [
      {"name": "owner", "type": "address"},
      {"name": "spender", "type": "address"},
      {"name": "value", "type": "uint256"},
      {"name": "nonce", "type": "uint256"},
      {"name": "deadline", "type": "uint256"}
    ]
  },
  "primaryType": "Permit",
  "domain": {
    "name": "USD Coin",
    "version": "2",
    "chainId": 1,
    "verifyingContract": "0xa0b8...usdc"
  },
  "message": {
    "owner": "0xVICTIM",
    "spender": "0xATTACKER_CONTRACT",
    "value": "115792089237316195423570985008687907853269984665640564039457584007913129639935",
    "nonce": "0",
    "deadline": "115792089237316195423570985008687907853269984665640564039457584007913129639935"
  }
}
Enter fullscreen mode Exit fullscreen mode

The value and deadline are both type(uint256).max — unlimited approval, never expires.

Step 3: Signature Harvesting

The attacker now holds a valid EIP-712 signature. No on-chain transaction has occurred. The victim's wallet shows no pending approvals. Block explorers show nothing.

Step 4: The Atomic Drain

When ready, the attacker submits a single transaction:

contract PhantomDrainer {
    function drain(
        IERC20Permit token,
        address victim,
        uint256 amount,
        uint256 deadline,
        uint8 v, bytes32 r, bytes32 s
    ) external {
        // Step 1: Set approval using stolen signature
        token.permit(victim, address(this), type(uint256).max, deadline, v, r, s);

        // Step 2: Drain immediately in same tx
        token.transferFrom(victim, msg.sender, amount);
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Profit Laundering

Funds flow through Tornado Cash / Railgun / cross-chain bridges within the same block. Total time from permit submission to clean exit: ~12 seconds.

4 Real-World Permit Drain Campaigns in 2026

Case 1: The OpenSea Signature Replay ($8.2M, January 2026)

Attackers harvested permit signatures from a compromised OpenSea notification email campaign. Over 2,400 wallets signed what they thought was a "listing confirmation." The signatures were permit approvals for WETH, USDC, and DAI. Drains executed 72 hours later across 14 blocks.

Case 2: The Uniswap Permit2 Abuse ($12.7M, February 2026)

Uniswap's Permit2 contract (a universal approval manager) was targeted through fake DEX aggregator frontends. Users granted Permit2.permit() approvals thinking they were authorizing a single swap. The attackers used the Permit2's SignatureTransfer to drain all approved tokens across multiple contracts.

Case 3: The Aave Frontend Clone ($6.1M, March 2026)

A DNS poisoning attack redirected a subset of Aave users to a pixel-perfect clone. The clone's "deposit" function actually requested ERC-2612 permit signatures for aTokens. Since aTokens implement ERC-2612, the attackers could drain deposited positions.

Case 4: The Multi-Chain Permit Sweep ($19.8M, March 2026)

A coordinated campaign across Ethereum, Arbitrum, Optimism, and Base. Attackers deployed identical phishing contracts on all chains, harvesting permit signatures through a fake "cross-chain bridge" interface. The unified drainer contract executed permit+transfer atomically on each chain within 3 blocks.

Why Traditional Defenses Fail

Approval Monitoring Is Blind

Tools like Revoke.cash and Etherscan's token approval checker only detect on-chain approve() calls. Permit signatures exist off-chain until execution. By the time the Approval event fires (during the permit() call), the transferFrom() has already been submitted in the same transaction.

Simulation Doesn't Catch It

Transaction simulation services (Blowfish, Pocket Universe) can detect permit signing requests — but only if:

  1. They support the specific token's EIP-712 domain
  2. The phishing site doesn't fingerprint and evade simulation extensions
  3. The user has the extension installed

In practice, only ~15% of DeFi users have transaction simulation tools active.

Hardware Wallets Don't Help

Hardware wallets prevent key theft, not social engineering. If a user physically confirms a permit signature on their Ledger or Trezor, the signed message is just as dangerous. The hardware wallet faithfully signs whatever the user approves.

4 Detection and Defense Patterns

Pattern 1: Permit Deadline Enforcement

Never sign a permit with deadline = type(uint256).max. Enforce short deadlines:

// GOOD: 5-minute deadline
uint256 deadline = block.timestamp + 300;

// BAD: Never-expiring permit
uint256 deadline = type(uint256).max;
Enter fullscreen mode Exit fullscreen mode

For protocol developers: Add a maximum deadline check in your frontend:

function validatePermitRequest(deadline: bigint): boolean {
  const maxDeadline = BigInt(Math.floor(Date.now() / 1000)) + 3600n; // 1 hour max
  if (deadline > maxDeadline) {
    console.error("SUSPICIOUS: Permit deadline too far in future");
    return false;
  }
  return true;
}
Enter fullscreen mode Exit fullscreen mode

Pattern 2: Permit Value Bounds

Never approve type(uint256).max via permit. Use exact amounts:

// GOOD: Exact amount needed
token.permit(msg.sender, address(router), exactSwapAmount, deadline, v, r, s);
router.swap(token, exactSwapAmount, minOutput);

// BAD: Unlimited approval via permit
token.permit(msg.sender, address(router), type(uint256).max, deadline, v, r, s);
Enter fullscreen mode Exit fullscreen mode

Pattern 3: Domain Separator Verification

Wallet frontends should verify the EIP-712 domain separator matches a known legitimate contract:

const KNOWN_DOMAINS: Record<string, { name: string; version: string; contract: string }> = {
  "USDC": { name: "USD Coin", version: "2", contract: "0xA0b8...3E7a" },
  "DAI":  { name: "Dai Stablecoin", version: "1", contract: "0x6B17...1d0F" },
};

function verifyDomainSeparator(domain: EIP712Domain): boolean {
  const known = Object.values(KNOWN_DOMAINS).find(
    d => d.contract.toLowerCase() === domain.verifyingContract?.toLowerCase()
  );
  if (!known) return false; // Unknown token — high risk
  return known.name === domain.name && known.version === domain.version;
}
Enter fullscreen mode Exit fullscreen mode

Pattern 4: Forta Permit Monitoring Bot

Deploy a Forta bot that monitors permit() calls paired with immediate transferFrom() in the same transaction:

from forta_agent import Finding, FindingSeverity, FindingType

PERMIT_SIG = "0xd505accf"  # permit(address,address,uint256,uint256,uint8,bytes32,bytes32)
TRANSFER_FROM_SIG = "0x23b872dd"  # transferFrom(address,address,uint256)

def handle_transaction(tx):
    findings = []
    traces = tx.traces

    permit_calls = [t for t in traces if t.input[:10] == PERMIT_SIG]
    transfer_calls = [t for t in traces if t.input[:10] == TRANSFER_FROM_SIG]

    for permit in permit_calls:
        # Check if transferFrom follows permit in same tx targeting same token
        token_addr = permit.to
        matching_transfers = [
            t for t in transfer_calls 
            if t.to == token_addr and t.trace_address > permit.trace_address
        ]

        if matching_transfers:
            findings.append(Finding({
                "name": "Permit + Immediate TransferFrom",
                "description": f"Atomic permit drain detected on {token_addr}",
                "alert_id": "PERMIT-DRAIN-1",
                "severity": FindingSeverity.Critical,
                "type": FindingType.Exploit,
                "metadata": {
                    "token": token_addr,
                    "victim": permit.input[10:74],  # owner param
                    "attacker": tx.from_,
                }
            }))

    return findings
Enter fullscreen mode Exit fullscreen mode

Solana Parallel: Durable Nonces and Off-Chain Authorization

Solana doesn't have ERC-2612 permits, but faces an analogous risk through durable transaction nonces. A user can sign a transaction with a durable nonce that remains valid indefinitely until the nonce is advanced. If an attacker tricks a user into signing a token transfer with a durable nonce, they hold a "phantom transaction" that can be submitted at any time.

Solana defense: Always verify the recent_blockhash in signing requests. If a transaction uses a durable nonce account instead of a recent blockhash, treat it as high-risk:

// Detection: Check if transaction uses durable nonce
fn is_durable_nonce_tx(tx: &Transaction) -> bool {
    if let Some(ix) = tx.message.instructions.first() {
        let program_id = tx.message.account_keys[ix.program_id_index as usize];
        if program_id == system_program::id() {
            // AdvanceNonceAccount instruction = 4
            return ix.data.first() == Some(&4);
        }
    }
    false
}
Enter fullscreen mode Exit fullscreen mode

Audit Checklist: 7-Point Permit Security Review

  1. Deadline bounds — Does the protocol enforce maximum permit deadlines?
  2. Value bounds — Are permits requested for exact amounts, not type(uint256).max?
  3. Nonce management — Is the nonce correctly incremented to prevent replay?
  4. Domain separator — Is the EIP-712 domain separator correctly constructed and chain-specific?
  5. Permit2 interaction — If using Uniswap Permit2, are SignatureTransfer and AllowanceTransfer paths both secured?
  6. Frontend validation — Does the dApp frontend validate permit parameters before requesting signatures?
  7. Monitoring — Are permit+transferFrom atomic sequences monitored post-deployment?

The Uncomfortable Truth

ERC-2612 was designed to improve UX — gasless approvals are genuinely useful. But the same mechanism that lets users approve without gas also lets attackers drain without traces. The fundamental problem isn't the standard; it's that off-chain signatures create an invisible attack surface that most security tools aren't designed to monitor.

Until wallets universally implement permit-aware transaction simulation and users learn to scrutinize signing requests as carefully as transactions, the Phantom Approval will remain one of DeFi's most effective attack vectors.


This analysis is part of the DeFi Security Deep Dives series. The techniques described are for defensive research only. If you discover a permit-based vulnerability, report it through the protocol's bug bounty program.

Top comments (0)