スマートコントラクト開発ツールの Hardhat と Foundry を実際に使って比較してみた

はじめに

こんにちは、次世代デジタル基盤開発事業部の三島です。
普段は、スマートコントラクトの開発やバックエンドの開発に携わっています。

本記事では、はじめにHardhat、Foundryを使用したスマートコントラクトの開発手順(コントラクト作成→テスト→デプロイ→コントラクトのメソッド実行)を紹介しています。
おわりに、実際の開発を通じて気づいた、両者の長所とそうでない点をまとめました。
また、今後どちらを選択するかについての個人的な見解も書きました。

Hardhat

Hardhatは、Nomic Foundationによって開発されたスマートコントラクトの開発ツールです。
Hardhatでは、テストやスクリプトにJavaScript(TypeScript)を使用します。
また、プラグインが豊富にあるため、要件に応じてプラグインを組み合わせることが可能です。

hardhat.org

[参考] よく使用されているHardhatのプラグイン:

  • @openzeppelin/hardhat-upgrades - npm
    アップグレード可能なコントラクトをデプロイする機能を提供するライブラリ

  • hardhat-deploy - npm
    アップグレード可能なコントラクトのデプロイの他に、コントラクトの依存関係を考慮したデプロイなどの発展的なデプロイの仕組みを提供するライブラリ

プロジェクトストラクチャ

./
├── contracts/
├── node_modules/
├── scripts/
├── test/
├── README.md
├── hardhat.config.js
├── package-lock.json
└── package.json

contractsがSolidityのソースディレクトリ、hardhat.config.jsが設定ファイルで、ネットワークやデプロイに使用するアカウント等を設定します。

外部ライブラリの管理には、npmを使用します。

テスト

Hardhatでは、JavaScriptもしくはTypeScriptでテストコードを書きます。
スマートコントラクトを操作するためのライブラリにethers.js、テスト用のライブラリにMocha、chaiを使用しています。
JavaScriptでの開発に慣れている方でしたら、スムーズにテストコードの作成に取り掛かれると思います。

テスト対象のスマートコントラクト contracts/MyNFT.js

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";

contract MyNFT is ERC721 {
  constructor() ERC721("My NFT", "MN") {}

  function mint(address to, uint256 tokenId) public {
    _mint(to, tokenId);
  }
}

※ 普段の業務では、TypeScriptを使用していますが、本記事ではコードサンプルにJavaScriptを使用しています。

テストコード test/MyNFT.js

const {
  loadFixture,
} = require("@nomicfoundation/hardhat-toolbox/network-helpers");
const { expect } = require("chai");

describe("MyNFT", () => {
  async function deployMyNFTFixture() {
    const [user] = await ethers.getSigners();

    const MyNFT = await ethers.getContractFactory("MyNFT");
    const myNFT = await MyNFT.deploy();

    return { user, myNFT };
  }

  describe("Mints", function () {
    it("Should mint token", async () => {
      const { user, myNFT } = await loadFixture(deployMyNFTFixture);

      await expect(myNFT.mint(user.address, 50)).to.changeTokenBalances(
          myNFT,
          [user],
          [1]
      );
    });

    it("Should emit Transfer events", async () => {
      const { user, myNFT } = await loadFixture(deployMyNFTFixture);

      await expect(myNFT.mint(user.address, 1))
          .to.emit(myNFT, "Transfer")
          .withArgs(ethers.ZeroAddress, user.address, 1);
    });
  });
});

デプロイ

スクリプトを使用して、デプロイします。

const hre = require("hardhat");

async function main() {

  const myNFT = await hre.ethers.deployContract("MyNFT");

  await myNFT.waitForDeployment();

  console.log(`MyNFT deployed to ${myNFT.target}`);
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

実際に、ローカルネットワークにデプロイしてみます。
今回は、Hardhatのビルトインネットワーク(Hardhat Network)を使用して、ローカルネットワークを作成し、デプロイします。

ローカルネットワークを起動

$ npx hardhat node
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: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 (10000 ETH)
Private Key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

...

networkにlocalhostを指定し、ローカルネットワークにコントラクトをデプロイします。
デプロイに成功すると、コントラクトのアドレスが出力されます。

$ npx hardhat run --network localhost scripts/deploy.js
MyNFT deployed to 0x5FbDB2315678afecb367f032d93F642f64180aa3```

Hardhat コンソール

Hardhat コンソールでは、ethersを利用してスマートコントラクトを操作することができます。

$ npx hardhat console --network localhost
Welcome to Node.js v18.17.0.
Type ".help" for more information.
# ↑でデプロイしたコントラクトのインスタンスを取得
> const myNFT = await hre.ethers.getContractAt("MyNFT", "0x5FbDB2315678afecb367f032d93F642f64180aa3")
undefined
> await myNFT.name()
'My NFT'

Foundry

Foundryは、rustによって記述されたスマートコントラクトの開発ツールです。
Hardhatよりも新しく、Solidityを使用してテストコードやスクリプトを作成することができます。

book.getfoundry.sh

プロジェクトストラクチャ

./
├── lib/
├── script/
├── src/
├── test/
├── README.md
└── foundry.toml

srcがSolidityのソースディレクトリ、foundry.tomlが設定ファイルで、ネットワークやプロファイルごとの設定を記載します。

Foundryでは、forgeを使用してライブラリをインストールします。インストールしたSolidityのライブラリは、lib/に格納されます。

テスト

Foundryでは、Solidityを使用してテストコードを書きます。

テスト対象のコードは、先ほど使用したMyNFT.solです。

テストコード test/MyNFT.t.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;

import {Test, console2} from "forge-std/Test.sol";
import {MyNFT} from "../src/MyNFT.sol";

contract MyNFTTest is Test {
    MyNFT public myNFT;

    address user = makeAddr("user");

    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);

    function setUp() public {
        myNFT = new MyNFT();
    }

    function test_Mint_TokenBalance() public {
        myNFT.mint(user, 1);

        assertEq(myNFT.balanceOf(user), 1);
    }

    function test_Mint_Event() public {
        // 期待されるEventを定義
        vm.expectEmit(true, true, true, true);
        emit Transfer(address(0), user, 1);

        // ↑のEventを発生させる処理を実行
        myNFT.mint(user, 1);
    }
}

Foundryのテストでは、Eventのアサートが初見の場合、???となると思います。
Eventがemitされる処理の前に、vm.expectEmit(...)emit Event(...)を書き、期待するEventを前もって定義します。
vm.expectEmit(...) の第1引数から第3引数は、期待するEventの第1引数から第3引数をそれぞれ比較の対象にするかを表しています。
第4引数は、Eventのデータ部(4番目以降の引数)を比較の対象にするかを表しています。
例えば、vm.expectEmit(true, true, false, false) の場合、Eventの第1引数と第2引数のみ比較の対象とします。

実際にFoundryのテストを書いてみて、最も衝撃を受けたのは実行速度でした。速すぎる!!!!!?
以前のプロジェクトで、60件以上のテストケースを、1秒前後で全て実行することができました。

他にも、Fuzz TestingやInvariant Testing等の発展的なテストメソッドも提供しています。
[参考] Foundryの発展的なテスト方法のリンク: Advanced Testing - Foundry Book

デプロイ

Foundryでは、基本的なデプロイの場合、コマンドラインからデプロイできます。
プロキシパターンを実装したコントラクト等の一手間混んだデプロイについては、Solidityでスクリプトを作成してデプロイをおこなうことができます。

今回は、ローカルネットワークにデプロイします。
Foundryでは、ローカルネットワークのツールとして、Anvilが提供されています。HardhatでのHardhat Networkに相当するものです。

ローカルネットワークを起動

$ anvil


                             _   _
                            (_) | |
      __ _   _ __   __   __  _  | |
     / _` | | '_ \  \ \ / / | | | |
    | (_| | | | | |  \ V /  | | | |
     \__,_| |_| |_|   \_/   |_| |_|

    0.2.0 (5be158b 2023-10-02T00:23:45.472182000Z)
    https://github.com/foundry-rs/foundry

Available Accounts
==================

(0) "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" (10000.000000000000000000 ETH)
(1) "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" (10000.000000000000000000 ETH)

...

Listening on 127.0.0.1:8545

ローカルネットワークの起動が完了したら、forge create <コントラクト名> のコマンドで、コントラクトをデプロイします。

$ forge create MyNFT --rpc-url http://127.0.0.1:8545 --private-key <署名に使用するプライベートキー>
[⠢] Compiling...
No files changed, compilation skipped
Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Transaction hash: 0xe190879b5a0dba14d90d27677a564c5a3b0d722cfa159b1e64144b500553c4a2

デプロイに成功すると、コントラクトのアドレスが出力されます。

Cast

Foundryでは、ネットワークへのリクエストやコントラクトの操作に、Castを使用します。
※ 頻繁に実行するメソッドや複雑な操作は、スクリプトを作成すると便利です。

コントラクトの name() を実行する

# cast call <コントラクトのアドレス> <メソッドのシグネイチャ> <引数1> <引数2> ...
$ cast call 0x5FbDB2315678afecb367f032d93F642f64180aa3 "name()(string)" --rpc-url http://127.0.0.1:8545

出力

My NFT

コントラクトの mint(to, tokenId) を実行する

# cast send <コントラクトのアドレス> <メソッドのシグネイチャ> <引数1> <引数2> ...
$ cast send 0x5FbDB2315678afecb367f032d93F642f64180aa3 "mint(address, uint256)" 0x5FbDB2315678afecb367f032d93F642f64180aa3 1  --rpc-url http://127.0.0.1:8545 --private-key <署名に使用するプライベートキー>

出力

blockHash               0x3c2779bb2e3290c21992f9261f3a6cdc3babe5a3acdb025d466c240587bc5559
blockNumber             2
...
transactionHash         0xda6b2031587374506a1391722e95f02a7e0571674fafd971f25c6534caf45bdd
transactionIndex        0
type                    2

トランザクションの送信に成功すると、レスポンスが出力されます。

まとめ

Hardhat

良いと思ったところ😄
  • プライグインが豊富にあるので、要件に応じてライブラリを柔軟に組み合わせることができる
  • JavaScript(TypeScript)を使用して、テストコードやスクリプトを柔軟に記述できる
  • Hardhat コンソールが便利
気になったところ🤔
  • テストの実行に時間がかかる

Foundry

良いと思ったところ😄
  • テストの実行速度が非常に速い
  • テスト実行時のメソッドの呼び出し階層の出力が見やすく、テストの修正やエラーの原因の特定が容易
  • npmで外部ライブラリを管理する必要がないので、プロジェクトの依存関係がシンプルになる (Solidityのライブラリだけ管理すればよい)
気になったところ🤔
  • Solidity自体が表現力に富んだ言語ではないので、テストコードやスクリプトの記述が冗長になることがある

HardhatとFoundryの長所とそうでないところをそれぞれ列挙してみました。
個人的に、今後はFoundryを使っていこうと思っています。
理由として、npmでライブラリを管理する必要がなく、テストコードの実行速度が速くて、テスト作成時の検証・修正をスムーズにおこなえる点があります。

また、HardhatとFoundryを併用することもできるため、必要に応じてお互いの機能を取り入れることもできます。
併用することで、テストコードはSolditityで書いて、デプロイスクリプトはTypeScriptで書くということも可能です。
※ 併用した場合、依存ライブラリが多くなる点や管理対象のファイルが増える点は考慮しておく必要があります。

[参考] HardhatとFoundryを併用する方法を記載したリンク:
Integrating with Foundry | Ethereum development environment for professionals by Nomic Foundation

おわりに

最後まで読んでいただきありがとうございました。 今後も、Web3.0に関連する記事を投稿していきたいと思います!

テコテックの採用活動について

テコテックでは新卒採用、中途採用共に積極的に募集をしています。

採用サイトにて会社の雰囲気や福利厚生、募集内容をご確認いただけます。

ご興味をいただけましたら是非ご覧ください。

www.tecotec.co.jp