Skip to main content

原文準拠 Phase 1:基礎

3.2 スマートコントラクトのセキュリティ

Midnight Academy Phase 1 / Unit 3 / 3.2 の原文準拠版。再入・整数オーバーフロー・アクセス制御など代表的な脆弱性と、Checks-Effects-Interactions など安全な開発の作法を正確に・やさしく。


📘 Academy原文準拠 | Phase 1 · Unit 3 · Lesson 3.2 Smart Contracts Security 内容に忠実な日本語版です。原文(英語)・図・動画は公式 Academy(外部リンク・別タブで開きます)を正本に。

スマートコントラクトを「書く」ことと、「安全に書く」ことは、まったくの別物です。ブロックチェーンのプログラムは、仲介者(intermediary)なしで本物の価値(トークンや機密データなど)を扱える、とても強力なものです。でも、もしコントラクトにバグがあれば、それは誰にでも悪用されうる。しかもブロックチェーンの取引は、一度確定したら取り消せません(irreversible)。

だからこそ、セキュリティは最優先(paramount)です。

歴史をふりかえると、2016 年の The DAO ハックでは 6,000 万ドルが消えました。2021 年の Poly Network では 6 億ドルが盗まれています(このときは犯人が返金しましたが)。どちらも、ブロックチェーンそのものへの高度な攻撃ではありませんでした。スマートコントラクトのバグを、誰かが突いただけなのです。

このレッスンでは、よくある脆弱性とその避け方を見ていきます。さらに、簡単な「危ない契約」の例と、それを直した版も見ます。こうした落とし穴を知っておくことで、Midnight(やほかのどんなブロックチェーン)でも、より安全に開発できるようになります。

よくある脆弱性(Common Vulnerabilities in Smart Contracts)

スマートコントラクト開発者を夜も眠れなくさせる、代表的な攻撃を順に見ていきましょう。

再入(Reentrancy)

再入攻撃のシーケンス図。攻撃者コントラクトが脆弱なコントラクトに引き出し(100)を要求し、残高が更新される前に再度引き出しを実行して、残高100のまま200 ETHを引き出してしまう様子

図(原文):あるコントラクトが外部呼び出しの最中に、もう一度自分の関数へ「入り直される」様子。

これは古典的な攻撃パターンです。あるコントラクトが外部のコントラクト(または別のアドレス)を呼び出したとき、その外部呼び出しが実行のとちゅうで元のコントラクトの中へ呼び戻して(call back)くるときに起こります。多くの場合、ループのように、あるいは何度もくり返して呼び戻されます。

もし元のコントラクトがまだ自分の状態(state)を更新していなければ、攻撃者はこれを突いて、本来1回だけ起こるべき処理を何度もくり返せてしまいます。

たとえば、危ないコントラクトが「ユーザーへ送金したあとで残高を引く」という順番だったとします。攻撃者は悪意のある fallback 関数を仕込み、残高が減らされる前に引き出し関数へ呼び戻すことで、同じ資金を何度も引き出せてしまうのです。

流れの一例:

  1. あなたのコントラクトが残高を確認する:「このユーザーは 10 ETH 持っている」
  2. あなたのコントラクトがユーザーへ 10 ETH を送る
  3. ユーザーのコントラクトがすぐにまた withdraw(引き出し)を呼ぶ
  4. あなたのコントラクトが残高を確認する:「ユーザーはまだ 10 ETH 持っている」(まだ更新していないから)
  5. あなたのコントラクトがさらに 10 ETH を送る
  6. コントラクトが空になるまでくり返す

まとめると、再入攻撃とは、外部のコントラクトが関数に「入り直し」、被害側のコントラクトが事態に気づく前に、引き出しなどの処理をくり返してしまうものです。

整数のオーバーフロー/アンダーフロー(Integer Overflow/Underflow)

整数オーバーフローのフローチャート。残高255のuint8にユーザーが1トークン追加し「255+1=256になるか?」を判定。安全な計算ではトランザクションが元に戻りエラー(オーバーフロー)になり、安全でないコードでは残高が0にラップ(巻き戻り)してユーザーが全残高を失う様子

これは見つけにくい厄介者です。古いスマートコントラクト言語では、数値に最大値があります。その最大値を超えると、値はゼロへ巻き戻る(wrap around)のです。車の走行距離計(odometer)がぐるっと回ってしまうのに似ています。ただし回るのは走行距離ではなく、誰かが無限のトークンを手にするという結果です。

攻撃者はこれを悪用して、たとえばトークン残高を 0 から巨大な数へ巻き戻すことができます。

図(原文):8 ビット整数が最大値を超えてゼロへ巻き戻る様子。

たとえば uint8 x = 255(8 ビットの最大値)のとき x += 1 をすると、古いコンパイラではエラーも出さずに 0 になります。これを使えば、残高をいじったり、条件チェックをすり抜けたりできてしまいます。

要するにオーバーフロー/アンダーフローとは、数値がプログラムの想定する範囲の外へ出てしまい、予期しない挙動を引き起こすことです。

あるトークン契約が「残高 − 送金額」を計算して、十分な残高があるかを確かめているとします。もし残高が 0 トークンなのに 1 を送ろうとすると、引き算がアンダーフローして巨大な数になり、チェックを通過してしまいます。最近のコンパイラはこれを自動で検査しますが、古い契約はいまも危険なままです。

アクセス制御の不備(Access Control Issues)

スマートコントラクトの機能を分類したツリー図。誰でも呼び出せるパブリック関数(トークンの送金など)と、制限付き関数に分かれ、後者はオーナーのみ(トークンの発行=ミント、コントラクトの一時停止、設定の変更)と役割ベース(管理者・モデレーター・バリデーターの各ロール)に分かれる様子

図(原文):パスワードのない管理画面が、誰でも触れる状態になっているイメージ。

脆弱性が、単に「誰がその関数を呼べるかをきちんと制限していないだけ」ということもあります。本来オーナーだけに限るべき関数で、その制限を付け忘れると、誰でも管理用の関数(トークンの発行=mint や、重要な設定の変更など)を呼べてしまいます。

実際、誰でも mint 関数を呼べて無限にトークンを作れた契約や、オーナーを変更する関数に制限が無く、誰でも乗っ取れた契約を見たことがあります。それは、管理画面(admin panel)をパスワードなしでインターネットに公開しているようなものです。

よくあるミス:

  • 重要な関数に onlyOwner を付け忘れる
  • 変更できないオーナーアドレスをハードコードしてしまう
  • 初期化(initialization)関数を何度でも呼べる状態にしてしまう
  • テスト用の関数を本番コードに残してしまう

ほかにも注意すべき攻撃(Other Attacks to Watch For)

世の中には脆弱性の「動物園」と言えるほど、たくさんの種類があります。

  • タイムスタンプ依存(Timestamp dependence):乱数(randomness)にブロックのタイムスタンプを使う(マイナーが少しだけ操作できてしまう)
  • ガス上限攻撃(Gas limit attacks):処理が高くつきすぎて完了できないループを作らせる
  • フロントランニング(Front-running):他人の取引を見てから、より高いガスで自分の取引を出し、先回りする

初心者としては、まず上で挙げたものをつかむのが大事です。要点は、ブロックチェーンのコードは公開されていて(public)、敵対的(adversarial)だということ。誰かがあらゆる方法であなたの契約を壊そうとする、と前提しなければなりません。

ただ、こわがりすぎないでください。ここでは「見たときに気づける」ように、高い視点(high level)で脆弱性を紹介しています。すべての攻撃手法を暗記する必要はありません。セキュリティには常に警戒(constant vigilance)が要る、とだけ理解しておけば十分です。発展(advanced)モジュールでは、Midnight 固有のセキュリティパターンを学びます。

安全な開発のためのベストプラクティス(Best Practices for Secure Development)

さんざんおどかしてしまったので、ここからは自分を守る方法をお見せします。

Checks-Effects-Interactions パターン

Checks-Effects-Interactionsを示す引き出し機能のフローチャート。まず「チェック:ユーザーの残高は十分か?」を確認し、いいえなら取引を拒否、はいなら「効果:ユーザーの残高を更新」してから「処理:ユーザーに資金を送信」して取引を完了する、確認→状態更新→外部呼び出しの順番

図(原文):チェック → 状態更新 → 外部呼び出し、の順番を示した流れ。

これが再入への主な防御です。つねに次の順番を守りましょう。

  • Check(確認) — まず条件を検証する
  • Effect(効果) — 自分の状態を更新する
  • Interact(やり取り) — 外部呼び出しは最後にする

外部のコントラクトを呼び出す前に状態を更新しておくことで、再入攻撃のすきま(窓)を閉じられます。

実績あるライブラリを使う(Use Established Libraries)

車輪の再発明はやめましょう。とくに「セキュリティの車輪」はそうです。OpenZeppelin には、オーナーシップ・トークン・再入ガード(reentrancy guard)など、あらゆる用途で十分に鍛えられた(battle-tested)コントラクトがあります。これらのライブラリは、数千ものプロジェクトに攻撃され・監査され・磨かれてきました。

OpenZeppelin の Ownable コントラクトを使えば、安全なオーナーシップが 1 行で手に入ります。ReentrancyGuard は再入攻撃をぴしゃりと止めます。コミュニティがすでに解いた問題を、わざわざ自分で書き直す理由はありません。

適切なアクセス制御(Proper Access Control)

機密性の高い関数は制限しましょう。オーナーシップのパターンロールベースのアクセス(role-based access)を使い、認可されたアドレスだけが管理用の関数を呼べるようにします。オーナーシップは constructor(コンストラクタ)で初期化し、オーナーを変更するあらゆる関数には注意を払いましょう。

修飾子(modifier)を惜しまず使いましょう。

  • 管理用の関数には onlyOwner
  • 複雑な権限にはロールベースのアクセス
  • 重要な変更にはタイムロック(time lock)

すべてを検証する(Validate Everything)

入力を決して信用しないこと。金額が妥当か、アドレスが有効か、条件を満たしているかを、ロジックを実行する前に確認します。

検証は、正直なミスと悪意のある試みの両方を捕まえるために使いましょう。

シンプルに保つ(Keep It Simple)

複雑なコードはバグを隠します。もし契約があまりに多くのことをやっているなら、小さな契約に分けましょう。1 つの契約には 1 つのはっきりした役割を。ネストしたループ、複雑なステートマシン、画面より長い関数を見かけたら、その中にはバグが隠れている、というのが経験則です。

テストと監査(Test and Audit)

すべてをテストしましょう。通常のケース、境界(edge)のケース、そして「これは絶対に起きないはず」というケースまで。もし契約が本物の価値を扱うなら、プロの監査(audit)を受けましょう。たしかに監査は高い。でも、ハッキングされるよりは安く済みます。

スマートコントラクトのセキュリティは、選べるオプションではありません。たった 1 つのバグが、プロジェクトも、ユーザーの資金も、あなたの信用も壊しかねません。ブロックチェーンはミスを許してくれないのです。

でも、それで動けなくなる必要はありません。シンプルに始め、実績あるパターンを使い、徹底的にテストし、経験を積みながら少しずつ複雑さを足していく。どんな専門家も、最初はセキュリティを真剣に受け止めた初心者でした。

開発者として押さえる点

  • 再入(Reentrancy)対策の基本は Checks-Effects-Interactions:確認 → 状態更新 → 外部呼び出し(外部呼び出しは必ず最後)。状態を先に更新してすきまを閉じる
  • 整数オーバーフロー/アンダーフローは、値が想定範囲の外へ巻き戻る問題。新しいコンパイラは自動検査するが、古い契約は危険。残高や条件チェックの計算には特に注意
  • アクセス制御:重要な関数に onlyOwner を忘れない/初期化関数を1回限りに/テスト関数を本番に残さない
  • 入力は信用しない=金額・アドレス・条件をすべて検証。OpenZeppelin の OwnableReentrancyGuard など、実績あるライブラリを使い、自前実装を避ける
  • コードはシンプルに、1契約1役割。本物の価値を扱うなら監査を受ける。チェーンの取引は取り消せない(irreversible)前提で設計する
  • ブロックチェーンのコードは公開・敵対的。Midnight 固有のセキュリティパターンは発展モジュールで学ぶ

やさしい版・公式へ

つぎに読むページ

➡️ 原文準拠コースの入口へ戻る。このコースについて(次のレッスンは順次追加します)