OpenZeppelinは、Ethereumプロジェクトのソフトウェア開発および運用のあらゆる側面を構築、管理、検査するためのセキュリティ製品の完全なスイートを提供します。
自分で決めたマイルストーンに沿って、dApps開発について勉強中!
Ethereumのドキュメントを読んでて、眠くなるもう少し実践的にいろいろ試しながら理解したいと思って、Solidityのサンプルをいろいろ触っています。
その中でOpenZeppelinっていうスイート(APIライブラリ?)が使いやすそうかなーと思ったので、ドキュメントのチュートリアルをやってみました。
詳しくは本家に書いてあるので、
この記事では気をつけたほうが良い点をまとめていきますー
1. NodeJSのプロジェクトを作ろう(Setting up a Node project)
$ mkdir learn && cd learn
$ npm init -y
2. スマートコントラクトをつくる(Developing smart contracts)
Ethereum開発フレームワークで人気なのは「Truffle」「Hardhat」の2つ。
それぞれに長所があるので、すべてを使いこなせると便利...だそうです!(使いこなせるか!?
今回は割と勢いのあるHardhatを使っていきますー
$ npm install --save-dev hardhat
$ npx hardhat
$ npm install --save-dev @openzeppelin/contracts
例では、access/Ownable.sol を読み込んでいます。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// Import Ownable from the OpenZeppelin Contracts library
import "@openzeppelin/contracts/access/Ownable.sol";
// Make Box inherit from the Ownable contract
contract Box is Ownable {
uint256 private _value;
event ValueChanged(uint256 value);
// The onlyOwner modifier restricts who can call the store function
function store(uint256 value) public onlyOwner {
_value = value;
emit ValueChanged(value);
}
function retrieve() public view returns (uint256) {
return _value;
}
}
onlyOwnerを継承して、
コントラクトのオーナーだけがstoreを呼び出せるようにしています。
デフォルトでは、コントラクトをデプロイしたアカウントがオーナーとなります。
これはtransferOwnership関数で変更することができます。
3. スマートコントラクトの導入と運用(Deploying and interacting)
- Hardhatでローカルブロックチェーンをひらきます
- ローカルブロックチェーンにスマートコントラクトをデプロイします
- デプロイしたものをチェーンにアタッチします
- これで実行可能に!!
Hardhatを使うと、1コマンドでローカルブロックチェーンのノードが起動できます。
ethers(Ethereumウォレット実装のためのJavaScriptユーティリティ)を使うためのプラグインも導入します。
$ npx hardhat node
$ npm install --save-dev @nomiclabs/hardhat-ethers ethers
Configファイルにプラグインを追加。
require('@nomiclabs/hardhat-ethers');
...
module.exports = {
...
};
デプロイ用のスクリプトをJavascriptで書きます。
先程のethersのgetContractFactoryを使います。
async function main () {
// We get the contract to deploy
const Box = await ethers.getContractFactory('Box');
console.log('Deploying Box...');
const box = await Box.deploy();
await box.deployed();
console.log('Box deployed to:', box.address);
}
main()
.then(() => process.exit(0))
.catch(error => {
console.error(error);
process.exit(1);
});
Hardhatを使ってローカルブロックチェーン用にデプロイします。
scripts/deploy.jsに書いたとおり、
スマートコントラクトのアドレスが表示されます。
$ npx hardhat run --network localhost scripts/deploy.js
Deploying Box...
Box deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
ローカルブロックチェーンのプロセスを閉じた場合、コントラクトを再デプロイする必要があるので注意です!
Hardhatでローカルブロックチェーンのconsoleをひらき、
トランザクションやクエリを送ることができます。
$ npx hardhat console --network localhost
//
> const Box = await ethers.getContractFactory('Box');
undefined
//
> const box = await Box.attach('0x5FbDB2315678afecb367f032d93F642f64180aa3')
undefined
> await box.store(42)
{
hash: '0x3d86c5c2c8a9f31bedb5859efa22d2d39a5ea049255628727207bc2856cce0d3',
...
> (await box.retrieve()).toString()
'42'
consoleでなく、スクリプトを実行することもできます。
async function main () {
// Set up an ethers contract, representing our deployed Box instance
const address = '0x5FbDB2315678afecb367f032d93F642f64180aa3';
const Box = await ethers.getContractFactory('Box');
const box = await Box.attach(address);
// Send a transaction to store() a new value in the Box
await box.store(23);
// Call the retrieve() function of the deployed Box contract
const value = await box.retrieve();
console.log('Box value is', value.toString());
}
main()
.then(() => process.exit(0))
.catch(error => {
console.error(error);
process.exit(1);
});
実行はHardhatのrunコマンドを使います。
$ npx hardhat run --network localhost ./scripts/index.js
Box value is 23
4. 自動実行テストを書く(Writing automated tests)
テスト用のライブラリ/プラグインを追加します。
詳細は省きますー
$ npm install --save-dev mocha chai
$ npm install --save-dev @openzeppelin/test-helpers
$ npm install --save-dev @openzeppelin/test-environment
手動でテスト実行します。
$ npx hardhat test
package.jsonにテスト起動コマンドを書いておくと、
自動起動してくれて便利です。
"scripts": {
+ "test": "mocha --exit --recursive"
}
$ npm test
> learn@1.0.0 test
> mocha --exit --recursive
Box
✓ retrieve returns a value previously stored (69ms)
1 passing (482ms)
5. パブリックテストネットワークにローンチ(Connecting to public test networks)
- テストネット用のノードをたてる
- テストネット用のアカウントを作成&デプロイ用ETHの補充
- テストネットデプロイ用に設定追加
ノードはAlchemyかInfuraが使いやすそう。
制限はありますが、開発の間は無料で使えそうです。
開発中はWhitelistを設けてノードへのアクセス制限をつけておきましょう...!
アカウントはコマンドラインから簡単に作成可能。
$ npx mnemonics
drama film snack motion ...
デプロイ先のノード情報を設定ファイルに追加。
+ const { alchemyApiKey, mnemonic } = require('./secrets.json');
...
module.exports = {
+ networks: {
+ rinkeby: {
+ url: `https://eth-rinkeby.alchemyapi.io/v2/${alchemyApiKey}`,
+ accounts: { mnemonic: mnemonic },
+ },
+ },
...
};
rinkeby(テストネット)に接続して、
先程生成したmnemonicが使えるアカウントリストを取得します。
もちろん、現在の持ちETHは0ですので、
いずれかのアカウントアドレスを用いて
無料ETH配布元(Faucet)からガス代を調達しておきましょう。
※同一mnemonicで複数アカウントが紐付いている
$ npx hardhat console --network rinkeby
Welcome to Node.js v12.22.1.
Type ".help" for more information.
> accounts = await ethers.provider.listAccounts()
[
'0xEce6999C6c5BDA71d673090144b6d3bCD21d13d4',
'0xC1310ade58A75E6d4fCb8238f9559188Ea3808f9',
...
> (await ethers.provider.getBalance(accounts[0])).toString()
'0'
デプロイ用のガス代が準備できたら、
ローカルネットと同様の手順でオンチェーン上にのせます。
$ npx hardhat run --network rinkeby scripts/deploy.js
Deploying Box...
Box deployed to: 0xD7fBC6865542846e5d7236821B5e045288259cf0
$ npx hardhat console --network rinkeby
Welcome to Node.js v12.22.1.
Type ".help" for more information.
> const Box = await ethers.getContractFactory('Box');
undefined
> const box = await Box.attach('0xD7fBC6865542846e5d7236821B5e045288259cf0');
undefined
> await box.store(42);
{
hash: '0x330e331d30ee83f96552d82b7fdfa6156f9f97d549a612eeef7283d18b31d107',
...
> (await box.retrieve()).toString()
'42'
実行状況はMetamaskか、RinkebyのEtherscanから確認できます。
6. スマートコントラクトをアップグレード(Upgrading smart contracts)
スマートコントラクトは基本的にチェーン上で不変です。
開発者が手を加えない限り、バグが存在したとしても、そこに存在し続けます。
バグ修正や機能追加は事前に機構を備えていないと、以下のような手順が必要となります。
- コントラクトの新バージョンをデプロイ
- 古いコントラクトから新しいコントラクトにすべての状態を手動で移行する(ガス代が非常に高くつく可能性あり)
- 古いコントラクトとやり取りしていたすべてのコントラクトを、新しいコントラクトのアドレスを使用するように更新
- すべてのユーザーに、新しいデプロイメントを使い始めるよう促す(ユーザーの移行が遅いため、両方のコントラクトが同時に使用されている場合に対応する)
めんどくさいですね。。
このような混乱を避けるために、コントラクトのコードを変更しても、状態、バランス、アドレスを維持するプラグインを追加しておきます。
この手順を踏み、デプロイスクリプトを対応させると、
簡単にアップグレード可能なスマートコントラクトが作成できます。
$ npm install --save-dev @openzeppelin/hardhat-upgrades
...
require('@nomiclabs/hardhat-ethers');
+ require('@openzeppelin/hardhat-upgrades');
...
module.exports = {
...
};
デプロイ用スクリプトはupgrades.deployProxyを使って初期化します。
const { ethers, upgrades } = require('hardhat');
async function main () {
const Box = await ethers.getContractFactory('Box');
console.log('Deploying Box...');
const box = await upgrades.deployProxy(Box, [42], { initializer: 'store' });
await box.deployed();
console.log('Box deployed to:', box.address);
}
main();
そして、アップグレード用のデプロイスクリプトでは、
upgrades.upgradeProxyをアップグレード先のアドレスに使用します。
(ここではBoxをBoxV2にアップグレードしています)
const { ethers, upgrades } = require('hardhat');
async function main () {
const BoxV2 = await ethers.getContractFactory('BoxV2');
console.log('Upgrading Box...');
await upgrades.upgradeProxy('0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0', BoxV2);
console.log('Box upgraded');
}
main();
スマートコントラクトのアドレスが固定されてしまうことを避けるために、
OpenZeppelinではProxyを利用しています。
これにより、トランザクション実行元とコードを分離することができ、
容易にコード部分のアップグレードが可能になります。
アップグレードの際、コンストラクタが呼び出されてしまうことを避けるため、
initialize関数が用意されていたり、デプロイ済みの変数は変更できないなどの制限もあります。
その他アップグレードに対応するスマートコントラクトを書く際の注意点詳細は、↓をご確認ください。
7. メインネットローンチの準備(Preparing for mainnet)
監査とセキュリティ(デプロイ前)
イーサリアム用スマートコントラクトのベストプラクティスをまとめたサイト。
すごく重要そうなので、あとでまとめよう...!
また、OpenZeppelinチームが監査もしてくださるそうです...!
事前のチェックリストはこちら。
...このあたりも大事なので、別記事にしよう...!
※監査はバグがないことを保証するものではありませんが、複数の経験豊富なセキュリティ・リサーチャーにコードをチェックしてもらうことは確実に役立つことを覚えておきましょう!
ソースコードの検証(デプロイ後)
コントラクトをメインネットにデプロイした直後に、そのソースコードを検証する必要があります。
SolidityコードをEtherscanやEtherchainなどのサードパーティに提出し、
そのサードパーティがコンパイルして、展開されたアセンブリと一致するかどうかを検証します。
方法としては、Etherscanウェブサイトでコントラクトを手動で検証。
または、Etherscan APIを取得することで、
Hardhat Etherscanプラグインを使ったコマンドでの検証をすることもできます。
API取得はこちら。
※アップグレード可能なコントラクトの場合、EtherscanはOpenZeppelinのプロキシ機構をきちんとサポートしていますが、他のエクスプローラーはそうではないかもしれません。
デプロイするアカウント(秘密鍵)の安全な管理
コントラクトのデプロイや操作に使用するアカウントは、デプロイ用のガス代、または預かり資産としてのETHを保持しており、ハッカーにとっては魅力的なターゲットとなります。
鍵の保護には万全を期し、必要に応じてハードウェアウォレットの使用も検討してください。
特定のアカウントをシステム内で特別な権限を持つように定義することができるので
特に注意を払って設定する必要があります。
管理者アカウントにマルチシグ(Multisig)を使用
管理者(administratorの略)アカウントは、システム内で特別な権限を持つアカウントです。
例えば、管理者は契約を一時停止する権限を持っている場合があります。
このようなアカウントが悪意のあるユーザーの手に渡れば、システムに大混乱を引き起こす可能性があります。
管理者アカウントのセキュリティを確保するには、
通常の外部所有のアカウントではなく、マルチシグなどの特別な契約を使用するのが良い方法です。
マルチシグ(Multisig)とは、マルチシグネチャー(Multi Signature)の略語。
複数の電子署名を必要とする状態をマルチシグと呼びます。
こちらのドキュメントでは、「Gnosis Safe」を推していました。
Metamaskの場合、ウォレットを作った後、
↓のサイトから複数アカウントを作ってマルチシグ状態を作ります。
しばらく仮想通貨の取引をしてますが、
この操作はしたことないのでやってみたいです...!
Ethereumメインネットだとガス代がえげつないので、
BSCかPolygonで試してみたいと思いますー
アップグレード管理者も外部所有アカウントからマルチシグに変更
プロジェクトの特別な管理者アカウントは、他のコントラクトをアップグレードする権限を持つアカウントです。
これはコントラクトのデプロイに使われる外部所有のアカウントがデフォルトになっています。
ローカルやテストネットでのデプロイではこれで十分ですが、
メインネットではコントラクトをより安全にする必要があります。
アップグレード用の管理者アカウントを手に入れた攻撃者は、
システム内のすべてのコントラクトを変更できてしまうからです...!
この点を考慮して、デプロイメント後に ProxyAdminのオーナーシップをマルチシグに変更しましょう。
方法としては、admin.transferProxyAdminOwnershipを使って、
ProxyAdminのコントラクトの所有権を移すことができます。
コントラクトをアップグレードする必要があるときは、prepareUpgradeを使って、
プロキシが更新されたときに使用できるように、
新しい実装コントラクトを検証&デプロイすることができます。
...この辺は実際に運用してみないとわからない部分が多いかな...^^;
プロジェクトガバナンス
多くの場合、システムパラメータの微調整からコントラクトの完全なアップグレードまで、
特別な権限を必要とする操作がプロジェクト内に存在します。
これらのアクションをどのように決定するかを選択する必要があります。
信頼された少数の開発者グループによるものか、
プロジェクトの全関係者による公開投票によるものか。
ここに正解はありません。
どのようなガバナンス方式を選択するかは、
何を作ろうとしているのか、どのようなコミュニティなのかによって大きく異なります。
…個人制作なので、あまり気にしないですが...
大きなプロジェクトだとトークンによる投票とかされてますよね。DAO!DAO!
おつかれさまでした!
いい感じの長さのチュートリアル。
スマートコントラクト自体も簡単なしくみでとてもわかり易かったです♪
特に印象深かったのが、デプロイ時にガス代がかかること。
デプロイ後のアップグレード&バグ修正が結構たいへんなこと。
Webと違って、しっかりきっちりテストしてからデプロイしたいですね...!
特に最後のメインネットローンチ前後のところはもう少し詳しく勉強したいです!!
...その次はOpenSeaにNFTのチュートリアルがあったので、
OpenZeppelin&Hardhatのドキュメントも確認しつつチャレンジしてみますー!