Back to blogEIP-7702 delegation drained Binance's hot wallet

EIP-7702 delegation drained Binance's hot wallet

Web3·October 31, 2026·11 min read·By CodeDecoders Engineering

At 20:20:59 UTC on 29 October, Ethereum block 26,300,753 included a transaction carrying 50 EIP-7702 authorizations, every one signed by the Binance hot wallet Etherscan labels Binance 14. One of them matched the wallet's nonce and pointed its code at a 1,674-byte sweeper contract deployed that afternoon. Three minutes and 24 seconds later, a single call moved out 167,464 ETH and 63 tokens, about 1.615 billion USDT among them. The Ethereum leg was one part of a theft Binance puts at around $6.3 billion across at least ten chains, most of it taken in about 20 minutes according to TRM Labs (WIRED).

No contract bug was exploited and, per Binance's incident update, no private key leaked. The attacker got signatures out of the infrastructure that exists to produce them. For anyone running custody, that is the point: an EIP-7702 delegation has no recipient and no amount, so a signer that enforces withdrawal rules has nothing to check, and one signature hands over the whole account. Signing policy has to decode payloads by type before it signs. Detection should key on code appearing on your own wallets, not on batch size, because legitimate services send 50-authorization transactions every few minutes.

What happened on Ethereum

We pulled the key transactions and blocks from a public Ethereum RPC on 31 October and recovered the signer of all 50 authorizations to check the published decodes. They hold up: every tuple recovers to Binance 14.

The Binance 14 takeover, block by block

Step 1 of 6

Rehearse (29 Oct, from 02:28 UTC)

The operator address 0x7c86...5ca0 deploys a first sweeper and tests it on throwaway wallets. A revised sweeper, 0x63e8...6fae, lands at 15:59 and is rehearsed again. TRM says preparation began at least 36 hours before the theft.

Ethereum was not where it started. XRP Ledger accounts were drained from 20:05 and Solana at 20:10 (808bits), with Bitcoin, Tron and several EVM chains inside the same half hour, mostly through plain signed transfers. Five key systems emptied in one window is not one stolen key. It is something that can get many signers to sign, which matches Binance's account: a compromised DevOps engineer's laptop gave the attacker access to "a service container" able to "submit unauthorized withdrawal transactions." Co-CEO Richard Teng has since said it "was not a single 'one-click' action" (CoinDesk), and Binance now says it found additional points of breach.

Fifty signatures to hit a moving nonce

An EIP-7702 authorization is a tuple of chain_id, delegate address and nonce, signed over keccak256(0x05 || rlp([chain_id, address, nonce])). Anyone can carry it inside their own type-4 (set code) transaction and pay the gas. When it is processed, the client checks that the authority's current nonce equals the tuple's, writes the delegation designator 0xef0100 || address as the account's code, and increments the nonce. A tuple that fails any check is skipped, and the transaction carries on.

That skip rule is what made the batch work. Binance 14 sends constantly: in the blocks we scanned it used nonces 18,914,766 to 18,914,808 between 20:17:11 and 20:20:59, about one every five seconds. A single authorization would have been stale before it was mined. In block 26,300,753, Binance 14's own transaction (index 133) consumed nonce 18,914,808. The attacker's type-4 transaction (index 138) followed, and the tuple for 18,914,809 matched. Thirty-four tuples were already dead, 15 were spare, and the protocol skipped all 49 without reverting.

The window also dates the signatures. Binance 14 spent nonce 18,914,775, the first in the batch, at 20:17:47. Nobody signs for a nonce that is already spent, so the batch was produced against the live wallet minutes before it was used. This was a signing path answering a request, not an old signature replayed.

Once the designator is set, every call to Binance 14 runs the sweeper's code against Binance 14's balance and permissions. It is the same borrow-the-logic, keep-the-state move behind upgradeable proxy contracts, except this upgrade needed one signature and no admin. The sweeper's bytecode embeds the operator's address (we checked the deployed code), and published decompilations show every function gated on that caller, so nobody else could call it.

Why the withdrawal policy had nothing to check

Custody policy is usually recipient allowlists, amount limits and velocity rules. Every one of those reads fields an authorization does not have:

Signed bytes0x02 || rlp([chain_id, nonce, fees, gas, to, value, data, ...])0x05 || rlp([chain_id, address, nonce])
Recipientto, or the token recipient in calldataNone
Amountvalue, or the token amount in calldataNone
What it grantsOne transferEvery future call to the account runs the delegate's code
How longSpent when minedUntil another authorization replaces or clears it
Who can submit itAnyone, and it does one thingAnyone, inside their own type-4 transaction
What transfer rules checkDestination, amount, velocityNothing, unless the signer decodes the 0x05 prefix

If the signing path treats "sign these bytes for wallet X" as one operation and only knows how to parse transfers, a 0x05 payload either fails open or never reaches the policy at all. Binance has not published a post-mortem, so we do not know which happened. We know the outcome: a withdrawal wallet produced 50 delegation signatures for code deployed that afternoon.

What the signer should enforce

Put the policy at the key boundary (HSM firmware, an MPC co-signer's policy engine), not in the service that submits withdrawals, because that service is exactly what was compromised here. It is the maker-checker problem again: a control only works if it fails independently of the thing it checks. Then four rules:

  1. Accept preimages, never digests. The signer hashes. A caller that can request a signature over a 32-byte hash can request anything.
  2. Classify by the first byte. 0x02 is an EIP-1559 transaction, 0x04 a set-code transaction, 0x05 an authorization. 808bits puts it as "route anything that starts with 0x05 to its own rules." A withdrawal wallet's automated path signs 0x02 and refuses the rest.
  3. Send delegation to a different queue. Authorizations and set-code transactions go to approvers the withdrawal service cannot impersonate, checked against an allowlist of audited delegates. Keep the escape hatch there too: an authorization to the zero address clears delegation, and it is how Binance recovered the wallet.
  4. Refuse chain_id 0. It makes the authorization valid on every EVM chain.
import { fromRlp, hexToBigInt, keccak256, toHex, type Hex } from 'viem';

type Decision =
  | { action: 'sign' }
  | { action: 'refuse'; reason: string }
  | { action: 'escalate'; reason: string }; // separate queue, separate approvers

const int = (h: Hex) => (h === '0x' ? 0n : hexToBigInt(h)); // RLP encodes zero as an empty string

export function decide(preimage: Uint8Array): Decision {
  const type = preimage[0];
  const body = toHex(preimage.subarray(1));

  if (type === 0x02) return checkWithdrawal(fromRlp(body, 'hex')); // recipient, amount, velocity
  if (type === 0x05) {
    const [chainId, delegate, nonce] = fromRlp(body, 'hex') as Hex[];
    if (int(chainId) === 0n) return { action: 'refuse', reason: 'chain_id 0 is valid on every chain' };
    const what = int(delegate) === 0n ? 'clear delegation' : `delegate to ${delegate}`;
    return { action: 'escalate', reason: `${what} at nonce ${int(nonce)}` };
  }
  if (type === 0x04) return { action: 'escalate', reason: 'set-code transaction from a hot wallet' };
  return { action: 'refuse', reason: `type 0x${type.toString(16)} is not a withdrawal` };
}

export async function sign(preimage: Uint8Array, key: Hsm) {
  const decision = decide(preimage);
  if (decision.action !== 'sign') throw new PolicyRefusal(decision);
  return key.signDigest(keccak256(preimage)); // the signer hashes; callers never pass a digest
}

Under this policy, the attacker's request becomes 50 escalations, one per nonce, all naming a delegate nobody has approved.

Watch your own wallets for code

A withdrawal wallet should never have code, which makes detection cheap: one eth_getCode per hot wallet per block catches a delegation in the block it lands.

import { createPublicClient, http, type Address, type Hex } from 'viem';
import { mainnet } from 'viem/chains';

const client = createPublicClient({ chain: mainnet, transport: http(process.env.RPC_URL) });

export function watchHotWalletCode(wallets: Address[], onCode: (wallet: Address, code: Hex) => Promise<void>) {
  return client.watchBlockNumber({
    emitMissed: true,
    onBlockNumber: async (blockNumber) => {
      for (const wallet of wallets) {
        const code = await client.getCode({ address: wallet, blockNumber });
        if (code) await onCode(wallet, code); // 0xef0100 followed by the delegate
      }
    },
  });
}

Binance 14 carried the sweeper's designator for 1,143 blocks. The handler has to stop automation, not just page someone. Binance 14 kept sending its own transactions after the takeover (we see nonce 18,914,814 at 20:22:23 and 18,914,832 at 20:28:23), and deposits plus that 200 million USDT top-up kept arriving for the attacker to sweep. Freeze top-ups, sweeps and deposit routing for the wallet in the same handler. It is the posture of catching a reconciliation break the day it happens, compressed from days to blocks.

Then shrink what a hot wallet controls elsewhere. Binance 14's ownership of a USDC smart wallet turned one delegation into a second, 823 million USDC loss. Ownerships, admin roles and large allowances belong to cold or multisig accounts.

Batch size is the wrong alarm

The obvious detection rule is "many authorizations in one transaction." A developer who decoded 30 mainnet blocks of type-4 traffic tested it the day after. A five-or-more rule fired five times in six minutes, every time on what looks like a batch delegation service, including one transaction with 50 authorizations from 50 different signers to one long-lived delegate.

The better discriminator is one signer over contiguous nonces, which the attack had and the service did not. Even that is weak alone: in 300 sampled blocks spanning seven days, 16 of 1,015 type-4 transactions (about 1.6%) were such stacks, all of three or five authorizations. What separated the attack was the combination: one authority, 50 contiguous nonces, a delegate deployed hours earlier with its operator hard-coded, a high-value wallet, and an outflow minutes later. That is a job for chain-wide monitoring of counterparties. For your own wallets, skip the heuristics: any code on a withdrawal wallet is an incident.

The freeze race only covers stablecoins

Elliptic says about $2.8 billion, roughly 44% of the total, has been frozen, almost all of it stablecoins. The remaining $3.3 billion is mostly Bitcoin, Ether and XRP, "which no one can freeze" (WIRED). Tether's first blacklist came 37 minutes after the sweep, by which time the attacker had already sold USDT for ETH at discounts of up to about 22% (on-chain walkthrough). Issuer freezes are a recovery layer measured in tens of minutes, and only for assets that have a freeze function. Your own kill switch should be measured in blocks.

The short version

EIP-7702 did not fail here. A signing path signed a payload type it should never have accepted from a withdrawal wallet, and one valid authorization turned Binance's busiest hot wallet into an attacker-controlled contract for almost four hours. Decode before you sign, route 0x05 and 0x04 to a separate quorum with an audited allowlist, watch your hot wallets for code every block, and wire that alert to stop the automation that feeds them.

If you run hot wallets behind a stablecoin product, this signing layer is where our stablecoin payment infrastructure work begins, and we are glad to walk through your signing policy with you.

Newsletter

New posts, in your inbox

Get an email when we publish a new deep-dive. No spam, unsubscribe anytime.

Start a Project

Let's build something extraordinary together.

Free consultation·Response within 24h·No commitment

info@codedecoders.io