こんにちは!CryptoGamesというブロックチェーンゲーム企業でエンジニアをしているかるでねです!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。
このブログ以外でも情報発信しているので、よければ他の記事も見ていってください。
https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58
今回はスマートコントラクトをアップグレードできる「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コントラクトを呼び出すことができるようになります。
つまり、ContractAやContractB自体をいじることなく、Proxyコントラクトの内部で保持している呼び出し先のコントラクトの情報を随時更新するだけで良いのです。
ここで「スマートコントラクトは変更できないから、呼び出し先のコントラクト情報は変更できないのでは?」という疑問を持つ方もいると思います。
これは単純で、変数として呼び出し先のコントラクト情報を持っていて、その変数の値を変えることができる関数を用意します。
そしてこの関数を実行できるのは、Proxyコントラクトをデプロイしたユーザーのみにしておけば良いです。
また、Proxyコントラクトからメインのコントラクトを呼び出すときは、delegatecall
を使用します。
delegatecall
は簡単にいうと、「呼び出したコントラクトでコードが実行される」呼び出し方法です。
つまりProxyコントラクトからLogicコントラクトを呼び出した際に、Proxyコントラクト内のデータを使用しながら処理を実行できるということになります。
より詳しくdelegatecall
について知るには以下の記事を参考にしてください。
Upgradableにする5つの方法
先ほどはコントラクトをUpgradableにする方法を確認しました。
ここではコントラクトをUpgradableにする5つの方法について紹介していきます!
方法としては以下の5つがあります。
Upgradableにする5つの方法
- 古いコントラクトと新しいコントラクトを作成して、古いコントラクトから新しいコントラクトにデータなどを移行する。
- データを保存するコントラクトとLogicコントラクトを作成して、2つのコントラクトに分離する。
- Proxyパターンを使用して、Proxyコントラクトから
delegatecall
でLogicコントラクトを呼び出す。 - メインのコントラクトが特定の機能を実行するための複数コントラクトのアドレスを保持し、簡単に実装を切り替えることができる。
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コントラクトを使用してdelegatecall
でLogicコントラクトを呼び出しているとき、Logicコントラクトではconstructor
を使用することができます。
正しくはconstructor
の処理が呼び出されません。
なぜならLogicコントラクトはデプロイ済みのコントラクトをdelegatecall
で呼び出すだけだからです。
そのためOpenzeppelinが提供しているInitializable.solのようなコントラクトを使用して、一度だけ呼び出される関数を用意する必要があります。
変数定義を揃える
ProxyコントラクトとLogicコントラクトの変数定義を同じ型を同じ順序に宣言する必要があります。
理由としては、ストレージの衝突と呼ばれる問題が起きる可能性があるためです。
ストレージの衝突とは、変数の格納位置が異なるために起きる問題です。
スマートコントラクトでのストレージデータについてには以下の記事が参考になります。
Proxyコントラクトからdelegatecall
でLogicコントラクトを呼び出すと、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 Proxy
やUUPS Proxy
と異なり、複数のコントラクトをUpgradableにすることができます。
メインのコントラクトを複数でデプロイされる時に使用されます。
1つのProxyコントラクトに対して、1つの処理がLogicコントラクトが紐づくイメージです。
その中継をBeacon Proxy
が補います。
これらをまとめると以下の図のようになります。(緑線はdelegatecall
呼び出しです)
この図からもわかるように、1回のトランザクションで複数のProxyコントラクトの実行が可能です。
また、Proxy Adminコントラクトの役割をBeacon Proxy
コントラクトが行うことができるのもポイントです。
Proxy Patternの実装
では早速Proxy Pattern
の実装をしていきましょう!
コードは以下に置いてあります。
https://github.com/cardene777/smart-contract-test/tree/main/ProxyPattern
セットアップ
まずは全ての実装において必要な最低限の実装をしていきましょう。
Node.jsとnpmのインストール
Node.jsとnpmをインストールしていない人は、以下の記事を参考にインストールしてください。
https://codelikes.com/mac-node-install/
ディレクトリの作成
作業するディレクトリを作成していきましょう。
$ 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.sol
とcontracts/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);
}
}
owner
とname
、age
という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を参考にしています。
https://forum.openzeppelin.com/t/uups-proxies-tutorial-solidity-javascript/7786
コントラクト作成
contracts/UUPSLogic.sol
とcontracts/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.sol
とcontracts/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
と同じです。
違う点としてはコントラクトのデプロイ時に、アップグレード可能なBeaconをdeployBeacon
を使用してデプロイしています。
その後、アップグレード可能な1つ以上のBeacon Procxy
をdeployBeaconProxy
を使用してデプロイしています。
コントラクトのアップグレードは、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では気になった記事などを共有しているので、ぜひフォローしてくれると嬉しいです!
参考
https://santexgroup.com/blog/is-it-possible-to-upgrade-a-smart-contract-once-deployed/
https://ethereum.org/ja/developers/docs/smart-contracts/upgrading/
https://blog.shinonome.io/transparent-proxy-pattern-contracts/
https://note.com/standenglish/n/n8728b074efd1
https://docs.openzeppelin.com/contracts/4.x/api/proxy
https://forum.openzeppelin.com/t/uups-proxies-tutorial-solidity-javascript/7786
https://qiita.com/biga816/items/b4f745e67588e8da7ed1
https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable
https://docs.openzeppelin.com/learn/upgrading-smart-contracts