👽

わたしの Solidity 開発で最初にやっておくこと with hardhat

2022/07/11に公開

はじめに

Solidity を利用したスマートコントラクト開発について何回か触れてきましたが、各言語各フレームワークにあるような初期構築に関する記事はあまりなかったので、せっかくなので自分がいつもやっていることをベースに紹介してみようかと思いました。
(僕の考えた最強の...とまでは思えていないので、そこまで期待せずによろしくお願いします...)

基本的に Hardhat を使うことが多いので、Hardhat をベースとした環境整備を行っていきます。

https://hardhat.org/

基本編

まず最低限ここまでやると良いのでは、という点を紹介していきたいと思います!

プロジェクトの作成

Hardhat にも他フレームワーク同様ボイラープレート作成機能があるので、一歩目としてこちらを利用します。

https://hardhat.org/advanced/building-plugins#using-the-hardhat-typescript-plugin-boilerplate

npx hardhat initでボイラープレート作成機能を起動し、対話形式で設定を入力していきます。
以下のように解答を選択してください。

  • What do you want to do? -> Create an advanced sample project that uses TypeScript
  • 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 (...)? (Y/n) -> y
% npx hardhat init .
Need to install the following packages:
  hardhat
Ok to proceed? (y) 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.9.9 👷‍

✔ What do you want to do? · Create an advanced sample project that uses TypeScript
✔ Hardhat project root: · /Users/linnefromice/repository/github.com/_linnefromice/linnefromice/sample-protocol
✔ Do you want to add a .gitignore? (Y/n) · y
✔ Do you want to install this sample project's dependencies with npm (hardhat @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers @nomiclabs/hardhat-etherscan dotenv eslint eslint-config-prettier eslint-config-standard eslint-plugin-import eslint-plugin-node eslint-plugin-prettier eslint-plugin-promise hardhat-gas-reporter prettier prettier-plugin-solidity solhint solidity-coverage @typechain/ethers-v5 @typechain/hardhat @typescript-eslint/eslint-plugin @typescript-eslint/parser @types/chai @types/node @types/mocha ts-node typechain typescript)? (Y/n) · y

これで TypeScript 込みの Hardhat Project を作成することができました!

Hardhat プロジェクトの設定整理

まず最初に Hardhat に含まれている部分を改善・拡張していくことで、環境整備を行います。
主にhardhat.config.tsを修正していきます。

Solidity バージョン & コンパイラ

[1] Solidity のバージョン

hardhat がサポートしている solidity のバージョンがあるので、その最大にしておきましょう。

https://hardhat.org/reference/solidity-support#supported-versions

[2] Solidity Compiler に関する設定

Compiler の定義も追加する

これらを加味して以下のように修正しましょう。

hardhat.config.ts
const config: HardhatUserConfig = {
  solidity: {
    // Docs for the compiler https://docs.soliditylang.org/en/v0.8.9/using-the-compiler.html
    version: "0.8.9",
    settings: {
      optimizer: {
        enabled: true,
        runs: 200
      }
    }
  },
  ...
}

Networks

ネットワークの設定です。

  • ローカルでの検証やテスト用に使うローカルネットワーク
  • 実際にステージングやプロダクションとして使う実際のブロックチェーンネットワーク

と、基本この2種類になると思うので分けて説明をします。

For Local

まず以下の2種類のネットワークを用意します。

[1] hardhat ... テスト用

  • テストはデフォルトで hardhat network 設定を利用します

[2] localhost ... ローカル動作確認用

  • デプロイタスクの実行などローカルで検証を行うため
    • hardhat --network localhostとコマンドを入れることで利用します
    • url のみ設定すれば、好きなツールでローカルネットワークを起動して利用できます
      • 基本的にhardhat nodeで良いと思いますが、truffleなども利用可能になります

ネットワークの詳細設定は基本的に未設定でも問題ないのですが、いくつか設定を入れておきます

const HARDHAT_CHAINID = 31337;
const DEFAULT_BLOCK_GAS_LIMIT = 30000000;
const GWEI = 1000 * 1000 * 1000;
const localNetwork: HardhatNetworkUserConfig = {
  blockGasLimit: DEFAULT_BLOCK_GAS_LIMIT,
  gas: DEFAULT_BLOCK_GAS_LIMIT,
  gasPrice: 3 * GWEI,
  throwOnTransactionFailures: true,
  throwOnCallFailures: true,
  allowUnlimitedContractSize: true,
}
const config: HardhatUserConfig = {
  ...
  networks: {
    hardhat: {
      ...localNetwork,
      chainId: HARDHAT_CHAINID,
    },
    localhost: {
      ...localNetwork,
      url: 'http://127.0.0.1:8545',
    },
  }

For Staging/Production

あまりカスタマイズしていないが最低限以下は設定するので、

  • accounts: ニーモニックを利用
  • chainId: ネットワークの Chain ID
  • url: rpc url
  • gasPrice

ネットワーク追加時、あるいは共通的な設定の修正はしやすいように関数にまとめておく

const getNetworkConfigs = (): Partial<NetworksParams<NetworkUserConfig>> => {
  const accounts = {
    mnemonic: MNEMONIC,
    path: "m/44'/60'/0'/0",
    initialIndex: 0,
    count: 20,
  }
  return {
    astar: {
      chainId: 592,
      url: "https://astar.api.onfinality.io/public",
      gasPrice: 5 * GWEI,
      accounts: accounts,
    },
    shiden: {
      chainId: 336,
      url: "https://shiden.api.onfinality.io/public",
      gasPrice: 3 * GWEI,
      accounts: accounts,
    }
  }
}
...
const config: HardhatUserConfig = {
  ...
  networks: {
    ...
    ...getNetworkConfigs()
  }
  ...
}

GasReporter

今回利用したボイラープレートに元々含まれているものですが、hardhat-gas-reporterによって自分が実装した contract や function による gas の消費量などのデータをテスト時に取得してくれます。

下記のように設定できます。

const COINMARKETCAP = process.env.COINMARKETCAP || ''
...
const config: HardhatUserConfig = {
  ...
  gasReporter: {
    enabled: true,
    currency: 'JPY',
    gasPrice: 20,
    token: 'ETH',
    coinmarketcap: COINMARKETCAP,
    showTimeSpent: true,
    showMethodSig: true,
  },
}

その他の設定は下記を参照してください。

https://www.npmjs.com/package/hardhat-gas-reporter

この修正を入れてテストを実行してみると、マトリクスが表示されるはずです。
確認してみてください!

% yarn hardhat test

  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 (390ms)

·------------------------------------|---------------------------|-------------|-----------------------------·
|        Solc version: 0.8.9         ·  Optimizer enabled: true  ·  Runs: 200  ·  Block limit: 30000000 gas  │
·····································|···························|·············|······························
|  Methods                           ·               20 gwei/gas               ·       1183.71 usd/eth       │
·············|·······················|·············|·············|·············|···············|··············
|  Contract  ·  Method               ·  Min        ·  Max        ·  Avg        ·  # calls      ·  usd (avg)  │
·············|·······················|·············|·············|·············|···············|··············
|  Greeter   ·  setGreeting(string)  ·          -  ·          -  ·      34407  ·            2  ·       0.81  │
·············|·······················|·············|·············|·············|···············|··············
|  Deployments                       ·                                         ·  % of limit   ·             │
·····································|·············|·············|·············|···············|··············
|  Greeter                           ·          -  ·          -  ·     383497  ·        1.3 %  ·       9.08  │
·------------------------------------|-------------|-------------|-------------|---------------|-------------·

  1 passing (395ms)

✨  Done in 2.41s.

Hardhat task

デプロイや運用、テストなどの1タスクを実行するためのサンプルとして、ボイラープレートに Hardhat Script を利用したデプロイスクリプトがあります。
本機能はタスクランナーとして使うことができ、Hardhat の機能を利用して基本的になんでもできるのですが、自分は Hardhat Task の方が好みなのでそのためのセットアップをします。

Task
https://hardhat.org/guides/create-task
Script
https://hardhat.org/guides/scripts

元々のデプロイスクリプト(scripts/deploy.ts)を Task に置き換えたものを作ります。

tasks/deployments/sample.ts
import { HardhatRuntimeEnvironment } from 'hardhat/types'
import { task } from 'hardhat/config'
import { Greeter__factory } from '../../typechain'

task("deploy:sample", "deploy:sample")
  .addOptionalParam('deployer', 'Deployer Address to deploy contracts')
  .setAction(async (
    { deployer }: { deployer: string },
    hre: HardhatRuntimeEnvironment
  ) => {
    console.log(`------- [deploy:sample] START -------`)
    const { ethers, network } = hre
    const _deployer = deployer
      ? await ethers.getSigner(deployer)
      : (await ethers.getSigners())[0]
    console.log(`network: ${network.name}`)
    console.log(`deployer: ${_deployer.address}`)

    console.log(`> deploy Greeter`)
    const greeter = await new Greeter__factory(_deployer).deploy("Hello, Hardhat!");
    await greeter.deployTransaction.wait()
    console.log(`>> deployed Greeter: ${greeter.address}`)

    console.log(`------- [deploy:sample] END -------`)
  })

合わせて、TypeChain にデプロイタスクのファイルを認識してもらうための修正を加えます

hardhat.config.ts
...
// Prevent to load scripts before compilation and typechain
const SKIP_LOAD = process.env.SKIP_LOAD === 'true'
if (!SKIP_LOAD) {
  const taskPaths = ['deployments']
  taskPaths.forEach((folder) => {
    const tasksPath = path.join(__dirname, 'tasks', folder)
    fs.readdirSync(tasksPath)
      .filter((_path) => _path.includes('.ts'))
      .forEach((task) => {
        require(`${tasksPath}/${task}`)
      })
  })
}
...

実際にこのタスクを実行して確認してみましょう。

# terminal 1: ローカルブロックチェーンネットワークを起動するため
% yarn hardhat node
# terminal 2: タスク実行
% yarn hardhat deploy:sample --network localhost
------- [deploy:sample] START -------
network: localhost
deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
> deploy Greeter
>> deployed Greeter: 0x5FbDB2315678afecb367f032d93F642f64180aa3
------- [deploy:sample] END -------
✨  Done in 1.38s.

こんな感じで実行できれば上手く移行できています!
適宜console.logを挟んでいたのはこのロギングのためでした。

実行コマンド (package.json)

諸々のコマンドを実行しやすくするために package.json に scripts を定義していきます。
自分は結構こまめに scripts に定義していて設定が肥大化するのですが、色々 task 作成や plugin にお世話になっていると、コマンドを忘れることが多いので、そのために都度追加するようにしています。

特徴としては、

  • 短くてもよく使うコマンドは定義しておく
    • hardhat test
    • hardhat coverage
    • hardhat compile with typechain
  • ネットワークごとの設定も管理しておく
  • task 定義したものは、task:という接頭辞を付与してグルーピングできるようにしておく
package.json
{
  "scripts": {
    "hardhat": "npx hardhat",
    "test": "hardhat test",
    "coverage": "hardhat coverage",
    "compile:only": "hardhat compile",
    "compile": "rm -rf typechain && SKIP_LOAD=true compile && SKIP_LOAD=true hardhat typechain",
    "hardhat:localhost": "hardhat --network localhost",
    "hardhat:arbitrum": "hardhat --network arbitrum",
    "hardhat:kovan": "hardhat --network kovan",
    "hardhat:mainnet": "hardhat --network mainnet",
    "task:deploy:sample:localhost": "hardhat:localhost deploy:sample",
  },
  ...
}

またここまで定義できたら、ベースを改善するたびに compile -> test -> (簡単な) deploy でデグレがないか確認しましょう。

yarn compile
yarn test
yarn task:deploy:sample:localhost

VSCode

最低でもコンパイラの設定入れておきましょう。
これが有効になっておらず、linter が効いていないことは多々あります...

.vscode/settings.json
{
  "solidity.compileUsingRemoteVersion": "v0.8.9+commit.e5eed63a"
}

好みではありますが、自動フォーマッターを入れても良いと思います。
(コマンドでやりたい人は設定不要です。)

こちらでもボイラープレートにデフォルトで含まれているprettier-plugin-solidityを利用します。

https://github.com/prettier-solidity/prettier-plugin-solidity

.vscode/settings.json
{
  "editor.formatOnSave": true,
  "files.insertFinalNewline": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "solidity.compileUsingRemoteVersion": "v0.8.9+commit.e5eed63a"
}
.prettierrc
{
  "overrides": [
    {
      "files": "*.sol",
      "options": {
        "tabWidth": 2
      }
    }
  ]
}

拡張編

マストではないと思いますが、やっていて開発上も運用上も便利だなと思った準備内容を紹介しておきます!

デプロイしたコントラクトを管理/操作するためのユーティリティ

ローカル検証でも本番でもコントラクトをデプロイした後、そのアドレスを参照したいことが多々発生します。
そのために、デプロイしたコントラクトのアドレスをファイルに保存したり、それを読み込む機構を入れておきます。

こちらの記事でも紹介したリポジトリなのですが、

https://zenn.dev/linnefromice/articles/use-graph-in-local

こちらにコントラクトアドレスを管理するための Utility クラスを作っています。
ぜひ参考にしてください。

https://github.com/linnefromice/sample-staking-protocol/blob/main/libs/utils/deployed-contracts.ts

こちらと同じでなくても良いのですが、こういった機構を利用することで、
デプロイ時には、下記のように利用することで、コントラクトアドレスを json ファイルに保存し、

tasks/deployments/sample.ts
+ ContractsJsonHelper.reset({ network: network.name }); // json ファイルをリセットする
...
const greeter = await new Greeter__factory(_deployer).deploy("Hello, Hardhat!")
await greeter.deployTransaction.wait();
+ ContractsJsonHelper.writeAddress({ // json ファイルにコントラクトアドレスを追記する
+   group: "contracts",
+   name: "greeter",
+   value: greeter.address,
+   network: network.name,
+ });
outputs/contracts-localhost.json
{
  "contracts": {
    "greeter": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0"
  }
}

Task などで下記のように読み込んで、コントラクトのインスタンスを取得することができます。

const { contracts: { greeter } } = ContractsJsonHelper.load({
  network: network.name,
})
const instance = Greeter__factory.connect(greeter, signer) // Greeter

まとめ

Hardhat の拡張だったり、自分自身で機構を準備したり、色々紹介してみましたがいかがだったでしょうか。
今後 Solidity 開発に興味がある人などの参考になればと思います!
もしこういうのもやっているよ!というのがあれば教えてください...(我流な部分が多いと思うので...)

基本的に Web3 プロジェクトは Github で公開されているので、有名な DeFi プロジェクトなどの repository からヒントを得やすいです!
僕は今回 aave を多分に参考にしていますが、皆さんも自分に合うプロジェクト構成をしているところが見つかると良いのかなと思います。

お読みいただきありがとうございました。

参考

https://github.com/aave/aave-v3-core

Discussion