コンテンツへスキップ

デプロイ後でも更新可能なスマートコントラクトを実装するためにプロキシパターンを活用する方法

この記事は株式会社 株式会社Gincoのテックブログとして書いています。

この記事では、スマートコントラクトをデプロイ後に更新するために利用されているプロキシパターンの活用方法を紹介します。

この方法を利用することで、スマートコントラクトをデプロイした後のバグ改修や機能改良を行えるようになります。

なぜ、更新可能なスマートコントラクトが必要か?

スマートコントラクトは、ブロックチェーン上に記録されたデータは不変であるという性質の通り、本来デプロイ後に更新を加えることはできません。しかし、それではデプロイ後にバグ改修が必要になったり、機能の改良がしたいという要求に応えたりすることが困難です。

この矛盾を解決し、更新可能なコントラクトを実装する方法として利用されているのが「プロキシパターン」です。

更新可能なスマートコントラクトの構成

「プロキシパターン」とは、すべての操作がプロキシコントラクトを経由し、ロジックコントラクトに転送されるようになっているアーキテクチャです。

基本的なプロキシパターンは、プロキシコントラクトとロジックコントラクトの2種類のコントラクトから構成されます。
プロキシコントラクトは、ユーザーから直接操作を受け付け、ロジックコントラクトにトランザクションを転送するのが役割です。ロジックコントラクトは転送されたトランザクションの処理を実行します。この時トランザクションによって操作されるデータの格納先はプロキシコントラクトです。

更新可能なコントラクトを実現するためには、プロキシコントラクトの向き先のロジックコントラクトを入れ替えられようにします。すなわち、プロキシコントラクトの向き先を新しいロジックコントラクトに変更することで、コントラクトは更新可能になります。

flowchart LR ユーザー --> |トランザクション|プロキシコントラクト プロキシコントラクト --> ロジックコントラクトv1 プロキシコントラクト --> ロジックコントラクトv2 プロキシコントラクト --> ロジックコントラクトv3

更新可能なスマートコントラクトの実装として、openzeppelinのProxy Upgrade Patternがあります。

コントラクトの更新方法

サンプルコード

solidity-by-example.orgのサンプルコードをお借りして、更新可能なコントラクトをデプロイしてみます。

今回は、ロジックコントラクトとしてCounterV1CounterV2、プロキシコントラクトとしてBuggyProxyのコードを使用します。

ロジックコントラクトv1のデプロイ

ロジックコントラクトv1として以下のCounterV1を用意します。count変数とinc関数を持つコントラクトです。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract CounterV1 {
    uint public count;

    function inc() external {
        count += 1;
    }
}

truffleを使用してデプロイします。※truffleの説明はこの記事では割愛します。

const CounterV1 = artifacts.require("./CounterV1");

module.exports = async (deployer) => {
  deployer.then(async () => {
    await deployer.deploy(CounterV1);
  });
};
$ truffle migrate --network testnet

デプロイしたコントラクトアドレスは、後ほどプロキシコントラクトの向き先の設定で使用するために控えておきます。truffleのコンソールやデプロイ先のブロックチェーンexplorerで確認することができます。

プロキシコントラクトのデプロイ

プロキシコントラクトとして以下を用意します。

  • implementation.delegatecallで呼び出しをロジックコントラクトに委譲して実行します。プロキシコントラクトに格納されたデータをロジックコントラクトの実装によって変更します。
  • upgradeToで向き先のロジックコントラクトを差し替えます。
  • fallbackは、プロキシコントラクトの他のどの関数とも一致しない呼び出しの場合に実行される関数です。プロキシコントラクトでは、fallbackdelegatecallを呼び出します。
  • receiveは、プロキシコントラクトの他のどの関数とも一致せず、かつ、calldataが空の呼び出しの場合に実行される関数です。プロキシコントラクトでは、receivedelegatecallを呼び出します。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract BuggyProxy {
    address public implementation;
    address public admin;

    constructor() {
        admin = msg.sender;
    }

    function _delegate() private {
        (bool ok, bytes memory res) = implementation.delegatecall(msg.data);
        require(ok, "delegatecall failed");
    }

    fallback() external payable {
        _delegate();
    }

    receive() external payable {
        _delegate();
    }

    function upgradeTo(address _implementation) external {
        require(msg.sender == admin, "not authorized");
        implementation = _implementation;
    }
}

truffleを使用してデプロイします。

const BuggyProxy = artifacts.require("./BuggyProxy");

module.exports = async (deployer) => {
  deployer.then(async () => {
    await deployer.deploy(BuggyProxy);
  });
};
$ truffle migrate --network testnet

プロキシコントラクトの向き先の設定

truffleを使用してプロキシコントラクトを操作します。
プロキシコントラクトのupgradeToをcallして、ロジックコントラクトv1を向き先として登録します。この操作権限はadmin(ここではプロキシコントラクトをデプロイしたアカウント)に限定されています。
これで、プロキシコントラクトはCounterV1の実装を呼び出せるようになります。

truffle console --network testnet
> let instance = await BuggyProxy.deployed()
> instance.upgradeTo("デプロイしたロジックコントラクトv1のアドレス")

ロジックコントラクトv2のデプロイ

ロジックコントラクトv2として以下のCounterV2を用意します。ロジックコントラクトv1にdec()関数を追加したコントラクトです。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract CounterV2 {
    uint public count;

    function inc() external {
        count += 1;
    }

    function dec() external {
        count -= 1;
    }
}

プロキシコントラクトの向き先を更新

truffleを使用してプロキシコントラクトを操作します。
upgradeTo()を使用して、implementationをロジックコントラクトv2で更新します。
これで、プロキシコントラクトはCounterV2に処理を委譲できるようになります。

truffle console --network testnet
> let instance = await BuggyProxy.deployed()
> instance.upgradeTo("デプロイしたロジックコントラクトv2のアドレス")

補足: プロキシコントラクトへのトランザクション送信

実際にプロキシコントラクトから、ロジックコントラクトの実装を使用する方法です。

truffle console --network testnet
> let accounts = await web3.eth.getAccounts()
> let instance = await BuggyProxy.deployed()
> instance.sendTransaction({from: accounts[0], value: 0, data: "0x371303c0"})
  • dataの値は、メソッド名、引数値をABI Encodeした値。
  • 今回は、inc (引数なし)をABI Encodeした 0x371303c0 を渡しています。
  • ABI Encodeできるサイト: https://abi.hashex.org/

まとめ

この記事では更新可能なスマートコントラクトを説明し、サンプルコードを用いて実際にコントラクトを更新する方法を試してみました。次はこの記事の続きとして、TransparentUpgradeableProxyを用いたコントラクトをデプロイしてみたいと思います。

株式会社 Ginco ではブロックチェーンを学びたい方、ウォレットについて詳しくなりたい方を募集していますので下記リンクから是非ご応募ください。

https://herp.careers/v1/ginco

参照