はじめに
諸事情により、ERC20を実装する必要が出てきたので、いまさらですがHardhatで開発環境を作成してテストまでやります。
Nodeのインストール
Nodeがインストールされていない場合、以下から、Nodeをダウンロードしてインストールします。
インストールの詳細は割愛します。
プロジェクトの作成
hardhatパッケージのインストール
mkdir hardhatsample
cd hardhatsample
npm init -y
npm install --save-dev hardhat
npm install --save-dev @nomicfoundation/hardhat-toolbox
npm install --save-dev @openzeppelin/contracts
コマンドは上から順に以下の内容となります。
- フォルダの作成
- フォルダに移動
- 初期化
- hardhatインストール(Proxy環境の場合は補足を参照ください。)
- hardhat-toolboxインストール
- openzeppelinインストール
※:他に影響を与えないように「--save-dev
」オプションを付けてローカルインストールとしています。
hardhat環境のインストール
以下のコマンドを実行しhardhat環境をインストールします。
npx hardhat
以下のように表示されるので、「Create a JavaScript project」を選択して「Enter」を押下します。
他に変更する場合はカーソルで選択できます。
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.6
? What do you want to do? ...
> Create a JavaScript project
Create a TypeScript project
Create an empty hardhat.config.js
Quit
その後、「Hardhat project root:」と「Do you want to add a .gitignore? (Y/n)」を聞かれますが、「Enter」を押下すれば問題ありません。
「Hardhat project root:」は、コマンドを実行したフォルダが選ばれるみたいです。
「Do you want to add a .gitignore? (Y/n)」は、「.gitignore」ファイルが自動生成してくれるので「y」にします。
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.6
√ What do you want to do? · Create a JavaScript project
√ Hardhat project root: · C:\XXXXXXXXXXXXXXXXXXX\hardhatsample
√ Do you want to add a .gitignore? (Y/n) · y
You need to install these dependencies to run the sample project:
npm install --save-dev "hardhat@^2.12.6" "@nomicfoundation/hardhat-toolbox@^2.0.0"
Project created
See the README.md file for some example tasks you can run
Give Hardhat a star on Github if you're enjoying it!
https://github.com/NomicFoundation/hardhat
GASゼロ環境
※※※ 通常は推奨されないようです。 ※※※
別のシステムが生成したウォレットアドレス(公開鍵)を使用してコントラクトを操作していると、GAS代が足りないというエラーが頻発してうっざ、GAS代を必要とするコントラクト操作をする為にウォレットアドレスにETH
を譲渡してとかやるのが面倒くさいので、GAS代ゼロでも動くようにhardhat.config.js
を変更します。
追加したのはnetworks
の部分です。
テストなどで使うhardhat
の設定を加えます。
gasPrice
を0
とすることで、コントラクトのGAS代がゼロになります。
initialBaseFeePerGas
を0
にすることで、最初のブロック生成のGAS代もゼロとなります。
また、accounts
を適当に2つ作ります。
GAS代がゼロなのでbalance
は不要となるので0
としています。
require("@nomicfoundation/hardhat-toolbox");
/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
solidity: "0.8.17",
networks: {
hardhat: {
gasPrice: 0,
initialBaseFeePerGas: 0,
accounts: [{
privateKey: "0x0000000000000000000000000000000000000000000000000000000000000001",
balance: "0",
}, {
privateKey: "0x0000000000000000000000000000000000000000000000000000000000000002",
balance: "0",
},],
},
},
};
コントラクト作成
contracts
フォルダにあるLock.sol
を削除します。
新しくTokenSample.sol
を作成します。
openzeppelin-contractsがERC20を実装しているSolidityを提供しているので、それを使います。
また、以下の機能を実装します。
- コンストラクタでオーナーアドレスと初期トークン数を指定できるようにする
- 増資メソッド(mint)を追加する、ただし実行可能なのはオーナーのみとする。
- 償却メソッド(burn)を追加する、ただし実行可能なのはオーナーのみとする。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract TokenSample is ERC20, Ownable {
constructor(
address owner,
uint256 initAmount
) ERC20("Token Sample", "TSC") {
transferOwnership(owner);
_mint(owner, initAmount);
}
function mint(uint256 amount) public onlyOwner {
_mint(msg.sender, amount);
}
function burn(uint256 amount) public onlyOwner {
_burn(msg.sender, amount);
}
}
ライセンス
// SPDX-License-Identifier: MIT
このコントラクトのライセンス表記みたいです。
ライセンスを記述しないと、コンパイルでワーニングが発生します。
import
2つERC20.sol
とOwnable.sol
をインポートしています。
ERC20.sol
は、ERC20を実装しています。
Ownable.sol
は、onlyOwner
を使用する為にインポートします。
constructor
コンストラクタは引数として、オーナーアドレスと初期トークン数を引数としています。
transferOwnership
で指定されたオーナーアドレスにオーナーを変更しています。
これは、Ownable.sol
のメソッドです。
privateメソッドで_transferOwnership
というのもありますが、アドレス0を排除しているメソッドを採用しています。
mint
増資メソッドです。
onlyOwner
をしているので、オーナーアドレスからの呼び出し以外はエラーとなります。
これにより、msg.sender
はオーナーアドレスとなります。
オーナーアドレスに指定されたトークン数が増資されます。
burn
償却メソッドです。
onlyOwner
をしているので、オーナーアドレスからの呼び出し以外はエラーとなります。
これにより、msg.sender
はオーナーアドレスとなります。
オーナーアドレスに指定されたトークン数が償却されます。
テストコード作成
test
フォルダにあるLock.js
を削除します。
新しくTokenSample.js
を作成します。
const { loadFixture } = require("@nomicfoundation/hardhat-network-helpers");
const { expect } = require("chai");
describe("Token", function () {
async function deployContract() {
const [deployer, owner] = await ethers.getSigners();
const Contract = await ethers.getContractFactory("TokenSample");
const initAmount = 10000 * 10000;
const contract = await Contract.deploy(owner.address, initAmount);
await contract.deployed();
return { contract, deployer, owner, initAmount };
};
describe("deploy", function () {
it("owner", async function () {
const { contract, deployer, owner, initAmount } = await loadFixture(deployContract);
expect(await contract.owner()).to.equal(owner.address);
});
it("name", async function () {
const { contract, deployer, owner, initAmount } = await loadFixture(deployContract);
expect(await contract.name()).to.equal("Token Sample");
});
it("symbol", async function () {
const { contract, deployer, owner, initAmount } = await loadFixture(deployContract);
expect(await contract.symbol()).to.equal("TSC");
});
it("initAmount", async function () {
const { contract, deployer, owner, initAmount } = await loadFixture(deployContract);
expect(await contract.totalSupply()).to.equal(initAmount);
expect(await contract.balanceOf(owner.address)).to.equal(initAmount);
expect(await contract.balanceOf(deployer.address)).to.equal(0);
});
});
describe("onlyOwner", function () {
it("mint <owner>", async function () {
const { contract, deployer, owner, initAmount } = await loadFixture(deployContract);
const totalSupply = await contract.totalSupply();
const balanceOf = await contract.balanceOf(owner.address);
const mintAmount = 100;
await contract.connect(owner).mint(mintAmount);
expect(await contract.totalSupply()).to.equal(totalSupply.add(mintAmount));
expect(await contract.balanceOf(owner.address)).to.equal(balanceOf.add(mintAmount));
});
it("mint <not owner>", async function () {
const { contract, deployer, owner, initAmount } = await loadFixture(deployContract);
const mintAmount = 100;
await expect(contract.connect(deployer).mint(mintAmount)).to.rejected;
});
it("burn <owner>", async function () {
const { contract, deployer, owner, initAmount } = await loadFixture(deployContract);
const totalSupply = await contract.totalSupply();
const balanceOf = await contract.balanceOf(owner.address);
const burnAmount = 100;
await contract.connect(owner).burn(burnAmount);
expect(await contract.totalSupply()).to.equal(totalSupply.sub(burnAmount));
expect(await contract.balanceOf(owner.address)).to.equal(balanceOf.sub(burnAmount));
});
it("burn <not owner>", async function () {
const { contract, deployer, owner, initAmount } = await loadFixture(deployContract);
const burnAmount = 100;
await expect(contract.connect(deployer).burn(burnAmount)).to.rejected;
});
});
describe("deploy fail", function () {
it("deploy zero address", async function () {
const Contract = await ethers.getContractFactory("TokenSample");
const initAmount = 1000 * 1000;
const zeroAddress = "0x0000000000000000000000000000000000000000";
await expect(Contract.deploy(zeroAddress, initAmount)).to.rejected;
});
});
});
deployContract
は、共有のデプロイメソッドです。
loadFixture
を使用することで、デプロイの時間を短縮できるらしいです。
deploy
デプロイされたコントラクトの状態を確認
-
owner
オーナーが指定されたものか確認 -
name
nameが固定値と同じであるか確認 -
symbol
symbolが固定値と同じであるか確認 -
initAmount
初期コインがtotalSupplyに反映されて、オーナーアドレスに反映されているか確認
onlyOwner
オーナー以外が実行できない事を確認
-
mint <owner>
オーナーはmint可能であることを確認
mintされたコインがtotalSupplyに反映されて、オーナーアドレスに反映されているか確認 -
mint <not owner>
オーナー以外はmint不可能であることを確認 -
burn <owner>
オーナーはburn可能であることを確認
burnされたコインがtotalSupplyに反映されて、オーナーアドレスに反映されているか確認 -
burn <not owner>
オーナー以外はburn不可能であることを確認
deploy fail
-
deploy zero address
デプロイのオーナーにゼロアドレスを設定するとエラーになることを確認
テスト実行
以下のコマンドでテストを実行します。
npx hardhat test test/TokenSample.js
結果は以下となり、無事
> npx hardhat test test/TokenSample.js
Token
deploy
✔ owner (1255ms)
✔ name
✔ symbol
✔ initAmount
onlyOwner
✔ mint <owner> (53ms)
✔ mint <not owner> (58ms)
✔ burn <owner> (42ms)
✔ burn <not owner>
deploy fail
✔ deploy zero address (47ms)
9 passing (2s)
デプロイコード
scripts
フォルダにあるdeploy.js
の内容を以下のようにします。
async function main() {
const [deployer, owner] = await ethers.getSigners();
const Contract = await ethers.getContractFactory("TokenSample");
const initAmount = 10000 * 10000;
const contract = await Contract.deploy(owner.address, initAmount);
await contract.deployed();
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
main
メソッドでデプロイを実行しています。
これは、テストコードのdeployContract
と中身はほぼ同じです。
デプロイ
ローカルでhardhatを立ち上げ、それにコントラクトをデプロイします。
2つのターミナルが必要となります。
まず1つ目のターミナルで以下のコマンドを実行します。
npx hardhat node
そして、2つ目のターミナルで以下のコマンドを実行します。
--network localhost
としており、ローカルにデプロイします。
npx hardhat run scripts/deploy.js --network localhost
1つ目のターミナルの結果は以下のようになります。
> npx hardhat node
Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/
Accounts
========
Account #0: 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf (0 ETH)
Account #1: 0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF (0 ETH)
eth_accounts
eth_chainId
eth_accounts
eth_blockNumber
eth_chainId (2)
eth_estimateGas
eth_getBlockByNumber
eth_feeHistory
eth_sendTransaction
Contract deployment: TokenSample
Contract address: 0xf2e246bb76df876cef8b38ae84130f4f55de395b
Transaction: 0xac088e0922bdab7aa100541fccc75f5356711665159c890ff289dd00f797f8fe
From: 0x7e5f4552091a69125d5dfcb7b8c2659029395bdf
Value: 0 ETH
Gas used: 1709291 of 1709291
Block #1: 0x59fb23cae0c9a77b3564fc40d29e3c88cddf5f73b15eb86188d97901af8d3d41
eth_chainId
eth_getTransactionByHash
eth_chainId
eth_getTransactionReceipt
ノードのJSON-RPCは「 http://127.0.0.1:8545/ 」がエンドポイントとなり、
コントラクトアドレスは「0xf2e246bb76df876cef8b38ae84130f4f55de395b」
これで、他の言語からコントラクトを呼び出すことができます。
※ただし、1つ目のターミナルを終了させてしまうと全て消えてしまうので、デプロイを再度する必要があります。
まとめ
とりあえず、シンプルなERC20をデプロイすることが出来ました。
ローカル環境で起動する事もできたので、別システム(JavaとかNodeとか)からのアクセスにも対応できそうです。
ウォレットアドレスを別システムが管理している場合、デプロイのオーナーアドレスは別システムのアドレスに変更することで対応可能です。
ソースコードは以下にあります。
補足:Proxy環境
Proxy環境で実行したい場合、hardhatのバージョンを2.6.0
にする必要があるようです。
npm install --save-dev hardhat@2.6.0
以下の記事を参考にしました。
なんでバージョンが上がると機能が劣化するんだ・・・機能管理してないんだろうな・・・