前回の記事でSolidityを使ったdApps開発&運用の流れは大まかに把握できました。
ガス代の関係で、最初はPolygonで練習することになるそうかなぁ...^^;
それか、Solidity自体は目標としているAsterNetworkでも対応してるっぽいので、
練習がてら何かしらローンチしてもおもしろそうかなー
概要は理解できたのですが、どうも細かい部分、特にセキュリティ部分についてはめちゃくちゃ不安。。
Web開発のときもそうなのですが、フレームワークに沿って開発してたらフレームワーク自体にセキュリティホールがあったり、想定してない環境で動作させて不具合がおきたりみたいな、経験が必要な部分ってどうしてもあると思います。。
ここばかりはやって慣れるしかないのですが、やはり先人の知恵というか経験はおさらいしておきたいですね。
ということで、OpenZeppelinのチュートリアルでも紹介されていた、「イーサリアムスマートコントラクト・ベストプラクティス」のサイトを読み解くのと、
OpenZeppelinが実施している監査の前にチェックすべき項目について見ていきたいと思います!
その1. イーサリアムスマートコントラクト・ベストプラクティス
最終更新が2年前なので、話半分(?)にはしょって見ていきます!!
ConsenSys Diligenceというセキュリティ監査チーム(法人ではないく??)によって管理されているとのことです。
以下、基本的に翻訳をしつつ、
私のコメントはここに記載しますー
1-1. スマートコントラクトのセキュリティに関する考え方
スマートコントラクトのプログラミングには、これまでとは異なるエンジニアとしての考え方が必要です。
失敗したときのコストが高く、変更が難しいため、
ウェブやモバイルの開発というよりも、
ハードウェアのプログラミングや金融サービスのプログラミングに似ているところがあります。
そのため、既知の脆弱性を防御するだけでは十分ではありません。
むしろ、新たな開発哲学を身につける必要があります。
- サーキットブレーカー: 物事がうまくいかないときにコントラクトを一時停止する
- レート制限、最大使用量: リスクのある金額を管理する
- 事前のインシデント対応策: バグフィックスや改良のための効果的なアップグレードパスを用意する
- コントラクトのロジックをシンプルにする
- コードをモジュール化してコントラクトや関数を小さくする
- 可能な限りパフォーマンスよりも分かりやすさを優先する
- システムの中で分散化が必要な部分にのみブロックチェーンを使う
- 新しいバグが発見されたら、すぐにコントラクトをチェックする
- ツールやライブラリはできるだけ早く最新版にアップグレードする。
- 役に立ちそうな新しいセキュリティ技術を採用する
ブロックチェーンの特性に注意!!
- 外部のコントラクト呼び出しには細心の注意を払い、悪意のあるコードを実行したり、制御フローを変更したりする可能性があります
- パブリック関数は公開されており、悪意を持ってどのような順序でも呼び出される可能性があることを理解しましょう
- スマートコントラクトのプライベートデータは誰でも見ることができることも留意しましょう
- ガスコストとブロックガスの制限を念頭に置いてください
- ブロックチェーン上ではタイムスタンプは不正確であり、マイナーは数秒のマージンの範囲内でトランザクションの実行時間に影響を与えることができることに注意してください
- ブロックチェーンではランダム性は自明ではありません。公平な乱数生成を心がけましょう
乱数生成についてはPoWのマイニングやゲームのシステム等にも密接に関わるので、
かなりセンシティブなトピックですね。。
「真のランダム性」を解決する方法として、検証可能な乱数関数(Verifiable Random Function (VRF))が提唱されています。
検証可能な乱数関数とは、入力を処理して検証可能な疑似乱数出力を生成する暗号関数です。
入力に証明と公開鍵を用いることで、秘密鍵を知らなくても、またそれを知る可能性がなくても、出力を検証することができます。
https://hackernoon.com/generating-randomness-in-blockchain-verifiable-random-function-ft1534ud
公開鍵を使って、自分が証明したことを検証可能にするというアイデアですね。
こういうの思いつくのすごいなーとホント思います。
ChainLinkが独自の乱数生成APIを公開してます。
...金($LINK)かかるんかぁ...。
お金かからん乱数生成方法ないかなぁ...
まぁまたコードで使うときがきたら考えよう。
単純性と複雑性のトレードオフ
堅牢性 VS アップグレード性
複雑さは潜在的な攻撃面を増加させるので、
非常に限られた機能を実行する場合はシンプルさに重きを置く。
モノリシック(自己完結型) VS モジュラー(機能分離型)
単純短命のコントラクトではソフトウェアエンジニアリングのベストプラクティスから離れ、
複雑永久なコントラクトの場合にはソフトウェアエンジニアリングのベストプラクティスに向かう傾向があります。
このあたりも場合によって使い分けが必要です。
重複 VS 再利用
Solidityでコントラクトコードを再利用する方法はたくさんあります。
一般的に、コードの再利用を達成するための最も安全な方法は、
あなたが所有する実績のある以前にデプロイされたコントラクトを使用することです。
OpenZeppelinのSolidity Libraryのような取り組みは、セキュアなコードを重複なく再利用できるようなパターンを提供することを目指しています。
ほんまありがたいで。
おおまかな考え方は理解できたかな。
やっぱチーム組んでやるDEXみたいなdAppsはしんどそうなので
もっと軽いやつから手をつけよう...!
1-2. 優れたコードパターンの例
実際のコードを見ながら、
ブロックチェーン開発ならではの推奨事項を見ていきますー
外部コントラクトメソッドの呼び出し
信頼されていないコントラクトへの呼び出しは、いくつかの予期せぬリスクやエラーを引き起こす可能性があります。
外部呼び出しは、そのコントラクトまたはそれに依存する他のコントラクトで悪意のあるコードを実行する可能性があります。
そのため、すべての外部呼び出しは、潜在的なセキュリティリスクとして扱われるべきです。
信頼されていないコントラクトをマークする
外部のコントラクトとやりとりするときは、変数やメソッド、コントラクトのインターフェイスに、それらとのやりとりが潜在的に安全ではないことを明確にする方法で名前を付けてください。
これは、外部のコントラクトを呼び出す自分の関数にも適用しましょう。
UntrustedBank.withdraw(100); // untrusted external call
TrustedBank.withdraw(100); // external but trusted bank contract maintained by XYZ Corp
function makeUntrustedWithdrawal(uint amount) {
UntrustedBank.withdraw(amount);
}
...というか使わないようにしよう!!
外部呼び出し後の状態変化を避ける
生の呼び出し(someAddress.call()の形式)やコントラクトの呼び出し(ExternalContract.someMethod()の形式)を使用しているかどうかにかかわらず、
悪意のあるコードが実行される可能性があることを想定してください。
ExternalContractが悪意のないものであっても、悪意のあるコードは、
それが呼び出すあらゆるコントラクトによって実行される可能性があります。
特に危険なのは、悪意のあるコードが制御の流れを乗っ取り、
再帰性による脆弱性につながるところです。(※既知の攻撃参照)
transfer()やsend()の使用禁止
.transfer()と.send()は、正確に2,300のガスを受取人に転送します。
このハードコードされたガスの規定の目的は、リエントランスの脆弱性を防ぐことでしたが、
これはガスのコストが一定であるという仮定の下でのみ意味があります。
現在、.call()の使用が推奨されていますが、
リエントランシー攻撃を緩和する効果はないので、他の予防策を講じる必要があります。
再帰性攻撃を防ぐには、checks-effects-interactionsパターンを使用することをお勧めします。
checks-effects-interactionsパターン
状態遷移を実行に移す前、誰が関数を呼び出したか、引数は範囲内か、十分な量のEtherを送ったか、その人はトークンを持っているか、など、最初にチェック行います。
すべてのチェックが通過した場合、第2段階として、現在のコントラクトの状態変数への影響が見積もられるべきです。
他コントラクトとのやりとりは、どの関数でも最後のステップ(第3段階)にすべきです。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
contract Fund {
/// @dev Mapping of ether shares of the contract.
mapping(address => uint) shares;
/// Withdraw your share.
function withdraw() public {
uint share = shares[msg.sender];
shares[msg.sender] = 0; // 送金額を0にして再帰的に資金をひきだせないようにする
payable(msg.sender).transfer(share); // チェック→送金
}
}
外部呼び出しでのエラー処理
Solidityには、生のアドレスで動作する低レベルの呼び出しメソッドとして、 address.call()、address.callcode()、address.delegatecall()、address.send()があります。
低レベルの呼び出しメソッドを使用する場合は、戻り値をチェックするなどして、
呼び出しが失敗する可能性を必ず処理してください。
これらの低レベルのメソッドは、例外を投げることはありませんが、
呼び出しに例外が発生した場合は、falseを返します。
一方、コントラクトの呼び出し(ExternalContract.doSomething()など)は、自動的にthrowを伝播します(例えば、doSomething()がthrowした場合、ExternalContract.doSomething()もthrowします)。
プッシュよりもプル優先
外部呼び出しは、偶発的または意図的に失敗することがあります。
このような失敗による損害を最小限に抑えるためには、
各外部呼び出しを、受信者が開始できる独自のトランザクションに分離する方がよい場合が多いです。
ユーザーに資金を自動的に押し付けるのではなく、ユーザーが資金を引き出すようにしたほうがよい。
(これは、ガス制限に関する問題が発生する可能性を減らすことにもなります)
信頼できないコードに delegatecall しない
delegatecall関数は、他のコントラクトの関数を呼び出し元のコントラクトに属しているかのように呼び出すために使用されます。
そのため、呼び出し側は呼び出し元のアドレスの状態を変更する可能性があります。
Etherはアカウントに強制的に送ることができることに注意
攻撃者は、任意のアカウントに強制的にEtherを送ることができ、
これを防ぐことはできません(revert()を行うフォールバック関数でもできません)。
攻撃者は、コントラクトを作成し、そのコントラクトに1weiを出資し、
selfdestruct(victimAddress)を呼び出すことでこれを行うことができます。
victimAddressではコードが起動されないので、防ぐことができません。
これは、ブロックリワードがマイナーのアドレスに送られる場合も同様で、
任意のアドレスにすることができます。
また、コントラクトのアドレスは事前に計算できるため、
コントラクトが展開される前にアドレスにEtherを送ることができます。
チェーン上のデータは公開されていることを忘れないように
プライバシーが問題となるアプリケーションを構築する場合は、ユーザーが情報を早期に公開することを要求しないようにしてください。
最適な戦略は、別々のフェーズを持つコミットメントスキームを使用することです。
最初に値のハッシュを使用してコミットし、後のフェーズで値を公開します。
じゃんけん大会の場合、(1)プレイヤーが手を出す、(2)乱数を検証、(3)プレイヤーが支払う、という順序で行います。
参加者の中には「オフラインになって」戻ってこない人がいる可能性に注意しましょう
返金や請求のプロセスを、特定の当事者が特定の行為を行い、他に資金を出す方法がないことに依存させないことが重要です。
例えば、ジャンケンゲームでよくある間違いは、両方のプレイヤーが手を提出するまで支払いを行わないことですが、悪意のあるプレイヤーは、自分の手を提出しないというアクションを実行します。
実際、プレイヤーが相手の明らかになった手を見て負けたと判断した場合、自分の手を提出する理由は全くありません。この問題は、ステートチャネルの決済の場面でも発生することがあります。
このような状況が問題となる場合には、
(1)参加していない参加者を回避する方法(おそらく時間制限)を用意する
(2)参加者が情報を提出することになっているすべての状況において、参加者に追加の経済的インセンティブを与える
などを検討する必要があります。
一般のDEXなどでは、コントラクトが終了するまでにオフラインになると
ガス代や余分な費用がかかっちゃったりすることがありますね。
このじゃんけん大会の例は起こりうるケースかもしれませんが、
実際に見かけることはあんまりないように思えます...!
一番大きい負の符号付き整数の否定に注意
多くのプログラミング言語と同様に、SolidityではNビットの符号付き整数は-2^(N-1)から2^(N-1)-1までの値を表すことができます。
これは、MIN_INTには正の値がないことを意味します。
否定は、数値の2の補数を求めるように実装されているので、最も負の数値の否定は同じ数値になります。
contract Negation {
function negate8(int8 _i) public pure returns(int8) {
return -_i;
}
function negate16(int16 _i) public pure returns(int16) {
return -_i;
}
int8 public a = negate8(-128); // -128
int16 public b = negate16(-128); // 128
int16 public c = negate16(-32768); // -32768
}
これを処理する1つの方法は、負算の前に変数の値をチェックし、
それがMIN_INTに等しい場合にスローすることです。
もう1つの方法は、より大きな容量を持つ型(int16ではなくint32など)を使用することで、
最も負の数が絶対に実現しないようにすることです。
int型では、MIN_INTに-1を掛けたり割ったりしたときに同様の問題が発生します。
...アナログ!!!
assert()で不変量を強制する
assertガードは、不変性のプロパティが変更されるなど、アサーションが失敗したときにトリガーされます。例えば、トークン発行契約において、トークンとエーテルの発行比率が固定されている場合があります。assert()を使えば、常にこの状態であることを確認することができます。
アサートガードは、コントラクトを一時停止してアップグレードを許可するなど、
他のテクニックと組み合わせる必要があります。
(そうしないと、常に失敗しているアサートで行き詰ってしまう可能性があります)。
※コントラクトはdeposit()関数を経由せずに強制的にエーテルに送ることができるため、
このアサーションは残高の厳密な平等ではないことに注意してください。
contract Token {
mapping(address => uint) public balanceOf;
uint public totalSupply;
function deposit() public payable {
balanceOf[msg.sender] += msg.value;
totalSupply += msg.value;
assert(address(this).balance >= totalSupply);
}
}
assert()、require()、revert()を適切に使用しましょう
便利な関数である assert と require は、条件をチェックし、
条件を満たさない場合は例外を発生させるために使用できます。
■ assert() : 内部エラーのテストや、不変性のチェックにのみ使用
■ require() : 入力やコントラクトの状態変数などの有効な条件が満たされているかどうかを確認
外部コントラクトの呼び出しからの戻り値を検証するために使用
このパラダイムに従うことで、形式解析ツールは、無効なオペコードに決して到達できないことを検証することができます。
つまり、コード内の不変量が侵害されておらず、コードが形式的に検証されていることを意味します。
pragma solidity ^0.5.0;
contract Sharer {
function sendHalf(address payable addr) public payable returns (uint balance) {
require(msg.value % 2 == 0, "Even value required."); //Require() can have an optional message string
uint balanceBeforeTransfer = address(this).balance;
(bool success, ) = addr.call.value(msg.value / 2)("");
require(success);
// Since we reverted if the transfer failed, there should be
// no way for us to still have half of the money.
assert(address(this).balance == balanceBeforeTransfer - msg.value / 2); // used for internal error checking
return address(this).balance;
}
}
整数部での丸めに注意
すべての整数分割は、最も近い整数に丸められます。
より高い精度が必要な場合は、乗算器の使用を検討するか、分子と分母の両方を格納します。
(将来的には、Solidityに固定小数点型が搭載される予定です)
// bad
uint x = 5 / 2; // 整数型なので答えは2になる
// good example.1
uint multiplier = 10;
uint x = (5 * multiplier) / 2;
// good example.2
uint numerator = 5; //分子
uint denominator = 2; //分母
アブストラクトコントラクトとインターフェースの間のトレードオフに注意
Solidity 0.4.11から導入されたインターフェイスは、アブストラクトコントラクトと似ていますが、機能を実装することはできません。
また、インターフェイスはストレージにアクセスできない、他のインターフェイスを継承できないなどの制限があり、一般的にはアブストラクトの方が実用的です。
しかし、インターフェースは実装前のコントラクトを設計するのに便利です。
さらに、コントラクトが抽象コントラクトを継承する場合、
オーバーライドによってすべての非実装関数を実装しなければ、
同様に抽象コントラクトになってしまうことを覚えておくことが重要です。
フォールバック関数
フォールバック関数とは?
Solidityのfallback関数は下記の特徴があります。
-
無名関数となる
-
引数を取ることができない
-
値を返すことができない
-
2300 gas以上の処理ができない(storageへの書き込み, コントラクトの作成, 大量のgasを消費する外部関数の呼び出し, Etherの送金など)
https://tomokazu-kozuma.com/about-fallback-function/ -
フォールバック関数はシンプルに
-
フォールバック関数のデータ長をチェックする
Payable関数と状態変数を明示的にマークする
Solidity 0.4.0から、エーテルを受け取っているすべての関数はpayable修飾子を使用しなければなりません。
関数と状態変数の可視性を明示的にマークする
関数は、external、public、internal、privateのいずれかを指定できます。
明示的にラベル付けすることで、誰が関数を呼び出したり、変数にアクセスしたりできるのかという誤った仮定を容易に捕らえることができます。
外部関数(external)
はコントラクトインターフェースの一部です。外部関数fは、内部で呼び出すことはできません(つまり、f()は動作しないが、this.f()は動作する)。外部関数は、大きなデータの配列を受け取るときに、より効率的な場合があります。
パブリック関数(public)
はコントラクトインターフェイスの一部であり、内部で呼び出すことも、メッセージを介して呼び出すこともできます。パブリックな状態変数に対しては、自動的にゲッター関数(後述)が生成されます。
内部関数(internal)
内部関数とステート変数は、内部でのみアクセスできます。
プライベート関数(private)
プライベート関数とステート変数は、それらが定義されているコントラクトでのみ表示され、派生コントラクトでは表示されません。
※コントラクト内にあるものは、プライベート変数も含めて、ブロックチェーン外部のすべてのオブザーバーから閲覧可能です
プラグマを特定のコンパイラバージョンにロックする
コントラクトは、最もテストされたのと同じコンパイラバージョンとフラグでデプロイされるべきですので、
プラグマをロックすることで、例えば、未発見のバグのリスクが高い最新のコンパイラを使ってコントラクトが誤ってデプロイされないようにすることができます。
// bad
pragma solidity ^0.4.4;
// good
pragma solidity 0.4.4;
eventを使ってコントラクトの活動を監視する
コントラクトがデプロイされた後、コントラクトのアクティビティを監視する方法があると便利です。
1つの方法は、コントラクトのすべてのトランザクションを見ることですが、
コントラクト間のメッセージコールがブロックチェーンに記録されていないため、
それでは不十分な場合があります。
さらに、それは入力パラメータのみを示し、実際に状態に加えられている変更を示しません。
また、eventを利用してユーザーインターフェイスの機能を起動することも可能です。
contract Charity {
// define event
event LogDonate(uint _amount);
mapping(address => uint) balances;
function donate() payable public {
balances[msg.sender] += msg.value;
// emit event
emit LogDonate(msg.value);
}
}
contract Game {
function buyCoins() payable public {
// 5% goes to charity
charity.donate.value(msg.value / 20)();
}
}
ビルトインがシャドーイングできることに注意
スコープによる変数名の重複に注意する。
シャドーイングの例はこちら。
こんなトリッキーな使い方する人いるんか...
function myFunc() {
let my_var = 'test';
if (true) {
let my_var = 'new test';
console.log(my_var); // new test
}
console.log(my_var); // test
}
myFunc();
tx.originの使用を避ける
他のコントラクトがあなたのコントラクトを呼び出すメソッドを持つことができます。
あなたのコントラクトは、あなたのアドレスがtx.originにあるので、そのトランザクションを承認します。
(承認には msg.senderを使用する必要があります)
※将来的にtx.originがEthereumのプロトコルから削除される可能性があります。
タイムスタンプの依存性
契約上の重要な機能を実行するためにタイムスタンプを使用する場合、
特に資金移動を伴うアクションの場合には、3つの主要な考慮事項があります。
1. タイムスタンプの操作
ブロックのタイムスタンプはマイナーによって操作される可能性があることに注意しましょう。
時間に依存するイベントの規模が15秒単位で変化しても整合性が保たれるのであれば、block.timestampを使用するのが安全です。
2. 15秒ルール
イエローペーパー(Ethereumの参照仕様)では、
ブロックがどれだけ時間的にドリフトできるかについての制約は規定されていませんが、
各タイムスタンプは親のタイムスタンプよりも大きくなければならないと規定されています。
人気のあるEthereumプロトコルの実装であるGethとParityは、
タイムスタンプが15秒以上先のブロックを拒否しています。
3. block.numberをタイムスタンプとして使用することは避ける
block.numberプロパティと平均ブロックタイムを使用して時間差を推定することは可能ですが、
ブロックタイムが変更される可能性があるため(フォークの再編成や難易度の高い爆弾など)、
これは将来の保証にはなりません。
多重継承の注意点
Solidityで多重継承を使用する場合、コンパイラがどのように継承グラフを構成するかを理解することが重要です。
コントラクトが展開されると、コンパイラは右から左へと継承を線形化します(キーワードの後、親は最も基本的なものから最も派生したものへとリストアップされます)。
以下はコントラクトAの線形化です。
最終 <- B <- C <- A
こんなに継承するほどややこしいの作るのはやめよう
contract Final {
uint public a;
function Final(uint f) public {
a = f;
}
}
contract B is Final {
int public fee;
function B(uint f) Final(f) public {
}
function setFee() public {
fee = 3;
}
}
contract C is Final {
int public fee;
function C(uint f) Final(f) public {
}
function setFee() public {
fee = 5;
}
}
contract A is B, C { // 最終 <- B <- C <- A
function A() public B(3) C(5) {
setFee();
}
}
型安全性のため、アドレスの代わりにインターフェイス型を使用する
関数がコントラクト・アドレスを引数に取る場合、生のアドレスではなく、
インターフェイスやコントラクト型を渡すのが良いでしょう。
その関数がソースコード内の他の場所で呼ばれる場合、コンパイラは追加の型安全性を保証します。
contract Validator {
function validate(uint) external returns(bool);
}
contract TypeSafeAuction {
// good
function validateBet(Validator _validator, uint _value) internal returns(bool) {
bool valid = _validator.validate(_value);
return valid;
}
}
contract TypeUnsafeAuction {
// bad
function validateBet(address _addr, uint _value) internal returns(bool) {
Validator validator = Validator(_addr);
bool valid = validator.validate(_value);
return valid;
}
}
外部所有アカウントのチェックにextcodesizeの使用を避ける
以下は、通話が外部所有アカウント(EOA)から行われたのか、契約アカウントから行われたのかを確認するためによく使われます。
チェック方法は簡単で、アドレスにコードが含まれていれば、それはEOAではなくコントラクトアカウントです。
しかし、コントラクトはコンストラクト中にソースコードを利用できません。
つまり、コンストラクタの実行中は、他のコントラクトを呼び出すことができますが、
そのアドレスの extcodesize はゼロを返します。
以下は、このチェックをどのように回避できるかを示す最小限の例です。
// bad
modifier isNotContract(address _a) {
uint size;
assembly {
size := extcodesize(_a)
}
require(size == 0);
_;
}
contract OnlyForEOA {
uint public flag;
// bad
modifier isNotContract(address _a){
uint len;
assembly { len := extcodesize(_a) }
require(len == 0);
_;
}
function setFlag(uint i) public isNotContract(msg.sender){
flag = i;
}
}
contract FakeEOA {
constructor(address _a) public {
OnlyForEOA c = OnlyForEOA(_a);
c.setFlag(1);
}
}
他のコントラクトが自分のコントラクトを呼べないようにするのが目的なら、
extcodesizeチェックで十分でしょう。
別の方法として、(tx.origin == msg.sender)の値をチェックする方法もありますが、
これにも欠点があります。
他にも extcodesize チェックが有効な場合があります。
そのすべてをここで説明することはできません。
EVMの基本的な動作を理解し、ご自身の判断で行ってください。
...むずいな。。。
OpenZeppelinがこのあたりを安全に扱えるようにしてくれてると思うんですが。。。
このあたりかな?↓
https://docs.openzeppelin.com/contracts/4.x/access-control
また実践しながら学ぼう!!
非推奨/歴史的推奨事項
ゼロでの除算に注意 (Solidity < 0.4)。
バージョン0.4以前のSolidityでは、数値をゼロで割ったときにゼロを返し、例外を発生させません。最低でもバージョン0.4を実行していることを確認してください。
関数とイベントを区別する (Solidity < 0.4.21)。
関数とイベントが混同されるのを防ぐために、イベントの前には大文字と接頭辞をつけることをお勧めします(Logをお勧めします)。関数については、コンストラクタを除き、常に小文字で始めます。
OpenZeppelinのチュートリアルでは0.8.4を使ってましたね!
バージョンアップ情報もウォッチしていきたいです!!
1-3. 既知の攻撃パターンと回避すべき脆弱性
以下は、スマートコントラクトを作成する際に知っておくべき、そして防御すべき既知の攻撃のリストです。
これは普通にちゃんと理解しておきたい。
ちなみにMythXというところが「スマートコントラクトの弱点の分類とテストケース」を公開しています。
簡単なSolidityコードもあるので、見てみると勉強になりますー↓
再入可能性(Reentrancy)
外部のコントラクトを呼び出すことの大きな危険性の1つは、
コントラクトが制御フローを引き継いで、
呼び出した関数が想定していなかったデータの変更を行うことができることです。
この種のバグには様々な形があり、
DAOの崩壊につながった主要なバグはどちらもこの種のバグでした。
これは昔大分やられたやつっぽい。
現在、以下で予防が推奨されています。
・外部のコントラクトを呼び出す前に、すべての状態変化を確認する
・再入を防ぐための関数修飾子の使用
単一関数での再入可能性
このバグの最初のバージョンは、最初の呼び出しが終了する前に、
繰り返し呼び出される可能性のある関数に関するものでした。
下の例では、ユーザーの残高は関数の最後まで0にならないので、
2回目(以降)の起動でも成功し、何度も何度も残高が引き出されることになります。
mapping (address => uint) private userBalances;
// INSECURE
function withdrawBalanceInsecurely() public {
uint amountToWithdraw = userBalances[msg.sender];
// At this point, the caller's code is executed, and can call withdrawBalance again
(bool success, ) = msg.sender.call.value(amountToWithdraw)("");
require(success);
userBalances[msg.sender] = 0;
}
// SECURE
function withdrawBalanceSecurely() public {
uint amountToWithdraw = userBalances[msg.sender];
userBalances[msg.sender] = 0;
// The user's balance is already 0, so future invocations won't withdraw anything
(bool success, ) = msg.sender.call.value(amountToWithdraw)("");
require(success);
}
クロスファンクション・リエントランシー
攻撃者は、同じ状態を共有する2つの異なる関数を使用して、
同様の攻撃を行うことができるかもしれません。
mapping (address => uint) private userBalances;
function transfer(address to, uint amount) {
if (userBalances[msg.sender] >= amount) {
userBalances[to] += amount;
userBalances[msg.sender] -= amount;
}
}
// INSECURE
function withdrawBalance() public {
uint amountToWithdraw = userBalances[msg.sender];
// At this point, the caller's code is executed, and can call transfer()
(bool success, ) = msg.sender.call.value(amountToWithdraw)("");
require(success);
userBalances[msg.sender] = 0;
}
このケースでは、攻撃者は withdrawBalance の外部呼び出しで自分のコードが実行されたときに transfer() を呼び出します。
残高がまだ0になっていないため、すでに出金を受けているにもかかわらず、トークンを送金することができてしまいます。
この脆弱性は、DAO攻撃でも利用されました。
同じ解決策でも、注意点は同じです。
また、この例では、両方の機能が同じ契約の一部であったことに注意してください。
しかし、コントラクトが状態を共有している場合、同じバグが複数のコントラクトにまたがって発生する可能性があります。
再帰性対策の落とし穴
再帰性は複数の機能、さらには複数のコントラクトにまたがって発生する可能性があるため、
単一の機能で再帰性を防ぐことを目的とした解決策は十分ではありません。
代わりに、すべての内部作業(つまり、状態の変更)を最初に終えてから、
外部関数を呼び出すことを推奨 しています。
このルールをしっかり守れば、リエントランシーによる脆弱性を回避することができます。
しかし、外部関数をすぐに呼び出さないようにするだけでなく、
外部関数を呼び出す関数を呼び出さないようにする必要があります。
例えば、次のようなものは安全ではありません。
mapping (address => uint) private userBalances;
mapping (address => bool) private claimedBonus;
mapping (address => uint) private rewardsForA;
function withdrawReward(address recipient) public {
uint amountToWithdraw = rewardsForA[recipient];
rewardsForA[recipient] = 0;
(bool success, ) = recipient.call.value(amountToWithdraw)("");
require(success);
}
// INSECURE
function getFirstWithdrawalBonusInsecurely(address recipient) public {
// Each recipient should only be able to claim the bonus once
require(!claimedBonus[recipient]);
rewardsForA[recipient] += 100;
// At this point, the caller will be able to execute getFirstWithdrawalBonus again.
withdrawReward(recipient);
claimedBonus[recipient] = true;
}
// SECURE
function untrustedGetFirstWithdrawalBonusSecurely(address recipient) public {
// Each recipient should only be able to claim the bonus once
require(!claimedBonus[recipient]);
claimedBonus[recipient] = true;
rewardsForA[recipient] += 100;
// claimedBonus has been set to true, so reentry is impossible
untrustedWithdrawReward(recipient);
}
他の解決策としてmutexの利用が挙げられます。
ある状態を「ロック」して、ロックの所有者のみが変更できるようにすることができます。
// Note: This is a rudimentary example, and mutexes are particularly useful where there is substantial logic and/or shared state
mapping (address => uint) private balances;
bool private lockBalances;
function deposit() payable public returns (bool) {
require(!lockBalances);
lockBalances = true;
balances[msg.sender] += msg.value;
lockBalances = false;
return true;
}
function withdraw(uint amount) payable public returns (bool) {
require(!lockBalances && amount > 0 && balances[msg.sender] >= amount);
lockBalances = true; //LOCKED
(bool success, ) = msg.sender.call(amount)("");
if (success) { // Normally insecure, but the mutex saves it
balances[msg.sender] -= amount;
}
lockBalances = false; //UNLOCKED
return true;
}
しかし、以下のような複数のコントラクトがある場合は気をつけなければなりません。
攻撃者は、getLock()を呼び出した後、決してreleaseLock()を呼び出さないことができます。
これを行うと、コントラクトは永遠にロックされ、それ以上の変更はできなくなります。
再帰性を防止するためにmutexを使用する場合、
ロックを要求してリリースしない方法がないことを慎重に確認する必要があります。
(mutexを使ったプログラミングには、デッドロックやライブロックなど、
他にも潜在的な危険性があります。この方法を取ることに決めた場合は、
ミューテックスについてすでに書かれている大量の文献を参考にしてください)。
// INSECURE
contract StateHolder {
uint private n;
address private lockHolder;
function getLock() {
require(lockHolder == address(0));
lockHolder = msg.sender;
}
function releaseLock() {
require(msg.sender == lockHolder);
lockHolder = address(0);
}
function set(uint newState) {
require(msg.sender == lockHolder);
n = newState;
}
}
あーこっちの方がわかりやすいけど、別の問題があるんやなぁ...
関数を外部読み出しできないようにすればええんとちゃうか??
フロントランニング
すべての取引は、実行される前にしばらくの間、メモリプールに表示されるため、
ネットワークの観測者は、ブロックに含まれる前のアクションを見て、それに反応することができます。
これを悪用した例として、分散型取引所では、買い注文のトランザクションを見て、
最初のトランザクションが含まれる前に2番目の注文をブロードキャストして実行することができます。
Displacement攻撃
オーダーをパクってガス代を高くして先に実行する。
この攻撃は、ガス価格をネットワークの平均値よりも高くして行うのが一般的で、多くの場合、10倍以上の倍率になります。
Insertion攻撃
相手のオーダーを見て、自分のオーダーを先に挿入する。
ベストオファーよりも高い価格でブロックチェーン資産に購入注文を出した場合、
①ベストオファーの価格で購入
②ベストオファーよりわずかに高い購入価格で同じ資産を売りに出す
Suppression攻撃(Block Stuffing攻撃)
攻撃者が意図的にブロックのガスリミットを埋めるような一連の取引を提出し、
他の取引がブロックチェーンに含まれないようにする攻撃。
過去にあった事例です↓
ブロックスタッフィング攻撃では、攻撃者が意図的にブロックのガスリミットを埋めるような一連の取引を提出し、他の取引がブロックチェーンに含まれないようにします。
攻撃者は、自分の取引が確実にマイナーに処理されるように、高い取引手数料を支払うことを選択できます。
攻撃者は、自分のトランザクションが消費するガスの量をコントロールすることで、採掘されてブロックに含まれるトランザクションの数に影響を与えることができます。
イーサリアムでは、ブロックチェーンを安定させ、ブロック生成率を一定に保つために、ブロックごとのガス消費量に上限を設けています。
ブロックマイナーは、自分の利益を最大化するために、提出された取引の中で最も手数料の高い取引を選択して含める傾向があります。
ブロックスタッフィングの主な目的は、マイナーに選ばれる確率が最も高い取引を作成し、その取引を実行することでブロックのガス消費量を削減することです。
ミティゲーションズ
フロントランニング問題の最善の対処法は、
トランザクションの順序や時間の重要性を排除することです。
例えば、市場では、バッチオークションを実装
(これは高頻度取引の懸念からも保護されます)。
また、プレコミット方式を採用する方法もあります。
他には、取引の際に許容できる最大または最小の価格範囲を指定することで、
価格のスリップを制限し、フロントランニングのコストを軽減することです。
タイムスタンプへの依存性
ブロックのタイムスタンプはマイナーによって操作される可能性があります。
タイムスタンプの直接的、間接的な使用には十分注意してください。
整数のオーバーフローとアンダーフロー
残高がuintの最大値(2^256)に達した場合、条件をチェックするゼロに戻ります。
そのため、実装の際にはuint値がそのような大きな数値に近づく機会があるかどうかを考慮しましょう。
管理者だけが変数の状態を変更する権限を持っていれば、安全かもしれません。
※どのユーザでもuint値を更新する関数を呼び出せるのであれば、攻撃を受けやすくなります
同じことがアンダーフローにも言えます。uintを0より小さくすると、アンダーフローが発生し、最大値になってしまいます。
uint8、uint16、uint24...などの小さいデータタイプでは、さらに簡単に最大値になってしまうので注意が必要です。
オーバーフローとアンダーフローのよくある間違いを軽減するための簡単な解決策として、算術関数にSafeMath.solライブラリを使用する方法があります。
日本人の方のブログがありました。
ドット関数で演算する感じです。
予期せぬ復帰を伴うDoS
単純なオークション契約を考えてみましょう。
攻撃者が、支払いを元に戻すフォールバック機能を持つスマートコントラクトを使って入札した場合、
攻撃者はどのオークションにも勝つことができます。
古いリーダーに返金しようとすると、返金が失敗すると元に戻ります。
つまり、悪意のある入札者は、自分のアドレスへの返金が必ず失敗することを確認しながら、
リーダーになることができるのです。
このようにして、他の人がbid()関数を呼び出すのを防ぎ、
永遠にリーダーであり続けることができるのです。
前述のように、代わりにプル型の支払いシステムを設定することをお勧めします。
推奨される解決策は、プッシュ型の支払いよりもプル型の支払いを優先することです。
ブロックガスリミットによるDoS
各ブロックには、消費できるガス量の上限があり、その結果、計算できる量が増えます。(ブロック・ガス・リミット)
使ったガスがこの上限を超えた場合、トランザクションは失敗します。
これにより、いくつかのDenial of Service(サービス拒否)の攻撃が考えられます。
Unbounded OperationsによるコントラクトのガスリミットDoS
全員に一度に支払いを行うと、ブロック・ガス・リミットを超えてしまう危険性があります。
これは、意図的な攻撃がなくても問題になる可能性がありますが、
攻撃者が必要なガスの量を操作できる場合は、特に問題となります。
攻撃者はたくさんのアドレスを追加して、それぞれのアドレスがごくわずかな払い戻しを受ける必要があります。
そのため、攻撃者の各アドレスに返金するためのガス代は、結果的にガソリンの上限を超えてしまい、
返金取引が全く行われなくなってしまう可能性があります。
これが、プッシュ型ではなくプル型の決済を推奨するもう一つの理由です。
どうしても未知のサイズの配列をループさせなければならない場合は、
複数のブロックを必要とする可能性を考慮し、
複数のトランザクションを必要とするように計画する必要があります。
次の例のように、どこまで進んだかを記録し、
その時点から再開できるようにしておく必要があります。
ブロックスタッフィングによるネットワーク上のDoSの制限
コントラクトにunbounded loopが含まれていなくても、
攻撃者は計算量の多いトランザクションを十分に高いガス価格で配置することで、
他のトランザクションがブロックチェーンに含まれるのを数ブロック分防ぐことができます。
これを行うために、攻撃者は、次のブロックが採掘されるとすぐに実行されるような十分に高いガス価格で、
ガスの上限をすべて消費するようなトランザクションをいくつか発行することができます。
どのガス価格もブロックへの組み込みを保証するものではありませんが、
価格が高ければ高いほど可能性は高くなります。
攻撃が成功した場合、他の取引はブロックに含まれません。
攻撃者の目的が、特定の時間以前に特定の契約への取引をブロックすることである場合もあります。
この攻撃は、ギャンブルアプリ「Fomo3D」で行われました。
このアプリは、最後に「キー」を購入したアドレスに報酬を与えるように設計されていました。
攻撃者はキーを購入した後、タイマーが作動して支払いが完了するまで、
13個のブロックを連続して詰めました。攻撃者が送信したトランザクションは、
各ブロックで790万個のガスを消費したため、ガスの制限により、
いくつかの小規模な「送信」トランザクション(1つあたり21,000個のガスを消費する)は許可されましたが、
buyKey()関数(30万個以上のガスを消費する)の呼び出しは一切許可されませんでした。
ブロックスタッフィング攻撃は、一定の時間内にアクションを必要とするあらゆるコントラクトに使用することができます。
しかし、他の攻撃と同様に、期待される報酬がそのコストを上回る場合にのみ、利益を得ることができます。
ガス不足グリーフィング
この攻撃は、一般的なデータを受け取り、それを使って低レベルのaddress.call()関数を介して別のコントラクトを呼び出す(「サブコール」)コントラクトに対して可能な場合があります。
これは、マルチシグネチャやトランザクションリレーのコントラクトによく見られます。
呼び出しが失敗した場合、コントラクトには2つの選択肢があります。
- トランザクション全体を元に戻す
- 実行を継続する
トランザクションのリレーするコントラクトの場合、取引をしたいが自分では実行できない人(例えば、ガソリン代を支払うためのETHがないなど)は、渡したいデータに署名し、署名付きのデータをあらゆる媒体で転送することができます。
そして、第三者である「フォワーダー」が、ユーザーに代わってこの取引をネットワークに送信することができます。
攻撃者はこれを利用してトランザクションを検閲し、少ないガス量で送信することでトランザクションを失敗させることができます。
この攻撃は「グリーフィング」の一種です。
※この攻撃は、トランザクションを失敗させるというものであり、攻撃者には直接利益をもたらしません
この問題に対処する1つの方法は、フォワーダーがサブコールを完了するのに十分なガスを提供することを要求するロジックを実装することです。
このシナリオでマイナーが攻撃を行おうとした場合、require文は失敗し、インナーコールは元に戻ります。
ユーザーは、他のデータと一緒に最小のgasLimitを指定することができます。
また、信頼できるアカウントのみにトランザクションの中継を許可する方法もあります。
contract Relayer {
mapping (bytes => bool) executed;
function relay(bytes _data) public {
// replay protection; do not call the same transaction twice
require(executed[_data] == 0, "Duplicate call");
executed[_data] = true;
innerContract.call(bytes4(keccak256("execute(bytes)")), _data);
}
}
// contract called by Relayer
contract Executor {
function execute(bytes _data, uint _gasLimit) {
require(gasleft() >= _gasLimit);
...
}
}
コントラクトへの強制的なETH送信
フォールバック機能を作動させることなく、強制的にコントラクトにETHを送信することが可能です。
これは、フォールバック機能に重要なロジックを入れたり、コントラクトの残高に基づいて計算したりする際に重要な考慮事項です。
以下の例を見てみましょう。
contract Vulnerable {
function () payable {
revert();
}
function somethingBad() {
require(this.balance > 0);
// Do something bad
}
}
コントラクトのロジックは、コントラクトへの支払いを不許可にすることで、
「何か悪いこと」が起こるのを不許可にするようです。
しかし、コントラクトに強制的にETHを送って、残高をゼロより大きくする方法がいくつか存在します。
コントラクトのself-destructメソッドでは、
ユーザーが余ったエーテルを送る受取人を指定することができます。
self-destructは、コントラクトのフォールバック機能を起動しません。
※コントラクトのアドレスを事前に計算し、コントラクトを展開する前にそのアドレスにETHを送信することも可能です。
バージョンアップによって防がれている歴史的攻撃について
- コールデプスアタック: 1024コールデプスの制限に達する前にすべてのガスが消費されてしまう攻撃。EIP150のハードフォークにおいて対応済み。
- コンスタンティノープル・リエントランシー・アタック: 攻撃者が制御フローをハイジャックし、EIP1283で有効になった残りのガスを使用することで、再帰性による脆弱性が発生します。対策済。
...いろいろ見てきたけど、
オレオレコードを書くと死ぬというのはよくわかった。
あと、トークン出し入れ系のコードを書く場合は、
ベストプラクティスを探してパクろう。
ここから少しペースアップ!!
1-4. リスクを軽減するためのソフトウェア・エンジニアリング
既知の攻撃から身を守るだけでは十分ではありません。ブロックチェーンにおける失敗のコストは非常に高いため、そのリスクを考慮してソフトウェアの書き方を変えていく必要があります。
私たちが提唱するのは、「失敗に備える」というアプローチです。自分のコードが安全かどうかを事前に知ることはできません。しかし、コントラクトが潔く失敗し、被害を最小限に抑えることができるように設計することは可能です。このセクションでは、失敗に備えるためのさまざまなテクニックを紹介します。
※うまく設計されていないフェイルセーフは、それ自体が脆弱性となる可能性があります。また、うまく設計された複数のフェイルセーフの間で相互に作用することもあります。コントラクトで使用するそれぞれの手法について熟考し、それらがどのように連携して堅牢なシステムを構築するかを慎重に検討してください。
壊れたコントラクトのアップグレード
スマートコントラクトの効果的なアップグレードシステムを設計について、2つのアプローチがあります。
1.レジストリ・コントラクトを使ってコントラクトの最新バージョンを保存する
2.DELEGATECALLを使ってデータとコールを転送する
どのような手法であっても、モジュール化とコンポーネント間の適切な分離が重要です。
特に、複雑なロジックをデータストレージから分離して、
機能を変更するためにすべてのデータを再作成する必要がないようにすることが大切です。
また、当事者がコードのアップグレードを決定する際に、安全な方法を用意することも重要です。
契約内容によっては、コードの変更は、信頼できる1人の関係者、メンバーのグループ、または関係者全員の投票によって承認される必要があります。
このプロセスに時間がかかる場合は、緊急停止やサーキットブレーカーなど、
攻撃を受けた際により迅速に対応するための他の方法がないかどうかを検討する必要があります。
いずれにしても、コントラクトをアップグレードする方法を用意しておくことが重要です。
そうしないと、避けられないバグが発見されたときに、コントラクトが使えなくなってしまいます。
アップグレードについてはプロキシでの対策をOpenZeppelinの方でしてくれているので
乗っかとけば問題ないかなー
サーキットブレーカー(コントラクト機能の一時停止)
サーキットブレーカーは、特定の条件が満たされた場合に実行を停止するもので、
新たなエラーが発見された場合に有効です。
例えば、バグが発見された場合、コントラクト内のほとんどのアクションが一時停止され、
現在アクティブなアクションは撤回のみとなる場合があります。
作動条件としては、
①サーキットブレーカーを作動させる権限を特定の信頼できる関係者に与えるか、
②特定の条件が満たされたときに自動的にサーキットブレーカーを作動させる
プログラム上のルールを設定することができます。
bool private stopped = false;
address private owner;
modifier isAdmin() {
require(msg.sender == owner);
_;
}
function toggleContractActive() isAdmin public {
// You can add an additional modifier that restricts stopping a contract to be based on another action, such as a vote of users
stopped = !stopped;
}
modifier stopInEmergency { if (!stopped) _; }
modifier onlyInEmergency { if (stopped) _; }
function deposit() stopInEmergency public {
// some code
}
function withdraw() onlyInEmergency public {
// some code
}
スピードバンプ(契約行為の遅延)
スピードバンプは行動を遅らせることで、悪意のある行動が起きても、回復する時間を確保する。
struct RequestedWithdrawal {
uint amount;
uint time;
}
mapping (address => uint) private balances;
mapping (address => RequestedWithdrawal) private requestedWithdrawals;
uint constant withdrawalWaitPeriod = 28 days; // 4 weeks
function requestWithdrawal() public {
if (balances[msg.sender] > 0) {
uint amountToWithdraw = balances[msg.sender];
balances[msg.sender] = 0; // for simplicity, we withdraw everything;
// presumably, the deposit function prevents new deposits when withdrawals are in progress
requestedWithdrawals[msg.sender] = RequestedWithdrawal({
amount: amountToWithdraw,
time: now
});
}
}
function withdraw() public {
if(
requestedWithdrawals[msg.sender].amount > 0
&& now > requestedWithdrawals[msg.sender].time + withdrawalWaitPeriod
) {
uint amountToWithdraw = requestedWithdrawals[msg.sender].amount;
requestedWithdrawals[msg.sender].amount = 0;
require(msg.sender.send(amountToWithdraw));
}
}
レート制限
レート制限とは、大幅な変更を禁止したり、承認を必要とするものです。
例えば、預金者は一定期間に預金総額の一定額または割合を引き出すことしかできません。
(例:1日の取引上限は最大100ETHなど)
その期間に追加の引き出しを行うと、失敗するか、何らかの特別な承認が必要となります。
あるいは、レートの制限をコントラクトレベルで行い、
一定期間にコントラクトが発行するトークンの量を制限することもできます。
また、初期の段階では、ユーザーごと(または契約全体)にETH流通量を制限することができ、
リスクを軽減することができます。
uint internal period; // how many blocks before limit resets
uint internal limit; // max ether to withdraw per period
uint internal currentPeriodEnd; // block which the current period ends at
uint internal currentPeriodAmount; // amount already withdrawn this period
constructor(uint _period, uint _limit) public {
period = _period;
limit = _limit;
currentPeriodEnd = block.number + period;
}
function withdraw(uint amount) public {
// Update period before proceeding
updatePeriod();
// Prevent overflow
uint totalAmount = currentPeriodAmount + amount;
require(totalAmount >= currentPeriodAmount, 'overflow');
// Disallow withdraws that exceed current rate limit
require(currentPeriodAmount + amount < limit, 'exceeds period limit');
currentPeriodAmount += amount;
msg.sender.transfer(amount);
}
function updatePeriod() internal {
if(currentPeriodEnd < block.number) {
currentPeriodEnd = block.number + period;
currentPeriodAmount = 0;
}
}
コントラクトのロールアウト
コントラクトは、多額の資金をリスクにさらす前に、
実質的かつ長期的なテスト期間を設けるべきである。
最低でも、以下のことが必要です。
- 100%のテストカバレッジ(またはそれに近い)で完全なテストスイートを持っている
- 自分のテストネットでデプロイする
- 充実したテストとバグバウンティを伴うパブリックテストネットでのデプロイ
- 徹底的なテストにより、様々なプレイヤーがコントラクトを大量に使用できるようにする。
- ベータ版のメインネットにデプロイし、リスク量を制限する
自動非推奨化
テスト期間中、一定の時間が経過すると、あらゆるアクションを阻止することで、自動的に廃止させることができます。例えば、アルファ版のコントラクトは数週間動作した後、最終的な出金以外のすべてのアクションを自動的にシャットダウンすることができます。
modifier isActive() {
require(block.number <= SOME_BLOCK_NUMBER);
_;
}
function deposit() public isActive {
// some code
}
function withdraw() public {
// some code
}
バグ・バウンティ・プログラム
バウンティプログラムを運営するためのヒントをご紹介します。
- 報奨金をどの通貨で分配するかを決める(BTCまたはETH)
- バウンティ報酬の推定総予算を決定する
- 予算から、3段階の報酬を決める(配布してもよい最小の報酬, 通常授与可能な最高額の報酬...)
- 非常に深刻な脆弱性の場合に付与される追加の範囲
- 懸賞金の審査員を決定します(通常は3人が理想的)
- 主任開発者が賞金審査員の一人であることが望ましい
- バグレポートを受け取ったら、リード開発者は審査員のアドバイスを受けながら、バグの深刻度を評価します。この段階での作業は、プライベートレポで行い、Githubに課題を提出してください。
- 修正すべきバグであれば、プライベートレポに開発者がテストケースを書き、それが失敗することでバグを確認
- 開発者は修正プログラムを実行し、テストが成功することを確認して、必要に応じて追加のテストを書きます
- 賞金稼ぎに修正プログラムを見せる。修正プログラムを公開レポにマージするのも一つの方法です
- 賞金稼ぎが修正プログラムについて他のフィードバックをしているかどうかを判断す
- 賞金稼ぎの審査員は、バグの可能性と影響の両方を評価した上で、報酬の大きさを決定します
- 賞金稼ぎの参加者には、プロセス全体を通して情報を提供し、報酬の送付が遅れないように努めます
報奨金プログラム、プログラマのスキル磨いて参戦したいけど、
まぁ難しいわなー^^;
1-5. トークンに特化したベストプラクティス
トークンを導入する際には、他のベストプラクティスに準拠する必要がありますが、いくつかのユニークな考慮事項もあります。
一般的に、トークンのスマートコントラクトは、受け入れられている安定した標準に従うべきです。
...このあたりは使用するフレームワークに従おう。
いまのところ、
EIP20: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md
EIP721: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
EIP1155: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1155.md
かな?
1-6. システムを文書化するためのベストプラクティス
多額の資金を必要とする契約や、ミッションクリティカルな要求をされる契約を開始する際には、
適切な文書を添付することが重要です。セキュリティに関連する文書には以下のようなものがあります。
仕様書とロールアウト計画書
- 仕様書、図、ステートマシン、モデルなど、監査人や審査員、コミュニティがシステムの意図するところを理解するのに役立つ文書(仕様書だけで多くのバグを発見することができ、修正にかかるコストが最も少ない)
- 目標期日を含むロールアウト計画
ステータス
- 現在のコードがどこに配置されているか
- コンパイラのバージョン、使用されているフラグ、デプロイされたバイトコードがソースコードと一致していることを確認するための手順
- ロールアウトの各フェーズで使用されるコンパイラのバージョンとフラグ
- デプロイされたコードの現在のステータス(未解決の問題、パフォーマンス統計などを含む)
既知の問題点
- コントラクトに関する主なリスク(例:全財産を失う可能性がある、ハッカーが特定の結果に投票できる)
- すべての既知のバグ/制限事項
- 潜在的な攻撃と緩和策
- 潜在的な利益相反
履歴
- テスト(使用統計、発見されたバグ、テスト期間を含む)
- コードをレビューした人(およびその主なフィードバック)
手順
- バグが発見された場合のアクションプラン(例:緊急時の選択肢、公的な通知プロセスなど)
- 何か問題が発生した場合のウィンドダウン・プロセス(例:攻撃前の残高の何割かを資金提供者が残りの資金から得る)
- 責任ある情報開示の方針 (例: 見つかったバグをどこに報告するか、バグ報奨金プログラムのルールなど)
- 失敗した場合の救済措置(例:保険、ペナルティファンド、救済措置なしなど)
連絡先情報
- 問題を連絡する相手
- プログラマーやその他の重要な関係者の名前
- 質問をすることができるチャットルーム
チーム開発、大変だなぁ。。。
1-7. セキュリティツール
これは開発ツール決めてから周辺を調査しようかなー
1-8. バグ報告報酬エコシステム(バグバウンティ)
リストが掲載されてますー
いろんな団体が個別にも開催してますが、
まとめサイトっぽいのもありますね。
ふぅ、ひとまずその1は終了!
その2. 監査の前の品質チェック - OpenZeppelin
OpenZeppelinは、監査を実施することにより、
お客様の分散システムが意図したとおりに動作するかどうかを検証します。
当社のエンジニアは、お客様のシステムのアーキテクチャとコードベースを完全にレビューし、
発見されたすべての問題に対する実行可能なフィードバックを含む詳細なレポートを作成します。
...これ無料なんかな?
「このレビューに出す前にチェックしてくださいー」という項目が公開されています。
- 自由なソフトウェアライセンスを選択
- 強力で多様なメンテナンスコアチームを作る
- バスファクター1を増やす:知識と責任を共有
- 良いリーダーを選ぶ
- クリーンなコードを書く
- 一貫したコードスタイルを徹底する
- ユニットテストのカバレッジを100%にする
- すべてのプルリクエストにグリーンテストを実施します
- 反復的な開発とテストのプロセスを設計します
- コードを公開する
- 良いREADMEを書く
- 公開APIの機能を文書化する
- プロトコルを文書化
- エンドユーザー向けのドキュメントを書く
- 依存関係が信頼できることを確認
- 既知の問題を確認し、新しい問題に注意
- スマートコントラクト開発のためのコミュニティで検証された標準であるOpenZeppelin Contractsを使う
- あなたのコミュニティを構築、運営する
コード寄りの内容ばかりかと思ったら、
体制づくりについてもかなり言及されてますね。
さいごに
...結局だらだら和訳になってしまった。。
全部は理解できてないけど、
大まかに気をつけるところはわかった!多分!!
これは結構昔の情報なので、
最新のセキュリティ状況もチェックしたいなー
...と思いつつ、最終的にはどこかにセキュリティチェックを委託したほうがよさそう...^^;
でも、最低限の知識は常にもっておきたいですね!!
さて、ドキュメントばかり読み漁ってたので、
程よいチュートリアルで手を動かしたいなぁー
OpenSeaのやろうかな
-
バスファクターとは、知識や能力のある人材が不足してプロジェクトが停滞してしまう前に、プロジェクトから突然消えてしまうチームメンバーの最小数のこと。(https://en.wikipedia.org/wiki/Bus_factor) ↩