概要
スマートコントラクト開発環境のFoundryを利用し、SoidityでNFTを作成する手順と、作成したNFTをローカルのテストネットで動作確認する手順です。
コントラクトの開発環境はTruffle
やRemix Ide
などが有名だが、今回利用するFoundry
はRust製の開発環境で、他のツールと比較して高速で使いやすい点で注目されている。
続編
リポジトリ(一部フォルダ構成は異なりますが、コマンドは同じものが利用可能です。)
利用技術
- Foundry
- Solidity
- OpenZeppelin
事前準備
環境
- macOS Ventura(v13.0)
- forge 0.2.0
Foundryのインストール
はじめに、開発環境のFoundryをインストールする。
Foundryはスマートコントラクトの開発に必要なツールチェーンを提供してくれており、デプロイやテストコマンド、テストネットの起動を簡単に実行できる。
Foundryのインストールのために、下記のコマンドを実行する。
curl -L https://foundry.paradigm.xyz | bash
foundryup
上記のコマンド実行時、下記の様なエラーが出た場合は、brew install libusb
を実行してlibusb
をインストールする。
dyld[32719]: Library not loaded: /usr/local/opt/libusb/lib/libusb-1.0.0.dylib
インストール完了後、コントラクト開発に必要なforge
, cast
, anvil
コマンドを実行できる様になる。
各コマンドの概要は以下のとおり。
コマンド | 概要 |
---|---|
forge |
スマートコントラクトのテスト、ビルド、デプロイを行うコマンド |
cast |
デプロイされているスマートコントラクトのメソッドを実行するコマンド |
anvil |
作成したテストネットの検証様に、テストネットを起動するために利用するコマンド |
開発
1. プロジェクトの作成
はじめに、forge init
でプロジェクトを作成する。
forge init foundry-nft-sample
その後、作成したプロジェクトに移動し、foundry
が提供する汎用ライブラリであるfoundry-rs/forge-std
のインストールを行う。
cd foundry-nft-sample
forge install foundry-rs/forge-std
インストールが完了後、サンプルプロジェクトのビルドとテストを実行する。
$ 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を利用して作成するのが簡単である。
forge-std
をインストールした際と同様に、forge install
コマンドでインストールを行う。
$ 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などでも実行できる様に、@
記法でライブラリを呼び出す様にする。
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を作成する。
// 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 環境変数の設定とシークレットフレーズの作成
ローカルのテストネットにデプロイするアカウントのシークレットフレーズと、デプロイするネットワークのアドレスを環境変数として指定する。
export MNEMONIC="ランダムで作成されたシークレットフレーズ"
export FOUNDRY=http://localhost:8545
シークレットフレーズは下記のサイトなどを利用して発行する。
その後環境変数を設定する。
source .env
3-2. テストネットの起動
はじめに、作成したコントラクトをデプロイする際に利用するテストネットを起動する。
$ 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
に指定したシークレットフレーズのアカウントを指定している。
// 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. テストネットの起動
でテストネットを起動した時とは別のターミナルを開き、デプロイ処理を実行する。
$ 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:
が、デプロイされたスマートコントラクトのコントラクトアドレスとなる。
~省略~
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のコントラクトアドレスとなる。
注意
anvil -m $MNEMONIC
コマンドで起動したテストネットを停止すると、デプロイしたSampleNFTの情報が消えます。
再起動した場合は、再度コントラクトのデプロイを実行し、コントラクトアドレスを取得してください。
4. 実行
テストネットにデプロイしたコントラクトに対して、cast
コマンドを利用し、transactionやcallリクエストを実行して動作確認する。
4-1. send
リクエストの実行
デプロイしたコントラクトの実行は、foundryのcast send
コマンドを利用する。
$ cast send ${実行するコントラクトのアドレス} \
"${実行するメソッド}" \
${引数1} ${引数2} ... \
--mnemonic ${実行するアカウントウォレットのシークレットフレーズ} \
--rpc-url ${コントラクトがデプロイされたチェーンのURL}
上記に従ってコマンドを構築し、Hello Contract
という文字列を持つnftを作成する
$ 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
コマンドは下記の様に利用する。
$ cast call ${実行するコントラクトのアドレス} \
"${実行するメソッド}" \
${引数1} ${引数2} ... \
--mnemonic ${実行するアカウントウォレットのシークレットフレーズ} \
--rpc-url ${コントラクトがデプロイされたチェーンのURL}
1つ目のNFTはtokenId=0
となる。0を指定してgetSentence()
を実行し、4-1
で記録したNFTの文章を下記のコマンドで確認する。
$ cast call 0x0c2065e8bf691b057f41a193ad0bf04f8c305428 \
"getSentence(uint256)(string)" \ # solidityで作成したメソッド
0 \ # 対象のtokenIdを指定する
--mnemonic $MNEMONIC \
--rpc-url $FOUNDRY
Hello Contract
Hello Contract
が表示され、正しくNFTが発行されたことを確認できた。