Solidity コントラクトの更新(update/upgrade) Proxy Contract


はじめに

「ブロックチェーンのサービス提供した後、バグを直せないと怖い。。。」
「コントラクトを更新したいけどどうすればいいの!?」
「コントラクトってそもそも更新できるんだろうか?」
「Proxy Contractって聞いたことあるけど何?」

などと思ったことは一度はあるのではないでしょうか?

Solidityでは、コントラクトをデプロイした後、通常は更新できません。機能を追加したコントラクトを再度デプロイすると別のアドレスになってしまいます。

Proxy Contractというコントラクトを使うことで、アップデート/アップグレードすることができます。

zeppelin OSの下記の記事を参考にしています。
https://blog.zeppelinos.org/proxy-patterns/

Proxy Patterns

Proxy ContractとLogic Contractに分けられます。
メッセージ呼び出しがProxy Contractを通過して、それらが最新のデプロイされたLogic Contractにリダイレクトさされます。アップグレードするには、新しいバージョンのコントラクトを展開し、新しいコントラクトアドレスを参照するようにProxy Contractを更新します。

Proxy Contract

Proxy Contractは delegetecall関数を実行します。

ユーザはProxy Contractへトランザクションを送信し、delegetecallを実行することで更新可能なコントラクトを実行します。このProxyコントラクト自体は更新しません。そのため、コントラクトを更新しても、同じアドレスに対してトランザクションを送信することになります。

pragma solidity ^0.5.0;

/**
 * @title Proxy
 */
contract Proxy {
  address public implementation;

  /**
  * @dev ロジックコントラクトのアドレスアドレスの設定
  */
  function upgradeTo(address _address) public {
    implementation = _address;
  }

  /**
  * @dev Fallback関数 ロジックコントラクトのdelegatecall()を実行する
  */
  function () payable external {
    // ロジックコントラクトのアドレス
    address _impl = implementation;
    require(_impl != address(0));

    assembly {
      // 0x40は次に利用可能な空きメモリポインタを格納する
      let ptr := mload(0x40)
      // ptrのメモリ領域に、calldataの0からcalldatasize分をコピーする
      calldatacopy(ptr, 0, calldatasize)
      /*
       gas:          関数を実行実行するために必要なgas
       _impl:        ロジックコントラクトのアドレス
       ptr:          データが始まる場所のメモリポインタ
       calldatasize: 渡すデータサイズ
       outdata:      delegatecallが実行された後に返却されるデータで使用されません
       outsize:      delegatecallが実行された後に返却されるデータのサイズで使用されません
      */
      let result := delegatecall(gas, _impl, ptr, calldatasize, 0, 0)
      // 返されたデータのサイズ
      let size := returndatasize
      // 空きメモリポインタに0からreturndata分をコピーする
      returndatacopy(ptr, 0, size)

      // 結果を返却する
      switch result
      // 0の場合:エラーのためrevert
      case 0 { revert(ptr, size) }
      // それ以外の場合:データを返す
      default { return(ptr, size) }
    }
  }
}

更新してみよう

TokenV0.sol / TokenV1.solのコントラクトを使って、更新できることを確認してみましょう。
Remixを使用して確認できます。

pragma solidity ^0.5.0;

/**
 * @title Token Version 0
 */
contract TokenV0 {
    mapping (address => uint) balances;

    function balanceOf(address _address) public view returns(uint) {
        return balances[_address];
    }

    function setBalance(address to, uint256 _balance) public {
        balances[to] = _balance;
    }
}
pragma solidity ^0.5.0;

import "./TokenV0.sol";

/**
 * @title Token Version 1
 */
contract TokenV1 is TokenV0 {

    function addBalance(address to, uint256 _balance) public {
        balances[to] += _balance;
    }
}

1. Proxyをdeployします。

2. TokenV0をdeployして、ProxyのupgradeTo()を実行します。

3. Proxyのコントラクトアドレスを、At Addressへ入力して、TokenV0を読み込みます。

4. setBalance()を実行します。

残高を100に設定することができました。

4. TokenV1をdeployします。

5. ProxyのupgradeTo()を、TokenV1のコントラクトアドレスを入力して実行します。

TokenV1へ更新することができました。

6. Proxyのコントラクトアドレスを、At Addressへ入力して、TokenV1を読み込みます。

TokenV0からTokenV1へ更新した後も、残高は100になります。

7. TokenV1のaddBalance()を実行します。

50を追加して、残高を150にすることができました。

まとめ

  • Proxy Contractを使うことで、コントラクトをアップグレードすることができました。
  • storageはProxy Contractへ保存されます。

※Storageは Eternal Storageというkey-value方式で記載することもできます。