2020.01.07

バージョンアップ可能なコントラクトの開発

Pocket

こんにちは。次世代システム研究室のT.M. です。

はじめに

Ethereum のスマートコントラクトはブロックチェーンのために基本的にはバージョンアップすることができません。しかし、現実的にはコントラクトに存在するバグを修正したり、機能追加するために、バージョンアップが必要です。そこで、Proxy コントラクトを利用した、既存のコントラクトのロジックを差し替えてバージョンアップさせる方法が広く知られています。そして、バージョンアップ可能なコントラクトを容易に開発・管理するツールとして、openzeppelin-sdk (旧ZeppelinOS) が用意されています。

openzeppelin-sdk を利用したバージョンアップ可能なコントラクトを開発するために、openzeppelinコマンドが用意されています。openzeppelinコマンドを利用すれば容易に開発可能ですが、コントラクトという外部にソースコードを公開し、また、直接的に価値のあるものを扱うことの多いものをopenzeppelinコマンドのブラックボックスとするのは、不安が大きいです。本稿では、openzeppelinコマンドを利用せず、その内部で行われているコントラクトのバージョンアップの仕組みを理解し、開発するコントラクトへ導入する方法を説明します。

Proxy コントラクト

Proxy コントラクトとは、ロジックコントラクトのproxy となるコントラクトです。コントラクトのバージョンアップの仕組みの根幹はProxy コントラクトにあります。その基本的な仕組みについては、以前本ブログで解説をしましたので、そちらをお読みください。

BaseUpgradeabilityProxy コントラクト

Proxy コントラクトは、あくまでロジックコントラクトのproxy であり、バージョンアップのインターフェイスはありません。バージョンアップをするための関数は、BaseUpgradeabilityProxy で実装をされており、このコントラクトを継承し、バージョンアップの関数を呼び出せるようにすることで、バージョンアップ可能なコントラクトを実装することができます。

_upgradeTo(address) で次のバージョンのロジックコントラクトのアドレスを変更することができ、バージョンアップとなります。BaseUpgradeabilityProxy を継承したコントラクトは_upgradeTo(address) を呼び出すupgradeTo(address) を実装する必要があります。

BaseAdminUpgradeabilityProxy コントラクト

BaseUpgradeabilityProxy コントラクトでは、コントラクトのバージョンアップをすることができましたが、コントラクトをバージョンアップする権限の委譲をすることができません。BaseAdminUpgradeabilityProxy コントラクトでは、コントラクトをバージョンアップすることのできるアドレスであるadmin を変更することができます。

changeAdmin(address) でadmin を変更することができます。また、admin() で現在のadmin を確認することができます。ただし、普通に呼び出してしまうと、関数呼び出しをproxy してしまうので、呼び出すためのcallObject のfrom をadmin にしないといけません。つまり、admin を知らない人が調べるためにはadmin() を呼び出すことができず、admin を知っている人がadmin の値が正しいかを確認するためにしか、admin() は利用できません。ロジックコントラクトのアドレスを確認する関数として、implementation() があります。これもadmin と同様にcallObject のfrom をadmin にしないといけません。
  modifier ifAdmin() {
    if (msg.sender == _admin()) {
      _;
    } else {
      _fallback();
    }
  }

  function admin() external ifAdmin returns (address) {
    return _admin();
  }

  function implementation() external ifAdmin returns (address) {
    return _implementation();
  }
このコントラクトでは、BaseUpgradeabilityProxy コントラクトとは違い、upgradeTo(address)changeAdmin(address) などの関数が実装されており、BaseAdminUpgradeability コントラクトを継承するだけで、バージョンアップ可能となります。ただし、admin が設定されていないと、implementation() を実行できないので、constructor でadmin の初期値を設定する実装をする必要があります。

AdminUpgradeabilityProxy コントラクト

BaseUpgradeabilityProxy コントラクトでは、constructor が実装されておらず、admin やロジックコントラクトのアドレスが設定されていませんでした。AdminUpgradeabilityProxy コントラクトでは、constructor が実装されており、引数にロジックコントラクトのアドレス、admin およびロジックコントラクトで実行する命令をパラメータとして、コントラクトをデプロイすることができます。

Proxy コントラクトはロジックコントラクトとは別の状態を持っています。そのため、ロジックコントラクトのcostructor で初期値を設定されていても、Proxy コントラクトには反映されません。そこで、初期値を設定するために、ロジックコントラクトでは、initialize() 関数を用意して、Proxy コントラクトのデプロイ後にProxy コントラクト経由で実行する必要があります。ただし、Proxy コントラクトのデプロイトランザクションとinitialize() のトランザクションが別トランザクションでは、その間に予期せぬinitialize() 実行がされてしまう可能性があります。それを防ぐために、Proxy コントラクトのconsturctor でロジックコントラクトのinitialize() を実行します。そのために、AdminUpgradeabilityProxy コントラクトでは、constructor 引数にinitialize() の実行命令があります。

initialize() の実行命令はencode されている必要があります。require('web3-eth-abi').encodeFunctionCall(initializeAbi, [param1, param2]) でencode をすることができます。

Initializable コントラクト

バージョンアップ可能なコントラクトを開発するために、Proxy コントラクトとして、AdminUpgradeabilityProxy コントラクトを継承すればよいです。一方、ロジックコントラクトには、initialize() を実装する必要があります。そのために、Initializable コントラクトを継承して、ロジックを実装すればよいです。Initializable コントラクトにはinitializer というmodifier が用意されており、initialize()initializer を付けることで、1度しか実行できない、という制約を付けることができます。
  modifier initializer() {
    require(initializing || isConstructor() || !initialized, "Contract instance has already been initialized");

    bool isTopLevelCall = !initializing;
    if (isTopLevelCall) {
      initializing = true;
      initialized = true;
    }

    _;

    if (isTopLevelCall) {
      initializing = false;
    }
  }

おわりに

openzeppelin-sdk におけるコントラクトのバージョンアップの仕組みを解説しました。本稿を理解することで、バージョンアップ可能なコントラクトを実装できるようになります。今後もopenzeppelin-sdk の情報を追いかけたいと思います。

次世代システム研究室では、グループ全体のインテグレーションを支援してくれるアーキテクトを募集しています。インフラ設計、構築経験者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ募集職種一覧からご応募をお願いします。

皆さんのご応募をお待ちしています。