スマートコントラクトを作ろう
🖋 コントラクトを作成する
これから、ETHとガス代を支払うことで、誰でもNFTをMintできるスマートコントラクトをSolidityで作成していきます。
- ここで作成するスマートコントラクトは、後でユースケースに合わせて自由に変更できます。
packages/contract/contractsディレクトリの下にNFTCollectible.solという名前のファイルを作成します。
Hardhatを使用する場合、ファイル構造は非常に重要ですので、注意する必要があります。ファイル構造が下記のようになっていれば大丈夫です 😊
packages
└── contract
└── contracts
└── NFTCollectible.sol
次に、コードエディタでプロジェクトのコードを開きます。
ここでは、VS Codeの使用をお勧めします。ダウンロードはこちらから。
VS Codeをターミナルから起動する方法はこちらをご覧ください。今後VS Codeを起動するのが一段と楽になるので、ぜひ導入してみてください。
コーディングのサポートツールとして、VS Code上でSolidityの拡張機能をダウンロードすることをお勧めします。ダウンロードは こちら から。
それでは、これからNFTCollectible.solの中身の作成していきます。NFTCollectible.solをVS Codeで開き、 下記を入力します。
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "hardhat/console.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
contract NFTCollectible is ERC721Enumerable, Ownable {
using SafeMath for uint256;
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
}
コードを詳しくみていきましょう。
// SPDX-License-Identifier: MIT
これは「SPDXライセンス識別子」と呼ばれ、ソフトウェア・ライセンスの種類が一目でわかるようにするための識別子です。
pragma solidity ^0.8.17;
これは、コントラクトで使用するSolidityコンパイラのバージョンです。上記の場合「このコントラクトを実行するときは、Solidityコンパイラのバージョン0.8.17のみを使用し、それ以下のものは使用しません」という意味です。コンパイラのバージョンがhardhat.config.jsで同じであることを確認してください。
もし、hardhat.config.jsの中に記載されているSolidityのバージョンが0.8.17でなかった場合は、NFTCollectible.solの中身をhardhat.config.jsに記載されているバージョンに変更しましょう。
import "hardhat/console.sol";
コントラクトを実行する際、コンソールログをターミナルに出力するためにHardhatのconsole.solのファイルをインポートしています。これは、今後スマートコントラクトのデバッグが発生した場合に、とても役立つツールです。
import "hardhat/console.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
contract NFTCollectible is ERC721Enumerable, Ownable {
using SafeMath for uint256;
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
ここでは、OpenZeppelinのERC721 EnumerableコントラクトとOwnableコントラクトを継承しています。
🗃 定数( constants)と変 数( variables)を保存する
これからコントラクトに、特定の変数や定数を保存していきます。
NFTCollectible.solの中のCounters.Counter private _tokenIds;の直下に以下のコードを追加しましょう。
uint public constant MAX_SUPPLY = 30;
uint public constant PRICE = 0.01 ether;
uint public constant MAX_PER_MINT = 3;
string public baseTokenURI;
まず、ここでは以下の3つを定数(constants)として定義します。
1. NFT の供給量( MAX_SUPPLY): コレクションでMint可能なNFTの最大数。
2. NFT の価格( PRICE): NFTを購入するのにユーザーが支払うETHの額。
3. 1 取引あたりの最大 Mint 数( MAX_PER_MINT): ユーザーが一度にMintできるNFTの上限限。
コントラクトがデプロイされたら、定数の中身を変更することはできません。
- これらコンスタントが取る値(
30、0.01 etherなど)は、自由に変更できます。
それから、下記を変数として定義します。
Base Token URI( baseTokenURI): JSONファイル(メタデータ)が格納されているフォルダのIPFS URL。
コントラクトの所有者(またはデプロイ先)が必要に応じてBase Token URIを変更できるように、これからbaseTokenURIのセッタ関数を記述していきます。
✍️:
publicは Solidity のアクセス修飾子です。 Solidity のアクセス修飾子に関しては、こちら をご覧ください。
publicを含む、他のアクセス修飾子について詳しく説明しています。
🤖 コンストラクタ( constructor)を記述する
コンストラクタ(constructor)の呼び出して、baseTokenURIを設定していきます。
🔩:
constructorとはconstructorはオプションの関数で、contractの状態変数を初期化するために使用されます。これから詳しく説明していくので、constructorに関しては、まず以下の特徴を理解してください。
contractは 1 つのconstructorしか持つことができません。
constructorは、スマートコントラクトの作成時に一度だけ実行され、contractの状態を初期化するために使用されます。
constructorが実行された後、コードがブロックチェーンにデプロイされます 。
NFTCollectible.solの中のstring public baseTokenURI;の直下に以下のコードを追加しましょう。
constructor(string memory baseURI) ERC721("NFT Collectible", "NFTC") {
setBaseURI(baseURI);
}
setBaseURI(baseURI);は、メタデータが存在する場所のBase Token URIを設定します。
この処理により、個々のNFTに対して手動でBase Token URIを設定する作業が軽減されます。
setBaseURI関数については、後で詳しく説明します。
また、ERC721("NFT Collectible", "NFTC")では、親コンストラクタ(ERC721)を呼び出して、NFTコレクションの名前とシンボルを設定します。
-
NFTコレクションの名前:
"NFT Collectible" -
NFTコレクションのシンボル:
"NFTC"
- NFTコレクションの名前とシンボルは任意で変更して大丈夫です 😊
🎟 いくつかの NFT を無料で配布する
スマートコントラクトは、一度ブロックチェーン上にデプロイしてしまうと中身を変更できません。
したがって、すべてのNFTを有料にすると、自分自身や友達、イベントの景品として無料でNFTを配布できなくなってしまいます。
なので今から、ある一定数(この場合は10個)のNFTをキープしておいて、ユーザーが無料でMintできる関数(reserveNFTs)をコントラクトに実装していきます。
下記を、constructorのコードブロック直下に追加しましょう。
function reserveNFTs() public onlyOwner {
uint totalMinted = _tokenIds.current();
require(totalMinted.add(10) < MAX_SUPPLY, "Not enough NFTs");
for (uint i = 0; i < 10; i++) {
_mintSingleNFT();
}
}
reserveNFTs数を呼び出すユーザーは、ガス代だけ払えばよいので、onlyOwnerとマークして、コントラクトの所有者だけが呼び出せるようにします。
tokenIds.current()を呼び出して、これまでにMintされたNFTの総数を確認します。
tokenIdは0から始まり、NFTがMintされるごとに+1されます。
次に、requireを使って、キープできるNFT(= 10個)がコレクションに残っているかどうかを確認します。
totalMinted.add(10) < MAX_SUPPLYは、現在MintされようとしているtokenIdに+10した数が、MAX_SUPPLY(この場合は30)を超えていないかチェックしています。
キープできるNFTがコレクションに残っていた場合、_mintSingleNFTを10回呼び出して10個のNFTをMintします。
_mintSingleNFT関数については、後で詳しく説明します。
✍️:
Ownable/onlyOwnerについてOwnableは、OpenZeppelin が提供するコントラクトへのアクセス制御を提供するモジュールです。このモジュールは、コントラクトの継承によって使用されます。
onlyOwnerという修飾子を関数に適用することで、関数の使用をコントラクトの所有者に限定することができます。
🔗 Base Token URI を設定する
これから、Base Token URIを効率よくコントラクトに取得して、tokenIdと紐付ける関数を実装していきます。
前回のレッスンでIPFSに保存したJSONファイルを覚えていますか?
#0番目のNFTコレクションのメタデータは、下記のエンドポイントを持つサーバーでホストされています。
https://gateway.pinata.cloud/ipfs/QmSvw119ALMN9SkP89Xj37jvqJik8jZrSjU5c1vgBhkhz8/0
上記のリンクを分解すると下記のようになります。
-
Base Token URI =
https://gateway.pinata.cloud/ipfs/QmSvw119ALMN9SkP89Xj37jvqJik8jZrSjU5c1vgBhkhz8 -
tokenId =
0
上記を踏まえ、下記をreserveNFTsのコードブロック直下に追加しましょう。
function _baseURI() internal view virtual override returns (string memory) {
return baseTokenURI;
}
function setBaseURI(string memory _baseTokenURI) public onlyOwner {
baseTokenURI = _baseTokenURI;
}
NFTのJSONメタデータは、IPFSの次のURLで入手できます: ipfs://QmZbWNKJPAjxXuNFSEaksCJVd1M6DaKQViJBYPK2BdpDEP/
setBaseURI()は、メタデータが存在する場所のBase Token URIを設定します。
setBaseURI(baseURI)を実行すると、OpenZeppelinの実装は各Base Token URIを自動的に推論します。
例:
-
tokenId =
1のメタデータは:ipfs://QmZbWNKJPAjxXuNFSEaksCJVd1M6DaKQViJBYPK2BdpDEP/1 -
tokenId =
2のメタデータはipfs://QmZbWNKJPAjxXuNFSEaksCJVd1M6DaKQViJBYPK2BdpDEP/2
しかし、setBaseURI()を実行する前に、コントラクトの最初で定義したbaseTokenURI変数が、コントラクトが使用すべきToken Base URIであることを明示する必要があります。
これを行うために、_baseURI()という空の関数をオーバーライドして、baseTokenURIを返すようにします。
また、コントラクトがデプロイされた後でもコントラクトの所有者がbaseTokenURIを変更できるように、onlyOwner