On 29 October 2026, about US$6.3 billion in assets flowed out of the hot wallets of Binance, one of the world's largest crypto exchanges. By Binance's own account the compromise began at around 20:05 Coordinated Universal Time (UTC) — 05:05 on 30 October in Japan.
This article lays out the events and then digs into one point most write-ups skip: assets can leave even when private keys are stored safely.
The tools that protect keys (HSMs, MPC, KMS) protect against the key being taken. Deciding whether what is about to be signed is a legitimate withdrawal is a different layer's job. The on-chain record points to that second layer being tested. This article covers that record, and the design builders of transfer and payment systems should adopt, with code whose behavior is checked by tests.
Timing and sourcing. This article rests on primary sources as of 01:00 JST on 1 November 2026. Binance has not published a technical post-mortem and the root cause is not settled. It separates facts confirmed in official notices, analytics firms' assessments (reports and hypotheses) and the author's inference. The diagrams and code are a generalized reference model and not a reconstruction of Binance's internal architecture. The teaching code is not usable as a production asset-management system.
Key points in 30 seconds
- What happened: Binance's hot wallets (internet-connected wallets used for instant withdrawals) were compromised and about US$6.3 billion left, across roughly ten chains.
- The cause is not settled: the first update said the incident "may involve" a compromised DevOps engineer's laptop that let an attacker reach a service container and submit unauthorized withdrawal transactions. Update #2 the same day revised it: the subsequent investigation found additional points of breach.
- Private keys: Binance says it has found no evidence of a leak so far. Absence of evidence is not proof that nothing leaked.
- Three different states of money: about $3 billion was frozen on-chain by issuers. Binance replenished 90% of the outflow from its own funds. The amount recovered from the attacker has not been published.
- Impact on users: withdrawals are suspended, with a phased reopening planned for 1 November. Deposits are said to be normal.
Before you hold real customer data
Web app & SaaS security audit — authorization, tenant isolation and config, verified in the code
Confirmed facts and timeline
Binance has published three official notices. The pages render in JavaScript, so the author checked them against the body text and publish/update times (UTC) returned by Binance's announcement API.
| Time (UTC) | JST | Event | Source and classification |
|---|---|---|---|
| 28 Oct, morning | 28 Oct | The attacker's operating wallet is funded via Tornado Cash | Cointegrity analysis (report) |
| 29 Oct ~20:05 | 30 Oct ~05:05 | Compromise of the hot wallets begins; first outflow on the XRP Ledger | Binance update #2 (confirmed); chain per Cointegrity |
| 29 Oct 20:20:59 | 30 Oct 05:20:59 | An EIP-7702 delegation is set on the hot wallet "Binance 14" | On-chain (confirmed; see below) |
| 29 Oct ~21:01 | 30 Oct ~06:01 | Tether freezes the main receiving wallet | Cointegrity (report) |
| 29 Oct ~22:27–22:33 | 30 Oct ~07:27–07:33 | Withdrawals suspended | Cointegrity (report); the suspension itself is confirmed in update #1 |
| 29 Oct 23:56 | 30 Oct 08:56 | Deposits arriving at Binance 14 are swept for the last time | Elliptic (report) |
| 30 Oct 02:23 | 30 Oct 11:23 | Update #1: laptop lead, no evidence of key leak, about $3B frozen | Binance (confirmed) |
| 30 Oct 11:36 | 30 Oct 20:36 | Update #1 revised to say deposits are unaffected and operating normally | Binance (confirmed) |
| 30 Oct 19:33 | 31 Oct 04:33 | Update #2: 20:05 start time, about $6.3B, the laptop lead revised, 80% covered | Binance (confirmed) |
| 31 Oct 14:24 | 31 Oct 23:24 | Update #3: 90% replenished, Proof of Reserves snapshot, withdrawals to reopen 1 Nov | Binance (confirmed) |
What changed between the first update and the second
The most important change is the account of how the attacker got in. Update #1 says:
initial investigation suggests that the security incident may involve a compromised DevOps engineer's laptop, which allowed an attacker to gain access to a service container and submit unauthorized withdrawal transactions.
Even then the wording is hedged: "may involve". Update #2 revised the explanation itself:
Our initial investigation suggested that the incident may involve a compromised laptop, however, our subsequent investigation has uncovered additional points of breach.
The "additional points of breach" have not been named. An article that quotes only the first update and says "the cause was a DevOps laptop" is already out of date.
Frozen, recovered and replenished are different things
Three words that are often mixed up in coverage:
| State | Meaning | Amount | Whose money, and where |
|---|---|---|---|
| Frozen | An issuer (Tether, Circle, etc.) made tokens at the attacker's addresses immovable | About $3B per Binance. Elliptic: $2.85B (44% of the theft). TRM: about $2.7B | Still at the attacker's addresses. Not returned |
| Recovered | The stolen assets themselves were taken back | Not published | — |
| Replenished | Binance prepared the same assets from its own funds | 90% of the outflow (update #3); ten main assets at 100% | Corporate treasury, the SAFU (user protection fund) and a futures insurance fund |
Freezing worked almost only for USDT and USDC. Elliptic writes that "nothing stolen on Bitcoin, the XRP Ledger, Solana or NEAR has been frozen". That reflects a property of blockchains: tokens with a central issuer can be stopped; native assets cannot.
Binance's statement on private keys
Update #1 says "We have found no evidence of any wallet private key leak." Update #2 says "At this stage, we have found no evidence that wallet private keys were leaked." Update #2 added "At this stage".
This is not a statement that nothing leaked. The on-chain record below is consistent with the view that Binance's signing system was made to produce signatures the attacker wanted. But that alone cannot settle whether a key was taken out.
What is still unknown
- The root cause, and the "additional points of breach" in update #2
- Whether keys were taken, or the signing service was abused
- How the attacker obtained the 50 pre-signed delegations described below
- Whether deposits were also paused around 00:00 UTC on 30 October, as some reports say (Binance's own page was revised to say deposits were unaffected)
- The breakdown of the replenishment funds (SAFU, insurance fund, treasury)
- Who carried it out. TRM Labs attributes it to North Korea's TraderTraitor "with high confidence" and Elliptic says it is likely. Binance has not named anyone and no government attribution is confirmed. That is an analytics firm's assessment, not an established fact
Background: holding a key is not the same as being made to sign
Now the technical part. First, two prerequisites for non-engineers.
Hot and cold wallets
A hot wallet is connected to the network and can answer withdrawal requests automatically. To process user withdrawals in seconds to minutes, an exchange keeps some assets there.
A cold wallet keeps keys in an environment cut off from the network. It is safer, but moving funds takes people and time.
An exchange's design is a balance between how much sits in the hot wallet and how it is topped up from cold storage. The hot wallet was the target here, and the loss still reached $6.3 billion — a sign of the exchange's scale.
The company seal and the vault keeper
A transfer on a blockchain is legitimate if it carries a "signature by a private key". By analogy, the private key is a company's registered seal.
Lock the seal in a sturdy vault (an HSM, for example) so it cannot be taken out — and if someone gains the standing to hand the vault keeper a document and say "stamp this", the document gets stamped. If the keeper stamps without reading the contents, it gets stamped whether it is a "payment order" or a "power of attorney".
The analogy has limits. A real person reads before stamping; a signing system only applies the rules its designers wrote. When the rule was "look at amount and destination", a kind of document with neither falls outside the rule.
Three layers to protect: key, signature, business
"Protect the assets" splits into at least three separate questions.
| Layer | Question | Example controls | If it is broken |
|---|---|---|---|
| 1 Key custody | Can the key be taken out? | HSM, MPC, KMS | The key is stolen and the attacker signs on their own |
| 2 Signing authorization | What may be signed? | Policy engine, decoding what is signed, approval flow | The legitimate signing system produces signatures nobody wanted |
| 3 Business authorization | Is the withdrawal legitimate as business? | Destination allowlist, limits, independent approval, anomaly detection | The signature looks fine but the movement of funds is not legitimate |
Most discussion concentrates on layer 1. Key-protection design is mature, with standards such as NIST SP 800-57. But layer 1 alone does not protect layers 2 and 3. As here, huge sums can leave while it is unclear whether layer 1 was broken at all, as far as is disclosed.
What HSMs, MPC and KMS do, and do not do
| Mechanism | Role | Does not |
|---|---|---|
| HSM (dedicated tamper-resistant device) | Keeps the key inside the device and signs there | Judge whether the contents of the request are legitimate; it serves whoever holds the privilege to call it |
| MPC (shared signing with split key) | A breach of one place does not expose the whole key | If enough participants agree to the same request it signs; whether the request is right is another matter |
| KMS (cloud key management) | Key storage, access control, usage records | A principal with IAM permission that calls the signing API gets a signature |
What they share is that a key mechanism does not decide who may request a signature or what may be signed. That is layer 2's responsibility.
What is visible on-chain: a signature with no amount and no destination
This section concerns publicly available records anyone can check.
What can be confirmed
A transaction published on Etherscan, at 20:20:59 UTC on 29 October 2026, is in block 26,300,753 and has these features:
- Transaction type 4 (the "set code" type of EIP-7702)
- The sender is an address labeled as the attacker's; the recipient is the Binance hot wallet labeled "Binance 14"
- It carries 50 authorizations, of which one is valid (nonce 18914809), with a single delegate address
The author fetched the same transaction from a public Ethereum RPC and checked these himself: type 4, 50 authorizations, chain ID 1, and one delegate address.
Author's observation: checking Binance 14's current code on the same RPC on 1 November 2026 returned empty (
0x). That indicates no delegation is set now (it was removed or replaced), but I have not checked when or by whom it was removed.
What EIP-7702 is (what the specification says)
EIP-7702, "Set Code for EOAs", has the status Final. Normally an externally owned account (EOA) only signs with its key to send funds and has no program. EIP-7702 lets an EOA run the code of a chosen contract as its own. The specification's essentials:
- The signature covers
[chain_id, address, nonce], and does not include an amount or a destination - If the authorization is valid, the account's code is set to
0xef0100 || address(a marker meaning "defer to the code at this address") - From then on, calls to the account are executed by the delegated code, in the account's own context
- A delegation that has been set is not rolled back even if execution of the transaction fails
The specification's Security Considerations say a poorly implemented delegate can let a malicious actor "take near complete control over a signer's EOA".
What can and cannot be said
| Can be said (confirmed) | Cannot be said |
|---|---|
| A record exists of an EIP-7702 delegation being set on Binance 14 | How the attacker obtained that signature |
| An authorization must be signed with the account's key (specification) | Whether a key was taken, or the signing system was made to sign |
| A delegation signature does not contain an amount or a destination (specification) | Whether Binance's signing system inspected this kind of signature (internal design is not public) |
Analytics firm Elliptic assesses that the attacker "appears to have made Binance's own systems sign", and relays Binance's account that this delegation "did not pass through our normal approval controls". TRM Labs also says it "points to a compromise of Binance's hot wallet signing infrastructure". But these are analyses, not Binance's official finding of cause. Some experts, such as at SlowMist, keep key leakage open as a hypothesis.
The author's view: why a policy that looks only at amount and destination is not enough
What follows is the author's inference from published facts. It is not established fact.
If a withdrawal policy is built as "the amount is within the limit and the destination is on the allowlist", a signature that is not shaped like a transfer (one that changes how an account behaves) can fall entirely outside what the policy evaluates. If the policy does not distinguish "a transfer or not" and does not read the actual bytes to be signed, this hole opens.
Whether Binance's policy was like that is unknown. What designers can take away is the principle of restricting which kinds of signature are allowed. The teaching code below turns it into code.
Reference architecture: the withdrawal and signing pipeline
Note: the diagram below is a generic reference model for an exchange or payment system. It is not a reconstruction of Binance's internal design.
[User]──withdrawal request──▶ [Withdrawal API] ──▶ [Policy engine] ──▶ [Signing service] ──▶ [Blockchain]
authentication, layers 2 and 3 layer 1 (HSM/MPC/KMS)
idempotency key · decode the actual the key never leaves
bytes to be signed
· refuse non-transfers
· reserve limits atomically
· verify approval (2 independent people)
│
[DevOps workstation] ─ ─ ✕ ─ ─ ▶ no direct line to the production signing path (people only see the approval screen)
│
▼
[Audit log (append-only)] ──▶ [Anomaly detection / emergency stop]
Threat model: from account compromise to unauthorized withdrawal
| Stage | What happens | Layer that stops it |
|---|---|---|
| 1. Foothold | A developer workstation, CI or container is compromised | Workstation separation, short-lived credentials, network separation |
| 2. Lateral movement | The compromised principal reaches privileges that can call the signing service | Least-privilege IAM and workload identity; restricting who can call signing |
| 3. Signing request | The attacker asks for a signature | Layer 2: inspect the actual bytes, refuse non-transfers by default |
| 4. Movement of funds | If signed, assets move | Layer 3: destination allowlist, limits, independent approval, cooling-off |
| 5. Exit | Assets leave and are dispersed | Anomaly detection, emergency stop, coordination with issuers (freezing) |
From DevOps workstation to production: the trust boundary
Update #2 revised the account, and the real route is unknown. If the path in update #1, "DevOps workstation → service container → withdrawal", were accurate, it would illustrate a typical risk of a person's machine being too close to production assets. In general, separate these:
- Development and operations workstations do not connect directly to the production signing path. Needed operations go through an approval flow (change management)
- A service's privileges (workload identity) are limited to that service's role. If a container can instruct withdrawals, whoever takes over the container gets that same power
- A person's long-lived credentials do not work against the production signing API. Use short-lived tokens and per-action approval
Teaching code: a withdrawal policy engine that sits before signing
Here is a TypeScript implementation of layers 2 and 3. The complete code and tests are in this site's repository at tests/unit/fixtures/withdrawal-policy.ts and tests/unit/withdrawal-policy.test.ts, and run in CI all the time.
Limits. This is an educational reference implementation. A real asset-management system needs a persistent database, authentication to the signing service, key operations, audit and incident handling, which this code does not replace.
Design approach
| Requirement | Implementation here |
|---|---|
| Amount type with explicit currency and minimum unit | Money { asset, minor: bigint }. Strings like "0.1" convert to integers without rounding; too many decimals are rejected |
| Avoid floating-point error | Amounts are always integers (bigint); number is not used |
| Judge the actual bytes to be signed | Decode the bytes to decide the kind, not the caller's description |
| Refuse non-transfer signatures | Delegations (type 4), approvals and unknown kinds are refused by default |
| Refuse bad destinations | Allowlist plus a cooling-off period after a destination is added |
| Approval binding, expiry, replay protection | An approval is bound to the digest of the request and expires. A request id can reserve only once, so reusing an approved request is refused |
| Separation of duties | The requester's own approval is invalid; two different people and two different roles are required |
| Atomic limit update | The check and the update are one indivisible operation |
| Idempotency | The same idempotency key returns the same decision and does not reserve twice; different content is rejected |
| Fail closed | Unexpected errors turn into a denial |
| Auditability | Every decision goes to an append-only log |
| Emergency stop | A circuit breaker (when open, deny everything). It is checked before the stored idempotent decision, so a replay of an already-allowed request is stopped too |
Decode the actual bytes to be signed
export interface SigningPayload {
readonly txType: number; // EIP-2718 transaction type. 0x04 is EIP-7702
readonly to: string;
readonly valueWei: bigint;
readonly data: string; // hex calldata. "0x" for a plain ETH transfer
}
export type Intent =
| { readonly op: "native_transfer"; readonly recipient: string; readonly amount: bigint }
| { readonly op: "erc20_transfer"; readonly token: string; readonly recipient: string; readonly amount: bigint }
| { readonly op: "delegation" | "approval" | "unknown" };
const SELECTOR = {
transfer: "0xa9059cbb", // transfer(address,uint256)
approve: "0x095ea7b3", // approve(address,uint256)
setApprovalForAll: "0xa22cb465", // setApprovalForAll(address,bool)
} as const;
/** Read the bytes, not the caller's label. Anything unreadable is "unknown". */
export function decodeIntent(p: SigningPayload): Intent {
if (p.txType === 0x04) return { op: "delegation" }; // a signature that hands over the account's code
const data = p.data.toLowerCase();
if (data === "0x") return { op: "native_transfer", recipient: p.to.toLowerCase(), amount: p.valueWei };
if (p.valueWei !== BigInt(0)) return { op: "unknown" };
const selector = data.slice(0, 10);
if (selector === SELECTOR.approve || selector === SELECTOR.setApprovalForAll) return { op: "approval" };
if (selector === SELECTOR.transfer && data.length === 10 + 64 + 64) {
return {
op: "erc20_transfer",
token: p.to.toLowerCase(),
recipient: "0x" + data.slice(34, 74),
amount: BigInt("0x" + data.slice(74, 138)),
};
}
return { op: "unknown" };
}
0xa9059cbb, 0x095ea7b3 and 0xa22cb465 are the function selectors of ERC-20 transfer(address,uint256), approve(address,uint256) and setApprovalForAll(address,bool), which the author confirmed against a public signature directory.
The point is to explicitly enumerate the allowed kinds and refuse everything else. A "ban the dangerous ones" approach (a blocklist) lets kinds of signature you have never heard of through.
Order of checks, and matching "claim" against "reality"
// Excerpt: WithdrawalPolicy.#decide
if (this.#circuitOpen) return audit(deny("circuit_open"));
// Judge the thing that will actually be signed, not the claim.
const intent = decodeIntent(req.payload);
const expectedToken = this.cfg.tokenContracts[asset]?.toLowerCase();
const kindOk =
(intent.op === "native_transfer" && asset === "ETH") ||
(intent.op === "erc20_transfer" && intent.token === expectedToken);
if (!kindOk) return audit(deny("operation_not_permitted"));
if (intent.recipient !== req.destination.toLowerCase() || intent.amount !== minor) {
return audit(deny("payload_mismatch"));
}
// ...allowlist, cooling-off, per-transaction limit, approval verification...
// Last, check and update the limit as ONE indivisible operation:
const reservationId = `rsv_${req.id}`;
const slot = await this.store.tryReserve(reservationId, req.accountId, asset, minor, this.cfg.dailyLimit[asset], now);
if (slot === "duplicate") return audit(deny("request_already_used"));
if (slot === "limit") return audit(deny("daily_limit"));
return audit({ kind: "allow", reservationId });
payload_mismatch checks that the amount and destination the request claims match what is actually signed. Not trusting the claim is the basic stance of layer 2.
Bind an approval to one specific request
export function requestDigest(r: WithdrawalRequest): string {
const p = r.payload;
const canonical = JSON.stringify([
r.id, r.accountId, r.amount.asset, r.amount.minor.toString(), r.destination,
p.txType, p.to.toLowerCase(), p.valueWei.toString(), p.data.toLowerCase(),
]);
return createHash("sha256").update(canonical).digest("hex");
}
What an approver agrees to is "this request". If the approval is bound to the digest, using it on another request (replay) is invalid. On top of that it has an expiry, the requester's own approval is invalid, and it requires two different people in two different roles.
A race condition: "check, then update" breaks
A limit check is sometimes written like this:
// Bad example (RacyStore): read → wait → decide → write
const used = this.#used.get(key) ?? BigInt(0); // 1. read
await sleep(5); // 2. wait on a round trip to the DB
if (used + amount > limit) return "limit"; // 3. decide on a stale value
this.#used.set(key, used + amount); // 4. write
return "reserved";
Another request slipping in between the read and the write is called a TOCTOU (time-of-check to time-of-use) gap. Take concrete numbers: a daily limit of 10 ETH, and 50 simultaneous requests of 0.9 ETH each. The right answer is that it stops at 11 (10 ÷ 0.9, rounded down).
| Implementation | Requests let through out of 50 concurrent |
|---|---|
| Check and update separated (the bad example above) | 50 (the limit blown through) |
| Check and update as one indivisible operation | 11 (stops at the limit, correctly) |
These are values the author measured in the tests. All of them got through because all 50 decided on a stale value of "nobody has used any yet".
In a real database you achieve this with a conditional update such as UPDATE ... SET used = used + $amount WHERE used + $amount <= $limit (judging success by the number of rows updated), or with a row lock. The in-memory implementation in the teaching code shows the same property by "reading and writing without an await in between". The reservation also needs a unique constraint per request id; the teaching code refuses a second reservation for the same request id as request_already_used, so resubmitting an approved request under a different idempotency key cannot spend the limit twice.
Test results
npx vitest run tests/unit/withdrawal-policy.test.ts ended with all 25 tests passing in the author's environment, and tsc type checking and ESLint passed too. What is verified:
- Amount parsing (too many decimals, negatives, zero and exponent notation are rejected)
- Decoding of signatures (transfer, delegation, approval, unknown). Calls with ETH attached, and calldata that is truncated or has extra bytes, cannot be read exactly and are refused. Address case is ignored. A native-transfer signature cannot satisfy a USDT request
- A small withdrawal to an allowlisted destination is allowed (ETH and USDT)
- Delegation, approval and fake-token signatures are refused even when the request looks harmless
- A request whose claimed destination and amount differ from what is actually signed is refused
- Unknown destinations and just-added (cooling-off) destinations are refused
- Over-limit requests, and requests above the approval threshold without approval, are refused
- Expired approvals, approvals reused on another request (changing the destination, amount or account invalidates them), self-approval, same-role-only approval, duplicated approvers and one person holding both roles are refused
- Spending an approved request twice by resubmitting it under a new idempotency key is refused (
request_already_used) - Exact boundaries (cooling-off, per-transaction limit, approval threshold, approval expiry) and the rolling 24-hour window (usable up to exactly the limit, released after 24 hours)
- Idempotency: a retried request reserves once; different content under the same key yields
idempotency_conflict. A transient failure is not remembered, so the same key can be retried - 50 concurrent requests do not exceed the limit (atomic reservation) / the implementation that separates check and update does
- The circuit breaker's deny-all (a replay of an already-allowed request is stopped too) and the logging of decisions (immutable). The breaker opens even if the audit store is down
- A store failure becomes a denial (fail closed). An audit-write failure after the reservation is also a denial, but with no compensation step the reservation is not released (pinned as a known limit)
The invariants were not weakened, and no test was disabled, to make them pass.
What this code does not show
- Connection to a real signing service, key operations, network authentication
- Limit consistency across multiple nodes with persistence
- Verification of the EIP-7702 signature itself (this code judges the request before it is signed)
- A statistical model for anomaly detection (threshold design)
- Authenticating approvers.
approverIdandroleare assumed to be verified input; verifying the approval signature itself is not included - Approval on the daily total. The approval threshold is per request, so splitting a withdrawal into smaller ones avoids it; a real system also applies a threshold to the running total
- Compensation. If the audit write fails after the reservation, the code returns a denial but the reservation is not released
- A denial is also stored under its idempotency key. Send a corrected request under a new idempotency key
What a Proof of Reserves can confirm
In update #3 Binance showed a Proof of Reserves snapshot as of 00:00 UTC on 31 October and said a full proof including zero-knowledge proofs would be published by 3 November (UTC).
As the author understands it, a PoR shows whether assets matching customers' deposits exist at a point in time. What it does not show matters just as much.
| What a PoR can confirm | What a PoR cannot confirm |
|---|---|
| Whether holdings cover the sum of customer balances, at a point in time | Whether key and signing management is safe |
| That some of the holdings sit at publicly visible addresses | How assets were lost, and whether it will recur |
| (With zero-knowledge proofs) that totals reconcile without revealing each customer's balance | Whether the liabilities (what is owed to customers) are all included (design-dependent) |
A PoR answers "is there enough now?", not "is it safely guarded?" Not confusing the two is basic to evaluating an exchange.
For executives: why money leaves even when keys are safe, and the order to decide
For non-engineers, what this incident suggests deciding.
Three questions
- Who looks after the vault key, and how far? Key custody (layer 1) can be protected by specialist products and procedures, where many companies already invest.
- Who decides the rule for "what may be stamped"? That is layer 2. Is the rule "just amount and destination", or written by someone who has since left?
- When money goes out, is there a human eye and a way to stop it? Layer 3. For large withdrawals the basics are approval by two or more independent people, and automatic stop (a circuit breaker) on anomalies.
Setting priorities
| Priority | Action | Purpose |
|---|---|---|
| High | Inventory credentials and privileges used to move production funds; remove anything a person's workstation can use to reach the production signing path | Guard the entrance to layer 2 |
| High | Set limits and the threshold for independent approval on withdrawals and transfers. If you run payments or transfers, confirm the limit holds under concurrency | Layer 3 |
| Medium | Keep audit logs of signing, approval and configuration changes where they cannot be tampered with | Afterward verification |
| Medium | Define and rehearse the emergency-stop procedure | Stop the damage from growing |
| Low | Read published post-mortems and compare against your own setup | Learning |
For a business that moves money, designing "stopping" as a feature is the executive decision. A service stopping is a loss, but smaller than being unable to stop while assets leave — and that judgment has to be made in peacetime.
Checklists
Design and implementation
- The signing service decodes the actual bytes to be signed and does not rely on the caller's description
- The allowed kinds of signature are enumerated, and delegations, approvals, authority transfers and unknown kinds are refused by default
- Destinations are allowlisted, with a cooling-off period after one is added
- An approval is bound to a specific request, expires, and cannot be given by the requester
- The limit "check and update" is atomic, with a concurrency test
- Idempotency keys are handled server-side, and different content under the same key is rejected
- Unexpected errors become denials (fail closed)
- Amounts are held as integers in the minimum unit (no floating point)
Operations and organization
- A person's workstation has no direct path to the production signing path
- A service's privileges (workload identity) are limited to its role
- Decisions, approvals and configuration changes leave an audit log that is append-only and kept elsewhere
- An emergency-stop procedure exists and has been rehearsed
- Large withdrawals need multiple independent approvals (separation of duties)
- On-chain and off-chain balances are reconciled automatically
Conclusion and what to watch next
- What can be said as fact: hot wallets were compromised and about US$6.3 billion left. About $3B is frozen, 90% has been replenished (with Binance's own funds), and no recovery has been published. Withdrawals are suspended, with a phased reopening planned for 1 November. On-chain there is a record of an EIP-7702 delegation set on Binance 14.
- What cannot yet be said: the root cause, whether keys leaked, how the attacker obtained the delegation signatures, who was responsible (the analytics firms' assessment is a hypothesis).
- The technical lesson: protecting the private key (layer 1) does not replace signing authorization (layer 2) and business authorization (layer 3). A design that decides what may be signed from the actual bytes and refuses by default is needed separately from key protection.
To watch: Binance's technical post-mortem (the cause, and the "additional points of breach" in update #2), the full Proof of Reserves (due 3 November), the handling of frozen assets, and the situation after withdrawals reopen.
If you want your transfer, payment or fund-movement design reviewed
The author has led the design and implementation of the reliability layer of a payment platform — idempotency, consistency and prevention of double charges. From the viewpoint of this article, a design review of your backend, payment APIs, cloud privileges and the controls on moving funds is something I can do.
What I can and cannot do, up front:
- I can: review the design of transfer and payment APIs, approval flows, limits and idempotency, cloud IAM and service privileges, and audit logging and anomaly detection, with a prioritized improvement plan and a design for concurrency and failure-path tests
- I cannot: provide specialist audits of crypto custody, evaluation of key management (operating HSMs or MPC), or incident forensics. Please consult specialist providers for those
- I cannot promise you will be "completely secure". I report what was found and what was not examined, as it is
See the scope and pricing through the button below.