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

本記事では、**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等の公式ドキュメントですが)。

https://note.com/standenglish/n/ne6b09a489f5c#a0f431f2-1611-4952-8b37-1f37d89c6358

https://note.com/turingum/n/n74318c04f424

# 動作環境
- 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プロジェクトの構築」](https://qiita.com/somk_2301/items/1d36522698e56b1cf22c#hardhat%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E6%A7%8B%E7%AF%89)をご覧ください。
# 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())ですぐに実現できたのは非常にありがたいなと感じました。
とはいえ、その裏側で何が起きているかを知っておいて損はないので、良い経験になりました。

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

# 参考

https://docs.openzeppelin.com/upgrades-plugins/1.x/hardhat-upgrades

https://forum.openzeppelin.com/t/uups-proxies-tutorial-solidity-javascript/7786

https://note.com/turingum/n/n74318c04f424#3d2ccc5d-5dfb-47a5-a2af-150f9ddf08c5

https://qiita.com/biga816/items/b4f745e67588e8da7ed1

https://note.com/standenglish/n/ne6b09a489f5c#a0f431f2-1611-4952-8b37-1f37d89c6358

https://chaldene.net/upgradable-contracts#st-toc-h-27