1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【UUPS Proxy Pattern】OpenZeppelin + Hardhatでアップグレード可能なスマートコントラクトを作成する

Last updated at Posted at 2024-02-06

本記事では、Proxy Patternとよばれるスマートコントラクトをアップグレード可能にする実装をHardhatで行います。

背景

「スマートコントラクトのアップグレード方法が存在する」こと自体は知っていたのですが、肝心の「スマートコントラクトのアップグレードの具体的な方法」についてこれまで私は理解していませんでした。
最近ようやくその具体的な方法であるProxy Patternという概念を知ったので、より理解を深めるために、実装を体験してみたいと思った次第です。

Proxy Patternってなに?

スマートコントラクトをアップグレード可能にするための方式です。
実装的には、スマートコントラクトを以下の2つに分割します。

  • ロジックが記述されたlogic contract
  • ユーザーの代わりにlogic contractを呼び出すProxy Contract

ロジックの追加・修正などのアップグレードを行いたいときは、新たなlogic contractをデプロイし、proxy contractから新たなlogic contractを呼び出せるように設定します。
一度デプロイされた同一のスマートコントラクトに変更を加えることはできませんが、logic contractを入れ替えることで、proxy contractとやり取りするユーザー目線ではスマートコントラクトがアップグレードされたように見えるということだと理解しています。

詳細については以下の記事がおすすめです。Proxy Patternの概要から分類、実装まで記載されています。
日本語記事も案外たくさんあったので、情報収集はしやすい分野なのかもと思いました(尤も最終的に信頼すべきはOpenZeppelin等の公式ドキュメントですが)。

動作環境

  • Node.js 20.11.0
  • Hardhat 2.19.4
  • Solidity 0.8.20

Hardhatプロジェクトの作成

適当なフォルダを作成し、Hardhatプロジェクトを起動します。

npx hardhat run

以下のコマンドで、必要なライブラリをインストールします。

$ npm install @openzeppelin/contracts @openzeppelin/contracts-upgradeable @openzeppelin/hardhat-upgrades 

詳細な方法は、こちらの記事の「Hardhatプロジェクトの構築」をご覧ください。

logic contractの作成

今回はERC20トークンのコントラクトを作成し、イベントの内容をアップデートしてみます。

まずはアップグレード前のコントラクト(今回はNewTokenV1.jsとしています)を作成します。

NewTokenV1.sol
pragma solidity ^0.8.20;

import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

contract NewTokenV1 is Initializable, ERC20Upgradeable, OwnableUpgradeable, UUPSUpgradeable{

    // コンストラクタが使えないため、代わりにinitialize関数で初期化を行う
    function initialize(address initialOwner) initializer public {
        __ERC20_init("NewToken", "NEW");
        __Ownable_init(initialOwner);
        __UUPSUpgradeable_init();
    }
    function mint(address _to, uint256 _amount) public onlyOwner {
        _mint(_to, _amount);
        emit MintEvent(_to,_amount);
    }

    function _authorizeUpgrade(address newImplementation) internal onlyOwner override
    {}

    event MintEvent(address indexed to,uint256 indexed amount);    
}

次に、アップグレード後のコントラクト(今回はNewTokenV2.jsとしています)を作成します。
変更点は、mint()実行時に発行されるイベントmintEventのみです。
文字列の引数をひとつ追加しています。

NewTokenV2.sol
pragma solidity ^0.8.20;

import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

contract NewTokenV2 is Initializable, ERC20Upgradeable, OwnableUpgradeable, UUPSUpgradeable{

    // コンストラクタが使えないため、代わりにinitialize関数で初期化を行う
    function initialize(address initialOwner) initializer public {
        __ERC20_init("NewToken", "NEW");
        __Ownable_init(initialOwner);
        __UUPSUpgradeable_init();
    }
    function mint(address _to, uint256 _amount) public onlyOwner {
        _mint(_to, _amount);
        emit MintEvent(_to,_amount,"mint finished");
    }

    function _authorizeUpgrade(address newImplementation) internal onlyOwner override
    {}
    // mintEventに文字列の引数discriptionを追加
    event MintEvent(address indexed to,uint256 indexed amount,string discription);    
}

作成したコントラクトをコンパイルします。

npx hardhat compile

コンパイルが完了すると、artifactsフォルダ、cacheフォルダが生成されます。

コントラクトのテスト

今回はテストコードを用いて、Proxy Patternによるアップグレードの動作を確認します。
以下のテストコード(今回はNewTokenProxyTestとしています)を作成します。

NewTokenProxyTest.sol
const {loadFixture} = require("@nomicfoundation/hardhat-toolbox/network-helpers");
const { expect } = require("chai");
const { ethers,upgrades } = require("hardhat");

describe("Fixture", function () {

    async function deployFaucetFixture() {
        // アカウントを取得
        const [owner,other] = await ethers.getSigners();
        // NewTokenV1,NewTokenV2それぞれのコントラクトを取得
        const NewTokenV1 = await ethers.getContractFactory("NewTokenV1");
        const NewTokenV2 = await ethers.getContractFactory("NewTokenV2");
        return { NewTokenV1,NewTokenV2,owner};
    }

    // アップグレード前の実装コントラクト(NewTokenV1)に関するテスト
    describe("NewTokenV1", function () {
        it("Should emit 'mintEvent' with 2 args", async function () {
            const { NewTokenV1, owner } = await loadFixture(deployFaucetFixture);
            // NewTokenV1をプロキシ経由でデプロイ
            const newTokenV1Instance = await upgrades.deployProxy(NewTokenV1,[owner.address]);

            // mint()実行時に、イベントMintEvent(引数2つ)が発行されることを確認
            const mintValue = 100;
            await expect(newTokenV1Instance.mint(owner.address,mintValue)).to.emit(newTokenV1Instance,"MintEvent").withArgs(owner.address,100) ;
        })
    })
    // アップグレード後の実装コントラクト(NewTokenV2)に関するテスト
    describe("NewTokenV2", function () {
        it("upgrade newTokenV1 -> newTokenV2", async function () {
            const {NewTokenV1, NewTokenV2,owner }= await loadFixture(deployFaucetFixture);
            // NewTokenV1をプロキシ経由でデプロイ
            const newTokenV1Instance = await upgrades.deployProxy(NewTokenV1,[owner.address]);
            // logic contractをNewTokenV1 → NewTokenV2に更新
            const newTokenV2Instance = await upgrades.upgradeProxy(await newTokenV1Instance.getAddress(),NewTokenV2)
            const mintValue = 100;
            // mint()実行時に、イベントMintEvent(引数3つ)が発行されることを確認
            await expect(newTokenV2Instance.mint(owner.address,mintValue)).to.emit(newTokenV2Instance,"MintEvent").withArgs(owner.address,100,"mint finished") ;
        })
    }) 
});

テストを実行します。

npx hardhat test
  Fixture     
    NewTokenV1
      ✔ Should emit 'mintEvent' with 2 args (2102ms)
    NewTokenV2
      ✔ upgrade newTokenV1 -> newTokenV2 (148ms)


  2 passing (2s)

テストが正常終了したことを確認できました。
これで、NewTokenV1 → NewTokenV2への移行によってMintEventの処理がしっかり変更されていることを確認できました。

以上で作業は終了です。

所感

Hardhatのようなフレームワークを使う上では、コントラクトのアップグレード自体はもとから機能として用意されているので、デプロイに使う関数をちょっと変えるだけ(deploy() → deployProxy())ですぐに実現できたのは非常にありがたいなと感じました。
とはいえ、その裏側で何が起きているかを知っておいて損はないので、良い経験になりました。

また、「アップグレード可能なコントラクトコードにはいろいろと縛りがある」ということを学べたのも大きかったです。変数の定義順を変えてはいけない(ストレージに格納されたデータとの整合性の問題)とかコンストラクタが使用できないとか、けっこう融通が利かないんだなと・・・
今回はイベントをひとつアップグレードしただけなのでそこまで意識していませんでしたが、もっと大規模なアップグレードをかける場合は、徹底的にチェックするべきですね。

参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?