# Binance 63億ドル流出（2026年10月）の技術解説 — 秘密鍵を守っても資産は出ていく。署名パイプラインを守る出金ポリシーの設計

> 2026年10月29日のBinanceホットウォレット流出（約63億米ドル）を公式発表とオンチェーン記録で整理し、秘密鍵の保護と『何に署名させるか』の認可が別問題である理由を、EIP-7702の委任署名と、テスト済みの出金ポリシーエンジンで解説します。

- 公開日: 2026-11-01
- 著者: 友田 陽大
- タグ: セキュリティ, 暗号資産, アーキテクチャ設計, 認証・認可, 信頼性, 決済
- URL: https://tomodahinata.com/blog/binance-hack-2026-wallet-signing-security-analysis
- カテゴリ: セキュリティ事件の技術解説
- 総合ガイド: https://tomodahinata.com/blog/gmo-desktop-cloud-hack-2026-security-analysis

## 要点

- Binanceは、2026年10月29日20:05（UTC）ごろにホットウォレットが侵害され、約63億米ドル（63億円ではない）相当を失ったと発表した。約30億ドルはオンチェーンで凍結されたが、凍結は回収ではない。補填の90%はBinance自身の資金による
- 初報の『DevOpsエンジニアのノートPC→サービスコンテナ→不正な出金トランザクション』は、第2報で『ほかにも侵入箇所が見つかった』と修正された。根本原因と秘密鍵の扱いは確定していない。『漏えいの証拠はない』は『漏えいしていない』ではない
- オンチェーンには、ホットウォレット『Binance 14』にEIP-7702の委任（トランザクション種別4）が設定された記録が残る。委任の署名が覆うのはチェーンID・委任先・nonceで、金額も送金先も含まない。金額と送金先だけを見る出金ポリシーでは、この種の署名を止められない
- HSM・MPC・KMSは『鍵を盗ませない』仕組みで、『何に署名するか』の正しさは保証しない。署名する実バイト列のデコード、送金以外の署名の既定拒否、承認の要求への束縛、限度額の原子的な予約、独立承認が、別の層として要る
- 本記事のTypeScript教材は25のテストで検証した。読み取りと書き込みの間にawaitを挟む実装は、1日の上限で11件のところ、50件の同時要求のうち50件を通した。原子的な予約では11件で止まった。教材は一般化した参照モデルで、実運用の資産管理システムではない

---

2026年10月29日、世界最大級の暗号資産取引所 Binance のホットウォレットから、約63億米ドル相当の資産が流出しました。Binance自身の発表では、侵害は協定世界時（UTC）の20時05分ごろ、日本時間では10月30日の5時05分ごろに始まっています。

この記事は事件の経緯を整理したうえで、多くの解説が触れていない一点を掘り下げます。**秘密鍵を安全に保管していても、資産は出ていきうる**ということです。

鍵を守る仕組み（HSM・MPC・KMS）が守るのは「鍵を盗ませないこと」です。「いま署名しようとしているものが、正当な出金か」を判断するのは、**別の層の仕事**です。オンチェーンに残った記録は、その別の層が問われた可能性を示しています。その記録と、送金・決済のシステムを作る側が取るべき設計を、テストで動作を確かめたコードで示します。

> **情報の時点と扱い**: 本記事は2026年11月1日1時（日本時間）までに確認した一次資料に基づきます。Binanceは技術的なポストモーテムをまだ公表しておらず、**根本原因は確定していません**。以下では、**公式発表で確認できる事実**、**調査会社の分析（報道・仮説）**、**筆者の推論**を書き分けます。構成図とコードは一般化した参照モデルで、**Binanceの内部構成を復元したものではありません**。教材のコードは、実運用の資産管理システムとして使えるものではありません。

## 30秒で分かる要点

- **何が起きたか**: Binanceのホットウォレット（インターネットにつながり、即時の出金に使う財布）が侵害され、約63億米ドル相当が流出した。対象は10前後のチェーンにわたる。
- **原因は確定していない**: 初報は「DevOpsエンジニアのノートPCが侵害され、攻撃者がサービスコンテナに入り、不正な出金トランザクションを送った可能性」を挙げた。同日の第2報は、**その後の調査で、ほかにも侵入箇所が見つかった**と修正した。
- **秘密鍵**: Binanceは「現時点で漏えいの証拠は見つかっていない」とする。証拠がないことは、漏えいがなかったことの証明ではない。
- **お金の状態は3種類**: 約30億ドルが発行体によって**凍結**された。Binanceは自社資金で流出分の90%を**補填**した。犯人からの**回収**額は公表されていない。
- **利用者への影響**: 出金は停止中で、11月1日に段階的に再開する予定。入金は通常どおりとされる。

## 公式発表で確認できる事実と時系列

Binanceは事件について3本の公式発表を出しています。発表ページはJavaScriptで描画されるため、同社のお知らせAPIが返す本文と、公開・更新の時刻（UTC）で照合しました。

| 時刻（UTC） | 日本時間 | 出来事 | 根拠と分類 |
| --- | --- | --- | --- |
| 10/28 午前 | 10/28 | 攻撃者の運用ウォレットに Tornado Cash 経由で資金が入る | Cointegrity の分析（報道） |
| 10/29 20:05ごろ | 10/30 05:05ごろ | ホットウォレットの侵害が始まる。最初の流出は XRP Ledger | Binance 第2報（確認済み）。チェーンは Cointegrity |
| 10/29 20:20:59 | 10/30 05:20:59 | ホットウォレット「Binance 14」にEIP-7702の委任が設定される | オンチェーン（確認済み。後述） |
| 10/29 21:01ごろ | 10/30 06:01ごろ | Tether が主な受け取りウォレットを凍結 | Cointegrity（報道） |
| 10/29 22:27〜22:33ごろ | 10/30 07:27〜07:33ごろ | 出金を停止 | Cointegrity（報道）。停止の事実は第1報で確認 |
| 10/29 23:56 | 10/30 08:56 | Binance 14 に入った入金が最後に吸い上げられる | Elliptic（報道） |
| 10/30 02:23 | 10/30 11:23 | **第1報**。ノートPCの可能性、鍵の漏えいの証拠なし、約30億ドル凍結 | Binance（確認済み） |
| 10/30 11:36 | 10/30 20:36 | 第1報を更新。「入金は影響を受けておらず通常どおり」と明記 | Binance（確認済み） |
| 10/30 19:33 | 10/31 04:33 | **第2報**。20:05の開始時刻、約63億ドル、ノートPC説の修正、80%を補填 | Binance（確認済み） |
| 10/31 14:24 | 10/31 23:24 | **第3報**。補填90%、準備金の証明（PoR）のスナップショット、出金は11/1再開予定 | Binance（確認済み） |

### 初報から第2報で変わったこと

最も重要な変更は、侵入経路の説明です。第1報はこう書いています。

> 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.

この時点でも「may involve（関係している可能性がある）」という慎重な書き方です。第2報は、この説明自体を修正しました。

> Our initial investigation suggested that the incident may involve a compromised laptop, however, our subsequent investigation has uncovered additional points of breach.

「ほかの侵入箇所」が何かは公表されていません。初報だけを引用して「原因はDevOpsのノートPC」と書いた記事は、すでに古い情報に基づいています。

### 「凍結」「回収」「補填」は別物

報道で混同されやすい3つの言葉を整理します。

| 状態 | 意味 | 金額 | 誰のお金で、どこにあるか |
| --- | --- | --- | --- |
| 凍結（Frozen） | 発行体（Tether・Circle等）が、攻撃者のアドレスにあるトークンを動かせなくした | Binance発表で約30億ドル。Ellipticは28.5億ドル（盗難額の44%）、TRMは約27億ドル | 攻撃者のアドレスに残ったまま。戻ってはいない |
| 回収（Recovered） | 盗まれた資産そのものが取り戻された | **公表なし** | — |
| 補填（Replenished） | Binanceが自社の資金で、流出した資産と同じ銘柄を用意した | 流出額の90%（第3報）。主要10銘柄は100% | 自社の財務・SAFU（利用者保護基金）・先物保険基金 |

凍結が効いたのは、ほぼUSDTとUSDCだけです。Ellipticは「ビットコイン、XRP Ledger、Solana、NEAR上で盗まれたものは凍結されていない」と書いています。中央の発行体を持つトークンは止められても、ネイティブな資産は止められない、というブロックチェーンの性質が表れています。

### 秘密鍵についての公式見解

第1報は「We have found no evidence of any wallet private key leak.」、第2報は「At this stage, we have found no evidence that wallet private keys were leaked.」と書いています。第2報では「At this stage（現時点で）」が加わりました。

これは「漏えいしていない」という断定ではありません。後述するオンチェーンの記録は、**Binanceの署名システムが、攻撃者の求める署名を生成させられた**という見方と矛盾しません。ただし、それだけでは、鍵が持ち出されたかどうかを決められません。

### まだ分かっていないこと

- 根本原因と、第2報の言う「ほかの侵入箇所」
- 鍵が持ち出されたのか、署名のサービスが悪用されたのか
- 攻撃者が、後述する50件の事前署名済みの委任をどう入手したか
- 10月30日0時台（UTC）に入金も一時停止したという報道の真偽（Binanceの公式ページは「入金は影響を受けていない」と更新している）
- 補填資金の内訳（SAFU・保険基金・財務の比率）
- 犯行の主体。TRM Labs は北朝鮮の TraderTraitor による犯行と「高い確度で」評価し、Elliptic も「可能性が高い」とする。Binance自身は特定しておらず、政府による帰属の発表も確認できない。これは**調査会社の評価**であり、確定した事実ではない

## 基礎知識: 秘密鍵を持つことと、署名させられることは違う

ここから技術の話に入ります。先に、非エンジニアの方にも必要な前提を二つだけ押さえます。

### ホットウォレットとコールドウォレット

**ホットウォレット**は、ネットワークにつながっていて、出金の要求に自動で応じられる財布です。利用者の出金を数秒から数分で処理するために、取引所はある程度の資産をここに置きます。

**コールドウォレット**は、ネットワークから切り離された環境に鍵を置く財布です。安全な代わりに、資金を動かすには人手と時間がかかります。

取引所の設計は「ホットウォレットにどれだけ置くか」「コールドからどう補充するか」の兼ね合いです。今回の標的はホットウォレットで、それでも被害額が63億ドルに達したことは、取引所の規模の大きさを示しています。

### 実印と、金庫番のたとえ

ブロックチェーン上の送金は、「秘密鍵による署名」で正当性が決まります。たとえるなら、秘密鍵は**会社の実印**です。

実印を頑丈な金庫（HSMなど）にしまい、持ち出せなくしても、**金庫番に「これに押してください」と書類を渡せる立場**を奪われれば、書類には押印されてしまいます。しかも、金庫番が書類の中身を読まずに押す運用なら、書類が「送金依頼」でも「委任状」でも押されます。

このたとえには限界があります。現実の押印は人が内容を読みますが、署名のシステムは、**何を見て押すかを、設計した人が決めた規則どおりにしか判断しません**。その規則が「金額と送金先だけを見る」だったとき、金額も送金先もない種類の書類が来たら、規則の外にあります。

## 守る層は三つある: 鍵・署名・業務

「資産を守る」は、少なくとも三つの別の問いに分かれます。

| 層 | 問い | 守る仕組みの例 | 破られると |
| --- | --- | --- | --- |
| 1 鍵の保管 | 鍵を持ち出せるか | HSM、MPC、KMS | 鍵が盗まれ、攻撃者が自分で署名できる |
| 2 署名の認可 | **何に署名してよいか** | ポリシーエンジン、署名内容のデコード、承認フロー | 正規の署名システムが、望まない署名をする |
| 3 業務の認可 | その出金は業務として正当か | 出金先の許可リスト、限度額、独立承認、異常検知 | 署名は正しい形でも、正当でない資金移動が通る |

多くの議論は、層1に集中します。鍵を守る設計は成熟しており、NIST SP 800-57のような標準もあります。しかし、**層1だけでは、層2と層3は守れません**。今回のように、層1が（公表の範囲では）破られたかどうか不明なまま、巨額の資金が出ていく事態が起こりえます。

### HSM・MPC・KMSは何をして、何をしないか

| 仕組み | 役割 | しないこと |
| --- | --- | --- |
| HSM（専用の耐タンパ装置） | 鍵を装置の外に出さず、装置の中で署名する | 署名の**要求の中身**が正当かは判断しない。呼び出せる権限を持つ者の要求には応じる |
| MPC（鍵を分割して共同で署名） | 一か所が破られても、鍵全体は盗まれない | 参加者の過半が**同じ要求に同意**すれば署名する。要求の中身の正しさは別の問題 |
| KMS（クラウドの鍵管理） | 鍵の保管・権限管理・使用の記録 | IAMの権限を持つ主体が署名APIを呼べば署名する |

共通するのは、**「誰が署名を依頼できるか」と「何に署名してよいか」を、鍵の仕組みは決めてくれない**ことです。これは、層2の設計の責任です。

## オンチェーンで見えたこと: 金額も送金先もない署名

ここは、公開されている記録で、誰でも確認できる範囲の話です。

### 確認できた事実

Etherscanに公開されている、2026年10月29日20:20:59（UTC）のトランザクションは、ブロック26,300,753に含まれ、次の特徴を持ちます。

- トランザクション種別は**4**（EIP-7702 の「コード設定」型）
- 送信元は攻撃者のものとラベルされたアドレス、宛先は「Binance 14」とラベルされたBinanceのホットウォレット
- **50件の認可（authorization）**が含まれる。そのうち有効なのは1件（nonce 18914809）で、委任先のアドレスは1つ

筆者は、公開のEthereum RPCから同じトランザクションを取得し、上記の内容（種別4、50件の認可、チェーンID 1、委任先アドレス1つ）を**自分で確認**しました。

> **筆者の観測**: 同じRPCで「Binance 14」の現在のコードを確認すると、2026年11月1日の時点で空（`0x`）でした。これは、現時点では委任が設定されていない（解除されたか、設定が置き換わった）ことを示しますが、**いつ・誰が解除したかは確認していません**。

### EIP-7702とは何か（仕様に書かれていること）

EIP-7702は「Set Code for EOAs」という提案で、**ステータスは最終（Final）**です。通常、暗号資産のアカウント（EOA）は、鍵で署名して送金するだけで、プログラムを持ちません。EIP-7702は、**EOAが、指定したコントラクトのコードを自分のものとして動かせるようにする**仕組みです。仕様の要点は次のとおりです。

- 署名の対象は `[chain_id, address, nonce]` の3つで、**送金額や送金先は含まれません**
- 認可が有効なら、そのアカウントのコードに `0xef0100 || address`（「このアドレスのコードに委ねる」という印）が書かれる
- 以後、そのアカウントへの呼び出しは、**委ねられた先のコードが、そのアカウントの立場で実行される**
- トランザクションの実行が失敗しても、設定された委任は**巻き戻されない**

仕様の「セキュリティ上の考慮」節は、委任先のコードの設計が悪いと、「悪意のある者が、署名者のアカウントを**ほぼ完全に**制御できる」と明記しています。

### 何が言えて、何が言えないか

| 言えること（確認済み） | 言えないこと |
| --- | --- |
| Binance 14 に、EIP-7702の委任が設定された記録がある | 攻撃者が**どうやって**その署名を入手したか |
| 委任の認可は、**そのアカウントの鍵で署名されている必要がある**（仕様） | **鍵が持ち出されたか**、署名システムが署名させられたか |
| 委任の署名は、金額・送金先を含まない（仕様） | Binanceの署名システムが、この種の署名を**検査していたかどうか**（内部構成は非公開） |

調査会社のEllipticは、攻撃者が「Binanceの**自分のシステムに署名させたように見える**」と分析し、Binance側の説明として、この委任が「通常の承認の管理を経なかった」と伝えています。TRM Labsも「ホットウォレットの署名インフラの侵害を指している」と述べます。ただし、**これらは分析であり、Binanceの公式な原因の認定ではありません**。SlowMistなど、鍵の漏えいの可能性を仮説に残す専門家もいます。

### 筆者の見解: なぜ「金額と送金先だけ」を見るポリシーは足りないのか

以下は、公表された事実からの**筆者の推論**です。確定した事実ではありません。

出金のポリシーを「金額が上限以内で、送金先が許可リストにある」と作ると、**送金の形をしていない署名**（アカウントの動作を変えるもの）は、そもそも判定の対象外になりえます。ポリシーが「送金か、送金でないか」を区別せず、署名する**実バイト列を読まない**設計だと、この穴が開きます。

今回、Binanceのポリシーがどうだったかは不明です。ただ、設計者が学べるのは、**「許可する署名の種類を限定する」**という原則です。後述の教材で、これをコードにします。

## 参照アーキテクチャ: 出金・署名パイプライン

> **注意**: 下の図は、取引所・決済システムの一般的な参照モデルです。**Binanceの内部構成を復元したものではありません**。

```text
 [利用者]──出金要求──▶ [出金API] ──▶ [ポリシーエンジン] ──▶ [署名サービス] ──▶ [ブロックチェーン]
                         認証・冪等      層2・層3の判定         層1（HSM/MPC/KMS）
                         キー受付        ・署名する実バイト列    鍵は外に出ない
                                         をデコード
                                         ・送金以外は拒否
                                         ・限度額を原子的に予約
                                         ・承認（独立2名）を検証
                                  │
        [DevOps端末] ─ ─ ✕ ─ ─ ▶ 本番の署名経路へ直接つながない（人は、承認の画面だけ）
                                  │
                                  ▼
                        [監査ログ（追記のみ）] ──▶ [異常検知・緊急停止]
```

### 脅威モデル: アカウントの侵害から不正出金につながる経路

| 段階 | 起こること | 止める層 |
| --- | --- | --- |
| 1. 足がかり | 開発者の端末・CI・コンテナなどが侵害される | 端末の分離、短命の認証情報、ネットワークの分離 |
| 2. 横展開 | 侵害した主体が、署名サービスを呼べる権限に到達する | IAM・ワークロードのIDの最小権限、署名サービスの呼び出し元の限定 |
| 3. 署名の要求 | 攻撃者が、署名を求める | **層2: 署名する実バイト列の検査、送金以外の既定拒否** |
| 4. 資金の移動 | 署名が通れば、資産が動く | **層3: 出金先の許可、限度額、独立承認、冷却期間** |
| 5. 持ち去り | 資産が出ていき、分散する | 異常検知、緊急停止、発行体との連携（凍結） |

### DevOps端末から本番へ: 信頼境界

ただし、第2報で説明は修正され、実際の経路は不明です。第1報が挙げた「DevOps端末→サービスコンテナ→出金」という経路が仮にそのとおりだとすれば、それは**人の端末が、本番の資産に近すぎる**設計の典型的なリスクにあたります。一般に、次を分けます。

- **開発・運用の端末は、本番の署名経路に直接つながない**。必要な操作は、承認フロー（変更管理）を経る
- **サービスの権限（ワークロードID）は、そのサービスの役割だけに絞る**。コンテナに、出金を指示できる権限を持たせるなら、そのコンテナを乗っ取った者も同じ権限を得る
- **人が持つ長期の認証情報を、本番の署名のAPIに通用させない**。短命のトークンと、都度の承認を使う

## 教材: 署名の前に置く出金ポリシーエンジン

ここから、層2・層3をTypeScriptで実装した教材を示します。コードの全文とテストは、このサイトのリポジトリの `tests/unit/fixtures/withdrawal-policy.ts` と `tests/unit/withdrawal-policy.test.ts` にあり、CIで常に実行されます。

> **限界の明示**: これは**教育用の参照実装**です。実運用の資産管理システムには、永続化されたデータベース、署名サービスとの認証、鍵の運用、監査、障害対応が必要で、この教材はその代わりになりません。

### 設計の方針

| 要件 | この教材での実装 |
| --- | --- |
| 通貨と最小単位を明示した金額型 | `Money { asset, minor: bigint }`。`"0.1"` のような文字列から、**丸めずに**整数へ変換。小数が多すぎれば拒否 |
| 浮動小数点誤差の回避 | 金額は常に整数（bigint）。`number` を使わない |
| 署名する実バイト列の判定 | 呼び出し元の申告ではなく、バイト列を**デコード**して種類を判断 |
| 送金以外の署名の拒否 | 委任（種別4）・承認（approve）・未知のものは**既定で拒否** |
| 不正な出金先の拒否 | 許可リスト＋追加後の冷却期間 |
| 承認の束縛・期限・リプレイ対策 | 承認は**要求のダイジェスト**に束縛され、期限を持つ。同じ要求IDの予約は一度きりで、承認済みの要求の使い回しは拒否 |
| 職務の分離 | 要求者本人の承認は無効。異なる2名、異なる2つの役割が必要 |
| 原子的な限度額更新 | 「確認」と「更新」を**一つの不可分な操作**にする |
| 冪等性 | 同じ冪等キーは同じ判断を返し、二重に予約しない。内容が違えば拒否 |
| フェイルクローズ | 想定外のエラーは**拒否**に倒す |
| 監査可能性 | すべての判断を、追記のみのログに残す |
| 緊急停止 | サーキットブレーカー（開くと全拒否）。冪等性の保存済みの判断より**先に**判定するので、許可済みの要求の再送も止まる |

### 署名する実バイト列をデコードする

```ts
export interface SigningPayload {
  readonly txType: number; // EIP-2718 のトランザクション種別。0x04 は EIP-7702
  readonly to: string;
  readonly valueWei: bigint;
  readonly data: string; // 16進のcalldata。ETHの単純送金は "0x"
}

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;

/** 呼び出し元のラベルではなく、バイト列を読む。読めないものは "unknown"。 */
export function decodeIntent(p: SigningPayload): Intent {
  if (p.txType === 0x04) return { op: "delegation" }; // アカウントのコードを委ねる署名
  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`・`0xa22cb465` は、それぞれ ERC-20の `transfer(address,uint256)`・`approve(address,uint256)`・`setApprovalForAll(address,bool)` の関数セレクタで、筆者は公開のシグネチャ辞書で確認しました。

**ポイント**は、許可する種類を**積極的に列挙**し、それ以外を拒否することです。「危険なものを禁止する」方式（ブラックリスト）だと、**知らない種類の署名**がすり抜けます。

### 判定の順序と、「申告」と「実体」の突き合わせ

```ts
// 抜粋: WithdrawalPolicy.#decide
if (this.#circuitOpen) return audit(deny("circuit_open"));

// 署名される「実体」を判定する。申告ではない。
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"));
}
// ...許可リスト・冷却期間・1回あたりの上限・承認の検証...
// 最後に、限度額の「確認と更新」を、一つの不可分な操作で行う:
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` です。**申告された内容を信じない**のが、層2の基本です。

### 承認は「特定の要求」に束縛する

```ts
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");
}
```

承認者が同意するのは「この要求」です。承認を**ダイジェスト**に束縛すれば、別の要求への流用（リプレイ）は無効になります。さらに、**期限**を持たせ、**要求者本人の承認を無効**にし、**異なる2名・異なる2役割**を要求します。

### 競合状態: 「確認してから更新」は破れる

限度額の確認を、次のように書くことがあります。

```ts
// 悪い例（RacyStore）: 読む → 待つ → 判断する → 書く
const used = this.#used.get(key) ?? BigInt(0); // 1. 読む
await sleep(5);                                // 2. DBへの往復で待つ
if (used + amount > limit) return "limit";     // 3. 古い値で判断する
this.#used.set(key, used + amount);            // 4. 書く
return "reserved";
```

「読んでから書くまでの間に、別の要求が割り込む」ことを**TOCTOU（確認時と使用時のずれ）**と呼びます。具体的な数字で見ます。1日の上限が10 ETH、1件が0.9 ETHの要求が**同時に50件**届くとします。正しくは11件（10 ÷ 0.9 の切り捨て）で止まるはずです。

| 実装 | 50件の同時要求のうち、通った件数 |
| --- | --- |
| 確認と更新を分けた実装（上の悪い例） | **50件**（上限を大幅に超過） |
| 確認と更新を一つの不可分な操作にした実装 | **11件**（正しく上限で止まる） |

これは、筆者が実際にテストで測った値です。全件が通ったのは、50件すべてが「まだ誰も使っていない」という**古い値**で判断したためです。

実際のデータベースでは、`UPDATE ... SET used = used + $amount WHERE used + $amount <= $limit` のような**条件つきの更新**（更新された行数で成否を判断）、または行ロックで実現します。教材のメモリ上の実装は、「await を挟まずに読み書きする」ことで、同じ性質を示しています。予約には要求IDごとの一意制約（UNIQUE）も必要で、教材は同じ要求IDの二重予約を `request_already_used` として拒否します。承認済みの要求を別の冪等キーで再送しても、限度額を二重に使えません。

### テスト結果

`npx vitest run tests/unit/withdrawal-policy.test.ts` の結果は**25テストすべて合格**（筆者の環境）、`tsc` の型検査とESLintも通りました。検証した項目は次のとおりです。

- 金額の変換（小数が多すぎる・負・ゼロ・指数表記を拒否）
- 署名のデコード（送金・委任・承認・未知）。ETH付きの呼び出し、途中で切れた・余分なバイトのあるcalldataは読み切れないので拒否。アドレスの大文字小文字は区別しない。USDTの要求にETH送金の署名は通らない
- 許可リストにある宛先への少額の出金は許可（ETHとUSDT）
- **委任・承認・偽のトークンの署名は、要求が無害に見えても拒否**
- 申告の宛先・金額と、署名の実体が違う要求を拒否
- 未知の宛先、追加直後（冷却期間中）の宛先を拒否
- 上限超過、承認が必要な額の承認なしを拒否
- 承認の期限切れ、別の要求への流用（宛先・金額・アカウントのどれを変えても無効）、本人承認、同じ役割だけ、同一人の重複、**1人が2つの役割を兼ねる承認**を拒否
- **承認済みの要求を、別の冪等キーで再送して二重に使うこと**を拒否（`request_already_used`）
- 境界値（冷却期間・1回の上限・承認のしきい値・承認の期限がちょうどのとき）と、24時間のローリング窓（上限ちょうどまで使え、24時間後に解放される）
- 冪等性: 同じ要求の再送は一度だけ予約。内容が違えば `idempotency_conflict`。一過性の障害は記憶されず、同じキーで再試行できる
- 50件の同時要求で上限を超えない（原子的な予約）／確認と更新を分けた実装は超える
- サーキットブレーカーの全拒否（**すでに許可された要求の再送も止まる**）と、判断のログの記録（変更不可）。監査ストアが落ちても遮断は開く
- ストアの障害が**拒否**になる（フェイルクローズ）。予約の後で監査ログの書き込みが失敗した場合も拒否になるが、補償処理はないので予約は戻らない（既知の限界として固定）

テストを通すために、不変条件を弱めたり、テストを無効にしたりはしていません。

### この教材が示さないこと

- 実際の署名サービスとの接続、鍵の運用、ネットワークの認証
- 永続化された、複数ノードにまたがる限度額の整合性
- EIP-7702の署名そのものの検証（本教材は、署名**される前**の要求を判定する）
- 異常検知の統計モデル（しきい値の設計）
- 承認者の認証。`approverId` と `role` は検証済みの入力を前提にしており、承認の署名そのものの検証は含まない
- 日次の累計に対する承認。承認のしきい値は**1件ごと**なので、小口に分割すれば承認を避けられる。実運用では累計にもしきい値を掛ける
- 補償処理。予約の後で監査ログの書き込みが失敗すると、拒否を返すが予約は戻らない
- 拒否された判断も、同じ冪等キーで保存される。内容を直した要求は、新しい冪等キーで送る

## 準備金の証明（PoR）で、何が確かめられるか

Binanceは第3報で、10月31日0時（UTC）時点の準備金の証明（Proof of Reserves）のスナップショットを示し、ゼロ知識証明を含む完全な証明を11月3日（UTC）までに公表予定としています。

筆者の理解では、PoRが示すのは**ある時点で、顧客の預かり資産に見合う資産があるか**です。これが示さないことも、同じくらい重要です。

| PoRで確かめられる | PoRでは確かめられない |
| --- | --- |
| 顧客の残高の合計に対して、保有資産が足りているかの**時点の状態** | **鍵や署名の管理が安全か** |
| 保有資産の一部が、公開のアドレスにあること | **資産がどう失われたか**、再発しないか |
| （ゼロ知識証明を使う場合）顧客ごとの残高を開示せずに、総額の整合を示せること | 負債（顧客への債務）が、**すべて**含まれているか（設計次第） |

PoRは、「いま足りているか」への答えで、「**安全に守られているか**」への答えではありません。両者を混同しないことが、取引所を評価するときの基本です。

## 経営層へ: 鍵を守ってもお金が出ていく理由と、決める順番

非エンジニアの方向けに、この事件から学べる判断を整理します。

### 3つの問い

1. **金庫の鍵の管理は、誰が、どこまで？** 鍵の保管（層1）は、専門の製品と手順で守れます。ここは、多くの企業が投資しています。
2. **金庫番に「何を押してよいか」の規則は、誰が決めている？** 層2です。**規則が「金額と送金先だけ」だったり、規則を書いた人が退職していたり**しませんか。
3. **お金が出ていくとき、人の目と、止める手段はあるか？** 層3です。高額の出金は**二人以上の独立した承認**、異常があれば**自動で止まる**（サーキットブレーカー）、が基本です。

### 優先順位のつけ方

| 優先度 | 行動 | 目的 |
| --- | --- | --- |
| 高 | 本番の資金移動に使う認証情報・権限の棚卸し。人の端末から本番の署名経路に届くものを除く | 層2の入口を守る |
| 高 | 出金・送金の**限度額と、独立承認の閾値**を決める。決済・送金の機能を持つなら、**同時実行でも上限が守られる**ことを確認 | 層3 |
| 中 | 署名・承認・設定変更の**監査ログ**を、改ざんされない場所に置く | 事後の検証 |
| 中 | 緊急停止の手順を決め、**訓練**する | 被害の拡大を止める |
| 低 | 公開されたポストモーテムを読み、自社の差分を確認する | 学習 |

決済・送金を扱う事業では、**「止まる」ことを機能として設計する**のが経営判断です。サービスが止まるのは損失ですが、止められないまま資産が出ていくより小さい、という判断を、**平時に**しておく必要があります。

## チェックリスト

### 設計・実装

- [ ] 署名サービスは、呼び出し元の申告でなく、**署名する実バイト列**をデコードして判定している
- [ ] 許可する署名の種類を**列挙**し、委任・承認・権限移譲・未知のものを**既定で拒否**している
- [ ] 出金先は許可リストで、追加後に**冷却期間**がある
- [ ] 承認は**特定の要求に束縛**され、期限を持ち、要求者本人は承認できない
- [ ] 限度額の「確認と更新」が**原子的**で、同時実行のテストがある
- [ ] 冪等キーを**サーバー側**で扱い、同じキーで内容が違えば拒否する
- [ ] 想定外のエラーは**拒否**（フェイルクローズ）になる
- [ ] 金額は**整数の最小単位**で持つ（浮動小数点を使わない）

### 運用・組織

- [ ] 人の端末から、本番の署名経路へ**直接**届かない
- [ ] サービスの権限（ワークロードID）は、役割だけに絞られている
- [ ] 判断・承認・設定変更の**監査ログ**が、追記のみで、別の場所に残る
- [ ] 緊急停止の手順があり、**訓練**されている
- [ ] 高額の出金に**独立した複数の承認**が要る（職務の分離）
- [ ] オンチェーンとオフチェーンの残高の**突き合わせ**を自動で行っている

## 現時点の結論と、今後の確認事項

- **事実として言えること**: ホットウォレットが侵害され、約63億米ドル相当が流出した。凍結は約30億ドル、補填は90%（Binance自身の資金）、回収は公表なし。出金は停止中で、11月1日に段階的に再開予定。オンチェーンには、EIP-7702の委任がBinance 14に設定された記録がある。
- **まだ言えないこと**: 根本原因、鍵の漏えいの有無、攻撃者が委任の署名を得た方法、犯行の主体（調査会社の評価は仮説）。
- **技術的な教訓**: 秘密鍵の保護（層1）は、署名の認可（層2）と業務の認可（層3）の代わりになりません。**何に署名してよいか**を、実バイト列で判定し、**既定で拒否**する設計が、鍵の保護とは別に必要です。

今後確認すべきは、Binanceの技術的なポストモーテム（原因と、第2報の「ほかの侵入箇所」）、Proof of Reservesの完全な証明（11月3日予定）、凍結資産の扱い、出金再開後の状況です。

## 送金・決済・資産移動の設計をレビューしたい方へ

筆者は、決済プラットフォームの信頼性レイヤー（冪等性・整合性・二重課金の防止）の設計・実装を主導してきました。この記事のような視点で、**貴社のバックエンド・決済API・クラウドの権限・資金移動の制御を設計レビュー**することができます。

対応できること、できないことを先に書きます。

- **できること**: 送金・決済のAPI、承認フロー、限度額と冪等性、クラウドのIAMとサービスの権限、監査ログと異常検知の設計レビュー。優先順位つきの改善案と、同時実行・失敗系のテストの設計
- **できないこと**: **暗号資産のカストディ（保管）の専門的な監査、鍵の管理（HSM・MPCの運用）の評価、インシデントのフォレンジック調査**。これらは専門の事業者にご相談ください
- 「完全に安全になる」とはお約束できません。見つかったことと、見ていない範囲を、そのまま報告します

範囲と料金は、下のボタンの先でご確認ください。
