スマート・コントラクトのセキュリティ
スマート・コントラクトは非常に柔軟であり、ブロックチェーン上にデプロイされたコードに基づくイミュータブルなロジックを実行しながら、大量の価値やデータを制御することができます。これにより、従来のシステムよりも多くの利点を提供する、トラストレスで分散型のアプリケーションの活気あるエコシステムが生まれました。同時に、スマート・コントラクトの脆弱性を悪用して利益を得ようとする攻撃者にとっても、機会を提供することになります。
イーサリアムのようなパブリック・ブロックチェーンは、スマート・コントラクトのセキュリティ確保という問題をさらに複雑にします。デプロイされたコントラクトのコードは、セキュリティの欠陥にパッチを当てるために変更することが_通常は_できません。また、スマート・コントラクトから盗まれた資産は追跡が非常に困難であり、不変性のためほとんど回収不可能です。
数字は様々ですが、スマート・コントラクトのセキュリティ上の欠陥によって盗まれたり失われたりした価値の総額は、軽く10億ドルを超えると推定されています。これには、分散型自律組織 (DAO) のハッキング (新しいタブで開きます) (360万ETHが盗まれ、現在の価格で10億ドル以上の価値)、Parityのマルチシグ・ウォレットのハッキング (新しいタブで開きます) (ハッカーによって3,000万ドルが失われた)、およびParityの凍結ウォレット問題 (新しいタブで開きます) (3億ドル以上のETHが永久にロックされた) などの注目を集めた事件が含まれます。
前述の問題により、開発者は安全で堅牢、かつ回復力のあるスマート・コントラクトの構築に労力を投資することが不可欠となっています。スマート・コントラクトのセキュリティは非常に重要な課題であり、すべての開発者が学ぶべきものです。このガイドでは、イーサリアム開発者向けのセキュリティ上の考慮事項について説明し、スマート・コントラクトのセキュリティを向上させるためのリソースを探ります。
前提条件
セキュリティに取り組む前に、スマート・コントラクト開発の基礎を理解しておいてください。
安全なイーサリアムのスマート・コントラクトを構築するためのガイドライン
1. Design proper access controls
スマート・コントラクトにおいて、public または external とマークされた関数は、任意の外部所有アカウント (EOA) またはコントラクト・アカウントから呼び出すことができます。他のユーザーがコントラクトとやり取りできるようにするには、関数の可視性を public に指定する必要があります。しかし、private とマークされた関数は、スマート・コントラクト内の関数からのみ呼び出すことができ、外部アカウントからは呼び出せません。すべてのネットワーク参加者にコントラクト関数へのアクセスを許可すると、特に誰でも機密性の高い操作(新しいトークンのミンティングなど)を実行できる場合、問題を引き起こす可能性があります。
スマート・コントラクト関数の不正使用を防ぐには、安全なアクセス制御を実装する必要があります。アクセス制御メカニズムは、スマート・コントラクト内の特定の関数を使用する権限を、コントラクトの管理を担当するアカウントなどの承認されたエンティティに制限します。Ownableパターンとロールベースの制御は、スマート・コントラクトにアクセス制御を実装するのに役立つ2つのパターンです。
Ownable pattern
Ownableパターンでは、コントラクト作成プロセス中に、あるアドレスがコントラクトの「オーナー」として設定されます。保護された関数には OnlyOwner 修飾子が割り当てられ、関数を実行する前にコントラクトが呼び出し元アドレスの身元を認証することが保証されます。コントラクトのオーナー以外のアドレスから保護された関数への呼び出しは常にリバートされ、望まないアクセスを防ぎます。
Role-based access control
スマート・コントラクトに単一のアドレスを Owner として登録すると、中央集権化のリスクが生じ、単一障害点となります。オーナーのアカウントキーが侵害された場合、攻撃者は所有されているコントラクトを攻撃できます。そのため、複数の管理アカウントを持つロールベースのアクセス制御パターンを使用する方が良い選択肢となる場合があります。
ロールベースのアクセス制御では、機密性の高い関数へのアクセスが、信頼できる参加者のグループ間で分散されます。たとえば、あるアカウントがトークンのミンティングを担当し、別のアカウントがアップグレードやコントラクトの一時停止を実行する場合があります。このようにアクセス制御を分散化することで、単一障害点が排除され、ユーザーのトラスト前提が軽減されます。
Using multi-signature wallets
安全なアクセス制御を実装する別のアプローチは、コントラクトの管理にマルチシグネチャ・アカウントを使用することです。通常のEOAとは異なり、マルチシグネチャ・アカウントは複数のエンティティによって所有され、トランザクションを実行するには最小限の数のアカウント(たとえば5つのうち3つ)からの署名が必要です。
アクセス制御にマルチシグを使用すると、ターゲット・コントラクトでのアクションに複数の当事者の同意が必要になるため、セキュリティの層が追加されます。これは、Ownableパターンの使用が必要な場合に特に役立ちます。攻撃者や悪意のある内部関係者が悪意のある目的で機密性の高いコントラクト関数を操作することがより困難になるためです。
2. Use require(), assert(), and revert() statements to guard contract operations
前述のように、スマート・コントラクトがブロックチェーンにデプロイされると、誰でもそのパブリック関数を呼び出すことができます。外部アカウントがコントラクトとどのようにやり取りするかを事前に知ることはできないため、デプロイする前に問題のある操作に対する内部の安全対策を実装することが理想的です。require()、assert()、および revert() ステートメントを使用して例外をトリガーし、実行が特定の要件を満たさない場合に状態の変更をリバートすることで、スマート・コントラクトの正しい動作を強制できます。
require(): require は関数の開始時に定義され、呼び出された関数が実行される前に事前定義された条件が満たされていることを確認します。require ステートメントは、関数を進める前に、ユーザー入力の検証、状態変数の確認、または呼び出し元アカウントの身元の認証に使用できます。
assert(): assert() は、内部エラーを検出し、コード内の「不変条件(インバリアント)」の違反を確認するために使用されます。不変条件とは、すべての関数実行において真であるべきコントラクトの状態に関する論理的なアサーションです。不変条件の例としては、トークン・コントラクトの最大総供給量や残高があります。assert() を使用すると、コントラクトが脆弱な状態に陥ることがなくなり、もし陥った場合は、状態変数へのすべての変更がロールバックされます。
revert(): revert() は、必要な条件が満たされない場合に例外をトリガーする if-else ステートメントで使用できます。以下のサンプル・コントラクトは、関数の実行を保護するために revert() を使用しています。
pragma solidity ^0.8.4;
contract VendingMachine {
address owner;
error Unauthorized();
function buy(uint amount) public payable {
if (amount > msg.value / 2 ether)
revert("Not enough Ether provided.");
// 購入を実行する。
}
function withdraw() public {
if (msg.sender != owner)
revert Unauthorized();
payable(msg.sender).transfer(address(this).balance);
}
}
3. Test smart contracts and verify code correctness
イーサリアム仮想マシン (EVM)で実行されるコードの不変性は、スマート・コントラクトが開発段階でより高いレベルの品質評価を要求することを意味します。コントラクトを広範囲にテストし、予期しない結果がないか観察することで、セキュリティが大幅に向上し、長期的にはユーザーを保護することになります。
一般的な方法は、コントラクトがユーザーから受け取ると予想されるモックデータを使用して、小さな単体テストを作成することです。単体テストは、特定の関数の機能をテストし、スマート・コントラクトが期待通りに機能することを確認するのに適しています。
残念ながら、単体テストを単独で使用した場合、スマート・コントラクトのセキュリティ向上に対する効果は最小限です。単体テストは、モックデータに対して関数が適切に実行されることを証明するかもしれませんが、単体テストの効果は作成されたテストの範囲にとどまります。そのため、スマート・コントラクトの安全性を損なう可能性のある、見落とされたエッジケースや脆弱性を検出することが困難になります。
より良いアプローチは、単体テストと、静的および動的解析を使用して実行されるプロパティベースのテストを組み合わせることです。静的解析は、制御フローグラフ (新しいタブで開きます)や抽象構文木 (新しいタブで開きます)などの低レベルの表現に依存して、到達可能なプログラムの状態と実行パスを分析します。一方、スマート・コントラクトのファジング (新しいタブで開きます)などの動的解析手法は、ランダムな入力値でコントラクトのコードを実行し、セキュリティ・プロパティに違反する操作を検出します。
形式的検証は、スマート・コントラクトのセキュリティ・プロパティを検証するためのもう1つの手法です。通常のテストとは異なり、形式的検証はスマート・コントラクトにエラーがないことを決定的に証明できます。これは、望ましいセキュリティ・プロパティを捉えた形式仕様を作成し、コントラクトの形式モデルがこの仕様に準拠していることを証明することによって達成されます。
4. Ask for an independent review of your code
コントラクトをテストした後、セキュリティ上の問題がないか他の人にソースコードを確認してもらうと良いでしょう。テストではスマート・コントラクトのすべての欠陥を発見することはできませんが、独立したレビューを受けることで脆弱性を発見する可能性が高まります。
Audits
スマート・コントラクトの監査を依頼することは、独立したコードレビューを実施する1つの方法です。監査人は、スマート・コントラクトが安全であり、品質の欠陥や設計エラーがないことを確認する上で重要な役割を果たします。
とはいえ、監査を特効薬として扱うことは避けるべきです。スマート・コントラクトの監査はすべてのバグを捕捉するわけではなく、主に追加のレビューラウンドを提供するように設計されており、初期の開発やテスト中に開発者が見落とした問題を検出するのに役立ちます。また、スマート・コントラクト監査のメリットを最大化するために、コードを適切に文書化し、インラインコメントを追加するなど、監査人と協力するためのベストプラクティスに従う必要があります。
- スマート・コントラクト監査のヒントとコツ (新しいタブで開きます) - @tinchoabbate
- 監査を最大限に活用する (新しいタブで開きます) - Inference
Bug bounties
バグバウンティ・プログラムの設定は、外部コードレビューを実装するための別のアプローチです。バグバウンティとは、アプリケーションの脆弱性を発見した個人(通常はホワイトハットハッカー)に与えられる金銭的な報酬です。
適切に使用すれば、バグバウンティはハッカーコミュニティのメンバーに、重大な欠陥がないかコードを検査するインセンティブを与えます。実際の例として、イーサリアム上で実行されるレイヤー2 (L2)プロトコルであるオプティミズム (新しいタブで開きます)で、攻撃者が無制限のイーサを作成できた可能性のある「無限マネーバグ」があります。幸いなことに、ホワイトハットハッカーがこの欠陥を発見 (新しいタブで開きます)してチームに通知し、その過程で多額の報酬を獲得しました (新しいタブで開きます)。
有用なストラテジーは、バグバウンティ・プログラムの報酬を、危険にさらされている資金の額に比例して設定することです。「スケーリング・バグバウンティ (新しいタブで開きます)」と呼ばれるこのアプローチは、個人が脆弱性を悪用するのではなく、責任を持って開示するための金銭的なインセンティブを提供します。
5. Follow best practices during smart contract development
監査やバグバウンティが存在するからといって、高品質のコードを書く責任が免除されるわけではありません。優れたスマート・コントラクトのセキュリティは、適切な設計および開発プロセスに従うことから始まります。
-
gitなどのバージョン管理システムにすべてのコードを保存する
-
すべてのコード変更をプルリクエスト経由で行う
-
プルリクエストには少なくとも1人の独立したレビュアーを確保する。プロジェクトに単独で取り組んでいる場合は、他の開発者を見つけてコードレビューを交換することを検討する
-
スマート・コントラクトのテスト、コンパイル、デプロイには開発環境を使用する
-
Cyfrin Aderyn (新しいタブで開きます)、Mythril、スリザーなどの基本的なコード解析ツールでコードを実行する。理想的には、各プルリクエストがマージされる前にこれを行い、出力の違いを比較するべきです
-
コードがエラーなしでコンパイルされ、Solidityコンパイラが警告を出さないことを確認する
-
(NatSpec (新しいタブで開きます)を使用して)コードを適切に文書化し、コントラクトのアーキテクチャの詳細をわかりやすい言葉で説明する。これにより、他の人がコードを監査およびレビューしやすくなります。
6. Implement robust disaster recovery plans
安全なアクセス制御の設計、関数修飾子の実装、およびその他の提案は、スマート・コントラクトのセキュリティを向上させることができますが、悪意のあるエクスプロイトの可能性を排除することはできません。安全なスマート・コントラクトを構築するには、「失敗に備える」ことと、攻撃に効果的に対応するためのフォールバック計画を持つことが必要です。適切なディザスタリカバリ計画には、以下のコンポーネントの一部またはすべてが組み込まれます。
Contract upgrades
イーサリアムのスマート・コントラクトはデフォルトでイミュータブルですが、アップグレードパターンを使用することで、ある程度の可変性を実現することが可能です。重大な欠陥によって古いコントラクトが使用できなくなり、新しいロジックをデプロイすることが最も実現可能な選択肢である場合、コントラクトのアップグレードが必要になります。
コントラクトのアップグレードメカニズムはそれぞれ異なりますが、「プロキシパターン」はスマート・コントラクトをアップグレードするためのより一般的なアプローチの1つです。プロキシパターン (新しいタブで開きます)は、アプリケーションの状態とロジックを_2つ_のコントラクトに分割します。最初のコントラクト(「プロキシ・コントラクト」と呼ばれます)は状態変数(ユーザーの残高など)を保存し、2番目のコントラクト(「ロジック・コントラクト」と呼ばれます)はコントラクト関数を実行するためのコードを保持します。
アカウントはプロキシ・コントラクトとやり取りし、プロキシ・コントラクトは delegatecall() (新しいタブで開きます) 低レベル呼び出しを使用してすべての関数呼び出しをロジック・コントラクトにディスパッチします。通常のメッセージ・コールとは異なり、delegatecall() は、ロジック・コントラクトのアドレスで実行されるコードが、呼び出し元のコントラクトのコンテキストで実行されることを保証します。これは、ロジック・コントラクトが常に(自身のストレージではなく)プロキシのストレージに書き込み、msg.sender と msg.value の元の値が保持されることを意味します。
ロジック・コントラクトへの呼び出しを委譲するには、そのアドレスをプロキシ・コントラクトのストレージに保存する必要があります。したがって、コントラクトのロジックをアップグレードすることは、別のロジック・コントラクトをデプロイし、新しいアドレスをプロキシ・コントラクトに保存するだけの問題です。プロキシ・コントラクトへの後続の呼び出しは自動的に新しいロジック・コントラクトにルーティングされるため、実際にコードを変更することなくコントラクトを「アップグレード」したことになります。
Emergency stops
前述のように、広範な監査やテストを行っても、スマート・コントラクトのすべてのバグを発見できるわけではありません。デプロイ後にコードに脆弱性が現れた場合、コントラクト・アドレスで実行されているコードを変更することはできないため、パッチを当てることは不可能です。また、アップグレードメカニズム(プロキシパターンなど)の実装には時間がかかる場合があり(多くの場合、さまざまな当事者からの承認が必要です)、攻撃者により多くの損害を与える時間を与えるだけになります。
最終手段は、コントラクト内の脆弱な関数への呼び出しをブロックする「緊急停止」機能を実装することです。緊急停止は通常、以下のコンポーネントで構成されます。
-
スマート・コントラクトが停止状態にあるかどうかを示すグローバルなブール変数。この変数はコントラクトのセットアップ時に
falseに設定されますが、コントラクトが停止されるとtrueに戻ります。 -
実行時にブール変数を参照する関数。このような関数は、スマート・コントラクトが停止していないときにアクセス可能であり、緊急停止機能がトリガーされるとアクセスできなくなります。
-
ブール変数を
trueに設定する緊急停止関数にアクセスできるエンティティ。悪意のあるアクションを防ぐために、この関数への呼び出しは信頼できるアドレス(コントラクトのオーナーなど)に制限できます。
コントラクトが緊急停止をアクティブにすると、特定の関数は呼び出せなくなります。これは、選択した関数をグローバル変数を参照する修飾子でラップすることによって実現されます。以下は、コントラクトにおけるこのパターンの実装を説明する例 (新しいタブで開きます)です。
// このコードは専門的な監査を受けておらず、安全性や正確性についていかなる保証も行いません。自己責任で使用してください。
contract EmergencyStop {
bool isStopped = false;
modifier stoppedInEmergency {
require(!isStopped);
_;
}
modifier onlyWhenStopped {
require(isStopped);
_;
}
modifier onlyAuthorized {
// ここでmsg.senderの認可を確認する
_;
}
function stopContract() public onlyAuthorized {
isStopped = true;
}
function resumeContract() public onlyAuthorized {
isStopped = false;
}
function deposit() public payable stoppedInEmergency {
// デポジットのロジックがここで実行される
}
function emergencyWithdraw() public onlyWhenStopped {
// 緊急引き出しがここで実行される
}
}
この例は、緊急停止の基本的な機能を示しています。
-
isStoppedは、最初はfalseと評価され、コントラクトが緊急モードに入るとtrueと評価されるブール値です。 -
関数修飾子
onlyWhenStoppedとstoppedInEmergencyはisStopped変数をチェックします。stoppedInEmergencyは、コントラクトが脆弱なときにアクセスできないようにすべき関数(deposit()など)を制御するために使用されます。これらの関数への呼び出しは単にリバートされます。
onlyWhenStopped は、緊急時に呼び出し可能であるべき関数(emergencyWithdraw() など)に使用されます。このような関数は状況の解決に役立つ可能性があるため、「制限された関数」のリストから除外されます。
緊急停止機能を使用すると、スマート・コントラクトの深刻な脆弱性に対処するための効果的な一時しのぎが提供されます。ただし、開発者が利己的な理由でそれをアクティブにしないことをユーザーが信頼する必要性が高まります。この目的のために、オンチェーンの投票メカニズム、タイムロック、またはマルチシグ・ウォレットからの承認の対象にすることで、緊急停止の制御を分散化することが可能な解決策となります。
Event monitoring
イベント (新しいタブで開きます)を使用すると、スマート・コントラクト関数への呼び出しを追跡し、状態変数の変更を監視できます。誰かが安全上重要なアクション(資金の引き出しなど)を実行するたびにイベントを発行するようにスマート・コントラクトをプログラムすることが理想的です。
イベントをログに記録し、オフチェーンで監視することで、コントラクトの操作に関する洞察が得られ、悪意のあるアクションのより迅速な発見に役立ちます。これは、チームがハッキングにより迅速に対応し、関数の一時停止やアップグレードの実行など、ユーザーへの影響を軽減するためのアクションを実行できることを意味します。
また、誰かがコントラクトとやり取りするたびにアラートを自動的に転送する既製の監視ツールを選択することもできます。これらのツールを使用すると、トランザクション量、関数呼び出しの頻度、または関連する特定の関数など、さまざまなトリガーに基づいてカスタムアラートを作成できます。たとえば、1回のトランザクションで引き出された金額が特定のしきい値を超えたときに届くアラートをプログラムできます。
7. Design secure governance systems
コアとなるスマート・コントラクトの制御をコミュニティメンバーに引き渡すことで、アプリケーションを分散化したいと考えるかもしれません。この場合、スマート・コントラクト・システムにはガバナンス・モジュールが含まれます。これは、コミュニティメンバーがオンチェーンのガバナンス・システムを介して管理アクションを承認できるようにするメカニズムです。たとえば、プロキシ・コントラクトを新しい実装にアップグレードする提案は、トークン保有者によって投票される場合があります。
分散型ガバナンスは、特に開発者とエンドユーザーの利益を一致させるため、有益な場合があります。それにもかかわらず、スマート・コントラクトのガバナンス・メカニズムは、誤って実装されると新たなリスクをもたらす可能性があります。考えられるシナリオは、攻撃者がフラッシュ・ローンを利用して(保有するトークン数で測定される)莫大な投票力を獲得し、悪意のある提案を押し通す場合です。
オンチェーン・ガバナンスに関連する問題を防ぐ1つの方法は、タイムロックを使用する (新しいタブで開きます)ことです。タイムロックは、特定の時間が経過するまでスマート・コントラクトが特定のアクションを実行するのを防ぎます。その他のストラテジーには、トークンがロックアップされている期間に基づいて各トークンに「投票の重み」を割り当てることや、現在のアドレスではなく過去の期間(たとえば、過去の2〜3ブロック)でのアドレスの投票力を測定することが含まれます。どちらの方法も、オンチェーンの投票を左右するために投票力を急速に蓄積する可能性を減らします。
安全なガバナンス・システムの設計 (新しいタブで開きます)、分散型自律組織 (DAO) におけるさまざまな投票メカニズム (新しいタブで開きます)、および分散型金融 (DeFi) を活用した一般的なDAOの攻撃ベクトル (新しいタブで開きます)の詳細については、共有リンクを参照してください。
8. Reduce complexity in code to a minimum
従来のソフトウェア開発者は、ソフトウェア設計に不必要な複雑さを導入しないようにアドバイスするKISS(「Keep It Simple, Stupid:シンプルにしておけ、愚か者」)の原則に精通しています。これは、「複雑なシステムは複雑な方法で失敗する」ため、コストのかかるエラーの影響を受けやすいという長年の考えに基づいています。
スマート・コントラクトが大量の価値を制御する可能性があることを考慮すると、スマート・コントラクトを作成する際には物事をシンプルに保つことが特に重要です。スマート・コントラクトを作成する際にシンプルさを実現するためのヒントは、可能な限りオープンツェッペリン・コントラクト (新しいタブで開きます)などの既存のライブラリを再利用することです。これらのライブラリは開発者によって広範に監査およびテストされているため、これらを使用することで、新しい機能をゼロから作成してバグを導入する可能性が減ります。
もう1つの一般的なアドバイスは、小さな関数を作成し、ビジネスロジックを複数のコントラクトに分割することでコントラクトをモジュール化しておくことです。よりシンプルなコードを書くことは、スマート・コントラクトの攻撃対象領域を減らすだけでなく、システム全体の正確性について推論し、考えられる設計エラーを早期に検出することを容易にします。
9. Defend against common smart contract vulnerabilities
Reentrancy
イーサリアム仮想マシン (EVM) は並行処理を許可していません。つまり、メッセージ・コールに関与する2つのコントラクトを同時に実行することはできません。外部呼び出しは、呼び出しが戻るまで呼び出し元のコントラクトの実行とメモリを一時停止し、戻った時点で実行が正常に進行します。このプロセスは、別のコントラクトに制御フロー (新しいタブで開きます)を転送するものとして正式に説明できます。
ほとんどの場合は無害ですが、信頼できないコントラクトに制御フローを転送すると、リエントランシーなどの問題が発生する可能性があります。リエントランシー攻撃は、元の関数の呼び出しが完了する前に、悪意のあるコントラクトが脆弱なコントラクトにコールバックしたときに発生します。このタイプの攻撃は、例を用いて説明するのが最適です。
誰でもイーサを入金および引き出しできるシンプルなスマート・コントラクト(「Victim」)を考えてみましょう。
// このコントラクトは脆弱です。本番環境では使用しないでください。
contract Victim {
mapping (address => uint256) public balances;
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdraw() external {
uint256 amount = balances[msg.sender];
(bool success, ) = msg.sender.call.value(amount)("");
require(success);
balances[msg.sender] = 0;
}
}
このコントラクトは、ユーザーが以前にコントラクトに入金したETHを引き出せるようにする withdraw() 関数を公開しています。引き出しを処理する際、コントラクトは以下の操作を実行します。
- ユーザーのETH残高を確認する
- 呼び出し元アドレスに資金を送信する
- 残高を0にリセットし、ユーザーからの追加の引き出しを防ぐ
Victim コントラクトの withdraw() 関数は、「チェック-インタラクション-エフェクト」パターンに従っています。実行に必要な条件が満たされているか(つまり、ユーザーのETH残高がプラスであるか)を_チェック_し、トランザクションの_エフェクト_(つまり、ユーザーの残高を減らすこと)を適用する前に、呼び出し元のアドレスにETHを送信することで_インタラクション_を実行します。
withdraw() が外部所有アカウント (EOA) から呼び出された場合、関数は期待通りに実行されます。msg.sender.call.value() は呼び出し元にETHを送信します。しかし、msg.sender がスマート・コントラクト・アカウントであり、withdraw() を呼び出した場合、msg.sender.call.value() を使用して資金を送信すると、そのアドレスに保存されているコードの実行もトリガーされます。
これがコントラクト・アドレスにデプロイされたコードだと想像してください。
contract Attacker {
function beginAttack() external payable {
Victim(victim_address).deposit.value(1 ether)();
Victim(victim_address).withdraw();
}
function() external payable {
if (gasleft() > 40000) {
Victim(victim_address).withdraw();
}
}
}
このコントラクトは、以下の3つのことを行うように設計されています。
- 別のアカウント(おそらく攻撃者のEOA)からの入金を受け入れる
- Victimコントラクトに1 ETHを入金する
- スマート・コントラクトに保存されている1 ETHを引き出す
ここには何も問題はありませんが、Attacker には、入ってくる msg.sender.call.value から残ったガスが40,000を超える場合、Victim の withdraw() を再度呼び出す別の関数がある点が異なります。これにより、Attacker は Victim に再入し、withdraw の最初の呼び出しが完了する_前_に、より多くの資金を引き出すことができます。サイクルは次のようになります。
- Attacker's EOA calls `Attacker.beginAttack()` with 1 ETH
- `Attacker.beginAttack()` deposits 1 ETH into `Victim`
- `Attacker` calls `withdraw() in `Victim`
- `Victim` checks `Attacker`’s balance (1 ETH)
- `Victim` sends 1 ETH to `Attacker` (which triggers the default function)
- `Attacker` calls `Victim.withdraw()` again (note that `Victim` hasn’t reduced `Attacker`’s balance from the first withdrawal)
- `Victim` checks `Attacker`’s balance (which is still 1 ETH because it hasn’t applied the effects of the first call)
- `Victim` sends 1 ETH to `Attacker` (which triggers the default function and allows `Attacker` to reenter the `withdraw` function)
- The process repeats until `Attacker` runs out of gas, at which point `msg.sender.call.value` returns without triggering additional withdrawals
- `Victim` finally applies the results of the first transaction (and subsequent ones) to its state, so `Attacker`’s balance is set to 0
要約すると、関数の実行が完了するまで呼び出し元の残高は0に設定されないため、後続の呼び出しは成功し、呼び出し元は残高を複数回引き出すことができます。この種の攻撃は、2016年のDAOハッキング (新しいタブで開きます)で起こったように、スマート・コントラクトから資金を枯渇させるために使用される可能性があります。リエントランシー・エクスプロイトの公開リスト (新しいタブで開きます)が示すように、リエントランシー攻撃は今日でもスマート・コントラクトにとって重大な問題です。
How to prevent reentrancy attacks
リエントランシーに対処するアプローチは、チェック-エフェクト-インタラクション・パターン (新しいタブで開きます)に従うことです。このパターンは、実行を進める前に必要なチェックを実行するコードが最初に来て、次にコントラクトの状態を操作するコードが続き、他のコントラクトやEOAとやり取りするコードが最後に来るように、関数の実行を順序付けます。
チェック-エフェクト-インタラクション・パターンは、以下に示す Victim コントラクトの改訂版で使用されています。
contract NoLongerAVictim {
function withdraw() external {
uint256 amount = balances[msg.sender];
balances[msg.sender] = 0;
(bool success, ) = msg.sender.call.value(amount)("");
require(success);
}
}
このコントラクトは、ユーザーの残高を_チェック_し、(ユーザーの残高を0にリセットすることで)withdraw() 関数の_エフェクト_を適用し、インタラクション(ユーザーのアドレスへのETHの送信)の実行に進みます。これにより、コントラクトは外部呼び出しの前にストレージを更新することが保証され、最初の攻撃を可能にしたリエントランシー条件が排除されます。Attacker コントラクトは依然として NoLongerAVictim にコールバックできますが、balances[msg.sender] は0に設定されているため、追加の引き出しはエラーをスローします。
別のオプションは、関数の呼び出しが完了するまでコントラクトの状態の一部をロックする相互排他ロック(一般に「ミューテックス」と呼ばれます)を使用することです。これは、関数が実行される前に true に設定され、呼び出しが完了した後に false に戻るブール変数を使用して実装されます。以下の例に見られるように、ミューテックスを使用すると、元の呼び出しがまだ処理されている間の再帰的な呼び出しから関数が保護され、リエントランシーが効果的に停止されます。
pragma solidity ^0.7.0;
contract MutexPattern {
bool locked = false;
mapping(address => uint256) public balances;
modifier noReentrancy() {
require(!locked, "Blocked from reentrancy.");
locked = true;
_;
locked = false;
}
// この関数はミューテックスによって保護されているため、`msg.sender.call`内からのリエントラント呼び出しで再び`withdraw`を呼び出すことはできません。
// `return`文は`true`と評価されますが、それでも修飾子内の`locked = false`文は評価されます。
function withdraw(uint _amount) public payable noReentrancy returns(bool) {
require(balances[msg.sender] >= _amount, "No balance to withdraw.");
balances[msg.sender] -= _amount;
(bool success, ) = msg.sender.call{value: _amount}("");
require(success);
return true;
}
}
また、アカウントに資金を送信する「プッシュ・ペイメント」システムの代わりに、ユーザーがスマート・コントラクトから資金を引き出す必要があるプル・ペイメント (新しいタブで開きます)システムを使用することもできます。これにより、未知のアドレスで誤ってコードをトリガーする可能性が排除されます(また、特定のサービス拒否攻撃を防ぐこともできます)。
Integer underflows and overflows
整数のオーバーフローは、算術演算の結果が許容される値の範囲外になり、表現可能な最小値に「ロールオーバー」するときに発生します。たとえば、uint8 は 2^8-1=255 までの値しか保存できません。255 より大きい値になる算術演算はオーバーフローし、車のオドメーターが最大走行距離 (999999) に達すると0にリセットされるのと同様に、uint を 0 にリセットします。
整数のアンダーフローも同様の理由で発生します。算術演算の結果が許容範囲を下回る場合です。uint8 で 0 をデクリメントしようとしたとすると、結果は単に表現可能な最大値 (255) にロールオーバーします。
整数のオーバーフローとアンダーフローはどちらも、コントラクトの状態変数への予期しない変更につながり、計画外の実行を引き起こす可能性があります。以下は、攻撃者がスマート・コントラクトの算術オーバーフローを悪用して無効な操作を実行する方法を示す例です。
pragma solidity ^0.7.6;
// このコントラクトはタイムボールトとして機能するように設計されています。
// ユーザーはこのコントラクトに預け入れることができますが、少なくとも1週間は引き出すことができません。
// ユーザーは待機時間を1週間の待機期間を超えて延長することもできます。
/*
1. TimeLockをデプロイする
2. TimeLockのアドレスを使用してAttackをデプロイする
3. 1 etherを送信してAttack.attackを呼び出す。すぐにetherを引き出すことができるようになります。
何が起きたのか?
AttackがTimeLock.lockTimeをオーバーフローさせ、1週間の待機期間の前に引き出すことができました。
*/
contract TimeLock {
mapping(address => uint) public balances;
mapping(address => uint) public lockTime;
function deposit() external payable {
balances[msg.sender] += msg.value;
lockTime[msg.sender] = block.timestamp + 1 weeks;
}
function increaseLockTime(uint _secondsToIncrease) public {
lockTime[msg.sender] += _secondsToIncrease;
}
function withdraw() public {
require(balances[msg.sender] > 0, "Insufficient funds");
require(block.timestamp > lockTime[msg.sender], "Lock time not expired");
uint amount = balances[msg.sender];
balances[msg.sender] = 0;
(bool sent, ) = msg.sender.call{value: amount}("");
require(sent, "Failed to send Ether");
}
}
contract Attack {
TimeLock timeLock;
constructor(TimeLock _timeLock) {
timeLock = TimeLock(_timeLock);
}
fallback() external payable {}
function attack() public payable {
timeLock.deposit{value: msg.value}();
/*
もし t = 現在のロック時間 であるなら、次のような x を見つける必要がある
x + t = 2**256 = 0
したがって x = -t
2**256 = type(uint).max + 1
したがって x = type(uint).max + 1 - t
*/
timeLock.increaseLockTime(
type(uint).max + 1 - timeLock.lockTime(address(this))
);
timeLock.withdraw();
}
}
How to prevent integer underflows and overflows
バージョン0.8.0以降、Solidityコンパイラは整数のアンダーフローとオーバーフローを引き起こすコードを拒否します。ただし、より低いコンパイラバージョンでコンパイルされたコントラクトは、算術演算を含む関数でチェックを実行するか、アンダーフロー/オーバーフローをチェックするライブラリ(SafeMath (新しいタブで開きます)など)を使用する必要があります。
Oracle manipulation
オラクルはオフチェーンの情報を取得し、スマート・コントラクトが使用できるようにオンチェーンに送信します。オラクルを使用すると、資本市場などのオフチェーン・システムと相互運用するスマート・コントラクトを設計でき、そのアプリケーションを大幅に拡張できます。
しかし、オラクルが破損して誤った情報をオンチェーンに送信した場合、スマート・コントラクトは誤った入力に基づいて実行され、問題を引き起こす可能性があります。これが「オラクル問題」の基礎であり、ブロックチェーン・オラクルからの情報が正確で、最新であり、タイムリーであることを確認するタスクに関係しています。
関連するセキュリティ上の懸念は、分散型取引所などのオンチェーン・オラクルを使用して資産のスポット価格を取得することです。分散型金融 (DeFi)業界のレンディング・プラットフォームは、ユーザーの担保の価値を決定して、いくら借りることができるかを決定するために、これを頻繁に行います。
分散型取引所 (DEX) の価格は、主にアービトラージャーが市場のパリティを回復するため、多くの場合正確です。しかし、特にオンチェーン・オラクルが(通常そうであるように)過去の取引パターンに基づいて資産価格を計算する場合、それらは操作される可能性があります。
たとえば、攻撃者はレンディング・コントラクトとやり取りする直前にフラッシュ・ローンを利用することで、資産のスポット価格を人為的に引き上げる可能性があります。DEXに資産の価格を照会すると、(攻撃者の大規模な「買い注文」が資産の需要を歪めるため)通常よりも高い値が返され、本来よりも多く借りることができるようになります。このような「フラッシュ・ローン攻撃」は、DeFiアプリケーション間の価格オラクルへの依存を悪用するために使用されており、プロトコルに数百万の資金損失をもたらしています。
How to prevent oracle manipulation
オラクルの操作を回避する (新しいタブで開きます)ための最小要件は、単一障害点を回避するために複数のソースから情報を照会する分散型オラクル・ネットワークを使用することです。ほとんどの場合、分散型オラクルには、オラクルノードに正しい情報を報告するように促す暗号経済的インセンティブが組み込まれており、中央集権型オラクルよりも安全です。
資産価格についてオンチェーン・オラクルに照会する予定がある場合は、時間加重平均価格 (TWAP) メカニズムを実装しているものを使用することを検討してください。TWAPオラクル (新しいタブで開きます)は、2つの異なる時点(変更可能)での資産の価格を照会し、得られた平均に基づいてスポット価格を計算します。より長い期間を選択すると、最近実行された大量の注文が資産価格に影響を与えることができないため、価格操作からプロトコルが保護されます。
開発者向けスマート・コントラクト・セキュリティ・リソース
スマート・コントラクトの分析とコードの正確性を検証するためのツール
-
テストツールとライブラリ - スマート・コントラクトの単体テスト、静的解析、動的解析を実行するための業界標準のツールとライブラリのコレクション。
-
形式的検証ツール - スマート・コントラクトの機能的な正確性を検証し、不変条件を確認するためのツール。
-
スマート・コントラクト監査サービス - イーサリアム開発プロジェクト向けにスマート・コントラクトの監査サービスを提供する組織のリスト。
-
バグバウンティ・プラットフォーム - バグバウンティを調整し、スマート・コントラクトの重大な脆弱性の責任ある開示に報酬を与えるためのプラットフォーム。
-
Fork Checker (新しいタブで開きます) - フォークされたコントラクトに関する利用可能なすべての情報を確認するための無料オンラインツール。
-
ABI Encoder (新しいタブで開きます) - Solidityコントラクトの関数とコンストラクタの引数をエンコードするための無料オンラインサービス。
-
Aderyn (新しいタブで開きます) - 抽象構文木 (AST) をトラバースして疑わしい脆弱性を特定し、読みやすいマークダウン形式で問題を出力するSolidity静的解析ツール。
スマート・コントラクトを監視するためのツール
- Tenderly Real-Time Alerting (新しいタブで開きます) - スマート・コントラクトやウォレットで異常または予期しないイベントが発生したときに、リアルタイムで通知を受け取るためのツール。
スマート・コントラクトの安全な管理のためのツール
-
Safe (新しいタブで開きます) - トランザクションを実行する前に、最小限の人数による承認を必要とする (M-of-N)、イーサリアム上で稼働するスマート・コントラクト・ウォレット。
-
オープンツェッペリン・コントラクト (新しいタブで開きます) - コントラクトの所有権、アップグレード、アクセス制御、ガバナンス、一時停止機能などの管理機能を実装するためのコントラクト・ライブラリ。
スマート・コントラクト監査サービス
-
コンセンシス・ディリジェンス (ConsenSys Diligence) (新しいタブで開きます) - ブロックチェーン・エコシステム全体のプロジェクトが、プロトコルのローンチ準備を整え、ユーザーを保護するように構築されていることを確認するのを支援するスマート・コントラクト監査サービス。
-
CertiK (新しいタブで開きます) - スマート・コントラクトおよびブロックチェーン・ネットワークにおける最先端の形式的検証テクノロジーの利用を先導するブロックチェーン・セキュリティ企業。
-
Trail of Bits (新しいタブで開きます) - セキュリティ研究と攻撃者の思考を組み合わせて、リスクを軽減しコードを強化するサイバーセキュリティ企業。
-
PeckShield (新しいタブで開きます) - ブロックチェーン・エコシステム全体のセキュリティ、プライバシー、ユーザビリティのための製品とサービスを提供するブロックチェーン・セキュリティ企業。
-
QuantStamp (新しいタブで開きます) - セキュリティおよびリスク評価サービスを通じて、ブロックチェーン技術のメインストリームへの普及を促進する監査サービス。
-
オープンツェッペリン (新しいタブで開きます) - 分散型システム向けのセキュリティ監査を提供するスマート・コントラクト・セキュリティ企業。
-
Runtime Verification (新しいタブで開きます) - スマート・コントラクトの形式的モデリングと検証を専門とするセキュリティ企業。
-
Hacken (新しいタブで開きます) - ブロックチェーン・セキュリティに360度のアプローチをもたらすWeb3サイバーセキュリティ監査機関。
-
ネザーマインド (新しいタブで開きます) - イーサリアムおよびスタークネット全体でスマート・コントラクトの整合性とユーザーの安全性を確保する、SolidityおよびCairoの監査サービス。
-
HashEx (新しいタブで開きます) - 暗号資産のセキュリティを確保するためのブロックチェーンおよびスマート・コントラクト監査に注力し、スマート・コントラクト開発、ペネトレーションテスト、ブロックチェーン・コンサルティングなどのサービスを提供。
-
Code4rena (新しいタブで開きます) - スマート・コントラクトのセキュリティ専門家が脆弱性を発見し、Web3をより安全にするためのインセンティブを提供する競争型監査プラットフォーム。
-
CodeHawks (新しいタブで開きます) - セキュリティ研究者向けにスマート・コントラクト監査コンペティションを主催する競争型監査プラットフォーム。
-
Cyfrin (新しいタブで開きます) - 製品やスマート・コントラクト監査サービスを通じて暗号資産セキュリティをインキュベートする、Web3セキュリティの有力企業。
-
ImmuneBytes (新しいタブで開きます) - 経験豊富な監査チームとクラス最高のツールを通じて、ブロックチェーン・システムのセキュリティ監査を提供するWeb3セキュリティ企業。
-
Oxorio (新しいタブで開きます) - 暗号資産企業やDeFiプロジェクト向けに、イーサリアム仮想マシン (EVM)、Solidity、ZK、クロスチェーン技術の専門知識を活かしたスマート・コントラクト監査およびブロックチェーン・セキュリティ・サービスを提供。
-
Inference (新しいタブで開きます) - EVMベースのブロックチェーンのスマート・コントラクト監査を専門とするセキュリティ監査企業。専門の監査人が潜在的な問題を特定し、デプロイ前に修正するための実用的な解決策を提案。
バグバウンティ・プラットフォーム
-
Immunefi (新しいタブで開きます) - セキュリティ研究者がコードをレビューし、脆弱性を開示して報酬を得ることで、暗号資産をより安全にする、スマート・コントラクトおよび分散型金融 (DeFi) プロジェクト向けのバグバウンティ・プラットフォーム。
-
HackerOne (新しいタブで開きます) - 企業とペネトレーションテスターやサイバーセキュリティ研究者を結びつける、脆弱性調整およびバグバウンティ・プラットフォーム。
-
HackenProof (新しいタブで開きます) - 暗号資産プロジェクト (DeFi、スマート・コントラクト、ウォレット、CEXなど) 向けの専門的なバグバウンティ・プラットフォーム。セキュリティ専門家がトリアージサービスを提供し、研究者は関連性が確認されたバグレポートに対して報酬を得る。
-
Sherlock (新しいタブで開きます) - スマート・コントラクト・セキュリティのためのWeb3の引受機関。関連するバグに対して公正に報酬が支払われるよう、監査人への支払いはスマート・コントラクトを通じて管理される。
-
CodeHawks (新しいタブで開きます) - 監査人がセキュリティコンテストやチャレンジに参加し、(将来的には) 独自のプライベート監査にも参加できる競争型バグバウンティ・プラットフォーム。
既知のスマート・コントラクトの脆弱性とエクスプロイトに関する出版物
-
コンセンシス: スマート・コントラクトの既知の攻撃 (新しいタブで開きます) - 最も重大なコントラクトの脆弱性に関する初心者向けの解説。ほとんどのケースでサンプルコードが付属。
-
SWC Registry (新しいタブで開きます) - イーサリアムのスマート・コントラクトに適用される共通脆弱性タイプ一覧 (CWE) 項目の厳選リスト。
-
Rekt (新しいタブで開きます) - 注目を集めた暗号資産のハッキングやエクスプロイトに関する定期的に更新される出版物。詳細な事後レポートも掲載。
スマート・コントラクト・セキュリティを学ぶためのチャレンジ
-
Awesome BlockSec CTF (新しいタブで開きます) - ブロックチェーン・セキュリティのウォーゲーム、チャレンジ、Capture The Flag (CTF) (新しいタブで開きます) コンペティション、およびその解決策の解説の厳選リスト。
-
Damn Vulnerable DeFi (新しいタブで開きます) - DeFiスマート・コントラクトのオフェンシブ・セキュリティを学び、バグハンティングやセキュリティ監査のスキルを構築するためのウォーゲーム。
-
Ethernaut (新しいタブで開きます) - 各レベルが「ハッキング」されるべきスマート・コントラクトとなっている、Web3/Solidityベースのウォーゲーム。
-
HackenProof x HackTheBox (新しいタブで開きます) - ファンタジー・アドベンチャーを舞台にしたスマート・コントラクトのハッキングチャレンジ。チャレンジを無事に完了すると、プライベートなバグバウンティ・プログラムへのアクセス権も得られる。
スマート・コントラクトを保護するためのベストプラクティス
-
コンセンシス: イーサリアム・スマート・コントラクト・セキュリティのベストプラクティス (新しいタブで開きます) - イーサリアムのスマート・コントラクトを保護するための包括的なガイドラインのリスト。
-
Nascent: シンプル・セキュリティ・ツールキット (新しいタブで開きます) - スマート・コントラクト開発のための、実践的でセキュリティに焦点を当てたガイドとチェックリストのコレクション。
-
Solidity Patterns (新しいタブで開きます) - スマート・コントラクト・プログラミング言語Solidityのための、安全なパターンとベストプラクティスの有用なコンパイル。
-
Solidityドキュメント: セキュリティに関する考慮事項 (新しいタブで開きます) - Solidityで安全なスマート・コントラクトを記述するためのガイドライン。
-
スマート・コントラクト・セキュリティ検証標準 (新しいタブで開きます) - 開発者、アーキテクト、セキュリティ・レビュアー、ベンダー向けにスマート・コントラクトのセキュリティを標準化するために作成された14部構成のチェックリスト。
-
スマート・コントラクトのセキュリティと監査を学ぶ (新しいタブで開きます) - セキュリティのベストプラクティスをレベルアップし、セキュリティ研究者を目指すスマート・コントラクト開発者のために作成された、究極のスマート・コントラクト・セキュリティおよび監査コース。