On 2026-10-30 a major exchange's hot wallets were drained. Public reporting says the attackers never saw a private key — they got the signing infrastructure to sign 50 EIP-7702 authorizations in one transaction. I wrote a decoder to look for that shape on-chain. I found it in the first 30 blocks. It was a product, not an attack.
The claim I wanted to test
Reports described the pattern as: one transaction, dozens of authorization_list entries, nonce values in sequence, all landing in a single block. If that shape is the signature of the attack, then a simple rule — more than N authorizations in one transaction = suspicious — should find it.
So I built the smallest tool that could check: RLP decoding, keccak256, secp256k1 recovery, no web3 dependency. EIP-7702 authorizations are signed over
keccak256(0x05 || rlp([chain_id, address, nonce]))
Recover the signer, compare it with the transaction's from, and you can answer the only three questions that matter structurally: who handed execution rights to which contract, does that contract have code, and how many authorizations are packed into this one transaction.
The sample
Blocks 26,303,375 – 26,303,404 — about six minutes of mainnet time. Every type-4 transaction:
| Metric | Value |
|---|---|
| type-4 transactions | 133 (~4.4 per block) |
| authorizations | 319 |
| unique signers | 308 |
| unique delegates | 104 |
chain_id = 0 (replayable everywhere) |
0 |
| delegate with no code | 1 (pointing at 0x0) |
| auth signer != tx sender | 38 / 133 |
Distribution of authorizations per transaction: 120 transactions carry exactly one. The rest are 2, 3, 4, 5, 6, 8, 21, 30, 32, 34 — and one with 50.
The transaction that broke my rule
Block 26,303,378, transaction 0x65f116e6…2e23:
- 50 authorizations, 50 distinct signers
- all pointing at the same delegate
0xe6b9…43ed(772 bytes of code) - submitted by a single sender
0x59aa…e16b - the same sender filed 34 / 32 / 30 / 21 / 8 / 5-authorization transactions in adjacent blocks
That is precisely the shape the incident was described with. It is also, almost certainly, a batch delegation service — one operator, steady cadence, every signer handing execution rights to the same deployed contract, and no assets leaving in the same window.
A rule of "5+ authorizations in one transaction = high severity" would have fired five times in six minutes. All five would have been wrong. An alarm that is 100% false positives is not an alarm.
What the rule should look like instead
Structure alone is not enough. You need behavior layered on top, ordered by how cheaply it can be verified:
- Is the delegate new? A first-seen contract plus a batch of authorizations starts to look like an attack. A delegate with months of history does not.
- Did assets move in the same window? In the reported incident, the authorizations were not the payload — the transfers three minutes later were. Authorization and transfer have to be evaluated together.
- Do the signers have any history with the sender? 38 of 133 transactions had signer != sender, which is a legitimate relayer pattern. It stops being unremarkable when the signers have never interacted with that sender before.
I implemented layer 1 only. Layers 2 and 3 need an indexer.
Update: I found the attack transaction
After publishing the first version of this post I went back and scanned blocks 26,300,750 – 26,300,800 for every transaction touching Binance 14 (0x28c6…1d60). It is in there.
- block 26,300,753 — 2026-10-29 20:20:47 UTC
- tx
0xad99264d…298b, type 4, from0x7c8601…5ca0, to Binance 14 - 50 authorizations, every one of them signed by Binance 14
- all delegating to
0x63e842b0…6fae(1,674 bytes of code),chain_id = 1 - nonces 18,914,824 down to 18,914,775 — contiguous, 50 in a row
The receipt is status: 1, 1,258,562 gas, and zero logs. That transaction moved nothing. It installed the delegation, and the transfer came afterwards.
The discriminator is the nonce, not the count. The batch-delegation product had 50 authorizations from 50 different signers. The attack had 50 authorizations from one signer with consecutive nonces — a stack of pre-signed delegations to replay one after another. You can detect that without an indexer: group authorizations by signer, and flag any signer that appears N times with contiguous nonces. A product doing mass migration never repeats a signer; an attacker building a replay stack always does.
That rule is now code, not a paragraph: w7702.py flags any signer appearing three or more times over contiguous nonces. Run against both transactions: the attack fires replay stack: 50 authorizations from 0x28c6… over contiguous nonces 18914775..18914824; the 50-signer product fires nothing. Eight unit tests, four of them for this rule alone.
And then I measured how often it fires on ordinary traffic. 300 randomly sampled blocks spanning seven days: 1,015 type-4 transactions, 16 replay stacks — about 1.6%. Every one of them carried 3 or 5 authorizations; none carried more than five. The attack carried 50.
So contiguity alone is a weak signal. A wallet pre-signing its next five delegations looks structurally identical to an operator pre-signing fifty, and the small version is routine: one sender in that sample filed three separate 5-nonce stacks back to back, which is exactly what an operational hot wallet does when it tops up its delegation budget.
The separating dimension in this data is scale, not shape. That is a worse answer than the one I wrote an hour ago, and it is the one the numbers support.
The delegate itself is purpose-built. Disassembling its 1,674 bytes yields exactly one embedded address — 0x7c8601…5ca0, the operator who sent the transaction. The destination is hardcoded into the contract, not supplied as an argument. A batch-delegation product is generic; this one already knows where the money goes.
One more thing the scan showed: across blocks 26,300,750 – 26,300,821 (about fourteen minutes after the delegation landed), no transaction calls into Binance 14 at all — only the hot wallet's own outbound traffic. So whatever moved the reported 167,464 ETH did not do it by calling the delegated account directly in that window.
Binance 14 returns 0x today — no code, delegation cleared.
My own tool was wrong first
The unit tests failed on the first run: my RLP encoder wrote integer 0 as 0x00. RLP encodes zero as the empty string 0x80.
The consequence is not cosmetic — a nonce = 0 authorization hashes differently, and recovery returns a fake signer. Nonce zero is exactly what a first-time authorization from a fresh account looks like, so the bug would have produced wrong answers on the transactions most worth inspecting.
Fixed, with a regression test. The same test round produced a real validation: in 0x73af…756f, the authorization signer is not the transaction sender, and the tool flags no authorization belongs to the transaction sender. That is a relayer submitting on someone's behalf — legal, and exactly the kind of "valid on-chain, unclear intent" case a human should look at.
What I did not verify
- The figures —
$5.8Bof outflows,$3Bfrozen,167,464 ETHin the first transfer — are still from public reporting. I confirmed the delegation transaction; I did not reconstruct the transfers themselves, and I have not traced the delegate contract's internal calls. - The 30-block sample above and the incident block are different windows; the earlier numbers (133 type-4 transactions) do not include the attack.
- 30 blocks is roughly six minutes. "4.4 type-4 transactions per block" does not extrapolate to a day.
- I did not audit any delegate bytecode. The tool emits structural warnings; deciding whether a delegate is malicious is still a human judgement.
- Zero
chain_id = 0authorizations means zero in this window, not zero on mainnet.
Why I am publishing the negative result
The interesting output of six minutes of scanning is not a detection — it is a disproof of a detection rule I was about to write. Shape-based rules are cheap to state and expensive to be wrong about in public. The data said no, so the rule changed.
Code and full numbers: security/w7702/ — decoder, incremental scanner, five tests, and the raw report with an explicit scope statement.
Top comments (0)