ef0100 (@ef0100_) header
ef0100 (@ef0100_) profile picture

Decode, replay and refuse EIP-7702 delegations. Built from the raw bytes of the @binance 2026-10-29 drain. C4PzJjgHzw16shEnUyw5PzbsAMWAyvbcNzn8N1Dszg3n

Joined October 2026
Photo by ef0100 (@ef0100_)Photo by ef0100 (@ef0100_)Photo by ef0100 (@ef0100_)Photo by ef0100 (@ef0100_)Photo by ef0100 (@ef0100_)Photo by ef0100 (@ef0100_)Photo by ef0100 (@ef0100_)Photo by ef0100 (@ef0100_)Photo by ef0100 (@ef0100_)Photo by ef0100 (@ef0100_)
Pinned Tweet
We pulled the Binance drain apart byte by byte: why only one of the 50 EIP-7702 signatures landed, what the chain was showing for almost 4 hours, the Solana side, and where a signer gets to say no. Everything reruns with npx ef0100.
5
8
351
By default, scan and watch go with the first RPC that answers. Now there's --cross-check. Every endpoint on the chain gets asked, and if any one of them sees a delegation, that's what you get, with a line saying who said what. The recipient check goes further. If the endpoints return different code, the transfer is held. Even for a delegate you allowed. And watch won't call a delegation cleared until every endpoint has answered. I threw 100,000 random answer sets at it. 33,644 had one endpoint saying "0x" while another saw a delegation. All 33,644 got reported and held.
9
Not every signer is written in TypeScript, so this went into guard serve too. npx ef0100 guard serve --rpc A --rpc B --rpc C The server now reads the signer's own code itself on every request, and every endpoint has to agree. In this test, two endpoints see a delegation and one says "0x". Even the wallet's own clear comes back CONTEXT_MISSING. Then 100,000 random answer sets through the guard: 0 signatures while any endpoint saw code.
19
The own-code rule is only as honest as eth_getCode. So ef0100 now asks several RPCs at once and only uses the answer if every one that answered matches. One node a block behind says "0x" while another already sees a delegation? That's a mismatch, and the guard refuses. Nobody gets outvoted. Same check from a shell: npx ef0100 owncode <address> --rpc A --rpc B --rpc C
22
Not everyone signs from TypeScript, so the guard also runs as a tiny local service. npx ef0100 guard serve --log Your signer, in Go or Rust or whatever, POSTs the bytes it's about to sign to 127.0.0.1 and gets back allow or refuse with reasons. No keys in it. Browser requests get a 403. --log writes one line per decision. Feed it tuple #15 from the real tx and it logs 0xac384c3e…2674ac, the exact hash that tuple was signed over.
1
33
Spent part of today on the boring part. The file that decides what a hot wallet signs, src/guard/policy.ts, now has every line under test and 94% of branches. Before PR #15 it was 92% and 81%. The new cases are the ones you'd actually worry about: codehash pinning, break-glass transfers, a 32-byte input that happens to parse as a transaction. CI fails now if coverage drops below the floor.
31
0.1.8 is on npm, and it's the easiest way in so far. If you sign with viem, wrap the account: const account = guardAccount(privateKeyToAccount(key), new Guard(profiles.hotWallet), ctx) On ethers v6 it's guardSigner(wallet, guard, ctx). Every signature goes through the guard first, judged on the exact bytes. Ask it to delegate to the Oct 29 sweeper and it throws before the key is touched. Allowed stuff signs byte-for-byte the same.
1
2
50
This is the rule that needs zero knowledge of the attack. If eth_getCode on your own key's address returns anything, something changed your account. From then on the guard signs the self-clear and break-glass transfers to cold addresses you listed. Nothing else. In 0.1.6 the hot-wallet profile also won't sign unless you tell it what that code is. Leaving it out used to quietly turn the rule off. Now it fails closed. On Oct 29 that wallet's code was 0xef0100… for 1,142 blocks, and its own key kept signing.
3
1
76
These are the two lines that refused all 50. src/guard/policy.ts:306 if (!allowed) out.push({ code: "DELEGATE_NOT_ALLOWED", … }) That one fires on all 50. src/guard/policy.ts:331 if (auth.nonce !== expected) That one fires on 49. #15 was the only tuple carrying the live nonce. Either is enough to refuse. Code's here: github.com/ef0100dev/ef0100
83
Funds are SAFU, they said. OK, but is the thing holding the key SAFU? ef0100 0.1.5 ships as a GitHub Action. Put it on a cron and if one of your addresses picks up an EIP-7702 delegation on any of 11 chains, the next run goes red. It only reads public chain data, never asks for a key. uses: ef0100dev/[email protected]
5
4
133
ef0100 0.1.3 is on npm. watch --webhook: every alarm and every clear goes to Slack, Discord or your own relay, as JSON. https only. No redirects. The URL is never printed. Also in this release: 6 guard invariants and pnpm bench. 117 tests. Published from GitHub Actions, with provenance.
14
15
596
Every Solana attack transaction bundled with ef0100, checked before signing: Drift, pre-signed tx #1: BLOCK. Drift, pre-signed tx #2: BLOCK. SwissBorg/Kiln hidden stake authorize: BLOCK. Owner-change phishing: BLOCK. Binance's 965,455.1 SOL leg, against its routine: BLOCK. The same parser took 300,000 damaged copies of these transactions. 0 crashes.
9
11
497
The Oct 29 drain had a Solana leg: 965,455.1 SOL out of Binance's hot wallet in one transfer. ef0100 sol, run with that wallet's own routine: TEMPLATE_DRIFT. Its withdrawals advance a durable nonce from its own pool; this one used a recent blockhash. BALANCE_FRACTION. 90.4% of its SOL. Verdict: BLOCK. Its normal batch 20 minutes later: OK.
18
21
982
New tests merged: guard invariants. Six rules, each against 20,000 random authorizations: chain_id 0: never signed. A nonce other than the live one: never signed. An unlisted delegate: never signed. Over the rate limit: never signed. After the key's own code changes: only the clear. 0 violations.
4
6
573
New in the repo: pnpm bench. Asking the ef0100 guard about tuple #15 costs 16 µs (median, p99 48 µs). One secp256k1 signature recovery costs 1,637 µs. So the check in front of your key is about a hundredth of the work a node already does per tuple.
12
14
589
We threw 510,000 hostile inputs at ef0100. Damaged copies of the real 4,916-byte transaction. Mangled preimages of tuple #15. Random digests. Authorizations signed by viem with random keys. 0 failures. Every run is seeded, so a failure would name its input.
2
4
354
ef0100 0.1.2 is on npm. New: watch. It asks 11 chains every 12 s whether an address is delegated, and prints a line when that changes. On Oct 29 the first sweep came 17 blocks after the delegation. Published from GitHub Actions, with provenance. npx ef0100 watch <address>
3
258
A compromised laptop asking a signer for bytes is the case a signing guard is for. ef0100 sits in front of the key and decides from the bytes: A bare digest: refused. An EIP-7702 authorization to an unlisted delegate: refused. A key whose own code changed: signs only the clear. It can't help if the key itself leaves.
Investigation update: Our initial investigation suggested that the security incident may have involved a compromised laptop, however our subsequent investigation has uncovered additional points of breach. This was a complex attack, and we are doing all we can in coordination with law enforcement to resolve it. We are still gathering the facts, and will share more directly as we have them. Binance will cover all losses from the SAFU fund and the company’s funds. User funds are safe. We are still on track towards the expected timeline to resume the withdrawal service in 45 hours. Deposits remain unaffected. The outflows ceased when we paused withdrawal service earlier. We are still working with partners to freeze many of the impacted assets, and have frozen around $3 billion thus far. We will continue to provide further updates through official Binance channels as more verified information becomes available.
3
2
212
Oct 29: one @binance hot wallet key signed 50 EIP-7702 authorizations. #15 applied. The wallet's code pointed at a sweeper for 1,142 blocks. The same 50, behind ef0100: REFUSE 50/50. And once a key's own code changes, it signs only the clear. npx ef0100 guard test github.com/ef0100dev/ef0100
16
20
1,383
While the wallet was delegated, the chain said so four ways: eth_getCode returned 0xef0100 + the delegate. The wallet emitted logs, which an EOA can't. A plain ETH transfer in cost 21,062 gas, not 21,000. Its own txs fell from up to 8 a block to 1. All public for 1,142 blocks.
2
6
272