Upgradeableなコントラクトを作ろう
このブログの対象読者
① Upgradeableなコントラクトを作ろうと考えている方。
② Solidityのlow level function delegatecallについて聞いたことがある方、理解している方。
はじめに
スマートコントラクトの特徴の一つとして、Censorship resistanceがあげられます。ネットワークにデプロイされたコントラクトは、変更することができません。誰にも変更できないスマートコントラクトは、利点として考えられます。DApps(Decentralized Applications)を使用するユーザーは、ユーザーが知らないうちにアプリケーションのロジックが変更されるということがありません。そのため、安心してアプリケーションを使うことができます。その一方、開発者にとっては難しい特徴ともいえます。例をあげると、
① デプロイしたコントラクトに生じたバグへの対処
② 新しい機能の追加
といった際に、コントラクトを変更することができません。この問題を解決するのが、"Upgradeable Contracts"です。
今回のブログでは、Upgradeableなコントラクトについて解説しているサイトをまとめます。そして、Upgradeableなコントラクト作成における、注意点のチェックリストを作ってみたいと思います。さらに、このチェックリストをもとに、メインネットにデプロイされているUpgradeableなコントラクトを見ていきます。
なお、コントラクトをメインネットにデプロイする際には、ご自身で参考資料を読み、テストネットで動作を確認するなどは、ご自身の責任でお願いします。
Upgradeableなコントラストは、Proxy Patternを利用することで可能になります。Proxy Patternについて詳しく知りたい方は、この記事を参考にしてください。
Upgradeabilityチェックリスト
チェックリスト
☐ constructorがないこと、初期化関数が別に定義されていること。
☐ Libraryを使用する際には、初期化のための関数があるものを使用する。
☐ constant variables以外のstate variablesは、initializeの中で初期化する。
constructorがないこと、初期化関数が別に定義されていることを確認する
constructorではなく、initializeを使います。
constructorについて
solidityではコントラクトを初期化する際、constructorを使います。constructorに書かれたコードは、デプロイ前に自動で実行されます。
initialize関数とは
Proxy patternが可能にするUpgradeableなコントラクトでは、constructorによる初期化が実行されません。そのため、constructorではなく、普通の関数を利用して、コントラクトを初期化する必要があります。Upgradeableなコントラクトを読むと、initializeという関数をよく目にします。この関数がconstructorの役割を果たしています。普通の関数として書かれた初期化関数(initialize)は、自動で実行されません。デプロイ後、この関数を手動で実行する必要があります。初期化を実行する関数は、constructorと同じように一度のみ実行されるべきです。一度のみ実行されるよう、関数を実装する必要があります。初期化関数の名前は、initializeである必要はありません。
Libraryを使用する際には、初期化のための関数があるものを使用する
スマートコントラクトの開発では、ライブラリを使う機会が多くあります。Upgradeableなコントラクトを作るときは、ライブラリの使用にも注意が必要です。先ほど書いたように、Upgradeableなコントラクトでは、constructorを使用することはできません。このことはライブラリにも当てはまります。Upgradeableなコントラクトを書く場合は、使用するライブラリにconstructorがないことを確認しましょう。
例えば、OpenZeppelinは、Upgradeableなコントラクトのためのライブラリを用意しています。Upgradeableなコントラクトのinitializeの中で
__Pausable_init();
などの記述がみられます。これは、OpenZeppelinのPausableUpgradeableライブラリにある、コントラクト初期化の関数を呼び出しています。
constant variables以外のstate variablesは、initializeの中で初期化する
Solidityでは、state variablesを宣言する際に、初期値を代入することができます。この初期化は、constructor内での初期化と同様と考えられます。
/// 宣言と初期化
contract Hokusai {
string public name = "Hokusai";
}
これまで書いた二つの注意点と同様の理由から、state variablesの初期化もinitializeの中で実行する必要があります。(constant variablesに関しては、該当しません。Upgradeableなコントラクトでも、constant variablesは宣言と同時に初期化が可能です)
/// Upgradeableなコントラクトにおける初期化
contract Hokusai {
string public name;
function initialize() external initializer {
name = "Hokusai";
}
}
以上が、代表的な注意点です。他にも、Upgradeableなコントラクト内で、新しいコントラクトを作る、delegatecall、selfdestruct使う際などには、注意が必要です。気になる方は、もとの記事を確認してください。
具体例
ここからは、上記のチェックリストを使って、実際にデプロイされているUpgradeableなコントラクトのコードを見ていきます。今回見ていくのは、Nouns DAOのNounsAuctionHouse.solです。Nouns DAOはフルオンチェーンのNFTプロジェクトです。NFTだけではなく、DAOによる運営にも注目が集まっています。NounsAuctionHouse.solでは、毎日一つのNoun(NFT)がオークションに出されています。
それでは見ていきます。
重要になるのは、NounsAuctionHouse.solにある、以下の関数です。
function initialize(
INounsToken _nouns,
address _weth,
uint256 _timeBuffer,
uint256 _reservePrice,
uint8 _minBidIncrementPercentage,
uint256 _duration
) external initializer {
__Pausable_init();
__ReentrancyGuard_init();
__Ownable_init();
_pause();
nouns = _nouns;
weth = _weth;
timeBuffer = _timeBuffer;
reservePrice = _reservePrice;
minBidIncrementPercentage = _minBidIncrementPercentage;
duration = _duration;
}
☐ Upgradeableなコントラクトでは、constructorがないこと、別の初期化の関数があることを確認する。
まず、NounsAuctionHouse.solにconstructorは存在しません。代わりに、initializeが使われています。チェックリストの1つ目は満たされていることが分かります。また、initializeではinitializerというOpenZeppelinのLibraryが使われています。これにより、この関数は初期化の際、一度のみ使用されることになります。
☐ Libraryを使用する際には、初期化のための関数があるものを使用する。
NounsAuctionHouse.solは以下のように、いくつかのLibraryを継承しています。
contract NounsAuctionHouse is INounsAuctionHouse, PausableUpgradeable, ReentrancyGuardUpgradeable, OwnableUpgradeable {}
これらのLibraryは、OpenZeppelinが提供するUpgradeableなコントラクトのためのLibraryです。initializeの中では、
__Pausable_init();
__ReentrancyGuard_init();
__Ownable_init();
を使い、これらのLibraryを初期化しています。チェックリストの二つ目もクリアしています。
☐ constant variables以外のstate variablesは、initializeの中で初期化する。
コントラクトの中で、以下のstate variablesが宣言されています。
// The Nouns ERC721 token contract
INounsToken public nouns;
// The address of the WETH contract
address public weth;
// The minimum amount of time left in an auction after a new bid is created
uint256 public timeBuffer;
// The minimum price accepted in an auction
uint256 public reservePrice;
// The minimum percentage difference between the last bid amount and the current bid
uint8 public minBidIncrementPercentage;
// The duration of a single auction
uint256 public duration;
これらのstate variablesは宣言されていますが、初期値が代入されていません。代入はinitialize内部で、次のように行われています。
nouns = _nouns;
weth = _weth;
timeBuffer = _timeBuffer;
reservePrice = _reservePrice;
minBidIncrementPercentage = _minBidIncrementPercentage;
duration = _duration;
三つ目の注意点もクリアしていることが確認できました。
NounsAuctionHouse.solは、ZoraのAuctionHouse.sol参考に作られています。Zoraのコントラクトは、Upgradeableなコントラクトとして作成されていません。興味がある方は、NounsとZoraのコントラクトを比べると、さらに理解が深まると思います。
最後に
今回のブログでは、Upgradeableなコントラクトを作成する際の注意点について紹介しました。Upgradeableなコントラクトは、開発の柔軟性を上げるなど、開発において大きなメリットをもたらします。ぜひ興味のある方は、試してみてください。
宣伝
「Hokusai API」を提供する日本モノバンドル株式会社では、エンジニアを採用中です!
ぜひフルリモートで、スピード感や大きな変化を楽しみながらぜひ働いてみませんか?
参考資料
Writing Upgradeable Contracts
The State of Smart Contract Upgrades
Proxy Upgrade Pattern
Using with Upgrades
Discussion