Smart Contract Solidity ブロックチェーン

スマートコントラクトをアップグレードできるUpgradable Contractsを1からわかりやすく解説

かるでね

こんにちは!CryptoGamesというブロックチェーンゲーム企業でエンジニアをしているかるでねです!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。

このブログ以外でも情報発信しているので、よければ他の記事も見ていってください。

今回はスマートコントラクトをアップグレードできる「Upgradable Contracts」について取り上げていきます!

スマートコントラクトは変更できない」とよく言われているため、「スマートコントラクトをアップグレードするって何?」と思う方もいると思います。

この記事では、まず「Upgradable Contracts」について、どんなものか、どのように実現するのか、メリット・デメリット、注意点などを説明していきます。

記事の後半では実際に「Upgradable Contracts」を実装していくので、ぜひ手を動かしてみてください!

それでは早速「Upgradable Contracts」についてみていきましょう!

Upgradable Contractsとは?

概要

Upgradable Contracts」とは、文字通り「アップグレード可能なコントラクト」のことです。

「スマートコントラクトって変更できないんじゃ…

こう思った方は鋭いです!

スマートコントラクトは一度デプロイされると変更することはできません。

しかし、新しいスマートコントラクトを作成し、使用しているスマートコントラクトを古いスマートコントラクトから差し替えた場合ではどうでしょうか?

スマートコントラクト自体は変更されておらず、単純に使用しているスマートコントラクトを差し替えているだけです。

この説明だけでは理解が難しいと思うので、これからしっかり解説していきます!

なぜUpgradableにするのか?

先ほども説明したように、スマートコントラクトは一度デプロイしたコードは変更できません。

変更できないことにより信頼性が高まりますが、その反面バグの修正や機能の追加をしたくても変更を加えることができません。

これはこれで不便です。

なぜなら、致命的な脆弱性があってハッカーに資金を取られてしまう危険性があっても、対処することができなくなるからです。

そのためスマートコントラクトを使用しているプロジェクトでは、アップデート可能なスマートコントラクトになっているものも少なくありません。

Upgradableにする方法

では、どのようにしてスマートコントラクトをUpgradableにしているのでしょうか?

答えは「Proxy Contract」というものを使用することで、アップデート可能なスマートコントラクトを実現しています。

Proxy Contract」なんて初めて聞いた人の方が多いはずです。

プロキシ(Proxy)についてWikipediaで調べると以下のように書かれています。

プロキシproxy; [ˈprɒksɪ])とは「代理」の意味である。インターネット 関連で用いられる場合は、特に内部ネットワークからインターネット接続を行う際、高速なアクセスや安全な通信などを確保するための中継サーバ「プロキシサーバ」を指す。プロキシサーバはインターネットへのアクセスを代理で行うサーバを指す。

https://ja.wikipedia.org/wiki/プロキシ

中継」とあるように、呼び出し元と実際に処理が実行されるスマートコントラクト(Logicコントラクト)の間に位置し、やり取りを中継することが役割のスマートコントラクトです。

以下のように図にすることと理解しやすいです。

まずはProxyコントラクトを呼び出して、呼び出したProxyコントラクトからさらにContractAコントラクトを呼び出しています。

Proxyコントラクトの呼び出し先を変更することで、以下のようにContractBコントラクトを呼び出すことができるようになります。

つまり、ContractAContractB自体をいじることなく、Proxyコントラクトの内部で保持している呼び出し先のコントラクトの情報を随時更新するだけで良いのです。

ここで「スマートコントラクトは変更できないから、呼び出し先のコントラクト情報は変更できないのでは?」という疑問を持つ方もいると思います。

これは単純で、変数として呼び出し先のコントラクト情報を持っていて、その変数の値を変えることができる関数を用意します。

そしてこの関数を実行できるのは、Proxyコントラクトをデプロイしたユーザーのみにしておけば良いです。

また、Proxyコントラクトからメインのコントラクトを呼び出すときは、delegatecallを使用します。

delegatecallは簡単にいうと、「呼び出したコントラクトでコードが実行される」呼び出し方法です。

つまりProxyコントラクトからLogicコントラクトを呼び出した際に、Proxyコントラクト内のデータを使用しながら処理を実行できるということになります。

より詳しくdelegatecallについて知るには以下の記事を参考にしてください。

Upgradableにする5つの方法

先ほどはコントラクトをUpgradableにする方法を確認しました。

ここではコントラクトをUpgradableにする5つの方法について紹介していきます!

方法としては以下の5つがあります。

Upgradableにする5つの方法

  • 古いコントラクトと新しいコントラクトを作成して、古いコントラクトから新しいコントラクトにデータなどを移行する。
  • データを保存するコントラクトとLogicコントラクトを作成して、2つのコントラクトに分離する。
  • Proxyパターンを使用して、ProxyコントラクトからdelegatecallLogicコントラクトを呼び出す。
  • メインのコントラクトが特定の機能を実行するための複数コントラクトのアドレスを保持し、簡単に実装を切り替えることができる。
  • Diamond patternを使用して、機能ごとに複数のコントラクトを作成する。

では1つずつ簡単に確認していきましょう。

2つのコントラクトを作成してデータを移行

古いコントラクトがデプロイされている状態で、新しいコントラクトをデプロイします。

その後、古いコントラクト内のデータや残高などを新しいコントラクトに送ります。

ここまで行った後最後にやらなければいけないことが、古いコントラクトを使用していたユーザーに新しいコントラクトを使用するように説得することです。

新しいコントラクトにはデータや残高がしっかり移行できていることを見せて、古いコントラクトに間違って資金などを送らないようにする必要があります。

この方法の課題点としては、新しいコントラクトをデプロイするたびにデータや残高の意向に時間がかかり、ガス代が高くなる可能性がある点です。

2つのコントラクトに分離

データを保持するコントラクト(ここではDataコントラクトとします)とLogicコントラクトの2つを作成する方法です。

Dataコントラクトには、ユーザーの残高やアドレスなどの状態を保持し、デプロイ時にLogicコントラクトのアドレスも保持することで、不正なコントラクトがDataコントラクトを呼び出したり、データを変更することを防ぎます。

ユーザーはデータを保持するDataコントラクトとのみやり取りをし、データはDataコントラクトに保存されます。

LogicコントラクトはDataコントラクトのアドレスを保持し、Dataコントラクトとやり取りしてデータを取得・設定します。

Dataコントラクトは変更できず、Logicコントラクトのアドレスを新しいコントラクトのアドレスにすることで、呼び出すLogicコントラクトを変更することができます。

複数のコントラクトの管理と、悪意のあるコントラクトからのアクセスを防ぐために、複雑な認証などを実装する必要があります。

Proxy Pattern

先ほどまで解説してきたProxyコントラクトを使用する方法です。

ここでも、データを保持するコントラクト(ここではProxyコントラクトとします)とLogicコントラクトの2つを作成します。

Proxy Patternでは以下のような特徴があります。

Proxy Pattern

  • ユーザーはProxyコントラクトのみやり取りをする。
  • ProxyコントラクトにはLogicコントラクトのアドレスを保持し、delegatecallを使用してLogicコントラクトを呼び出す。
  • Logicコントラクトはdelegatecallで呼び出されるため、データは全てProxyコントラクトに保持され、そのデータをユーザーに返す。

先ほども説明しましたが、より詳しくdelegatecallについて知るには以下の記事を参考にしてください。

Solidityにはfallback関数と呼ばれる、存在しない関数の呼び出しをされたときに呼ばれる関数が存在し、Proxyコントラクトではこのfallback関数を実行する必要があります。

なぜなら、ProxyコントラクトにはLogicコントラクト内に存在する関数が存在しないため、ユーザーがLogicコントラクト内の関数を指定して実行してもProxyコントラクトがエラーを起こしてしまうためです。

このfallback関数が呼ばれた時にどのような処理をするかというと、delegatecallで現在使用しているLogicコントラクトを呼び出します。

Proxyコントラクトが呼び出すLogicコントラクトのアドレスを変更することで、呼び出されるLogicコントラクトを変更することができます。

Proxy Patternはここまでの2つの方法よりも複雑で、脆弱性をつかれる可能性もあるためしっかり理解しておく必要があります。

Strategy Pattern

メインのコントラクトと特定の機能を実行する複数のコントラクト(Satelliteコントラクト)が存在する構成です。

メインのコントラクトでは、データと各Satelliteコントラクトのinterfaceとアドレスを保持しています。

新しいSatelliteコントラクトを実装し、そのアドレスをメインのコントラクトに渡すことで簡単に機能を追加することができます。

Proxy Patternと似ていますが、ユーザーが操作するメインのコントラクトがデータを保持している点が異なります。

Strategy Patternのメリットとしては、コア部分に影響を与えることなく限定的な変更を加えることができる点です。

しかし、メインのコントラクトがハッキングされると使用できなくなってしまう欠点もあります。

Diamond pattern

Proxy Patternの改良版です。

処理を実行するコントラクトが1つではなく、機能ごとに複数のコントラクト(facets)を作成します。

Proxyコントラクトでは、関数セレクタごとにfacetsアドレスの対応付マッピングを作成します。

ユーザーが関数を呼び出すと、Proxyコントラクトはマッピングを確認し、その関数の実行を担当するfacetsを探し出し、delegatecallで呼び出します。

Diamond patternのメリットとしては以下になります。

小さな変更でアップグレードできる

全てのコードを変更することなく、機能の一部分を変更することでコントラクトをアップグレードできます。

Proxy Patternだと小さなアップグレードでも、新しいコントラクトを作成する必要がありました。

コントラクトの24KB制限に対応できる

コントラクトのサイズには24KBという制限があります。

多くの機能を1つのコントラクトに実装する際はこの制限に引っかかってしまいます。

しかし、Diamond patternであれば機能を複数のコントラクトで分割することができるため、サイズ制限に対応することができます。

特定の機能ごとにアクセス権を制御できる

Proxty Patternでは処理を実行するコントラクト全体でしかアクセス制御ができず、アクセス権を持っているとコントラクト全体を変更できていました。

しかし、Diamond patternではコントラクトの機能ごとにアクセス制御ができます。

Upgradabaleのメリット・デメリット

次にコントラクトのアップグレードのメリットとデメリットを確認していきます。

メリット

メリット

  • 脆弱性を簡単に修正できる。
  • 新機能の追加実装ができる。
  • 開発者はさまざまな機能を試すことができ、時間をかけて改善ができる。

デメリット

デメリット

  • コードの不変性を否定し、分散化とセキュリティに影響を与える。
  • 開発者がコントラクトを恣意的に変更しないことを信頼する必要がある。
  • コードが複雑になり脆弱性につながる。
  • 後で変更できるからと、準備が完璧でない状態でプロジェクトをローンチしてしまう可能性がある。
  • アクセス制御を適切に行わないことや中央集権かにより、悪意あるユーザーが不正なアップグレードを行う危険性がある。

Upgradabaleの注意点

この章の最後として、コントラクトのアップグレードの注意点について確認していきます。

適切なアクセス制御

アクセス制御や認証を適切に設定し、不正を行うようなコントラクトからのアップグレードを防止する。

コントラクトの所有者だけがコントラクトの更新を行えるようにする」、などの制御を必ず行うことが重要。

セキュリティを高める

コントラクトのアップグレードは複雑な作業のため、脆弱性が出現する危険性があります。

そのため、セキュリティの監査をお願いしたり、Bug Bountyで脆弱性がないか確認してもらうなどのセキュリティを高める必要があります。

Bug Bountyについては以下の記事を参考にしてください。

アップグレードの実行手順の工夫

コントラクトをアップグレードできることは、ユーザーからするとアップグレードを実施できる人を信用する必要があります。

そのためマルチシグウォレット(複数のウォレットによる認証)やDAO内での投票などの工夫をすることが良いとされます。

コストの計算

コントラクトのアップグレードにはコストがかかります。

具体的には、古いコントラクトから新しいコントラクトへデータや残高を移行する際、複数トランザクションが発生するためガス代が高くなります。

ガス代をしっかり計算してどれくらいのコストがかかるか把握しておくことは重要です。

タイムロックの導入

タイムロックとは、システムに変更が適用されるとき一定時間特定のアクションを実行できないようにすることです。

コントラクトのアップグレード中に特定の処理が実行できてしまうと、予期しない処理が実行され最悪の場合資金を失うことにもつながります。

あらかじめ設定された期間他のユーザーが処理を実行できないようにすることで、予期しない処理の実行を防ぐことができます。

constructorは使えない

Proxyコントラクトを使用してdelegatecallLogicコントラクトを呼び出しているとき、Logicコントラクトではconstructorを使用することができます。

正しくはconstructorの処理が呼び出されません。

なぜならLogicコントラクトはデプロイ済みのコントラクトをdelegatecallで呼び出すだけだからです。

そのためOpenzeppelinが提供しているInitializable.solのようなコントラクトを使用して、一度だけ呼び出される関数を用意する必要があります。

変数定義を揃える

ProxyコントラクトとLogicコントラクトの変数定義を同じ型を同じ順序に宣言する必要があります。

理由としては、ストレージの衝突と呼ばれる問題が起きる可能性があるためです。

ストレージの衝突とは、変数の格納位置が異なるために起きる問題です。

スマートコントラクトでのストレージデータについてには以下の記事が参考になります。

ProxyコントラクトからdelegatecallLogicコントラクトを呼び出すと、Proxyコントラクトのストレージデータを使用するため、Logicコントラクトの変数の型や順序が異なると予期しない処理をしてしまう可能性があります。

詳しく解説すると長くなるので、以下の記事を参考にしてください。

Proxy Pattern

前章では「Upgradable Contracts」について説明してきました。

この章ではProxyコントラクトについてもう少し詳しく取り上げていきます。

Proxyパターンとは、Logicコントラクトとユーザーとの間にProxyコントラクトが存在する構成のいくつかのパターンのことを指します。

前章の図のように、ユーザーがProxyコントラクにアクセスし、ProxyコンラクトからLogicコントラクトを呼び出します。

そして、Proxyコントラクトが呼び出すメインのコントラクトを別のコントラクトに変更することで、全体で見るとUpgradableなコントラクトとみなせます。

Proxyパターンには以下の3種類が存在します。

Proxy Pattern

  • Transparent Proxy
  • UUPS Proxy
  • Beacon Proxy

この章では上記3種類のProxyパターンについて解説していきます。

Transparent Proxy

ProxyコントラクトからLogicコントラクトをdelegatecallで呼び出します。

delegatecallについては以下の記事を参考にしてください。

そしてProxyコントラクトがdelegatecallで呼び出すコントラクトを変更する際は、管理者のみが実行できるProxyAdminコントラクトを実行する。

これらをまとめると以下の図のようになります。(緑線はdelegatecall呼び出しです)

ユーザーはProxyコントラクトのみ実行することができ、Proxyコントラクトはあらかじめ指定されたコントラクトをdelegatecallで呼び出すことができます。

管理者はProxyAdminコントラクトを呼び出して、Proxyコントラクトがdelegatecallで呼び出すコントラクトを変更することができます。

また、ProxyAdminコントラクトは管理者を変更したり、管理者権限を変更することもできます。

UUPS Proxy

ほとんど先ほどのTransparent Proxyと同じですが、ProxyAdminコントラクトの位置のみ異なります。

Transparent Proxyでは、管理者のみがProxyAdminコントラクトを実行できましたが、UUPS ProxyではProxyコントラクトからProxyAdminコントラクトを呼び出すことができてしまいます。そのため、適切に権限制御を行わないと誰でもProxyコントラクトがdelegatecallで呼び出すコントラクトを変更することができてしまいます。

また、ProxyコントラクトからProxyAdminコントラクトを呼び出すということは、ProxyAdminコントラクトの呼び出し部分を削除することもできてしまいます。

これらをまとめると以下の図のようになります。(緑線はdelegatecall呼び出しです)

Beacon Proxy

Transparent ProxyUUPS Proxyと異なり、複数のコントラクトをUpgradableにすることができます。

メインのコントラクトを複数でデプロイされる時に使用されます。

1つのProxyコントラクトに対して、1つの処理がLogicコントラクトが紐づくイメージです。

その中継をBeacon Proxyが補います。

これらをまとめると以下の図のようになります。(緑線はdelegatecall呼び出しです)

この図からもわかるように、1回のトランザクションで複数のProxyコントラクトの実行が可能です。

また、Proxy Adminコントラクトの役割をBeacon Proxyコントラクトが行うことができるのもポイントです。

Proxy Patternの実装

では早速Proxy Patternの実装をしていきましょう!

コードは以下に置いてあります。

セットアップ

まずは全ての実装において必要な最低限の実装をしていきましょう。

Node.jsとnpmのインストール

Node.jsとnpmをインストールしていない人は、以下の記事を参考にインストールしてください。

ディレクトリの作成

作業するディレクトリを作成していきましょう。

$ mkdir proxy-pattern
$ cd proxy-pattern

モジュールのインストール

では次に必要なモジュールのインストールをしていきます。

$ npm i @openzeppelin/contracts @openzeppelin/contracts-upgradeable @openzeppelin/hardhat-upgrades chai dotenv hardhat hardhat-gas-reporter

Hardhat

ではSolidityの開発環境であるHardhatの環境を作成していきましょう。

簡単にテストなどもできるので、ぜひ使ったことない人は使ってみてください!

$ npx hardhat

888    888                      888 888               888
888    888                      888 888               888
888    888                      888 888               888
8888888888  8888b.  888d888 .d88888 88888b.   8888b.  888888
888    888     "88b 888P"  d88" 888 888 "88b     "88b 888
888    888 .d888888 888    888  888 888  888 .d888888 888
888    888 888  888 888    Y88b 888 888  888 888  888 Y88b.
888    888 "Y888888 888     "Y88888 888  888 "Y888888  "Y888

👷 Welcome to Hardhat v2.12.5 👷‍

? What do you want to do? … 
❯ Create a JavaScript project
  Create a TypeScript project
  Create an empty hardhat.config.js
  Quit

上記を実行するとこのように色々出てくるので、全てEnterを押してください。

Give Hardhat a star on Github if you're enjoying it! 💞✨

     https://github.com/NomicFoundation/hardhat

Please take a moment to complete the 2022 Solidity Survey: https://hardhat.org/solidity-survey-2022

上記のように出力されればHardhatの環境を作成できました。

.
├── README.md
├── contracts
│   └── Lock.sol
├── hardhat.config.js
├── node_modules
├── package-lock.json
├── package.json
├── scripts
│   └── deploy.js
└── test
    └── Lock.js

上記のようになっていれば完璧です。

hardhat.config.js

hardhat.config.jsファイルを開いてください。

require("@nomicfoundation/hardhat-toolbox");
require('@openzeppelin/hardhat-upgrades');

require("dotenv").config();
require("hardhat-gas-reporter")

module.exports = {
  solidity: {
    version: "0.8.17",
    settings: {
      optimizer: {
        enabled: true,
        runs: 200,
      },
    },
  },
  networks: {
    hardhat: {
      blockGasLimit: 30_000_000,
    },
  },
  gasReporter: {
    enabled: true,
    currency: 'USD',
    gasPrice: 21
  }
};

上記を貼り付けて保存してください。

これでセットアップは終わりです。

Transparent Proxy

では、Transparent Proxyの実装をしていきましょう!

コントラクト作成

contracts/TransparentLogic.solcontracts/TransparentLogicV2.solというファイルを作成しましょう。

$ touch contracts/TransparentLogic.sol
$ touch contracts/TransparentLogicV2.sol
TransparentLogic.sol

ではTransparentLogic.solファイルを開いて以下を貼り付けてください。

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

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

contract TransparentLogic is Initializable {
    address public owner;
    string public name;
    uint256 public age;

    /// @notice 一度だけ実行される関数。constructorの代わり。
    function initialize() public initializer {
        owner = msg.sender;
    }

    /// @notice プロフィール情報を更新する関数。
    /// @param _name 名前。
    /// @param _age 年齢。
    function setProfile(string memory _name, uint256 _age) external {
        name = _name;
        age = _age;
    }

    /// @notice プロフィール情報を取得する関数。
    /// @return name 名前。
    /// @return age 年齢。
    function getProfile() external view returns(string memory, uint256){
        return (name, age);
    }

}

ownernameageという3つの変数を定義しています。

一度だけ実行されるinitialize関数を定義し、実行したユーザーをowner変数に格納しています。

setProfile関数では、name変数とage変数に引数で受け取った値を格納しています。

getProfile関数では、name変数とage変数の値を取得しています。

TransparentLogicV2.sol

次に更新ようのコントラクトである、TransparentLogicV2.solファイルを開いて以下を貼り付けてください。

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

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

contract TransparentLogicV2 is Initializable {
    address public owner;
    string public name;
    uint256 public age;
    string public favoriteFruit;


    /// @notice 一度だけ実行される関数。constructorの代わり。
    function initialize() public initializer {
        owner = msg.sender;
    }

    /// @notice プロフィール情報を更新する関数。
    /// @param _name 名前。
    /// @param _fruit 好きな果物。
    /// @param _age 年齢。
    function setProfile(
        string memory _name,
        string memory _fruit,
        uint256 _age
    ) external {
        name = _name;
        favoriteFruit = _fruit;
        age = _age;
    }

    /// @notice プロフィール情報を取得する関数。
    /// @return name 名前。
    /// @return favoriteFruit 好きな果物。
    /// @return age 年齢。
    function getProfile() external view returns(
        string memory, string memory, uint256
    ){
        return (name, favoriteFruit, age);
    }

}

新たにfavoriteFruitという変数が追加されているのが確認できます。

これでコントラクトの作成は完了です。

テスト作成

コントラクトが作成できたので、次にテスト用のファイルを作成していきましょう。

$ touch test/test-transparent.js

では作成したtest-transparent.jsを開いて以下を貼り付けてください。

const { expect } = require("chai");
const { ethers, upgrades } = require("hardhat");

describe("TransparentLogic", function () {
    beforeEach(async function () {
        // アドレスの作成
        [owner, wallet1, wallet2] = await ethers.getSigners();

        // TransparentLogicとTransparentLogicV2コントラクトを取得。
        transparentLogic = await ethers.getContractFactory("TransparentLogic", owner);
        transparentLogicV2 = await ethers.getContractFactory("TransparentLogicV2", owner);

        // TransparentLogicコントラクトをデプロイ。
        transparentLogicInstance = await upgrades.deployProxy(transparentLogic);
    });

    describe('test TransparentLogic', function () {
        it("Owner変数の値を確認", async function () {
            expect((await transparentLogicInstance.owner())).to.equal(owner.address);
        })

        it("setProfile関数を実行して、name: Cardene, age: 23を格納", async function () {
            await transparentLogicInstance.setProfile("Cardene", 23);
            expect((await transparentLogicInstance.name())).to.equal("Cardene");
            expect((await transparentLogicInstance.age())).to.equal(23);

        })

        it("transparentLogicV2コントラクトに更新", async function () {
            const transparentLogicV2Instance = await upgrades.upgradeProxy(
                transparentLogicInstance.address,
                transparentLogicV2
            );
        })

        it("setProfile関数を実行して、name: Cardene V2, favoriteFruit: Pear, age: 30を格納", async function () {
            await transparentLogicV2Instance.setProfile("Cardene V2", "Pear", 30);
            expect((await transparentLogicV2Instance.name())).to.equal("Cardene V2");
            expect((await transparentLogicV2Instance.age())).to.equal(30);
            expect((await transparentLogicV2Instance.favoriteFruit())).to.equal("Pear");
        })
    });
});

簡単に手順をまとめると以下になります。

手順

  • TransparentLogicコントラクトをデプロイ。
  • TransparentLogicコントラクト内のowner変数の値を確認。
  • TransparentLogicコントラクト内のsetProfile関数を実行して、name変数とage変数の値が変更されているか確認。
  • TransparentLogicV2コントラクトに差し替え。
  • TransparentLogicV2コントラクトのsetProfile関数を実行して、name変数とfavoriteFruit変数、age変数の値が変更されているか確認。

deployProxyを使ってデプロイすることで、のちにコントラクトをアップグレードすることができる。

そしてupgradeProxyを実行することで、実際にコントラクトをアップグレードしています。

テストの実行

ではテストを実行していきましょう。

以下のコマンドを実行してください。

$ npx hardhat compile
$ npx hardhat test

実行して以下のように出力されていればテスト成功です!

Compiled 2 Solidity files successfully

  TransparentLogic
    test TransparentLogic
Owner Address: [object Promise]
      ✓ Owner変数の値を確認
Name: [object Promise]
Age: [object Promise]
      ✓ name: Cardene, age: 23の格納
Name V2: [object Promise]
Age V2: [object Promise]
Fruit V2: [object Promise]
      ✓ Upgrade Contract

·-------------------------------------|---------------------------|-------------|-----------------------------·
|        Solc version: 0.8.17         ·  Optimizer enabled: true  ·  Runs: 200  ·  Block limit: 30000000 gas  │
······································|···························|·············|······························
|  Methods                                                                                                    │
·······················|··············|·············|·············|·············|···············|··············
|  Contract            ·  Method      ·  Min        ·  Max        ·  Avg        ·  # calls      ·  usd (avg)  │
·······················|··············|·············|·············|·············|···············|··············
|  TransparentLogic    ·  setProfile  ·          -  ·          -  ·      74204  ·            1  ·          -  │
·······················|··············|·············|·············|·············|···············|··············
|  TransparentLogicV2  ·  setProfile  ·          -  ·          -  ·      97662  ·            1  ·          -  │
·······················|··············|·············|·············|·············|···············|··············
|  Deployments                        ·                                         ·  % of limit   ·             │
······································|·············|·············|·············|···············|··············
|  TransparentLogic                   ·          -  ·          -  ·     395099  ·        1.3 %  ·          -  │
······································|·············|·············|·············|···············|··············
|  TransparentLogicV2                 ·          -  ·          -  ·     459451  ·        1.5 %  ·          -  │
·-------------------------------------|-------------|-------------|-------------|---------------|-------------·

  3 passing (2s)

UUPS Proxy

では次にUUPS Proxyを実装していきましょう。

UUPS Proxyの実装は以下のTutorialを参考にしています。

コントラクト作成

contracts/UUPSLogic.solcontracts/UUPSLogicV2.solというファイルを作成しましょう。

$ touch contracts/UUPSLogic.sol
$ touch contracts/UUPSLogicV2.sol
UUPSLogic.sol

ではUUPSLogic.solファイルを開いて以下を貼り付けてください。

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

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

contract UUPSLogic is Initializable, ERC20Upgradeable, UUPSUpgradeable, OwnableUpgradeable {
    string public myName;
    uint256 public myAge;

    /// @notice 一度だけ実行される関数。constructorの代わり。
    function initialize() public initializer {
        __ERC20_init("Cardene Token", "CARD");
        __Ownable_init();
        __UUPSUpgradeable_init();
        _mint(msg.sender, 1000 * 10 ** decimals());
    }

    /// @notice msg.senderがコントラクトのアプグレード許可されていない時に処理を元に戻す関数。
    /// @dev 基本的にonlyOwnerなどのアクセス修飾子をつける。
    function _authorizeUpgrade(address) internal override onlyOwner {}

    /// @notice プロフィール情報を更新する関数。
    /// @param _name 名前。
    /// @param _age 年齢。
    function setProfile(string memory _name, uint256 _age) external {
        myName = _name;
        myAge = _age;
    }

    /// @notice プロフィール情報を取得する関数。
    /// @return name 名前。
    /// @return age 年齢。
    function getProfile() external view returns(string memory, uint256){
        return (myName, myAge);
    }

}

TransparentLogic.solと同じような構成ですが、色々継承しているのが確認できます。

initialize関数の中でもERC20トークンをmintしたりなどを行なっています。

UUPSLogicV2.sol

次に更新ようのコントラクトである、UUPSLogicV2.solファイルを開いて以下を貼り付けてください。

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

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

contract UUPSLogicV2 is Initializable, ERC20Upgradeable, UUPSUpgradeable, OwnableUpgradeable {
    string public myName;
    uint256 public myAge;
    string public favoriteFruit;

    /// @notice 一度だけ実行される関数。constructorの代わり。
    function initialize() public initializer {
        __ERC20_init("Cardene Token", "CARD");
        __Ownable_init();
        __UUPSUpgradeable_init();
    }

    /// @notice msg.senderがコントラクトのアプグレード許可されていない時に処理を元に戻す関数。
    /// @dev 基本的にonlyOwnerなどのアクセス修飾子をつける。
    function _authorizeUpgrade(address) internal override onlyOwner {}

    /// @notice プロフィール情報を更新する関数。
    /// @param _name 名前。
    /// @param _fruit 好きな果物。
    /// @param _age 年齢。
    function setProfile(
        string memory _name,
        string memory _fruit,
        uint256 _age
    ) external {
        myName = _name;
        favoriteFruit = _fruit;
        myAge = _age;
    }

    /// @notice プロフィール情報を取得する関数。
    /// @return name 名前。
    /// @return favoriteFruit 好きな果物。
    /// @return age 年齢。
    function getProfile() external view returns(
        string memory, string memory, uint256
    ){
        return (myName, favoriteFruit, myAge);
    }

}

こちらもTransparentLogicV2.solと同じような構成です。

これでコントラクトの作成は完了です。

テスト作成

コントラクトが作成できたので、次にテスト用のファイルを作成していきましょう。

$ touch test/test-uups.js

では作成したtest-uups.jsを開いて以下を貼り付けてください。

const { expect } = require("chai");
const { ethers, upgrades } = require("hardhat");

describe("UUPSLogic", function () {
    beforeEach(async function () {
        // アドレスの作成
        [owner, wallet1, wallet2] = await ethers.getSigners();

        // UUPSLogicとUUPSLogicV2コントラクトを取得。
        UUPSLogic = await ethers.getContractFactory("UUPSLogic", owner);
        UUPSLogicV2 = await ethers.getContractFactory("UUPSLogicV2", owner);

        // UUPSLogicコントラクトをデプロイ。
        UUPSLogicInstance = await upgrades.deployProxy(
            UUPSLogic,
            { kind: 'uups' }
        );
    });

    describe('test UUPSLogic', function () {
        it("setProfile関数を実行して、name: Cardene, age: 23を格納", async function () {
            await UUPSLogicInstance.setProfile("Cardene", 23);
            expect((await UUPSLogicInstance.myName())).to.equal("Cardene");
            expect((await UUPSLogicInstance.myAge())).to.equal(23);

        })

        it("UUPSLogicV2コントラクトに更新", async function () {
            UUPSLogicV2Instance = await upgrades.upgradeProxy(
                UUPSLogicInstance.address,
                UUPSLogicV2
            );
        })

        it("setProfile関数を実行して、name: Cardene V2, favoriteFruit: Pear, age: 30を格納", async function () {
            await UUPSLogicV2Instance.setProfile("Cardene V2", "Pear", 30);
            expect((await UUPSLogicV2Instance.myName())).to.equal("Cardene V2");
            expect((await UUPSLogicV2Instance.myAge())).to.equal(30);
            expect((await UUPSLogicV2Instance.favoriteFruit())).to.equal("Pear");
        })
    });
});

簡単に手順をまとめると以下になります。

テストの手順もtest-transparent.jsと同じにしてあります。

deployProxyを使ってデプロイすることで、のちにコントラクトをアップグレードすることができる。

そしてupgradeProxyを実行することで、実際にコントラクトをアップグレードしています。

Transparent Proxyと比べて違う点としては、deployProxyを実行している部分で引数に{ kind: 'uups' }と指定している部分です。

ここでUUPS Proxyによるデプロイであることをプログラムに伝えています。

テストの実行

ではテストを実行していきましょう。

以下のコマンドを実行してください。

$ npx hardhat compile
$ npx hardhat test test/test-uups.js

実行して以下のように出力されていればテスト成功です!

UUPSLogic
    test UUPSLogic
Warning: A proxy admin was previously deployed on this network

    This is not natively used with the current kind of proxy ('uups').
    Changes to the admin will have no effect on this new proxy.

      ✓ setProfile関数を実行して、name: Cardene, age: 23を格納
Warning: A proxy admin was previously deployed on this network

    This is not natively used with the current kind of proxy ('uups').
    Changes to the admin will have no effect on this new proxy.

      ✓ UUPSLogicV2コントラクトに更新
Warning: A proxy admin was previously deployed on this network

    This is not natively used with the current kind of proxy ('uups').
    Changes to the admin will have no effect on this new proxy.

      ✓ setProfile関数を実行して、name: Cardene V2, favoriteFruit: Pear, age: 30を格納

·-------------------------------------|---------------------------|-------------|-----------------------------·
|        Solc version: 0.8.17         ·  Optimizer enabled: true  ·  Runs: 200  ·  Block limit: 30000000 gas  │
······································|···························|·············|······························
|  Methods                                                                                                    │
·······················|··············|·············|·············|·············|···············|··············
|  Contract            ·  Method      ·  Min        ·  Max        ·  Avg        ·  # calls      ·  usd (avg)  │
·······················|··············|·············|·············|·············|···············|··············
|  TransparentLogic    ·  setProfile  ·          -  ·          -  ·      71934  ·            1  ·          -  │
·······················|··············|·············|·············|·············|···············|··············
|  TransparentLogicV2  ·  setProfile  ·          -  ·          -  ·      95317  ·            1  ·          -  │
·······················|··············|·············|·············|·············|···············|··············
|  UUPSLogic           ·  upgradeTo   ·          -  ·          -  ·      39160  ·            2  ·          -  │
·······················|··············|·············|·············|·············|···············|··············
|  Deployments                        ·                                         ·  % of limit   ·             │
······································|·············|·············|·············|···············|··············
|  UUPSLogic                          ·          -  ·          -  ·    1566159  ·        5.2 %  ·          -  │
······································|·············|·············|·············|···············|··············
|  UUPSLogicV2                        ·          -  ·          -  ·    1518171  ·        5.1 %  ·          -  │
·-------------------------------------|-------------|-------------|-------------|---------------|-------------·

  3 passing (2s)

Beacon Proxy

では最後にBeacon Proxyを実装していきましょう。

コントラクト作成

contracts/BeaconLogic.solcontracts/BeaconLogicV2.solというファイルを作成しましょう。

$ touch contracts/BeaconLogic.sol
$ touch contracts/BeaconLogicV2.sol
BeaconLogic.sol

ではBeaconLogic.solファイルを開いて以下を貼り付けてください。

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

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

contract BeaconLogic is Initializable {
    address public owner;
    string public name;
    uint256 public age;

    /// @notice 一度だけ実行される関数。constructorの代わり。
    function initialize() public initializer {
        owner = msg.sender;
    }

    /// @notice プロフィール情報を更新する関数。
    /// @param _name 名前。
    /// @param _age 年齢。
    function setProfile(string memory _name, uint256 _age) external {
        name = _name;
        age = _age;
    }

    /// @notice プロフィール情報を取得する関数。
    /// @return name 名前。
    /// @return age 年齢。
    function getProfile() external view returns(string memory, uint256){
        return (name, age);
    }

}

コントラクトの名前以外TransparentLogic.solと同じになっています。

BeaconLogicV2.sol

次に更新ようのコントラクトである、BeaconLogicV2.solファイルを開いて以下を貼り付けてください。

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

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

contract BeaconLogicV2 is Initializable {
    address public owner;
    string public name;
    uint256 public age;
    string public favoriteFruit;


    /// @notice 一度だけ実行される関数。constructorの代わり。
    function initialize() public initializer {
        owner = msg.sender;
    }

    /// @notice プロフィール情報を更新する関数。
    /// @param _name 名前。
    /// @param _fruit 好きな果物。
    /// @param _age 年齢。
    function setProfile(
        string memory _name,
        string memory _fruit,
        uint256 _age
    ) external {
        name = _name;
        favoriteFruit = _fruit;
        age = _age;
    }

    /// @notice プロフィール情報を取得する関数。
    /// @return name 名前。
    /// @return favoriteFruit 好きな果物。
    /// @return age 年齢。
    function getProfile() external view returns(
        string memory, string memory, uint256
    ){
        return (name, favoriteFruit, age);
    }

}

こちらもTransparentLogicV2.solと中身は同じです。

テスト作成

コントラクトが作成できたので、次にテスト用のファイルを作成していきましょう。

$ touch test/test-beacon.js

では作成したtest-beacon.jsを開いて以下を貼り付けてください。

const { expect } = require("chai");
const { ethers, upgrades } = require("hardhat");

describe("TransparentLogic", function () {
    beforeEach(async function () {
        // アドレスの作成
        [owner, wallet1, wallet2] = await ethers.getSigners();

        // BeaconLogicとBeaconLogicV2コントラクトを取得。
        beaconLogic = await ethers.getContractFactory("BeaconLogic", owner);
        beaconLogicV2 = await ethers.getContractFactory("BeaconLogicV2", owner);

        // BeaconLogicコントラクトをデプロイ。
        beaconInstance = await upgrades.deployBeacon(beaconLogic);
        beaconLogicInstance = await upgrades.deployBeaconProxy(
            beaconInstance,
            beaconLogic
        );
    });

    describe('test BeaconLogic', function () {
        it("Owner変数の値を確認", async function () {
            expect((await beaconLogicInstance.owner())).to.equal(owner.address);
        })

        it("setProfile関数を実行して、name: Cardene, age: 23を格納", async function () {
            await beaconLogicInstance.setProfile("Cardene", 23);
            expect((await beaconLogicInstance.name())).to.equal("Cardene");
            expect((await beaconLogicInstance.age())).to.equal(23);

        })

        it("transparentLogicV2コントラクトに更新", async function () {
            await upgrades.upgradeBeacon(
                beaconInstance.address,
                beaconLogicV2
            );
            beaconLogicV2Instance = beaconLogicV2.attach(beaconLogicInstance.address)
        })

        it("setProfile関数を実行して、name: Cardene V2, favoriteFruit: Pear, age: 30を格納", async function () {
            await beaconLogicV2Instance.setProfile("Cardene V2", "Pear", 30);
            expect((await beaconLogicV2Instance.name())).to.equal("Cardene V2");
            expect((await beaconLogicV2Instance.age())).to.equal(30);
            expect((await beaconLogicV2Instance.favoriteFruit())).to.equal("Pear");
        })
    });
});

こちらもほとんどtest-transparent.jsと同じです。

違う点としてはコントラクトのデプロイ時に、アップグレード可能なBeacondeployBeaconを使用してデプロイしています。

その後、アップグレード可能な1つ以上のBeacon ProcxydeployBeaconProxyを使用してデプロイしています。

コントラクトのアップグレードは、upgradeBeacon関数を実行したのちに新しくデプロイするコントラクトにアタッチすることで、Beaconを新しいバージョンにしています。

Beaconがアップグレードされると、アップグレードされたBeaconを指す全てのBeaconProxyは新しいコントラクトを使用することになります。

テストの実行

ではテストを実行していきましょう。

以下のコマンドを実行してください。

$ npx hardhat compile
$ npx hardhat test test/test-beacon.js

実行して以下のように出力されていればテスト成功です!

TransparentLogic
    test BeaconLogic
Warning: A proxy admin was previously deployed on this network

    This is not natively used with the current kind of proxy ('beacon').
    Changes to the admin will have no effect on this new proxy.

      ✓ Owner変数の値を確認
Warning: A proxy admin was previously deployed on this network

    This is not natively used with the current kind of proxy ('beacon').
    Changes to the admin will have no effect on this new proxy.

      ✓ setProfile関数を実行して、name: Cardene, age: 23を格納
Warning: A proxy admin was previously deployed on this network

    This is not natively used with the current kind of proxy ('beacon').
    Changes to the admin will have no effect on this new proxy.

      ✓ transparentLogicV2コントラクトに更新
Warning: A proxy admin was previously deployed on this network

    This is not natively used with the current kind of proxy ('beacon').
    Changes to the admin will have no effect on this new proxy.

      ✓ setProfile関数を実行して、name: Cardene V2, favoriteFruit: Pear, age: 30を格納

·--------------------------------|---------------------------|-------------|-----------------------------·
|      Solc version: 0.8.17      ·  Optimizer enabled: true  ·  Runs: 200  ·  Block limit: 30000000 gas  │
·································|···························|·············|······························
|  Methods                                                                                               │
··················|··············|·············|·············|·············|···············|··············
|  Contract       ·  Method      ·  Min        ·  Max        ·  Avg        ·  # calls      ·  usd (avg)  │
··················|··············|·············|·············|·············|···············|··············
|  BeaconLogic    ·  setProfile  ·          -  ·          -  ·      77106  ·            1  ·          -  │
··················|··············|·············|·············|·············|···············|··············
|  BeaconLogicV2  ·  setProfile  ·          -  ·          -  ·     100564  ·            1  ·          -  │
··················|··············|·············|·············|·············|···············|··············
|  UUPSLogic      ·  upgradeTo   ·          -  ·          -  ·      32699  ·            1  ·          -  │
··················|··············|·············|·············|·············|···············|··············
|  Deployments                   ·                                         ·  % of limit   ·             │
·································|·············|·············|·············|···············|··············
|  BeaconLogic                   ·          -  ·          -  ·     395099  ·        1.3 %  ·          -  │
·································|·············|·············|·············|···············|··············
|  BeaconLogicV2                 ·          -  ·          -  ·     459451  ·        1.5 %  ·          -  │
·--------------------------------|-------------|-------------|-------------|---------------|-------------·

  4 passing (3s)

最後に

今回はスマートコントラクトをアップグレードできる「Upgradable Contracts」について取り上げてきました!

いかがだったでしょうか?

ポイント

  • Upgradable Contractsが何か理解できた!」
  • Upgradable Contractsの仕組みを理解できた!」
  • Upgradable Contractsの実装方法を理解できた!」

上記に当てはまっていれば嬉しいです!

Upgradable Contracts」でできることや、今回実装では紹介できなかった方法でもコントラクトをアップグレードできるので、ぜひ自分でも調べてみてください!

もし何か質問などがあれば以下のTwitterなどから連絡ください!

普段はSolidityやブロックチェーン、Web3についての情報発信をしています。

Twiiterでは気になった記事などを共有しているので、ぜひフォローしてくれると嬉しいです!

参考

-Smart Contract, Solidity, ブロックチェーン
-,