【2024年版】HardhatでERC20トークンを発行してみる


本記事では、Hardhatを用いてEthereum上で独自トークンのコントラクトを発行する手順を記載します。 

:::note info
本記事は2024年1月時点の公式ドキュメントを基に記述しています。
開発の際はなるべく公式ドキュメントから最新情報を参照することをおすすめします。
:::
# 背景
筆者の知る限り、スマートコントラクト開発をするうえで広く使われているフレームワークは、今回使用するHardhatと、MetaMaskやInfura等でも知られるConsensysが開発した「Truffle(とGanache)」だったように思います。
しかし2023年9月に、Consensysは**TruffleとGanacheを終了し、Hardhatへの移行のサポートを開始**すると発表しました。

https://consensys.io/blog/consensys-announces-the-sunset-of-truffle-and-ganache-and-new-hardhat?utm_source=github&utm_medium=referral&utm_campaign=2023_Sep_truffle-sunset-2023_announcement

そんなわけでHardhatの重要性は今後さらに高まることが予想される（すでに高まっている）ので、改めて勉強する意図も含め記事の執筆に至りました。
# 動作環境
- Node.js 20.11.0
- Hardhat 2.19.4
- Solidity 0.8.20
 

# 事前準備
1. [MetaMask(ブラウザ拡張機能)のインストール](https://metamask.io/download/)
1. [Alchemyへのユーザー登録](https://auth.alchemy.com/signup)
1. [AlchemyのコンソールからSepoliaネットワークで作成したアプリの、API KEYとURLの取得](https://docs.alchemy.com/docs/how-to-deploy-a-smart-contract-to-the-sepolia-testnet)
1. [FaucetからSepoliaETHの取得](https://sepoliafaucet.com/)
# Hardhatプロジェクトの構築
※Hardhat公式サイトのQuickstartに可能な限り則って手順を進めていきます。
まずプロジェクト用フォルダを作成し、移動します。
```
mkdir HardhatProject
cd HardhatProject
```
Hardhatをnpm installします。
```
npm install --save-dev hardhat
```
Hardhatプロジェクトを作成します。
```
npx hardhat init
```
いろいろ聞かれますが、**すべてデフォルトのままEnterを入力します。** そうすると以下の設定になっているはずです。
- What do you want to do? → Create a JavaScript project 
- Hardhat project root: → 作成したプロジェクトのルート
- Do you want to add a .gitignore? (Y/n) → y
- Do you want to install this sample project's dependencies with npm (hardhat @nomicfoundation/hardhat-toolbox)? (Y/n) → 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.19.4

√ What do you want to do? · Create a JavaScript project 
√ Hardhat project root: · C:\Users\shiomi\HardhatProject
√ Do you want to add a .gitignore? (Y/n) · y
√ Do you want to install this sample project's dependencies with npm (hardhat @nomicfoundation/hardhat-toolbox)? (Y/n) · y
```
:::note warn
ここではhardhat-toolboxをインストールしています。
hardhatには機能を拡張するプラグインが複数存在しますが、hardhat-toolboxは以下のプラグインをまとめたパッケージのようです。
- hardhat-ethers
- hardhat-chai-matchers
- hardhat-network-helpers
- hardhat-verify
- hardhat-gas-reporter
- solidity-coverage
- Typechain

hardhat-ethersやhardhat-waffleといった、開発を行う上でほぼ使うであろうプラグインは、現在hardhat-toolboxに含まれています。
個別にインストールする必要はありません。
:::
https://hardhat.org/hardhat-runner/plugins/nomicfoundation-hardhat-toolbox

Hardhatプロジェクトの作成が完了した直後は、以下のプロジェクト構成になっていると思います。

HardhatProject/
　├ contracts/
　 　└ Lock.sol
　├ node_modules/
　├ scripts/
　 　└ deploy.js
　├ test/
 　 　└ test.js
　├ .gitignore
　├ hardhat.config.js
　├ package-lock.json
　├ package.json
　├ README.md

 このうち、Lock.sol、deploy.js、test.jsは不要なので削除をおすすめします(サンプルプロジェクト用のコードです)。

# コントラクト記述
独自のERC20トークン発行に使用するOpenZeppelinのライブラリをインストールします。
```
npm install @openzeppelin/contracts
```
contractsフォルダの配下にファイルを作成し（今回はNewToken.solとしています）、以下のコードをコピペします。
ライセンス記述は省略しています。
```solidity:NewToken.sol
pragma solidity ^0.8.20;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract NewToken is ERC20{
    address public owner;
    constructor(uint initialSupply) ERC20("NewToken", "NEW") {
        owner = msg.sender;
        _mint(owner,initialSupply);
    }
    
    function Mint(address _to,uint _amount) public {
    _mint(_to,_amount);
    emit MintEvent(_to,_amount);
    }

    event MintEvent(address indexed to,uint256 indexed amount);
}
```
コントラクトをコンパイルします。
```
npx hardhat compile
```
コンパイルが完了すると、artifactsフォルダ、cacheフォルダが生成されます。

:::note warn
プロキシを使用している場合、コンパイラのダウンロードが行えず上記のコマンドが失敗するかもしれません。そのときはコンパイルより先に環境変数HTTP_PROXYを設定する必要があります。
```
set HTTP_PROXY=http://{ユーザー名}:{パスワード}@{ホスト名}:{ポート番号}
```
:::
# コントラクトのテスト
testフォルダの配下にファイルを作成し（今回はNewTokenTest.jsとしています）、以下のコードをコピペします。
```solidity:NewTokenTest.sol
const {loadFixture} = require("@nomicfoundation/hardhat-toolbox/network-helpers");
const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("Fixture", function () {

    // 各テストで使いまわす処理(コントラクトのデプロイなど)はひとつの関数にまとめ、適宜loadFixture()で呼び出す
    async function deployFaucetFixture() {
        // アカウントを2個取得
        const [owner, otherAccount] = await ethers.getSigners();
        const ownerAddress =await  owner.getAddress();
        const otherAddress =await  otherAccount.getAddress();
        // トークンの初期供給量を1000に設定し、ownerアカウントでコントラクトをデプロイする
        const initialSupply = 1000;
        const NewToken = await ethers.getContractFactory("NewToken");
        const newToken = await NewToken.deploy(initialSupply);
        const newTokenAddress =newToken.address;
        return { newToken, newTokenAddress,initialSupply, owner, ownerAddress,otherAccount ,otherAddress};
    }


    // デプロイに関するテスト
    describe("Deployment", function () {
        // ownerに1000トークン付与されているか
        it("Should mint owner 1000 tokens when deployed", async function () {
            const { newToken,  ownerAddress, initialSupply} = await loadFixture(deployFaucetFixture);
            const ownerBalance =await newToken.balanceOf(ownerAddress)
            // console.log("ownerBalance:"+ownerBalance);
            await expect(ownerBalance).to.equal(initialSupply);
        });
        // ownerに適切な権限付与がなされているか
        it("Should set the right owner", async function () {
            const { newToken, ownerAddress } = await loadFixture(deployFaucetFixture);
            expect(await newToken.owner()).to.equal(ownerAddress);
        });
    });
    // 関数Mint()に関するテスト
    describe("Mint", function () {
        // ownerがMintを呼び出した時、指定した数量だけownerにトークンが付与されるか
        it("Should mint to owner", async function () {
            const { newToken, ownerAddress} = await loadFixture(deployFaucetFixture);
            const mintValue = 100;
            await expect(newToken.Mint(ownerAddress,mintValue)).to.changeTokenBalance(newToken,ownerAddress,mintValue);
        });
        // otherAccountがMintを呼び出した時、指定した数量だけotherAccountにトークンが付与されるか
        it("Should mint to otherAccount", async function () {
            const { newToken, otherAddress} = await loadFixture(deployFaucetFixture);
            const mintValue = 100;
            await expect(newToken.Mint(otherAddress,mintValue)).to.changeTokenBalance(newToken,otherAddress,mintValue);
        });
        // 関数Mint()を呼び出した時、イベントMintEvent()が呼び出されるか
        it("Should emit 'mint' event", async function () {
            const { newToken, ownerAddress } = await loadFixture(deployFaucetFixture);
            const mintValue = 100;
            await expect(newToken.Mint(ownerAddress,mintValue)).to.emit(newToken,"MintEvent").withArgs(ownerAddress,100) ;
        });
    });
});
```
テストを実行します。
```
npx hardhat test
```
テストがすべて成功すると以下のような表示が出てきます。
```
  Fixture
    Deployment
      ✔ Should mint owner 1000 tokens when deployed (1470ms)
      ✔ Should set the right owner
    Mint
      ✔ Should mint to owner
      ✔ Should mint to otherAccount
      ✔ Should emit 'mint' event


  5 passing (2s)
```


# テストネットへのデプロイ
scriptsフォルダの配下にファイルを作成し（今回はNewTokenDeploy.jsとしています）、以下のコードをコピペします。
```solidity:NewTokenDeploy.js
const { ethers } = require("hardhat");

async function main() {
    const initialSupply = 1000000;
    const NewToken = await ethers.getContractFactory("NewToken");
    const newToken = await NewToken.deploy(initialSupply);
    await newToken.waitForDeployment();
    console.log("deployment succeeded.")
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
```
まずはHardhatが用意してくれているローカルネットワークにデプロイして、コードが正常に動作するか確認します。
```
npx hardhat run scripts/NewTokenDeploy.js
```
特にエラーが発生することなく、console.logで仕込んだメッセージ(deployment succeeded.)が表示されればOKです。

次にコントラクトをSepoliaテストネットへデプロイするための準備を行います。
事前準備で取得したAPI KEYとURL、MetaMaskの秘密鍵はここで必要になります。API KEYと秘密鍵に関しては、dotenvで管理するか、Hardhatが用意するvarsオブジェクトでの管理を行うとよいでしょう。

https://hardhat.org/hardhat-runner/docs/guides/configuration-variables

今回はvarsオブジェクトでの管理をしていきます。
まずAPI KEYを環境変数（今回はALCHEMY_API_KEYとしています）にセットします。
```
npx hardhat vars set ALCHEMY_API_KEY
√ Enter value: · {API KEYを入力}
```
秘密鍵も環境変数（今回はACCOUNT_PRIVATE_KEYとしています）にセットします。
```
npx hardhat vars set ACCOUNT_PRIVATE_KEY
√ Enter value: · {秘密鍵を入力}
```
以下のコマンドで、入力した環境変数の値が適切にセットされているか確認できます。もしミスがあった場合には、再度上記のコマンドで環境変数をセットします。
```
npx hardhat vars get ALCHEMY_API_KEY
npx hardhat vars get ACCOUNT_PRIVATE_KEY 
```
次に、hardhatの設定ファイルににSepoliaテストネットへ接続するための情報を追記します。
hardhat.config.jsに以下のコードをコピペします。
```
require("@nomicfoundation/hardhat-toolbox");
const { vars } = require("hardhat/config");

/** @type import('hardhat/config').HardhatUserConfig */

// vars.get()で、セットした環境変数を取得できる
const ALCHEMY_API_KEY = vars.get("ALCHEMY_API_KEY");
const ACCOUNT_PRIVATE_KEY = vars.get("ACCOUNT_PRIVATE_KEY");
module.exports = {
  solidity: "0.8.20",
  networks: {
    sepolia: {
      // {Alchemyのコンソールで取得したURL}${ALCHEMY_API_KEY}となる
      url: `https://eth-sepolia.g.alchemy.com/v2/${ALCHEMY_API_KEY}`,
      accounts: [ACCOUNT_PRIVATE_KEY],
    }
  }
};
```
準備が整ったので、Sepolaテストネットにデプロイしてみます。

:::note warn
このとき、入力した秘密鍵に対応するアカウントからガス代が支払われます。残高不足のエラーを回避するために、最低一回はFaucetからSepoliaETHを取得しておくことに注意が必要です。
:::
```
npx hardhat run scripts/NewTokenDeploy.js  --network sepolia
```


Sepolia Etherscanから、デプロイが完了したことを確認します。
MetaMaskのブラウザ拡張機能を開き、右上のメニューから「エクスプローラーで表示」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3673338/6e1d9141-f516-3d37-e735-cc59cf96fe94.png)

Sepolia EtherScanのサイトへ遷移するので、トランザクション一覧から「Contract Creation」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3673338/4d90b81d-c27e-658e-d5e7-e6bb572c961e.png)

出てくるのが、今回デプロイした独自トークンのコントラクトです。
ここまでの手順で正常にデプロイされたことは確認できたのですが、せっかくなのでMetaMask上で独自トークンを表示させます。
そのために独自トークンのコントラクトアドレスをコピーします(画面に表示されているコントラクトアドレスの右側のボタンを押すと自動でコピーできます)。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3673338/089155ea-06e1-4477-193a-2457280514ff.png)
MetaMaskへ戻り、「トークンをインポート」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3673338/28a33c19-6857-dab3-655b-f231cdb4637e.png)
「トークンコントラクトアドレス」欄にコピーしたコントラクトアドレスをペーストします。
ほかの欄は自動で入力されるので、そのまま「次へ」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3673338/bc2c628e-0ea9-e1ac-f819-f2c58009ef41.png)
「インポート」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3673338/aec488e8-5954-49d3-c08a-fc202cddd34e.png)
これで、MetaMask上で独自トークンを表示させることができました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3673338/1252b973-c44e-68ec-f7ee-ed37729d3b63.png)

以上で作業は終了です。

2022年にTruffle + Ganacheを用いて、今回と同じようなチュートリアルレベルの開発を試したこともあるのですが、それと比べるとずいぶん楽に開発できる印象を受けました。
特にローカルネットワークをほぼ何の準備もなしに構築できる点が最高でした。



# 参考
- 公式チュートリアル。
 
https://hardhat.org/tutorial 

- OpenZeppelinの公式ドキュメント(ERC20に関する部分)。
 
https://docs.openzeppelin.com/contracts/5.x/api/token/erc20#IERC20-approve-address-uint256-

- Hardhatのチュートリアルに沿って記述された記事。特にテスト部分について詳細に書かれているので、より詳しく知りたい方におすすめ。

 https://qiita.com/ogataso/items/bad0da9bd95ad95d0283#%E3%83%86%E3%82%B9%E3%83%88%E3%83%8D%E3%83%83%E3%83%88%E3%81%B8%E3%81%AE%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4