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:
| Contract | Address | Role |
|---|---|---|
| PermitDrainerContract | 0x0927ba1f2a31875b0dd7b28eec3a3e74b4620653 | Exploit engine — verifies signatures, batches permit+drain |
| Poisoner (EIP-7702) | 0x9F68523efdc91ADbE53c3776Aa927f41aB4FE17E | EOA with EIP-7702 delegation to a Poisoner contract — address poisoning + permit phishing front |
| Multicall3 | 0xca11bde05977b3631167028862be2a173976ca11 | DELEGATECALLed 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:
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.
Unlimited approval with non-expiring deadline: The permit was signed with
value = type(uint256).maxanddeadline = type(uint256).max. Once signed, the approval is permanent and covers the victim’s entire balance indefinitely.No on-chain trace before the drain: Because the permit signature is collected off-chain (e.g. via the
PhishingFrontContractUI), the victim has no on-chain indicator that their funds are at risk until the drain transaction executes.Atomic batched execution: By DELEGATECALLing Multicall3, the drainer atomically executes
permit+ all threetransferFromcalls 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
[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) implementsexecuteBatch(Call[])gated byrequire(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 forUSDC.permit(owner=victim, spender=drainer, value=MAX, deadline=MAX). The signature(v=0x1b, r=0x4695b3..., s=0x2af278...)was captured by the attacker.[This tx] Attacker submits drain: Attacker EOA (
0xafb2423f) callsPermitDrainerContract.0xcaa5c23f(calldata)at block 24671606. The calldata encodes 4 sub-calls: 1permit+ 3transferFrom.[Drainer: signature checks]: The drainer contract invokes the
ecrecoverprecompile twice via STATICCALL toaddress(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.
- First call: recovers an intermediate address (output:
[Drainer: DELEGATECALL to Multicall3]: The drainer DELEGATECALLs
Multicall3.tryAggregate(false, calls[4])(selector0xbce38bd7,requireSuccess=false), executing as the drainer’s own address in the drainer’s storage context.[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 callsecrecover(permitHash, v, r, s)→ returns victim address. Signature valid. USDC approval storage updated:allowance[victim][drainer] = MAX. EmitsApproval(owner=victim, spender=drainer, value=MAX_UINT256)(log index 229).[Multicall: sub-call 2 — transferFrom #1]: Calls
USDC.transferFrom(victim, 0x6fe314fd..., 88,315.421631 USDC)(raw:0x14900263bf). Succeeds. EmitsTransfer(log index 230).[Multicall: sub-call 3 — transferFrom #2]: Calls
USDC.transferFrom(victim, 0xf1a50bbe..., 264,946.264893 USDC)(raw:0x3db0072b3d). Succeeds. EmitsTransfer(log index 231).[Multicall: sub-call 4 — transferFrom #3]: Calls
USDC.transferFrom(victim, 0x9f6f1ac4..., 1,413,046.746096 USDC)(raw:0x14900263bf0). Succeeds. EmitsTransfer(log index 232).[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)
| # | From | To | Amount (USDC) | Log Index |
|---|---|---|---|---|
| 1 | Victim 0x051bb76f...ab664 | 0x6fe314fd...b566 | 88,315.421631 | 230 |
| 2 | Victim 0x051bb76f...ab664 | 0xf1a50bbe...fd2b | 264,946.264893 | 231 |
| 3 | Victim 0x051bb76f...ab664 | 0x9f6f1ac4...e17e | 1,413,046.746096 | 232 |
| Total | 1,766,308.432620 USDC |
Approval Event (This Transaction)
| Log | Token | Owner | Spender | Value |
|---|---|---|---|---|
| 229 | USDC | 0x051bb76f...ab664 (victim) | 0x0927ba1f...0653 (drainer) | MAX_UINT256 |
Victim Net Loss
| Address | Token | Change |
|---|---|---|
0x051bb76ff78366de530e293fdb1158c2079ab664 | USDC | −1,766,308.432620 USDC (~$1.77M) |
Post-Transaction Fund Distribution (Per Incident Brief)
The three USDC recipient wallets subsequently swapped proceeds for ETH:
| Wallet | ETH Received | USD Equivalent |
|---|---|---|
0x9f6f1ac48e4c7e53495a99ce49974cd1914fe17e | 616.81 ETH | ~$1.4M |
0xf1a50bbeba19a85db20432c6c201aa89604dfd2b | 114.24 ETH | ~$266K |
0x6fE314fD4CF845f35fc461eD98e2FB8d9356B566 | 38.14 ETH | ~$89K |
| Total | 769.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
| Artifact | Value / Location |
|---|---|
| Exploit tx | 0xfd7417af8433e3d9bcbed3f965307c800a24eb4e98f42cebfab6ca6064f5a642 |
| Block | 24671606 |
| Victim address | 0x051bb76ff78366de530e293fdb1158c2079ab664 |
| Attacker EOA | 0xafb2423f447d3e16931164c9970b9741aab1723e |
| Drainer contract | 0x0927ba1f2a31875b0dd7b28eec3a3e74b4620653 (unverified, TAC-recovered) |
| Poisoner (EIP-7702) | 0x9F68523efdc91ADbE53c3776Aa927f41aB4FE17E (EOA w/ EIP-7702 delegation to Poisoner contract) |
| Permit signature | v=0x1b, r=0x4695b349..., s=0x2af278eb... |
| Permit spender | 0x0927ba1f2a31875b0dd7b28eec3a3e74b4620653 (drainer) |
| Permit deadline | 0xffffffff... (MAX, never expires) |
| Permit value | 0xffffffff... (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 entry0xcaa5c23f— fall through to thedefaulthandler. - 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 constant0x625ce9e32a44c8dba0ebbfbc3832785093f446ef329df8bd829d010989b7a0d0. 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 selector0xbce38bd7(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 callexecuteBatch, 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
0x9F68523eis 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
0x9F68523eis 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 Key | Pre-Value | Post-Value | Interpretation |
|---|---|---|---|
0x018d877... | 0x33c49d (small balance) | 0x149036285c | Recipient wallet USDC balance increase |
0x3709d63... | 0x33c49db (small balance) | 0x149036285cb | Recipient wallet USDC balance increase |
0x681b082... | (absent) | 0x3db0072b3d | Second drain wallet balance created |
0xbd51495... | (absent) | 0xfffffe64bfd03513 | Victim 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
| Field | Value |
|---|---|
| Primary Category | Social Engineering / Phishing |
| Secondary Category | EIP-2612 Permit Abuse via EIP-7702 Poisoner |
| CWE | CWE-284 (Improper Access Control via stolen credential) |
| Attack Type | Permit signature phishing drain |
| Funds at Risk | Any USDC (or EIP-2612-compatible token) in victim wallet at time of drain |
| Protocol Exploited | None — this is user-level phishing, not a protocol bug |
| Prevention | Revoke 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