The October 29 Binance theft: one signature, 50 nonces and a hot wallet that stopped being theirs
On October 29, 2026, at 20:20:59 UTC, the Binance hot wallet known as Binance 14 stopped being an ordinary Ethereum account and started running someone else’s code. Three minutes and twenty-four seconds later, that code pulled 167,464 ETH and 1.615 billion USDT out of the account in a single transaction. The account kept receiving deposits for almost four hours, and every time something came in, it was swept again. Binance regained control at 00:10:35 on the 30th.
I spent the following day reconstructing what happened with the chain as my only source: Blockscout, a public RPC node and a disassembler. Throughout this piece I keep what I verified apart from what third parties say, because in the first 24 hours of an incident like this everything circulates, and some of it is false.
What I verified and what I didn’t
Everything in this article that carries a hash or an address I checked against Ethereum mainnet between 12:00 and 18:00 UTC on October 30, using three public tools and no API key:
- The Blockscout API v2 (
https://eth.blockscout.com/api/v2/) for transactions, authorization lists and transfers. - The public RPC
https://ethereum-rpc.publicnode.comforeth_getCode,eth_call,eth_getBlockByNumberandeth_getLogs. It is not an archive node, so it can’t tell you an account’s state at an old block. - pyevmasm to disassemble the contract. With Foundry,
cast disassembledoes the same.
What I couldn’t verify is attributed to whoever says it. The total figures for the theft (5.3, 6.3 and 6.4 billion dollars have all been published), what happened on the other nine chains that are mentioned, the way in (Binance points to a DevOps engineer’s laptop) and the attribution to Lazarus (made by TRM Labs and Chainalysis). None of that is on the Ethereum chain, and I only looked at the Ethereum chain.
Dollar prices are Blockscout’s at the time of writing (ETH at $2,225). Yesterday, when the funds left, they were worth more.
The addresses
| Role | Address |
|---|---|
| Victim: Binance 14 | 0x28C6c06298d514Db089934071355E5743bf21d60 |
| Operator | 0x7C8601d4645b93cea45F71e118A729332e6B5CA0 |
| Sweeper (delegated contract) | 0x63e842B054Bd624a465454D533C33F573e9c6fAe |
| Recipient 1 | 0x5227f603C1eE34CE3C06eFe6477003BDfCCB2f8c |
| Recipient 2 | 0xA97bF7836F8A08F9Ed91D41453D371493FAE8d10 |
Labels such as “Binance 14” come from explorer services, not from the account itself. I use them because they are what you will see on Etherscan, but remember that nobody has signed them.
EIP-7702 in five minutes
Until May 2025, Ethereum had two kinds of account that never mixed. Externally owned accounts (EOAs) have a private key and no code; contracts have code and no key. If your key signed something, that something was a transaction, and the transaction did one thing and was over.
EIP-7702, activated on mainnet with the Pectra fork on May 7, 2025, breaks that separation on purpose. An EOA can sign an authorization that says: when someone calls my address, run contract X’s code as if it were mine. The mechanism has four pieces:
- A new transaction type,
0x04, with an extra field:authorization_list. - Each authorization is a tuple
[chain_id, address, nonce, y_parity, r, s]. The signature belongs to the delegating EOA, not to whoever sends the transaction. The account paying for gas and the account delegating can be two different ones. Here they were. - When processing it, the node writes a delegation designator into the EOA’s account: the bytes
0xef0100followed by the contract’s address. From then on, any call to the EOA runs that code with the EOA’s storage, balance and identity. Inside the contract,msg.senderis the EOA itself. - The delegation does not expire. It stays until the EOA signs another authorization pointing to a different address, or to the zero address to revoke it.
Point 4 is what turns a signature into a permanent door. A transaction signed by mistake runs once. A 7702 authorization signed by mistake gives whoever controls the delegated contract a key to the account that no longer depends on using the private key again. While the delegation stays in place, the sweep repeats with ordinary transactions that anyone can pay for.
The feature was designed for good things: letting a user wallet pay its own gas in tokens, batch several actions into one, or set a spending limit, without migrating to a smart account. Here it was used for the opposite.
The type 4 transaction: 50 signatures, one valid
The delegation landed in block 26,300,753, at 20:20:59 UTC, in transaction 0xad99264d5532ae0dd04b2004eebda05f25657e70dacb764f7ac8d4ccd051298b. It was sent by the operator 0x7C86…5CA0, who paid the gas, and it carries 50 authorizations. All 50 delegate to the same sweeper 0x63e842…6fAe, and all 50 are signed by the same account: Binance 14.
The structure, field by field, is this (an EOA delegating to a contract has this shape):
Transaction type 0x04 (EIP-7702)
├── chain_id, nonce, max_priority_fee, max_fee, gas_limit
├── destination = 0x28C6…1d60 (Binance 14 itself)
├── value = 0, data = 0x
└── authorization_list = [
{ chain_id: 1, address: 0x63e842…6fAe, nonce: 18914775, y_parity, r, s },
{ chain_id: 1, address: 0x63e842…6fAe, nonce: 18914776, ... },
... 48 more, consecutive nonces ...
{ chain_id: 1, address: 0x63e842…6fAe, nonce: 18914824, ... }
]
The 50 nonces run in sequence, from 18,914,775 to 18,914,824. When recovering the signer of each signature, Blockscout returns the authority field, and in all 50 it is the same: 0x28C6…1d60. That settles two things that had been published as rumor and that I can now treat as checked: there were 50 authorizations, and the nonces were these.
The trick of including 50 lies in how the protocol validates an authorization. The nonce in each tuple has to match the account’s current nonce exactly at the moment it is processed. If it doesn’t match, that authorization is dropped without aborting the transaction: the rest keep being tried. The attacker couldn’t know for sure what nonce Binance 14 would be at when the transaction was mined, because it is a hot wallet with constant traffic and the nonce keeps rising. So they signed a window of 50 nonces around the expected value and sent all 50 together. Whichever one hit would go through.
The one with nonce 18,914,809 hit, number 35 on the list. The transaction detail shows the per-authorization status: 18,914,809 reads ok and the other 49 read invalid_nonce. One is enough: the account was delegated.
The gas cost confirms it. The transaction used 1,258,562 gas. The intrinsic cost of 50 authorizations is 21,000 + 50 × 25,000 = 1,271,000, and each authorization whose account already exists refunds 12,500. That is 1,271,000 − 12,500 = 1,258,500, almost exactly what was observed. The gas alone says that 50 authorizations were processed and that 49 had a stale nonce.
That all 50 signatures come from Binance 14’s key is the hard fact. What that fact implies is not. The chain can’t tell a signature made by an attacker who controls the key from one made by Binance itself; it only sees that the signature is valid. That this was a theft comes from Binance, not from the chain. If it was, 50 consecutive signatures with sequential nonces point to someone having access to the key or to the signing flow that uses it, not to a flaw in the contract or in EIP-7702. The protocol did exactly what it defines: it executed a delegation signed by its owner.
The dress rehearsal was in plain sight hours earlier. At 13:51 and 14:19 the operator made two single-authorization type 4 delegations on two test accounts and swept them. At 16:20 it repeated the exercise on a third one, this time also testing withdrawETH. The final sweeper was deployed at 15:59, in transaction 0x97408e75…. The operator’s gas came from the 0.1 ETH Tornado Cash pool (0x12D66f87…, labeled TornadoCash_Eth_01), in three withdrawals of 0.098 ETH on the 28th and 29th.
The sweeper, from the bytecode
Contract 0x63e842…6fAe has 1,674 bytes of runtime code. I pulled it with eth_getCode and disassembled it with pyevmasm. You don’t need to write a disassembler: the library exists and solves this in ten lines.
from pyevmasm import disassemble_all
code = bytes.fromhex(open("sweeper.hex").read()[2:])
for ins in disassemble_all(code):
print(f"{ins.pc:04x}: {ins}")
The entry dispatcher compares the first four bytes of the calldata (the selector) against five functions:
001c: CALLDATALOAD
001d: PUSH1 0xe0
001f: SHR ; selector = calldata[0:4]
0021: PUSH4 0x01e33667 EQ ... ; withdrawToken(address,address,uint256)
002c: PUSH4 0x043ff9d2 EQ ... ; callExecute(address,address,uint256,bytes)
0037: PUSH4 0x4782f779 EQ ... ; withdrawETH(address,uint256)
0042: PUSH4 0x8a179be4 EQ ... ; sweep(address,address[])
004c: PUSH4 0x97dc97cb SUB... ; authorizedCaller()
All five selectors resolve on 4byte and openchain without ambiguity. One of them, 0x043ff9d2, is missing from 4byte but present on openchain: callExecute(address,address,uint256,bytes).
Access control is the first thing every function with side effects does. This sequence appears identically on entry to sweep, withdrawETH, withdrawToken and callExecute:
0088: CALLVALUE PUSH2 0x83 JUMPI ; if you send ETH with the call, revert
...
00e2: PUSH20 0x7c8601d4645b93cea45f71e118a729332e6b5ca0
00f7: CALLER ; msg.sender
00f8: SUB ; authorizedCaller - msg.sender
00f9: PUSH2 0x2e7 JUMPI ; if non-zero, jump to revert
The operator’s address 0x7C86…5CA0 is embedded in the bytecode as a constant. The only account that can call any function with side effects is that one. It is the classic onlyAuthorized, but with the address burned into the code instead of sitting in a storage variable, so it can’t be changed. The authorizedCaller() view (selector 0x97dc97cb) returns that same constant from PUSH20 … MSTORE RETURN, without going through access control.
There is a receive (it accepts ETH with no data: the STOP at 0x0019 after checking that the calldata is empty) and no fallback with logic: any selector outside the five lands on INVALID. There is no SELFDESTRUCT, no DELEGATECALL, no storage: everything is constants and calldata. It is a stateless, disposable contract.
sweep(address to, address[] tokens) is the core. It walks the list of tokens:
- If the token is the zero address, it treats the entry as ETH: it reads
SELFBALANCE, and if the balance is above 1 ether (0xde0b6b3a7640000) it sends the balance minus 1 ether with aCALL. It always leaves 1 ETH in the account; the bytecode does not say why. - If it is a regular token, it makes a
staticcalltobalanceOf(address(this))(selector0x70a08231), and if the balance is greater than zero it callstransfer(to, balance)(selector0xa9059cbb). - If a transfer fails, it does not abort the sweep: it emits an event with a code (I saw
LOG2with the same topic0x394e1e4b…on the failure branches) and moves on to the next token. That is why a singlesweepcan move 65 assets even if three of them revert.
The sweep’s tolerance for failures is what makes it dangerous: you hand it a long list of tokens, it sweeps every one it can and doesn’t get stuck on the ones it can’t. I reconstructed it as pseudo-Solidity to read at a glance:
function sweep(address payable to, address[] calldata tokens) external onlyAuthorized {
for (uint256 i = 0; i < tokens.length; i++) {
address token = tokens[i];
if (token == address(0)) {
uint256 bal = address(this).balance;
if (bal > 1 ether) { (bool ok,) = to.call{value: bal - 1 ether}(""); /* if it fails, emit event and continue */ }
} else {
(bool s, bytes memory r) = token.staticcall(abi.encodeCall(IERC20.balanceOf, (address(this))));
if (!s || r.length < 32) continue; // odd token: skip it
uint256 bal = abi.decode(r, (uint256));
if (bal > 0) token.call(abi.encodeCall(IERC20.transfer, (to, bal))); // if it fails, continue
}
}
}
callExecute is the other mechanism, and the one that explains the two USDC movements. It checks that the target has code (EXTCODESIZE; if it is zero it reverts with NoCodeAtExecutor) and forwards an execute(address,uint256,bytes) call to it (selector 0xb61d27f6). It is the execution pattern of ERC-4337-style smart accounts. Binance 14, already carrying the sweeper’s code, used it to order one of its own smart accounts to release the USDC. More on that shortly.
A note on the contract’s “source”. Blockscout shows a verified Target.sol, but the file itself warns in its header that it is an AI reconstruction from the bytecode made at 00:30 UTC on October 30, not the original code. The runtime compiles byte for byte identically, so the logic is reliable; the contract and parameter names are guesses. What I describe above comes from the disassembly, not from that source.
How the deposits were drained
Binance 14 is a deposit hot wallet: it receives customer deposits around the clock. The attacker didn’t empty it once; they built a sweep loop. Here is the rate of outflows from the account in ten-minute windows:
20:20 38 outflows (delegation starts, first sweep)
20:30 11
20:40 1
...
22:50 18
23:00 28
00:00 1
00:10 1 (revocation at 00:10:35)
00:20 157 (already back under Binance's control, legitimate deposit outflows)
Between the first big sweep and the revocation, the operator launched more sweep calls as money came in: at 21:04, 21:44, 22:39 and 23:56, plus five empty sweeps from 23:36 on, when nothing was left to take (23,100 gas each, the cost of walking a list and transferring nothing).
The clearest case of the loop is the 200 million USDT transfer. Between the 21:04 sweep and the 21:44 one, 200.4 M USDT arrived at Binance 14 in 95 transfers (per USDT Transfer logs), 200 M of it in a single one from 0x22132139bf7f3921b1cadeab931f4fbf7bf2bc9e. At 21:44 the sweep 0x55ba4dab… took it. In other words: money that reached Binance’s already compromised account and went out the back door minutes after it came in.
The flow of funds
Aggregating the transfers from the seven sweeps that carried loot (at today’s prices):
| Asset | Amount | Approx. value |
|---|---|---|
| USDT | 1,819 M | ~$1,818 M |
| USDC | 823 M | ~$823 M |
| ETH (sweeps) | 172,624 ETH | ~$384 M |
| WBETH | 29,934 | ~$73.6 M |
| RLUSD | 62.6 M | ~$62.6 M |
| XAUT (tokenized gold) | 11,374 | ~$44.4 M |
| UNI | 6.2 M | ~$43.2 M |
| ENA, PEPE, USD1, LINK, AAVE… | various | tens of $M each |
What I could add up on Ethereum alone, at today’s quotes, exceeds 3.2 billion dollars, and that leaves out a good part of the 122 minor tokens in the last sweep. At yesterday’s prices the figure is higher. I neither reach nor validate the 5,300-6,400 M published as the multi-chain total: that would include the other nine networks, which I haven’t looked at.
The split by destination:
- Recipient 1 (
0x5227f6…2f8c): the bulk. USDT, USDC, the 167,464 ETH from the first sweep, and the 823 M USDC that went out throughcallExecute. - Recipient 2 (
0xA97bF7…8d10): the 122 minor tokens and the last 2,962 ETH.
Both recipients are EOAs with no code (eth_getCode returns 0x for both). From there the money didn’t sit still:
- The ETH was consolidated. Recipient 1 sent 195,460 ETH to
0x8532FA14…at 20:38, 2,000 ETH to the same address at 01:14, and 17,260 ETH to0x03b0eE5d…. Recipient 2 sent 3,000 ETH to0xc2096a12…and 7,647 ETH to0x54401f80…. Those addresses accumulate the ETH and still hold balances today: 197,460, 40,600 and 17,260 ETH. It has not moved to any mixer or bridge from there, at least as far as I looked. - Binance 16’s 28,005 ETH enter the picture. At 20:24:47, four minutes after the first sweep, the Binance 16 hot wallet (
0xDFd5293D…) sent 28,005.6 ETH to recipient 1 in a normal type 2 transaction, signed with its own key, nonce 15,703,491. It is not a 7702 delegation, it is an ordinary transfer. A second Binance hot wallet sending ETH to the attacker’s account with a normal transaction fits with the compromise not being limited to Binance 14, but reaching the system that signs for several accounts. But that is inference: the chain only proves the transfer left Binance 16, signed by Binance 16. - The stablecoins started being converted. Recipient 1 swapped tokens for ETH on Uniswap (selector
0x791ac947, theswapExactTokensForETHSupportingFeeOnTransferTokensof the V2 router0x7a250d56…) in hundreds of operations, and moved USDT in blocks of 400-500 M to other EOAs. Recipient 2 did the same with the V3 router.
And here the limit of this kind of attack shows up when what you steal is centralized stablecoins. Checking the issuers’ blacklists, recipient 1 is already blocked on USDT and USDC, and recipient 2 on USDC:
# isBlackListed(address) on USDT -> selector 0xe47d6060
# isBlacklisted(address) on USDC -> selector 0xfe575a87
curl -s -X POST https://ethereum-rpc.publicnode.com \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{
"to":"0xdAC17F958D2ee523a2206206994597C13D831ec7",
"data":"0xe47d6060000000000000000000000000'\
'5227f603c1ee34ce3c06efe6477003bdfccb2f8c"},"latest"]}'
# -> 0x...01 (blocked)
Tether and Circle freeze with a single eth_call: once an address is on the blacklist, its USDT or USDC are stuck and can’t be transferred. That explains the rush to swap into ETH on Uniswap over the following hours. ETH, DAI and decentralized tokens can’t be frozen; the 823 M of USDC and a good part of the USDT that didn’t get converted in time, can. In practice, part of the loot is already worthless to the attacker.
The revocation
At 00:10:35 on October 30, Binance 14 sent itself the type 4 transaction 0xafbf94c9…: a single authorization delegating to 0x0000000000000000000000000000000000000000. Delegating to the zero address is EIP-7702’s canonical way of clearing a delegation. Ten minutes later, the account’s outflows shoot up again (157 in the 00:20 window), and these are already legitimate deposit withdrawals. Today eth_getCode on Binance 14 returns 0x: the account is an EOA with no code again.
That Binance could revoke means that by 00:10 it already controlled Binance 14’s key again (the revocation is signed by the account itself). The hole stayed open for 3 hours and 50 minutes. That is a long time for an incident of this size, and why it took almost four hours to close is probably the most uncomfortable question for Binance.
What isn’t known
With the chain in front of us, these questions stay unanswered, and it is best not to invent answers:
- How the attacker got to sign with the key. The chain proves the 50 signatures are Binance 14’s, not how they were obtained. Binance points to a compromised DevOps laptop. That is its version, and it is plausible, but I can’t verify it on-chain.
- Whether Binance 16 was equally compromised or was just used as a stepping stone. The 28,005 ETH transfer is normal and signed by Binance 16, nothing more.
- The real total and the other chains. My 3,200 M is Ethereum only, and only what I could add up. The 5,300 to 6,400 M figures come from analytics firms and from Binance itself.
- The attribution. Lazarus is the claim of TRM Labs and Chainalysis. Using Tornado Cash to fund the gas and the consolidation pattern are compatible with state-backed groups, but “compatible with” is not “is”. The use of 7702 with batched authorizations, specifically, has been seen this year in automated drainers aimed at ordinary users’ wallets, not only in large operations.
Lessons for exchanges and custodians
The protocol worked as written. The failure is upstream, in how an organization signs and what it watches. What this case makes clear:
EIP-7702 turns a stolen signature into persistent access. Before, stealing a signature got you one transaction. Now, stealing a 7702 authorization gets you the account until someone revokes it. The threat model of anyone who holds keys changed on May 7, 2025, whether they knew it or not.
Signing policy must reject type 4 by default. A deposit account has no legitimate reason to sign a delegation authorization. The HSM or signing service should refuse to sign 0x04 transactions and 7702 authorization tuples except against an explicit, reviewed allowlist. If Binance’s signing flow had rejected type 4 for its hot wallets, these 50 signatures would not have existed.
You have to monitor EOA code. A hot wallet that suddenly has code (eth_getCode stops returning 0x) is a top-priority alarm. It is cheap: a cron job that queries eth_getCode for every hot account every few seconds and alerts if it changes. That is what cuts the 3 hours and 50 minutes down to minutes. A minimal detection rule:
import json, subprocess
RPC = "https://ethereum-rpc.publicnode.com"
HOT_WALLETS = ["0x28C6c06298d514Db089934071355E5743bf21d60", ...]
def code_of(addr):
body = {"jsonrpc":"2.0","id":1,"method":"eth_getCode","params":[addr,"latest"]}
out = subprocess.run(["curl","-s","-X","POST",RPC,
"-H","content-type: application/json",
"-d",json.dumps(body)], capture_output=True, text=True).stdout
return json.loads(out)["result"]
for w in HOT_WALLETS:
code = code_of(w)
# EIP-7702: a delegated EOA starts with the 0xef0100 designator
if code != "0x":
delegate = "0x" + code[8:] if code.startswith("0xef0100") else None
alert(f"{w} has code: {code[:20]}… delegated to {delegate}")
Anyone who wants something finer can listen with eth_getLogs for the event that Pectra doesn’t emit (there is no native delegation log) and rebuild it by looking at type 4 transactions whose authority is one of their accounts; but a periodic eth_getCode already catches 100% of cases, because the delegation leaves a trace in the state.
Response time matters as much as prevention. The attacker built a sweep loop precisely because they knew the account would keep receiving deposits. Every minute the delegation was in place, new money arrived and left. An automatic deposit cutoff on detecting code in a hot wallet would have stopped the bleeding even without an instant revocation.
Centralized stablecoins are the attacker’s weak point, not yours. That Tether and Circle froze the recipients within hours is the best news of the incident. If your exchange has a relationship with the issuers, the window to ask for a freeze is measured in minutes, and every minute counts before the attacker swaps to ETH on a DEX.
How to reproduce the checks
All of this can be verified without an API key. The delegation:
curl -s https://eth.blockscout.com/api/v2/transactions/\
0xad99264d5532ae0dd04b2004eebda05f25657e70dacb764f7ac8d4ccd051298b \
| python3 -c 'import json,sys; t=json.load(sys.stdin); al=t["authorization_list"]; \
print("authorizations:",len(al)); \
print("ok:",[a["nonce"] for a in al if a["status"]=="ok"])'
# authorizations: 50
# ok: [18914809]
The sweeper’s bytecode and its disassembly:
curl -s -X POST https://ethereum-rpc.publicnode.com \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_getCode",
"params":["0x63e842B054Bd624a465454D533C33F573e9c6fAe","latest"]}' \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["result"])' > sweeper.hex
python3 -c 'from pyevmasm import disassemble_all; \
print("\n".join(str(i) for i in disassemble_all(bytes.fromhex(open("sweeper.hex").read()[3:]))))'
And that Binance 14 is no longer delegated:
curl -s -X POST https://ethereum-rpc.publicnode.com \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_getCode",
"params":["0x28C6c06298d514Db089934071355E5743bf21d60","latest"]}'
# {"result":"0x"}
Conclusion
There was no failure in Ethereum, or in EIP-7702, or even in the attacker’s contract, which is almost insultingly simple: 1,674 bytes, five functions, zero state. What failed was the usual link, key custody, but with a new difference that changes the math. Since Pectra, a single signature pulled from a hot wallet is no longer worth one transaction; it is worth the whole account, for as long as it takes them to notice.
The 50-authorization trick is elegant and cheap: it costs 25,000 gas per attempt, and only one needs to hit the nonce. Anyone who custodies keys on Ethereum has to assume that signing a type 4 transaction is, in effect, handing over the account, and treat that signature with the care that deserves: reject it by default, watch the code of their EOAs, and have a kill switch that doesn’t depend on recovering the key first.
The good part is that the money leaves a public trail that anyone can follow with curl, and that centralized stablecoins can be frozen. The bad part is that the 3 hours and 50 minutes the door stood open were paid for by everyone who deposited into that account during the night of October 29 to 30.