イーサリアム スマート コントラクトのセキュリティ研究と分析

イーサリアム スマート コントラクトのセキュリティ研究と分析

Ethereum 開発が初めての場合は、この記事を読む前に、Ethereum スマート コントラクトのヒッチハイク ガイドを読むことをお勧めします。

Ethereum スマート コントラクトのセキュリティを学ぶのは非常に難しい作業です。 ConsenSys のスマート コントラクトのベスト プラクティスや Solidity のドキュメントのセキュリティに関する考慮事項など、優れたガイドやコンパイルはごくわずかです。しかし、実際にコードを書かないと、これらの概念を覚えて理解するのは困難です。

少し違ったアプローチを試してみたいと思います。スマート コントラクトのセキュリティを向上させるための推奨戦略をいくつか説明し、コード例をいくつか示します。また、スマート コントラクトを保護するために使用できるコード サンプルもいくつか紹介します。これらが、将来実際のコードを書く際の精神的な警告として、物事の一種の筋肉記憶を作成するのに役立つことを願っています。

では、早速ベストプラクティスを見ていきましょう。

コードはすぐに失敗し、失敗について大声で語る

シンプルだが強力な優れたプログラミング方法は、コードをできるだけ早く失敗させることです。そして、声に出して話しましょう。控えめに動作する関数の例を見てみましょう。

 // 安全でないコードなので使用しないでください。
契約 BadFailEarly { uint 定数 DEFAULT_SALARY = 50000;マッピング(文字列 => uint) nameToSalary;
  関数 getSalary(文字列名) 定数は (uint) を返します {
    if (bytes(name).length != 0 && nameToSalary[name] != 0) { return nameToSalary[name];
    } そうでない場合は、DEFAULT_SALARY を返します。
    }
  }
}

契約が失敗したり、不安定な状態や一貫性のない状態で実行され続けることを静かに回避したいと考えています。 getSalary 関数が、保存された給与を返す前に条件をチェックするのは良いことです。問題は、これらの条件が満たされない場合にデフォルト値が返されることです。これにより、呼び出し元からエラーが隠される可能性があります。これは極端な例ですが、このようなプログラミングは非常に一般的です。通常は間違いを犯すことへの恐れですが、この恐れがアプリを台無しにします。実際のところ、失敗が早ければ早いほど、問題を見つけやすくなります。エラーを隠すと、エラーがコードの他の部分に伝播し、追跡が困難な不整合が発生する可能性があります。より正しい方法は次のとおりです。

コントラクト GoodFailEarly { マッピング(文字列 => uint) nameToSalary;関数 getSalary(文字列名) 定数は (uint) を返します {
    bytes(name).length == 0 の場合、 throw;
        nameToSalary[name] == 0の場合、throw; nameToSalary[名前]を返します。
  }
}

このバージョンは、前提条件を分離して各障害を分離するという、別の理想的なプログラミング パターンも示しています。これらのチェックの一部 (特に内部状態に依存するもの) は、関数ハンドラを使用して実装できることに注意してください。

プッシュ決済よりもプル決済をサポート

Ether の転送ごとに、コードが実行される可能性が生じます。受信アドレスは、エラーをスローする可能性のあるフォールバック関数を実装できます。したがって、送信呼び出しがエラーなしで実行されることを決して信頼してはなりません。解決策: 契約ではプッシュ支払いよりもプル支払いをサポートする必要があります。この一見無害そうな入札機能コードを見てみましょう。

 // 安全でないコードなので使用しないでください。
契約 BadPushPayments { アドレス fastestBidder; uint 最高入札額;関数入札() {
    msg.value < 最高入札額の場合、 throw;
    最高入札者 != 0 の場合 {
      // 前回の落札者に入札を返す
      最高入札者の場合 (!最高入札者.send(最高入札額)) {
        投げる;
      }
    }
    最高入札者 = msg.sender;
    最高入札額 = msg.value;
  }
}

コントラクトが send 関数を呼び出してその戻り値をチェックしていることに注意してください。これは妥当なようです。しかし、関数の途中で send を呼び出すので、安全ではありません。なぜ?上で述べたように、送信によって別のコントラクトのコード実行がトリガーされる可能性があることに注意してください。

誰かがアドレスから入札し、誰かがそのアドレスに資金を送信するたびにエラーが発生すると想像してください。誰かがこれより高い金額で入札しようとしたらどうなりますか?送信呼び出しは常に失敗し、入札が異常になります。エラーで完了した関数呼び出しでは、状態は変更されません (変更はすべてロールバックされます)。つまり、誰も入札できず、契約は失敗します。

最も簡単な解決策は、支払いを別の関数に分離し、ユーザーが契約ロジックの残りの部分とは独立して資金を要求(引き出し)できるようにすることです。

契約 GoodPullPayments { アドレス fastestBidder; uint 最高入札額;マッピング(アドレス => uint) 払い戻し;関数 bid() 外部 {
    msg.value < 最高入札額の場合、 throw;

    最高入札者 != 0 の場合 {
      払い戻し[最高入札者] += 最高入札;
    }

    最高入札者 = msg.sender;
    最高入札額 = msg.value;
  } 関数 withdrawBid() 外部 {
    uint 払い戻し = 払い戻し[msg.sender];
    払い戻し[msg.sender] = 0;
    if (!msg.sender.send(払い戻し)) {
      払い戻し[msg.sender] = 払い戻し;
    }
  }
}

今回は、マッピングを使用して各最高入札者の返金額を保存し、資金を引き出す機能を提供します。送信呼び出しに問題が発生した場合、影響を受けるのは入札者のみです。これは、他の多くの問題 (再入可能性など) を解決する単純なパターンです。したがって、覚えておいてください: イーサを送信するときは、プッシュ支払いよりもプル支払いを優先してください。

このパターンを簡単に使用できるコントラクトを実装しました。使用方法を示す例を以下に示します。

関数コードの整理: 条件、アクション、相互作用

フェイルファスト原則の延長として、関数を次のように配置することをお勧めします。まず、すべての前提条件を確認します。次に、契約の状態を変更します。そして最後に、他の契約とやり取りします。

条件、アクション、相互作用。この機能構造に従えば、多くの問題を回避できます。このパターンを使用した関数の例を見てみましょう。

 function auctionEnd() { // 1. 条件
  (現在 <= オークション開始 + 入札時間)
    投げる; // オークションはまだ終了していません
  (終了)の場合
    投げる; // この関数はすでに呼び出されています

  // 2. 効果
  終了 = true;
  オークション終了(最高入札者、最高入札者); // 3. インタラクション
  if (!受益者.send(最高入札額))
    投げる;
  }
}

最初に条件がチェックされるため、これは fail-fast の原則と一致しています。また、他の契約との潜在的に危険なやり取りも終了します。

プラットフォームの限界を理解する

Ethereum 仮想マシン (EVM) には、契約で実行できる内容に関して多くの厳しい制限があります。これらはプラットフォーム レベルのセキュリティ上の懸念事項ですが、これに気付いていないと、特定の契約のセキュリティが危険にさらされる可能性があります。一見正しいように見える次の従業員ボーナス管理コードを見てみましょう。

 // 安全でないコードなので使用しないでください。
契約BadArrayUse { address[] employees;関数payBonus() {
    (var i = 0; i < employees.length; i++) {
      アドレス employee = employees[i];
      uint ボーナス = calculateBonus(従業員);
      従業員.送信(ボーナス);
    }
       } 関数 calculateBonus(address employee) は (uint) を返します {
    // 高価な計算がいくつか...
  }
}

コードを読んでみると、非常にわかりやすく、正しいようです。ただし、プラットフォームの制限に基づく 3 つの潜在的な問題が隠れています。

最初の問題は、値 0 を保持するために必要な最小の型である uint8 型になることです。配列に 255 を超える要素がある場合、ガスがなくなるまで関数ループは終了しません。予期せぬ事態がなく、制限がより高い、明示的に型指定されたユニットを使用する方が適切です。可能であれば、var を使用して変数を宣言することは避けてください。これを修正しましょう:

 // まだ安全でないコードなので、使用しないでください。
契約BadArrayUse { address[] employees;関数payBonus() {
    (uint i = 0; i < employees.length; i++) {
      アドレス employee = employees[i];
      uint ボーナス = calculateBonus(従業員);
      従業員.送信(ボーナス);
    }
       } 関数 calculateBonus(address employee) は (uint) を返します {
    // 高価な計算がいくつか...
  }
}

2 番目に考慮すべきことはガス制限です。 Gas は、ネットワーク リソースに対して課金するための Ethereum のメカニズムです。状態を変更するすべての関数呼び出しはガスを消費します。 calculateBonus が、多くのプロジェクトの利益を計算するのと同じように、いくつかの複雑な計算に基づいて各従業員のボーナスを計算すると想像してください。これにより大量のガスが消費され、トランザクションまたはブロックのガス制限に簡単に達する可能性があります。トランザクションがガス制限に達した場合、すべての変更は元に戻りますが、手数料は引き続き支払われます。ループを使用する場合は、変動するガスコストに注意してください。ボーナス計算を for ループから分離して、契約を最適化しましょう。ただし、従業員が増えるにつれてガスコストが増加するため、これは依然として問題となることに注意してください。

 // 安全でないコードなので使用しないでください。
契約BadArrayUse { address[] employees;マッピング(アドレス => uint) ボーナス;

    関数payBonus() {
    (uint i = 0; i < employees.length; i++) {
      アドレス employee = employees[i];
      uint ボーナス = ボーナス[従業員];
      従業員.送信(ボーナス);
    }
       } 関数 calculateBonus(address employee) は (uint) を返します {
    uint ボーナス = 0;
    // ボーナスを変更する高価な計算... bonuses[employee] = bonus;
  }
}

最後に、コールスタックの深さの制限です。 EVM コール スタックには 1024 のハード制限があります。つまり、ネストされたコールの数が 1024 に達すると、コントラクトは失敗します。攻撃者は、コントラクトを 1023 回再帰的に呼び出してから、コントラクトの関数を呼び出すことができ、この制限により送信が暗黙的に失敗します。 PullPaymentCapable.sol は上記で説明しましたが、これを使用するとプル支払いを簡単に実装できます。 PullPaymentCapable を継承し、asyncSend を使用すると、この問題から保護されます。

これらすべての問題に対処するコードの修正バージョンを次に示します。

 './PullPaymentCapable.sol' をインポートします。
契約 GoodArrayUse は PullPaymentCapable です {
  住所[] 従業員;マッピング(アドレス => uint) ボーナス;関数payBonus() {
    (uint i = 0; i < employees.length; i++) {
      アドレス employee = employees[i];
      uint ボーナス = ボーナス[従業員]; asyncSend(従業員、ボーナス);
    }
  }
  関数 calculateBonus(address employee) は (uint) を返します {
    uint ボーナス = 0;
    // 高価な計算がいくつか...
    ボーナス[従業員] = ボーナス;
  }
}

まとめると、(1) 使用する型の制限、(2) コントラクトのガスコストの制限、(3) コールスタックの深さの制限を覚えておくことが重要です。

テストを書く

テストを書くのは大変な作業ですが、回帰の問題を回避することができます。回帰エラーは、以前は正しかったコンポーネントが最近の変更によって壊れた場合に発生します。

近々、テストに関するより詳細なガイドを書く予定ですが、興味があれば、Truffle のテスト ガイドを確認してください。

フォールトトレランスと自動バグ報奨金

両方のアイデアのきっかけを与えてくれた Peter Borah に感謝します。コードレビューとセキュリティ監査だけではセキュリティは十分ではありません。私たちのコードは最悪のシナリオに備える必要があります。スマート コントラクトには脆弱性があり、安全に回復する方法があるはずです。それだけでなく、これらの脆弱性をできるだけ早く検出するように努めるべきです。契約に自動バグ報奨金制度を組み込むと役立ちます。

仮想トークン コントラクトにおける、この単純な自動バグ報奨金の実装を見てみましょう。

 './PullPaymentCapable.sol' をインポートします。'./Token.sol' をインポートします。
契約のバウンティは PullPaymentCapable { bool public claimed; です。マッピング(アドレス => アドレス) パブリック研究者;

  関数() {
    (主張された)場合、投げる;
  関数createTarget()は(トークン)を返します{
    トークンターゲット = 新しいトークン(0);
    研究者[ターゲット] = msg.sender;ターゲットを返します。
  } 関数クレーム(トークンターゲット) {
    アドレス researcher = 研究者[ターゲット];
    if (研究者 == 0) throw;

    // トークン契約の不変条件をチェックする
    target.totalSupply() が target.balance の場合、 throw;
    }
    asyncSend(研究者、this.balance);
    主張 = 真実;
  }
}

これまでと同様に、支払いを安全に保つために PullPaymentCapable を使用します。このバウンティ契約により、研究者は監査したいトークン契約のコピーを作成できます。誰でも、バウンティ契約アドレスにトランザクションを送信することで、バグバウンティに貢献できます。研究者がトークン契約のコピーを破損した場合(この場合のように、トークンの総供給量をトークン残高と異なるものにした場合)、その研究者には報奨金が支払われます。報奨金が請求されると、契約はそれ以上の資金を受け入れなくなります (名前のない関数は契約のフォールバック関数と呼ばれ、契約に資金が直接送信されるたびに実行されます)。

ご覧のとおり、これは別の契約であり、元のトークン契約を変更する必要がないという優れた特性があります。 GitHub で誰でも利用できる完全な実装がこちらにあります。

フォールトトレランスに関しては、元の契約を修正し、追加の安全メカニズムを追加する必要があります。簡単なアイデアとしては、契約管理者に緊急時の手段として契約を凍結させることです。継承を通じてこの動作を実装する 1 つの方法を見てみましょう。

契約停止可能{アドレスパブリックキュレーター; bool 公開が停止しました。
  修飾子 stopInEmergency { if (!stopped) _ } 修飾子 onlyInEmergency { if (stopped) _ } 関数 Stoppable(address _curator) {
    _curator == 0 の場合は throw;
    キュレーター = _curator;
  } 関数emergencyStop() 外部 {
    msg.sender != curator の場合、 throw;
    停止 = true;
  }
}

Stoppable を使用すると、契約を停止できる管理者のアドレスを指定できます。 「契約停止」とはどういう意味ですか?これは、関数修飾子 stopInEmergency および onlyInEmergency を使用して Stoppabl から継承されたサブコントラクトによって定義されます。

例を見てみましょう:

 './PullPaymentCapable.sol' をインポートします。'./Stoppable.sol' をインポートします。
契約 StoppableBid は Stoppable、PullPaymentCapable { アドレス public fastestBidder; uint パブリック最高入札額;関数 StoppableBid(アドレス _curator)
    止められる(_curator)
    PullPaymentCapable() {} 関数 bid() 外部 stopInEmergency {
    msg.value <= 最高入札額の場合、 throw;

    最高入札者 != 0 の場合 {
      最高入札者、最高入札者を非同期に送信します。
    }
    最高入札者 = msg.sender;
    最高入札額 = msg.value;
  } 関数withdraw() 緊急時のみ {
    自殺(キュレーター)
  }
}

この例では、契約の作成時に定義されたキュレーターによって入札を停止できるようになりました。 StoppableBid は通常モードであり、入札機能のみを呼び出すことができます。何か異常が発生し、契約が矛盾した状態になった場合、マネージャーが介入して緊急状態をアクティブにすることができます。これにより、入札関数が呼び出されなくなり、撤回関数が実行されるようになります。

この場合、緊急モードでは管理者は契約を破棄して資金を回収するだけです。しかし、実際のケースでは、回復ロジックはより複雑になる可能性があります (資金を所有者に返還するなど)。こちらは GitHub で誰でも利用できる実装です。

入金金額を制限する

スマート コントラクトを攻撃から保護するもう 1 つの方法は、その範囲を制限することです。攻撃者は、数百万ドルを管理する契約を標的にする可能性が最も高くなります。すべてのスマート コントラクトがそれほどリスクが高い必要はありません。特に実験を実行している場合はそうです。この場合、契約で受け入れる資金の額を制限すると役立つ場合があります。これは簡単で、契約アドレスの残高にハード制限を追加するだけです。

これを行う方法の簡単な例を次に示します。

契約LimitFunds { uint LIMIT = 5000;関数() { スロー; } 関数deposit() {
    if (this.balance > LIMIT) は throw;
    ...
  }
}

この短いフォールバック関数は、契約への直接支払いを拒否します。契約残高が必要な限度額を超えた場合、または例外が発生した場合は、まず入金機能がチェックされます。動的制限や管理制限などのより興味深い機能も簡単に実装できます。

シンプルなモジュールコードを書く

安全性は、私たちの意図とコードが実際に実行できる操作が一致することから生まれます。特にコードが巨大で乱雑な場合、これを検証するのは非常に困難です。これが、シンプルなモジュールコードを記述することが重要である理由です。

つまり、関数はできる限り短く、コード スニペットは最小限に抑え、ファイルはできる限り小さくし、独立したロジックはモジュールに分割し、各モジュールに単一の責任を持たせる必要があります。

名前を付けることは、コーディング時に意図を表現する最良の方法の 1 つでもあります。コードをできるだけ明確にするために、選択する名前を慎重に検討してください。

不適切な命名の例を見てみましょう。 The DAO の関数を見てみましょう。関数コードは非常に長いので、ここではコピーしません。

最大の問題は、長すぎて複雑すぎることです。関数はできるだけ短くし、最大 30 ~ 40 行のコードにするようにしてください。理想的には、1 分以内に関数を読み取って、その機能を理解できる必要があります。もう 1 つの問題は、685 行目の Transfer イベントの名前が適切でないことです。この名前は、transfer という関数と 1 文字だけ異なります。これは皆を混乱させるでしょう。一般的に、イベントの名前は「Log」で始まることが推奨されます。この場合、LogTransfer という名前の方が適切でしょう。

スマート コントラクトは、できるだけシンプルかつモジュール化され、適切な名前が付けられるようにしてください。これは、他の人や自分自身がコードをレビューするのに非常に役立ちます。

すべてのコードをゼロから書かないでください

最後に、古いことわざにあるように、「パスワードを転送しないでください」。スマートコントラクトコードにも当てはまると思います。お金を扱い、コードとデータは公開され、新しい実験的なプラットフォームで実行されているため、リスクは高くなります。どこでも混乱が起こる可能性があります。

これらのプラクティスは、スマート コントラクトを保護するのに役立ちます。しかし最終的には、スマート コントラクトを構築するためのより優れた開発ツールを作成する必要があります。ここには、より優れた型システム、Serenity Abstractions、Rootstock プラットフォームなど、興味深い取り組みがいくつかあります。

すでに適切に記述された安全なコードが多数存在し、フレームワークが登場し始めていました。私たちは、OpenZeppelin と呼ばれるこの GitHub リポジトリでいくつかのベストプラクティスをまとめ始めました。ぜひご覧いただき、新しいコードやセキュリティ監査に貢献してください。

要約する

要約すると、この記事で説明したセキュリティ モデルは次のとおりです。

  1. コードはすぐに失敗し、失敗について大声で語る

  2. プッシュ決済よりもプル決済をサポート

  3. 関数コードの整理: 条件、アクション、相互作用

  4. プラットフォームの限界を理解する

  5. テストを書く

  6. フォールトトレランスと自動バグ報奨金

  7. 入金金額を制限する

  8. シンプルなモジュールコードを書く

  9. すべてのコードをゼロから書かないでください

安全なスマート コントラクト開発パターンに関するディスカッションに参加したい場合は、Slack にご参加ください。一緒にスマートコントラクトの発展に取り組みましょう!

スマート コントラクト セキュリティに関する当社の取り組みの最新情報を入手するには、Medium と Twitter をフォローしてください。


<<:  ダークネット市場がモネロをサポート、暗号通貨コミュニティの注目を集める

>>:  KYC-Chainは、2016年アジア太平洋フィンテックイノベーションラボの最終候補に選ばれた唯一のブロックチェーン企業です。

推薦する

オーストラリアのデータセンター運営会社が初の太陽光発電ビットコイン鉱山を建設へ

CCNによると、オーストラリアのデータセンター運営会社とその暗号通貨子会社は、再生可能エネルギーで稼...

分析レポート:北京初の「ビットコイン採掘事件」判決

北京初のビットコイン「マイニング」契約訴訟の第二審判決が発表された。第二審裁判所である北京市第三中級...

S9 Hydro 水冷マイナー ユーザーガイド

1. 材料の準備 1.ハードウェア: Antminer S9 Hydro ベアメタル、マイニング ...

CZ 公開書簡: 2022 年は回復力が鍵

データから判断すると、 2022年は暗号通貨業界にとって浮き沈みの激しい年でした。 Binanceと...

2019 年の鉱業業界について大物たちは何と言っているでしょうか?

最近、BHBポンジースキームが崩壊し、1994年生まれのトレーダーはオフィスに閉じ込められた。彼は投...

Huobi BETHには20,000以上のETHが担保されており、ロックされたETHの年間収益は117.42%にも達します。

12月1日午後8時、Ethereum 2.0メインネットが正式に開始され、活気に満ちた光景がもたら...

dynv6 無料セカンダリドメイン名登録および DNS 動的解決サービス利用ガイド

dynv6 は、世界中のユーザーに高品質の動的ドメイン名解決 (DDNS) サービスを提供することに...

ビットコインは2015年の最大の勝者の1つだ

金価格は今年10%近く下落し、米国の主要株価指数はほぼ横ばい、エネルギー商品はすべて30%以上下落し...

「流動性マイニング」に追いつけない?次の DeFi がどこにあるかを研究してみませんか?

この2週間で、「暗号通貨の世界における1日は、現実世界の1年と同じである」ということが改めて証明され...

全国人民代表大会代表の呉暁玲氏:中央銀行はビットコイン取引プラットフォームの監督に責任を持つべきだ

3月9日、全国人民代表大会の代表で全国人民代表大会財政経済委員会副委員長の呉暁玲氏は、第一金融日報の...

ブロックチェーンコードの脆弱性を狙うハッカーたちは、オンラインの「富」を狙っている

記者 張嘉興たった1行のコードで64億元が消えた。この驚くべきハッキング作戦は今年4月に発生しました...

519の暴落はパニック後の最高のチャンス

著者 |ハシピ分析チーム...

Mediachain: 最大の公開メディアメタデータデータベースを構築したい

私たちは著作権に対する意識が非常に弱い時代に生きています。インターネット上の著作権侵害は最も深刻で、...