いまさらHardhatでERC20

# はじめに

諸事情により、ERC20を実装する必要が出てきたので、いまさらですがHardhatで開発環境を作成してテストまでやります。

# Nodeのインストール

Nodeがインストールされていない場合、以下から、Nodeをダウンロードしてインストールします。
インストールの詳細は割愛します。

https://nodejs.org/ja/

# プロジェクトの作成

## hardhatパッケージのインストール

```shell
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環境をインストールします。

```shell
npx hardhat
```

以下のように表示されるので、「Create a JavaScript project」を選択して「Enter」を押下します。
他に変更する場合はカーソルで選択できます。


```shell
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」にします。

```shell
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ゼロ環境

**<font color="red">※※※ 通常は推奨されないようです。 ※※※</font>**

別のシステムが生成したウォレットアドレス（公開鍵）を使用してコントラクトを操作していると、GAS代が足りないというエラーが頻発して~~うっざ~~、GAS代を必要とするコントラクト操作をする為にウォレットアドレスに`ETH`を譲渡してとかやるのが面倒くさいので、GAS代ゼロでも動くように`hardhat.config.js`を変更します。

追加したのは`networks`の部分です。
テストなどで使う`hardhat`の設定を加えます。
`gasPrice`を`0`とすることで、コントラクトのGAS代がゼロになります。
`initialBaseFeePerGas`を`0`にすることで、最初のブロック生成のGAS代もゼロとなります。
また、`accounts`を適当に２つ作ります。
GAS代がゼロなので`balance`は不要となるので`0`としています。

```js: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](https://github.com/OpenZeppelin/openzeppelin-contracts)がERC20を実装しているSolidityを提供しているので、それを使います。
また、以下の機能を実装します。
- コンストラクタでオーナーアドレスと初期トークン数を指定できるようにする
- 増資メソッド（mint）を追加する、ただし実行可能なのはオーナーのみとする。
- 償却メソッド（burn）を追加する、ただし実行可能なのはオーナーのみとする。


```solidity: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**
２つ`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`を作成します。

```javascript: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**<br>オーナーが指定されたものか確認
- **name**<br>nameが固定値と同じであるか確認
- **symbol**<br>symbolが固定値と同じであるか確認
- **initAmount**<br>初期コインがtotalSupplyに反映されて、オーナーアドレスに反映されているか確認

**onlyOwner**
オーナー以外が実行できない事を確認
- **mint &lt;owner&gt;**<br>オーナーはmint可能であることを確認<br>mintされたコインがtotalSupplyに反映されて、オーナーアドレスに反映されているか確認
- **mint &lt;not owner&gt;**<br>オーナー以外はmint不可能であることを確認
- **burn &lt;owner&gt;**<br>オーナーはburn可能であることを確認<br>burnされたコインがtotalSupplyに反映されて、オーナーアドレスに反映されているか確認
- **burn &lt;not owner&gt;**<br>オーナー以外はburn不可能であることを確認

**deploy fail**
- **deploy zero address**<br>デプロイのオーナーにゼロアドレスを設定するとエラーになることを確認

# テスト実行

以下のコマンドでテストを実行します。

```sh
npx hardhat test test/TokenSample.js
```

結果は以下となり、無事

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

```javascript: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を立ち上げ、それにコントラクトをデプロイします。

２つのターミナルが必要となります。

まず１つ目のターミナルで以下のコマンドを実行します。

```powershell
npx hardhat node
```

そして、２つ目のターミナルで以下のコマンドを実行します。
`--network localhost`としており、ローカルにデプロイします。

```powershell
npx hardhat run scripts/deploy.js --network localhost
```

１つ目のターミナルの結果は以下のようになります。

```powershell:１つ目のターミナル
> 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」

これで、他の言語からコントラクトを呼び出すことができます。

※ただし、１つ目のターミナルを終了させてしまうと全て消えてしまうので、デプロイを再度する必要があります。


# まとめ

とりあえず、シンプルなERC20をデプロイすることが出来ました。
ローカル環境で起動する事もできたので、別システム（JavaとかNodeとか）からのアクセスにも対応できそうです。
ウォレットアドレスを別システムが管理している場合、デプロイのオーナーアドレスは別システムのアドレスに変更することで対応可能です。

ソースコードは以下にあります。

https://github.com/tnakagawa/hardhatsample


# 補足：Proxy環境

Proxy環境で実行したい場合、hardhatのバージョンを`2.6.0`にする必要があるようです。

```powershell
npm install --save-dev hardhat@2.6.0
```

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

https://qiita.com/oatnnimi/items/5145b1cff0367835672f

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