Hardhatで始めるスマートコントラクト開発

今回はSolidityの開発環境であるHardhatを使用して、開発環境のセットアップを行います。 Truffleなどと並んで有名な開発環境です。 OpenZeppelinやUniswapなどでも使用されています。
2022.06.30

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Hardhatとは

HardhatはSolidtyの開発環境です。 コントラクトのテストをローカルで実行できたり、果てはコントラクトのデプロイまで、Solidityでスマートコントラクトを開発するのに便利なツールです。 個人的な推しポイントはデバッグの簡単さです。 EVM上のスタックトレースの表示やプリントでバッグなどの機能が便利です。

似たようなものとしては、Truffle Suiteが該当します。

使ってみる

今回はHardhatの環境を立ち上げてみます。 開発が始められるところまでがゴールです。

インストール

インストール

$ mkdir hardhat
$ cd hardhat
$ npm init -y
$ npm install --save-dev hardhat

プロジェクトの作成

プロジェクト作成

$ npx hardhat
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.9.9 ?‍

✔ What do you want to do? · Create a basic sample project
✔ Hardhat project root: · /path/to/project/of/hardhat
✔ Do you want to add a .gitignore? (Y/n) · y
✔ Help us improve Hardhat with anonymous crash reports & basic usage data? (Y/n) · false
✔ Do you want to install this sample project's dependencies with npm (@nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers)? (Y/n) · y

これで先程作成したhardhatディレクトリにファイルが展開されます。

ディレクトリ構造

hardhat
├── README.md
├── contracts
│   └── Greeter.sol # コントラクト
├── hardhat.config.js # 設定ファイル
├── package-lock.json
├── package.json
├── scripts
│   └── sample-script.js # デプロイ用のスクリプト
└── test
    └── sample-test.js # テストコード

今回はサンプルプロジェクトを作成するように選択したので、Greeterというコントラクトが用意されています。

コントラクトの中身

Greeter.sol

//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

import "hardhat/console.sol";

contract Greeter {
    string private greeting;

    constructor(string memory _greeting) {
        console.log("Deploying a Greeter with greeting:", _greeting);
        greeting = _greeting;
    }

    function greet() public view returns (string memory) {
        return greeting;
    }

    function setGreeting(string memory _greeting) public {
        console.log("Changing greeting from '%s' to '%s'", greeting, _greeting); // Hardhatのローカルチェーンで実行した場合にのみ表示される。
        greeting = _greeting;
    }
}

Solidityについては今回は深く触れませんが、以下のような機能を持ったコントラクトです。

  • greet()を呼び出すとコントラクト内に保存された文字列が取得できる。
  • setGreeting()を呼び出すと文字列をセットできる。

テストを回してみる

テストを実行する

$ npx hardhat test
Downloading compiler 0.8.4
Compiled 2 Solidity files successfully


  Greeter
Deploying a Greeter with greeting: Hello, world!
Changing greeting from 'Hello, world!' to 'Hola, mundo!'
    ✔ Should return the new greeting once it's changed (669ms)


  1 passing (671ms)

無事にテストが通ったみたいです。

sample-test.js

const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("Greeter", function () {
  it("Should return the new greeting once it's changed", async function () {
    // コントラクトのデプロイ
    const Greeter = await ethers.getContractFactory("Greeter");
    const greeter = await Greeter.deploy("Hello, world!");
    await greeter.deployed();

    // 初期設定が期待通りか検証
    expect(await greeter.greet()).to.equal("Hello, world!");

    // 値を変更
    const setGreetingTx = await greeter.setGreeting("Hola, mundo!");

    // トランザクションが通るまで待機
    await setGreetingTx.wait();

    // 値が変わっているか検証
    expect(await greeter.greet()).to.equal("Hola, mundo!");
  });
});

テストコードはJavaScriptで書かれています。 テストフレームワークはMochaとChaiの拡張です。 ethereumクライアントはether.jsです。

テストコードとしては、ブロックチェーン上にデプロイされたコントラクトをether.jsクラアントを使用して呼び出し、その結果を検証するといった形です。

テストはHardhatのローカルで起動可能なブロックチェーンを利用しています。 なのでガス代は無料で使い放題です(お得!)。 ブロックチェーンはテスト起動時に初期化されるので気をつけてください。

デプロイする

ノードの起動

今回はHardhatに付属しているブロックチェーン上にデプロイします。 これは外部のチェーンとは関係ない独立した開発用のチェーンです。

ノードの起動

Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/

Accounts
========

WARNING: These accounts, and their private keys, are publicly known.
Any funds sent to them on Mainnet or any other live network WILL BE LOST.

Account #0: 0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA (10000 ETH)
Private Key: 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

.
.
.

Account #19: 0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB (10000 ETH)
Private Key: 0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb

WARNING: These accounts, and their private keys, are publicly known.
Any funds sent to them on Mainnet or any other live network WILL BE LOST.

ノードが起動しました。 127.0.0.1:8545がノードのアドレスとポートです。

起動時に20個のアドレスが払い出されています。 このPrivateKeyを使用すればHardhatのローカルチェーンで活動ができます。 このアドレス他の人に知られているのでメインネットなどで使い回さないように気をつけてください。

最初は10000 ETH持っています。 これは、ローカルなので意味はないですが、メインネットでこれだけ持っていたら一生ガス代に困りませんね...

デプロイする

デプロイ

$ npx hardhat run --network localhost scripts/sample-script.js 
Greeter deployed to: 0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

無事デプロイされました。 --networkオプションを指定することで、設定ファイルに書かれた任意のネットワークに切り替えて実行できます。

ノード側のログ

web3_clientVersion (2)
eth_accounts
eth_chainId
eth_blockNumber
eth_chainId (2)
eth_estimateGas
eth_getBlockByNumber
eth_feeHistory
eth_sendTransaction
  Contract deployment: Greeter
  Contract address:    0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
  Transaction:         0xDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD
  From:                0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
  Value:               0 ETH
  Gas used:            497026 of 497026
  Block #1:            0xEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE

  console.log:
    Deploying a Greeter with greeting: Hello, Hardhat!

eth_chainId
eth_getTransactionByHash
eth_chainId
eth_getTransactionReceipt

色々表示されていますね。 すべてノードのAPIコールのログです。

eth_sendTransactionの部分にトランザクションの詳細が表示されています。

デプロイスクリプト

scripts/sample-script.js

// このスクリプトはhardhatコマンドを通して実行する必要があります。
const hre = require("hardhat");

async function main() {
  // Hardhatは実行前にコンパイルを行ってくれます。
  // 明示的に行いたい場合は以下の用にしてください。
  // await hre.run('compile');

  
  const Greeter = await hre.ethers.getContractFactory("Greeter");
  const greeter = await Greeter.deploy("Hello, Hardhat!");

 // コントラクトのデプロイ
  await greeter.deployed();

 // コントラクトのアドレスを表示
  console.log("Greeter deployed to:", greeter.address);
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

デプロイスクリプトは上記のようなものです。 今回はデプロイだけが書かれていますが、コントラクトの初期設定などもここに書くと便利です。

まとめ

Hardhatを使用することで簡単にSolidityの開発環境を用意することができました。 また、機会があれば簡単なコントラクトの開発の記事も書いてみようと思います。