
Decode, replay and refuse EIP-7702 delegations. Built from the raw bytes of the @binance 2026-10-29 drain. C4PzJjgHzw16shEnUyw5PzbsAMWAyvbcNzn8N1Dszg3n
Joined October 2026
- Tweets24
- Following3
- Followers53
- Likes4
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.
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.
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.
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
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.
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.
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.
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.
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
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]
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.
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.
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.
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.
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.
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
















