0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

いまさらHardhatでERC20

Posted at

はじめに

諸事情により、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の設定を加えます。
gasPrice0とすることで、コントラクトのGAS代がゼロになります。
initialBaseFeePerGas0にすることで、最初のブロック生成のGAS代もゼロとなります。
また、accountsを適当に2つ作ります。
GAS代がゼロなのでbalanceは不要となるので0としています。

hardhat.config.js
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)を追加する、ただし実行可能なのはオーナーのみとする。
TokenSample.sol
// 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.solOwnable.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を作成します。

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の内容を以下のようにします。

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つ目のターミナルの結果は以下のようになります。

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

以下の記事を参考にしました。

なんでバージョンが上がると機能が劣化するんだ・・・機能管理してないんだろうな・・・

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?