On 29 October 2026 an attacker took about $6.3 billion from Binance across ten blockchains. Most chains lost money through ordinary signed transfers. Ethereum was different: one Binance hot wallet, the one Etherscan labels "Binance 14", was made to run code the attacker controlled, and its entire balance (167,464 ETH and 63 tokens, about 1.6 billion USDT among them) left in a single call.
The feature that made this possible is EIP-7702, which went live with Ethereum's Pectra upgrade on 7 May 2025. The transactions are public, so you can check what happened yourself. This post walks through them and ends with a small script you can point at any transaction or wallet.
What EIP-7702 lets an ordinary account do
Before Pectra, an externally owned account (EOA, an account controlled by a private key) could hold no code. EIP-7702 added transaction type 0x04, which carries an authorization_list. Each entry is a tuple [chain_id, address, nonce, y_parity, r, s]: the account's owner signs keccak(0x05 || rlp([chain_id, address, nonce])), and once the transaction is mined the account's code becomes a 23-byte delegation marker, 0xef0100 followed by address. From then on, a call to the account runs the code at address, with the account's own balance and storage.
Three details of the spec matter here:
-
Anyone can submit the authorization. The signer (the "authority") is recovered from the signature. The sender of the type
0x04transaction can be a completely different account, and it pays the gas. -
The nonce must match exactly. A tuple is accepted only if its
nonceequals the authority's current nonce. If it does not, the tuple is skipped and the transaction carries on. A valid tuple increments the authority's nonce by one. - Revocation is another authorization. Delegating to the zero address clears the code.
Why the attacker signed 50 nonces
Binance 14 was busy: it sent about 630 transactions in the hour before the attack, one every six seconds or so, and each one moved its nonce. A single authorization signed for one nonce would almost certainly be stale by the time it reached a block. So the transaction at 20:20:59 UTC carried 50 authorizations, all for the same chain (chain ID 1) and the same delegate contract, with nonces from 18,914,824 counting down to 18,914,775.
The order is the clever part. Tuples are processed in sequence. Going downwards, every nonce above the wallet's current one fails and is skipped. The one that matches is applied and bumps the nonce by one, so every remaining tuple is now too low and fails too. Exactly one authorization lands. In ascending order the opposite happens: after the first match, the next tuple matches the new nonce, then the next, and the wallet's nonce jumps by a dozen or more, which would break the exchange's own queued withdrawals and draw attention. That last point is my reading of the spec, not something Binance or the investigators have said.
The chain shows the result. Binance 14's nonce was 18,914,808 at the end of the previous block. A transaction Binance 14 itself sent five places earlier in the same block used 18,914,808, the attacker's tuple for 18,914,809 was accepted, and the wallet ended the block at 18,914,810, with its code set to the delegation marker.
Three minutes and 24 seconds later, at 20:24:23 UTC, a second transaction called the wallet and emptied it. The delegation then stayed on the wallet for 3 hours and 49 minutes, while customer deposits kept arriving and were swept again. At 00:10:35 UTC Binance 14 itself sent a type 0x04 transaction delegating to the zero address, which wiped the code.
Check it yourself
This script needs Node 18 or later and no packages. It reads a transaction, prints its authorization list, and compares the target account's nonce and code around that block. Old blocks need an endpoint that keeps archive state; the public dRPC endpoint did when this was written.
// Node 18+, no packages. Usage: node inspect-7702.mjs <transaction hash>
const RPC = process.env.RPC ?? "https://eth.drpc.org"; // needs archive state for old blocks
async function rpc(method, params) {
const res = await fetch(RPC, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
});
const { result, error } = await res.json();
if (error) throw new Error(error.message);
return result;
}
const num = (hex) => Number(BigInt(hex));
const hex = (n) => "0x" + n.toString(16);
// EIP-7702: a delegated account's code is 0xef0100 followed by the 20-byte delegate address.
const delegateOf = (code) =>
code.startsWith("0xef0100") && code.length === 48 ? "0x" + code.slice(8) : null;
const tx = await rpc("eth_getTransactionByHash", [process.argv[2]]);
const block = num(tx.blockNumber);
console.log(`type ${num(tx.type)} transaction in block ${block}, sent by ${tx.from}`);
const auths = tx.authorizationList ?? [];
const nonces = auths.map((a) => num(a.nonce));
console.log(`${auths.length} authorizations, nonces ${nonces[0]} ... ${nonces.at(-1)}`);
console.log("delegate:", [...new Set(auths.map((a) => a.address))].join(", "));
// This transaction is addressed to the wallet it hijacks, so tx.to is the account to check.
const who = tx.to;
const before = num(await rpc("eth_getTransactionCount", [who, hex(block - 1)]));
const after = num(await rpc("eth_getTransactionCount", [who, hex(block)]));
console.log(`nonce of ${who}: ${before} before the block, ${after} after it`);
console.log("code after the block delegates to:", delegateOf(await rpc("eth_getCode", [who, hex(block)])) ?? "nothing");
console.log("code today delegates to:", delegateOf(await rpc("eth_getCode", [who, "latest"])) ?? "nothing");
Run against the authorization transaction on 30 October, it printed:
type 4 transaction in block 26300753, sent by 0x7c8601d4645b93cea45f71e118a729332e6b5ca0
50 authorizations, nonces 18914824 ... 18914775
delegate: 0x63e842b054bd624a465454d533c33f573e9c6fae
nonce of 0x28c6c06298d514db089934071355e5743bf21d60: 18914808 before the block, 18914810 after it
code after the block delegates to: 0x63e842b054bd624a465454d533c33f573e9c6fae
code today delegates to: nothing
One shortcut to be aware of: the script treats tx.to as the hijacked account because this attacker addressed the transaction to it. In general the authority is whoever signed each tuple, and you recover it from y_parity, r and s.
What this means if you run signing infrastructure
Binance says its first finding is a compromised DevOps engineer's laptop that may have given access to a service container used to submit unauthorized withdrawals, that it has found no evidence of a leaked private key, and that it has since found more points of breach. The full attack path is not public yet. TRM Labs, which attributes the attack to North Korea's TraderTraitor unit, reads the 50 consecutive nonces as a sign that the attacker's access was limited in time, or that signatures had to be requested through a system, and calls a compromise of Binance's hot-wallet signing infrastructure the most likely explanation.
Whatever the post-mortem says, a few checks follow directly from the spec:
-
A signer should know what it is signing. An EIP-7702 authorization is not a transaction. It is the 32-byte hash of a message that starts with the byte
0x05. A signing service that will sign any digest it is handed can sign one of these without anyone noticing. Parse the payload and refuse everything that is not a transaction shape you expect. -
Treat code on your own EOAs as an alarm. A wallet that should never delegate should always return
0xfrometh_getCode. Polling that once per block for your hot wallets costs almost nothing, and thedelegateOfcheck above is the whole test. Here the marker was visible on-chain 3 minutes and 24 seconds before the main sweep. - Watch the chain ID. These authorizations used chain ID 1. The spec also accepts 0, which is valid on every chain, so a policy should reject 0 unless you mean it.
-
Have the revocation ready. Clearing a delegation is one type
0x04transaction to the zero address, signed by the wallet's key. Binance sent it at 00:10:35, 3 hours and 49 minutes after the attacker's authorization landed.
The EIP's authors saw the risk for wallets. Their rationale says apps must not be able to ask users to sign authorizations, adding: "There is no safe way to provide this interface." The same reasoning applies to a backend signer.
I keep a fuller account of the incident, with a timeline in UTC and Sri Lanka time and the frozen amounts by issuer, on my blog.
If you run a signing service, do you check for 0x05-prefixed authorization digests today, or does it sign any hash it is given?
This article was written with the help of AI. The transaction data was read from a public Ethereum node with the script above, and the other facts come from the linked reports.
Top comments (0)