Incident Report: USDC Permit Phishing Drain (~$1.77M)

Transaction: 0xfd7417af8433e3d9bcbed3f965307c800a24eb4e98f42cebfab6ca6064f5a642 Chain: Ethereum Mainnet (Chain ID 1) Block: 24671606 Date: 2026-03-16 17:38:59 UTC Incident Name: usdc-permit-phishing-drain


1. Executive Summary

A victim wallet (0x051bb76ff78366de530e293fdb1158c2079ab664) was drained of 1,766,308.43 USDC (~$1.77M) via an EIP-2612 Permit phishing attack. In a prior interaction, the victim was socially engineered into signing an EIP-2612 permit signature off-chain while interacting with an EIP-7702-delegated address (0x9F68523efdc91ADbE53c3776Aa927f41aB4FE17E) running a Poisoner contract — a known address poisoning / phishing tool (Solidity 0.8.34, documented by Wintermute). The attacker EOA (0xafb2423f447d3e16931164c9970b9741aab1723e) then submitted a single exploit transaction calling a purpose-built permit drainer contract (0x0927ba1f2a31875b0dd7b28eec3a3e74b4620653). The drainer verified two ecrecover signatures, DELEGATECALLed Multicall3 to atomically execute one permit (granting itself unlimited USDC approval on behalf of the victim) followed by three transferFrom calls, distributing the stolen USDC across three controlled wallets. No flash loan, no protocol exploit, and no on-chain approval by the victim were required — the stolen permit signature alone enabled the entire drain.


2. Root Cause

2.1 Vulnerable Contract

There is no single “vulnerable” protocol contract. The attack vector is the EIP-2612 Permit standard as implemented in USDC (FiatTokenV2_2) at:

  • Proxy: 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48
  • Implementation: 0x43506849d7c04f9138d1a2050bbf3a0c054402dd

The malicious infrastructure consists of:

ContractAddressRole
PermitDrainerContract0x0927ba1f2a31875b0dd7b28eec3a3e74b4620653Exploit engine — verifies signatures, batches permit+drain
Poisoner (EIP-7702)0x9F68523efdc91ADbE53c3776Aa927f41aB4FE17EEOA with EIP-7702 delegation to a Poisoner contract — address poisoning + permit phishing front
Multicall30xca11bde05977b3631167028862be2a173976ca11DELEGATECALLed to batch four calls atomically

2.2 Vulnerable Function

USDC.permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)
// Selector: 0xd505accf

2.3 Vulnerable Code

EIP-2612’s permit function allows a token holder to grant an ERC-20 approval via an off-chain ECDSA signature rather than an on-chain approve transaction. The USDC FiatTokenV2_2 implementation delegates signature verification to its EIP-712 helper (0x800c32eaa2a6c93cf4cb51794450ed77fbfbb172, selector 0x6ccea652 isValidSignatureNow), which calls the ecrecover precompile (address 0x0000000000000000000000000000000000000001) to recover the signer from the typed-data hash and the provided (v, r, s).

The critical parameters from the exploit transaction (extracted from trace_callTracer.json):

permit(
  owner:    0x051bb76ff78366de530e293fdb1158c2079ab664,  // victim
  spender:  0x0927ba1f2a31875b0dd7b28eec3a3e74b4620653,  // drainer contract
  value:    0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff,  // type(uint256).max
  deadline: 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff,  // never expires
  v:        0x1b,
  r:        0x4695b34921eb1d3dacb1a8822b939767500f0d85374dbed61a34b0972a57dbcb,
  s:        0x2af278ebb9d7349ae552b69524ed60353f803dc4c40a5e60b38f25876e0d14ee
)

The EIP-712 signature was verified successfully by the ecrecover precompile (output: 0x051bb76ff78366de530e293fdb1158c2079ab664, the victim address), confirming the victim signed this exact Permit typed-data.

2.4 Why It’s Vulnerable

EIP-2612 Permit is not inherently vulnerable — the vulnerability is social engineering that weaponizes the Permit standard. The key properties that make Permit dangerous when abused:

  1. Gasless approval: The victim can be tricked into signing a Permit without ever sending an on-chain transaction. The victim sees only a MetaMask “Sign” dialog (not a transaction confirmation), which many users treat as less consequential than a transaction.

  2. Unlimited approval with non-expiring deadline: The permit was signed with value = type(uint256).max and deadline = type(uint256).max. Once signed, the approval is permanent and covers the victim’s entire balance indefinitely.

  3. No on-chain trace before the drain: Because the permit signature is collected off-chain (e.g. via the PhishingFrontContract UI), the victim has no on-chain indicator that their funds are at risk until the drain transaction executes.

  4. Atomic batched execution: By DELEGATECALLing Multicall3, the drainer atomically executes permit + all three transferFrom calls in a single transaction, eliminating any window for mitigation between approval and drain.

The PermitDrainerContract’s own access control [recovered — approximation]: The contract uses a keccak256 hash check against a hardcoded constant (0x625ce9e32a44c8dba0ebbfbc3832785093f446ef329df8bd829d010989b7a0d0) derived from the calldata, serving as an obfuscated selector guard. It also executes two ecrecover precompile calls (to address(0x1)) before the DELEGATECALL, likely to verify the attacker’s own authorization or to replay the victim’s permit signature internally. After passing these checks it DELEGATECALLs Multicall3’s tryAggregate (selector 0xbce38bd7) with the four batched calls.


3. Attack Execution

3.1 High-Level Flow

  1. [Off-chain, prior tx] Phishing via EIP-7702 Poisoner: The victim interacted with address 0x9F68523e, an EOA that has EIP-7702 delegation to a Poisoner contract. The Poisoner (Solidity 0.8.34, documented by Wintermute) implements executeBatch(Call[]) gated by require(tx.origin == thief), enabling the attacker to execute arbitrary batched calls from that address. During this interaction (likely address poisoning or a phishing dApp), the victim was tricked into signing an EIP-712 typed-data signature for USDC.permit(owner=victim, spender=drainer, value=MAX, deadline=MAX). The signature (v=0x1b, r=0x4695b3..., s=0x2af278...) was captured by the attacker.

  2. [This tx] Attacker submits drain: Attacker EOA (0xafb2423f) calls PermitDrainerContract.0xcaa5c23f(calldata) at block 24671606. The calldata encodes 4 sub-calls: 1 permit + 3 transferFrom.

  3. [Drainer: signature checks]: The drainer contract invokes the ecrecover precompile twice via STATICCALL to address(0x1):

    • First call: recovers an intermediate address (output: 0x0000db5c8b030ae20308ac975898e09741e70000) — likely a validation of drainer-internal parameters.
    • Second call: recovers the attacker EOA 0xafb2423f447d3e16931164c9970b9741aab1723e — confirming the caller is an authorized operator.
  4. [Drainer: DELEGATECALL to Multicall3]: The drainer DELEGATECALLs Multicall3.tryAggregate(false, calls[4]) (selector 0xbce38bd7, requireSuccess=false), executing as the drainer’s own address in the drainer’s storage context.

  5. [Multicall: sub-call 1 — permit]: Calls USDC.permit(victim, drainer, MAX, MAX, 0x1b, r, s). USDC proxy DELEGATECALLs FiatTokenV2_2, which DELEGATECALLs the EIP-712 helper. The helper calls ecrecover(permitHash, v, r, s) → returns victim address. Signature valid. USDC approval storage updated: allowance[victim][drainer] = MAX. Emits Approval(owner=victim, spender=drainer, value=MAX_UINT256) (log index 229).

  6. [Multicall: sub-call 2 — transferFrom #1]: Calls USDC.transferFrom(victim, 0x6fe314fd..., 88,315.421631 USDC) (raw: 0x14900263bf). Succeeds. Emits Transfer (log index 230).

  7. [Multicall: sub-call 3 — transferFrom #2]: Calls USDC.transferFrom(victim, 0xf1a50bbe..., 264,946.264893 USDC) (raw: 0x3db0072b3d). Succeeds. Emits Transfer (log index 231).

  8. [Multicall: sub-call 4 — transferFrom #3]: Calls USDC.transferFrom(victim, 0x9f6f1ac4..., 1,413,046.746096 USDC) (raw: 0x14900263bf0). Succeeds. Emits Transfer (log index 232).

  9. [Post-tx, subsequent txs] Recipient wallets swap USDC to ETH on DEXes and distribute to final wallets (per incident brief: 616.81 ETH, 114.24 ETH, 38.14 ETH to three addresses).

3.2 Detailed Call Trace

CALL  AttackerEOA (0xafb2423f)
  → PermitDrainerContract (0x0927ba1f)  [selector: 0xcaa5c23f, depth 0]
      │
      ├─ STATICCALL  → ecrecover precompile (0x0000...0001)  [depth 1]
      │    input:  hash=0x0e74be6cdb92f343d53ca0e0da8868fc4cfeca25d4731dc3c3ad1c47797d1b28
      │            v=0x1b, r=0xb175cbc4601aa75d8fdfa840a9a7d99275e6012f65399c219ff761cbdcbd0c34
      │            s=0x711ca4be5d3b5f4d6b72e2caa4c4d26d81d07ccf6e0f53cd769f49a4ccf027c2
      │    output: 0x0000db5c8b030ae20308ac975898e09741e70000  (intermediate addr)
      │
      ├─ STATICCALL  → ecrecover precompile (0x0000...0001)  [depth 1]
      │    input:  hash=0x0e74be6cdb92f343d53ca0e0da8868fc4cfeca25d4731dc3c3ad1c47797d1b28
      │            v=0x1c, r=0xadfd81ddc8d4aa3c2c717e30db13c6dba54306a79ff0a4671ab203176c1bee09
      │            s=0x507a2a8f78aaf72fc160e9349f320bac1650d76b5233000b6d3572282f2e7a96
      │    output: 0xafb2423f447d3e16931164c9970b9741aab1723e  (attacker EOA ✓)
      │
      └─ DELEGATECALL  → Multicall3 (0xca11bde0)  [selector: 0xbce38bd7 tryAggregate, depth 1]
           │  (executes as PermitDrainerContract; requireSuccess=false)
           │
           ├─ CALL  → USDC proxy (0xa0b869)  [selector: 0xd505accf permit, depth 2]
           │     DELEGATECALL → FiatTokenV2_2 (0x435068)  [depth 3]
           │       DELEGATECALL → USDC EIP712 helper (0x800c32)  [depth 4]
           │             [selector: 0x6ccea652 isValidSignatureNow]
           │         STATICCALL → ecrecover (0x0000...0001)  [depth 5]
           │               input:  permitHash=0x83857223ce797c961ccea8a6bc00411e49d5d42feb54a4fa4f9ad1d372ad7bb8
           │                       v=0x1b, r=0x4695b349..., s=0x2af278eb...
           │               output: 0x051bb76ff78366de530e293fdb1158c2079ab664  (victim ✓)
           │         → approval set: allowance[victim][drainer] = MAX_UINT256
           │         → emit Approval (log 229)
           │
           ├─ CALL  → USDC proxy (0xa0b869)  [selector: 0x23b872dd transferFrom, depth 2]
           │     DELEGATECALL → FiatTokenV2_2 (0x435068)  [depth 3]
           │         from: victim (0x051bb76f), to: 0x6fe314fd, amount: 88,315.421631 USDC
           │         → emit Transfer (log 230)
           │
           ├─ CALL  → USDC proxy (0xa0b869)  [selector: 0x23b872dd transferFrom, depth 2]
           │     DELEGATECALL → FiatTokenV2_2 (0x435068)  [depth 3]
           │         from: victim (0x051bb76f), to: 0xf1a50bbe, amount: 264,946.264893 USDC
           │         → emit Transfer (log 231)
           │
           └─ CALL  → USDC proxy (0xa0b869)  [selector: 0x23b872dd transferFrom, depth 2]
                DELEGATECALL → FiatTokenV2_2 (0x435068)  [depth 3]
                    from: victim (0x051bb76f), to: 0x9f6f1ac4, amount: 1,413,046.746096 USDC
                    → emit Transfer (log 232)

4. Financial Impact

Source: analysis_0xfd7417af8433e3d9bcbed3f965307c800a24eb4e98f42cebfab6ca6064f5a642/funds_flow.json

Token Transfers (This Transaction)

#FromToAmount (USDC)Log Index
1Victim 0x051bb76f...ab6640x6fe314fd...b56688,315.421631230
2Victim 0x051bb76f...ab6640xf1a50bbe...fd2b264,946.264893231
3Victim 0x051bb76f...ab6640x9f6f1ac4...e17e1,413,046.746096232
Total1,766,308.432620 USDC

Approval Event (This Transaction)

LogTokenOwnerSpenderValue
229USDC0x051bb76f...ab664 (victim)0x0927ba1f...0653 (drainer)MAX_UINT256

Victim Net Loss

AddressTokenChange
0x051bb76ff78366de530e293fdb1158c2079ab664USDC−1,766,308.432620 USDC (~$1.77M)

Post-Transaction Fund Distribution (Per Incident Brief)

The three USDC recipient wallets subsequently swapped proceeds for ETH:

WalletETH ReceivedUSD Equivalent
0x9f6f1ac48e4c7e53495a99ce49974cd1914fe17e616.81 ETH~$1.4M
0xf1a50bbeba19a85db20432c6c201aa89604dfd2b114.24 ETH~$266K
0x6fE314fD4CF845f35fc461eD98e2FB8d9356B56638.14 ETH~$89K
Total769.19 ETH~$1.75M

Transaction Cost

  • Gas used: 184,062
  • Gas price: 1.4457 gwei
  • Tx cost: 0.000266 ETH (~$0.62)

Note: The attacker EOA (0xafb2423f) did not directly receive USDC in this transaction; all funds went to the three drain wallets.


5. Evidence

5.1 Key On-Chain Artifacts

ArtifactValue / Location
Exploit tx0xfd7417af8433e3d9bcbed3f965307c800a24eb4e98f42cebfab6ca6064f5a642
Block24671606
Victim address0x051bb76ff78366de530e293fdb1158c2079ab664
Attacker EOA0xafb2423f447d3e16931164c9970b9741aab1723e
Drainer contract0x0927ba1f2a31875b0dd7b28eec3a3e74b4620653 (unverified, TAC-recovered)
Poisoner (EIP-7702)0x9F68523efdc91ADbE53c3776Aa927f41aB4FE17E (EOA w/ EIP-7702 delegation to Poisoner contract)
Permit signaturev=0x1b, r=0x4695b349..., s=0x2af278eb...
Permit spender0x0927ba1f2a31875b0dd7b28eec3a3e74b4620653 (drainer)
Permit deadline0xffffffff... (MAX, never expires)
Permit value0xffffffff... (MAX_UINT256)

5.2 Drainer Contract Behavior [recovered — approximation]

From TAC analysis of 0x0927ba1f2a31875b0dd7b28eec3a3e74b4620653:

  • Dispatcher: Handles known selectors (0x0625d079, 0x06fdde03, 0x08b4c2d6, 0x095ea7b3) with internal CALLPRIVATE stubs. All unknown selectors — including the actual exploit entry 0xcaa5c23f — fall through to the default handler.
  • Selector guard: The default handler computes a hash of the incoming 4-byte calldata via an internal helper (CALLPRIVATE 0x10ca) and checks it against hardcoded constant 0x625ce9e32a44c8dba0ebbfbc3832785093f446ef329df8bd829d010989b7a0d0. If mismatch: STOP (not revert).
  • Dual ecrecover authorization: Two STATICCALL invocations to the ecrecover precompile verify that the caller is the authorized attacker EOA (0xafb2423f).
  • DELEGATECALL to Multicall3: After passing authorization, delegatecall(gas(), 0xca11bde0..., ...) with selector 0xbce38bd7 (tryAggregate) batches 4 sub-calls.

5.3 Poisoner Contract (EIP-7702 Delegation at 0x9F68523e)

Address 0x9F68523efdc91ADbE53c3776Aa927f41aB4FE17E is an EOA with EIP-7702 code delegation to a known Poisoner contract (Solidity 0.8.34). This contract type has been documented by Wintermute and Blockaid as infrastructure for address poisoning scams.

// Poisoner contract — EIP-7702 delegated code at 0x9F68523e
// Documented by Wintermute; see: blockaid.io/blog/a-deep-dive-into-address-poisoning
pragma solidity 0.8.34;

contract Poisoner {
    address immutable thief;  // set to tx.origin at deployment

    struct Call {
        address target;
        uint256 value;
        bytes data;
    }

    function executeBatch(Call[] calldata calls) external payable {
        require(tx.origin == thief, WhoAreYou());  // <-- only the deployer can execute
        for (uint256 i = 0; i < calls.length; i++) {
            (bool success, ) = calls[i].target.call{value: calls[i].value}(calls[i].data);
            require(success, "Delegated call failed");
        }
    }

    receive() external payable { }
}

Key properties:

  • thief = tx.origin: Only the original deployer can call executeBatch, ensuring exclusive attacker control.
  • executeBatch(Call[]): Arbitrary batched external calls — can execute any contract function on behalf of the Poisoner address.
  • EIP-7702 delegation: The address 0x9F68523e is an EOA whose code is set to this contract via EIP-7702. This means it appears as a normal wallet to the victim but can execute contract logic when the attacker initiates calls.
  • Address poisoning: The Poisoner contract is primarily designed for address poisoning scams (sending small “dust” transactions from lookalike addresses to confuse victims into copying the wrong address). In this incident, it was also used as the phishing surface to collect the victim’s EIP-2612 Permit signature.
  • TAC recovery: Our initial TAC decompilation of this address showed only a reverting fallback — this is expected because the on-chain bytecode at 0x9F68523e is an EIP-7702 delegation pointer, not the full Poisoner bytecode. The actual contract code lives at the delegation target.

5.4 State Diff (trace_prestateTracer.json)

The prestate tracer confirms USDC storage changes:

Storage KeyPre-ValuePost-ValueInterpretation
0x018d877...0x33c49d (small balance)0x149036285cRecipient wallet USDC balance increase
0x3709d63...0x33c49db (small balance)0x149036285cbRecipient wallet USDC balance increase
0x681b082...(absent)0x3db0072b3dSecond drain wallet balance created
0xbd51495...(absent)0xfffffe64bfd03513Victim balance after drain (near-zero residual)
0xc2fe146...0xc (nonce 12)0xd (nonce 13)Victim’s permit nonce consumed

The victim permit nonce advancing from 12 to 13 (storage key 0xc2fe146...) provides definitive on-chain proof that a valid EIP-712 Permit signature from the victim was consumed in this transaction.


6. Vulnerability Classification

FieldValue
Primary CategorySocial Engineering / Phishing
Secondary CategoryEIP-2612 Permit Abuse via EIP-7702 Poisoner
CWECWE-284 (Improper Access Control via stolen credential)
Attack TypePermit signature phishing drain
Funds at RiskAny USDC (or EIP-2612-compatible token) in victim wallet at time of drain
Protocol ExploitedNone — this is user-level phishing, not a protocol bug
PreventionRevoke Permit approvals; use hardware wallets with EIP-712 display; avoid signing Permit for unknown dApps; be wary of EIP-7702 delegated EOAs posing as normal wallets

7. References

  • Analysis directory: analysis_0xfd7417af8433e3d9bcbed3f965307c800a24eb4e98f42cebfab6ca6064f5a642/
  • Call trace: analysis_0xfd7417af8433e3d9bcbed3f965307c800a24eb4e98f42cebfab6ca6064f5a642/trace_callTracer.json
  • Funds flow: analysis_0xfd7417af8433e3d9bcbed3f965307c800a24eb4e98f42cebfab6ca6064f5a642/funds_flow.json
  • Drainer TAC: analysis_0xfd7417af8433e3d9bcbed3f965307c800a24eb4e98f42cebfab6ca6064f5a642/0x0927ba1f2a31875b0dd7b28eec3a3e74b4620653/contract.tac
  • Drainer recovered: analysis_0xfd7417af8433e3d9bcbed3f965307c800a24eb4e98f42cebfab6ca6064f5a642/0x0927ba1f2a31875b0dd7b28eec3a3e74b4620653/recovered.sol
  • EIP-2612 Permit standard: https://eips.ethereum.org/EIPS/eip-2612
  • EIP-7702 (Set EOA account code): https://eips.ethereum.org/EIPS/eip-7702
  • Blockaid address poisoning deep dive: https://www.blockaid.io/blog/a-deep-dive-into-address-poisoning