SolidityでNFTを作成しFoundryを利用してローカルのテストネットにデプロイする

# 概要

スマートコントラクト開発環境のFoundryを利用し、SoidityでNFTを作成する手順と、作成したNFTをローカルのテストネットで動作確認する手順です。

コントラクトの開発環境は`Truffle`や`Remix Ide`などが有名だが、今回利用する`Foundry`はRust製の開発環境で、他のツールと比較して高速で使いやすい点で注目されている。

https://ethereum-blockchain-developer.com/124-remix-vs-truffle-vs-hardhat-vs-foundry/00-overview/

続編

https://qiita.com/sey323/items/81fd07947dc34cf84644

リポジトリ(一部フォルダ構成は異なりますが、コマンドは同じものが利用可能です。)

https://github.com/sey323/python-dapps-sample

## 利用技術

- Foundry
- Solidity
- OpenZeppelin


# 事前準備

## 環境

- macOS Ventura(v13.0)
- forge 0.2.0

## Foundryのインストール

はじめに、開発環境のFoundryをインストールする。

Foundryはスマートコントラクトの開発に必要なツールチェーンを提供してくれており、デプロイやテストコマンド、テストネットの起動を簡単に実行できる。

https://book.getfoundry.sh/

Foundryのインストールのために、下記のコマンドを実行する。

```sh:
curl -L https://foundry.paradigm.xyz | bash
foundryup
```

上記のコマンド実行時、下記の様なエラーが出た場合は、`brew install libusb`を実行して`libusb`をインストールする。

```sh:
dyld[32719]: Library not loaded: /usr/local/opt/libusb/lib/libusb-1.0.0.dylib
```

インストール完了後、コントラクト開発に必要な`forge`, `cast`, `anvil`コマンドを実行できる様になる。

各コマンドの概要は以下のとおり。

| コマンド | 概要 |
| - | - |
| `forge` | スマートコントラクトのテスト、ビルド、デプロイを行うコマンド |
| `cast` | デプロイされているスマートコントラクトのメソッドを実行するコマンド |
| `anvil` | 作成したテストネットの検証様に、テストネットを起動するために利用するコマンド |

# 開発

## 1. プロジェクトの作成

はじめに、`forge init`でプロジェクトを作成する。

```sh:
forge init foundry-nft-sample
```

その後、作成したプロジェクトに移動し、`foundry`が提供する汎用ライブラリである`foundry-rs/forge-std`のインストールを行う。


```sh:
cd foundry-nft-sample
forge install foundry-rs/forge-std
```

インストールが完了後、サンプルプロジェクトのビルドとテストを実行する。

```sh:
$ forge build
[⠢] Compiling...
[⠒] Compiling 6 files with 0.8.19
[⠆] Solc 0.8.19 finished in 886.83ms
Compiler run successful

$ forge test
[⠢] Compiling...
No files changed, compilation skipped

Running 2 tests for test/Counter.t.sol:CounterTest
[PASS] testIncrement() (gas: 28334)
[PASS] testSetNumber(uint256) (runs: 256, μ: 27398, ~: 28409)
Test result: ok. 2 passed; 0 failed; finished in 9.09ms
```

ビルドとテスト実行が正常に完了すれば、環境構築は完了。

## 2. NFTの作成

### 2-1. ライブラリのインストール
Solidityを利用してNFTを作成する場合、OpenZeppelinのライブラリを利用する。OpenZeppelinは、ERC721やERC20準拠のコントラクトを作成するインターフェースを提供しているため、基本的なNFTを作成する場合は、こちらのAPIを利用して作成するのが簡単である。

https://www.openzeppelin.com/

`forge-std`をインストールした際と同様に、`forge install`コマンドでインストールを行う。

```sh:
$ forge install openzeppelin/openzeppelin-contracts
Installing openzeppelin-contracts in "/Users/sey323/Workspace/programs/tmp/foundry-nft-sample/lib/openzeppelin-contracts" (url: Some("https://github.com/openzeppelin/openzeppelin-contracts"), tag: None)
    Installed openzeppelin-contracts v4.8.3
```

#### `remappings.txt`の作成

forgeでは`remappings.txt`を作成することで、インストールしたライブラリを呼び出す際に、別名を宣言できる。

今回は同じコードをremixなどでも実行できる様に、`@`記法でライブラリを呼び出す様にする。

```text:remappings.txt
ds-test/=lib/forge-std/lib/ds-test/src/
forge-std/=lib/forge-std/src/
@openzeppelin/=lib/openzeppelin-contracts/
```

### 2-2. 開発

次にNFTを作成する。今回はNFTを発行する際に、所有者のアドレスとNFTに記録したい文章を引数として渡すことで、その文章が記録されたNFTを作成する。

```typescript:src/SampleNFT.sol
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.14;

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";

contract ProposalNFT is ERC721Enumerable {
    mapping(uint256 => bytes) private _sentences;

    constructor() ERC721("SampleNFT", "SPLNFT") {}

    /**
     * NFTのmint
     */
    function mintNft(address ownerAddress, string memory sentence) public {
        // 最新のトークンIDを取得する。
        uint256 tokenId = totalSupply();
        // トークンIDに文章を紐付け
        _sentences[tokenId] = bytes(sentence);
        // NFTの発行
        _mint(ownerAddress, tokenId);
    }

    /**
     * トークンIDに紐づく文章を表示する
     */
    function getSentence(uint256 tokenId) public view returns (string memory) {
        return string(_sentences[tokenId]);
    }
}
```

## 3. デプロイ

作成したコントラクトを、ローカルのテストネットにデプロイする。

### 3.1 環境変数の設定とシークレットフレーズの作成

ローカルのテストネットにデプロイするアカウントのシークレットフレーズと、デプロイするネットワークのアドレスを環境変数として指定する。

```:.env
export MNEMONIC="ランダムで作成されたシークレットフレーズ"
export FOUNDRY=http://localhost:8545
```

シークレットフレーズは下記のサイトなどを利用して発行する。

https://iancoleman.io/bip39/

その後環境変数を設定する。

```sh:
source .env
```

### 3-2. テストネットの起動

はじめに、作成したコントラクトをデプロイする際に利用するテストネットを起動する。

```sh:
$ anvil -m $MNEMONIC
                             _   _
                            (_) | |
      __ _   _ __   __   __  _  | |
     / _` | | '_ \  \ \ / / | | | |
    | (_| | | | | |  \ V /  | | | |
     \__,_| |_| |_|   \_/   |_| |_|

    0.1.0 (e0afc7c 2023-04-22T00:12:06.184818000Z)
    https://github.com/foundry-rs/foundry

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

(0) "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" (10000 ETH)
(1) "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" (10000 ETH)
~ 省略 ~
(9) "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC" (10000 ETH)

Private Keys
==================

(0) 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
(1) 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d

~ 省略 ~
(9) 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6

Wallet
==================
Mnemonic:          ${.envで$MNEMONICに指定したシークレットフレーズ}
Derivation path:   m/44'/60'/0'/0/


Base Fee
==================

1000000000

Gas Limit
==================

30000000

Genesis Timestamp
==================

1682166468

Listening on 127.0.0.1:8545
```

### 3-3. デプロイスクリプトの作成

コントラクトをデプロイするために利用する、デプロイスクリプトを作成する。

デプロイするアカウントは、`.env`の`MNEMONIC`に指定したシークレットフレーズのアカウントを指定している。

```typescript:src/Deploy.s.sol
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.19;

import {Script} from "forge-std/Script.sol";
import {SampleNFT} from "../src/SampleNFT.sol";

contract Deploy is Script {
    address internal deployer;
    SampleNFT internal sampleNFT;

    function setUp() public virtual {
        (deployer, ) = deriveRememberKey(vm.envString("MNEMONIC"), 0);
    }

    function run() public {
        vm.startBroadcast(deployer);
        sampleNFT = new SampleNFT();
        vm.stopBroadcast();
    }
}
```

### 3-4. デプロイ

`3-2. テストネットの起動`でテストネットを起動した時とは別のターミナルを開き、デプロイ処理を実行する。

```sh:
$ source .env # 別ターミナルなので再度読み込み
$ forge script Deploy --broadcast --rpc-url $FOUNDRY 
[⠢] Compiling...
[⠢] Compiling 14 files with 0.8.19
[⠆] Solc 0.8.19 finished in 879.12ms
Compiler run successful
Script ran successfully.

## Setting up (1) EVMs.

==========================

Chain 31337

Estimated gas price: 5 gwei

Estimated total gas used for script: 2000068

Estimated amount required: 0.01000034 ETH

==========================

###
Finding wallets for all the necessary addresses...
##
Sending transactions [0 - 0].
⠁ [00:00:00] [#################################################################################] 1/1 txes (0.0s)
Transactions saved to: /Users/sey323/Workspace/programs/tmp/qiita-foundry-nft-sample/broadcast/Deploy.s.sol/31337/run-latest.json

##
Waiting for receipts.
⠉ [00:00:00] [#############################################################################] 1/1 receipts (0.0s)
##### anvil-hardhat
✅ Hash: 0x6b69612773390a2c0cbce75cb891ebd47b449ea46c65eeb91070892fb1cc0e5e
Contract Address: 0x0c2065e8bf691b057f41a193ad0bf04f8c305428
Block: 1
Paid: 0.006154056 ETH (1538514 gas * 4 gwei)


Transactions saved to: /Users/sey323/Workspace/programs/tmp/qiita-foundry-nft-sample/broadcast/Deploy.s.sol/31337/run-latest.json



==========================

ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.
Total Paid: 0.006154056 ETH (1538514 gas * avg 4 gwei)

Transactions saved to: /Users/sey323/Workspace/programs/tmp/qiita-foundry-nft-sample/broadcast/Deploy.s.sol/31337/run-latest.json
```

実行後トランザクションの結果が表示される。実行結果の詳細は、`broadcast/Deploy.s.sol/31337/run-latest.json`に保存される。

`3-2. テストネットの起動`のターミナルを確認するとトランザクションログが表示される。例えばこのうちの`Contract created:`が、デプロイされたスマートコントラクトのコントラクトアドレスとなる。

```sh
~省略~
eth_getBlockByNumber
eth_feeHistory
eth_sendRawTransaction
eth_getTransactionReceipt

    Transaction: 0x6b69612773390a2c0cbce75cb891ebd47b449ea46c65eeb91070892fb1cc0e5e
    Contract created: 0x0c2065e8bf691b057f41a193ad0bf04f8c305428 # コントラクトアドレス
    Gas used: 1538514

    Block Number: 1
    Block Hash: 0xba43195d4f617bc69823efd109e17595251de8c9c19b108df55b865035501e2b
    Block Time: "Sat, 22 Apr 2023 13:06:21 +0000"

eth_getTransactionByHash
~省略~
```

今回の場合は`0x0c2065e8bf691b057f41a193ad0bf04f8c305428`が、デプロイしたNFTのコントラクトアドレスとなる。

:::note warn
注意
`anvil -m $MNEMONIC`コマンドで起動したテストネットを停止すると、デプロイしたSampleNFTの情報が消えます。
再起動した場合は、再度コントラクトのデプロイを実行し、コントラクトアドレスを取得してください。
:::

## 4. 実行

テストネットにデプロイしたコントラクトに対して、`cast`コマンドを利用し、transactionやcallリクエストを実行して動作確認する。

### 4-1. `send`リクエストの実行

デプロイしたコントラクトの実行は、foundryの`cast send`コマンドを利用する。

```sh:
$ cast send ${実行するコントラクトのアドレス} \
            "＄{実行するメソッド}" \
            ${引数1} ${引数2} ... \
            --mnemonic ${実行するアカウントウォレットのシークレットフレーズ} \
            --rpc-url ${コントラクトがデプロイされたチェーンのURL}
```

上記に従ってコマンドを構築し、`Hello Contract`という文字列を持つnftを作成する


```sh:
$ cast send 0x0c2065e8bf691b057f41a193ad0bf04f8c305428 \
            "mintNft(address,string)" \
            "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" \ # 1つ目の引数
            "Hello Contract" \ # 2つ目の引数
            --mnemonic $MNEMONIC \
            --rpc-url $FOUNDRY

blockHash               0x0f1eaea8f37561755e408471f5f9d6d34b546dc2df3f2195e34e6dd37d0bf739
blockNumber             2
contractAddress         
cumulativeGasUsed       123976
effectiveGasPrice       3887820950
gasUsed                 123976
logs                    [{"address":"0x0c2065e8bf691b057f41a193ad0bf04f8c305428","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x0000000000000000000000000000000000000000000000000000000000000000","0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266","0x0000000000000000000000000000000000000000000000000000000000000000"],"data":"0x","blockHash":"0x0f1eaea8f37561755e408471f5f9d6d34b546dc2df3f2195e34e6dd37d0bf739","blockNumber":"0x2","transactionHash":"0xf59d5ecc1594441e03641bd03bb05c5903a8c4a339be157298572789b8c73e27","transactionIndex":"0x0","logIndex":"0x0","transactionLogIndex":"0x0","removed":false}]
logsBloom               0x00000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000020000000000000100000800000000000000000000000010000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000200000000000000000000000002000000000000000000020000000000000000000000000000000000000000000000000000000000000000000
root                    
status                  1
transactionHash         0xf59d5ecc1594441e03641bd03bb05c5903a8c4a339be157298572789b8c73e27
transactionIndex        0
type                    2
```

### 4-2. `call`リクエスト

callリクエストを実行し、NFTに文章が刻まれていることを確認する。`cast call`コマンドは下記の様に利用する。

```sh:
$ cast call ${実行するコントラクトのアドレス} \
            "＄{実行するメソッド}" \
            ${引数1} ${引数2} ... \
            --mnemonic ${実行するアカウントウォレットのシークレットフレーズ} \
            --rpc-url ${コントラクトがデプロイされたチェーンのURL}
```

1つ目のNFTは`tokenId=0`となる。0を指定して`getSentence()`を実行し、`4-1`で記録したNFTの文章を下記のコマンドで確認する。

```sh:
$ cast call 0x0c2065e8bf691b057f41a193ad0bf04f8c305428 \
            "getSentence(uint256)(string)" \ # solidityで作成したメソッド
            0 \ # 対象のtokenIdを指定する
            --mnemonic $MNEMONIC \
            --rpc-url $FOUNDRY
Hello Contract
```

`Hello Contract`が表示され、正しくNFTが発行されたことを確認できた。

# 参考資料

https://book.getfoundry.sh/
https://www.openzeppelin.com/