メインコンテンツまでスキップ

lesson-1_スマートコントラクトを作ろう

🖋 コントラクトを作成する

これから、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の上限限。

コントラクトがデプロイされたら、定数の中身を変更することはできません。

  • これらコンスタントが取る値(300.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の総数を確認します。

  • tokenId0から始まり、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修飾子を記述しています。

🎰 NFT を Mint する関数を実装する

次に、ユーザーがNFTをMintする際に3点のチェックを実施する関数 mintNFTsを実装していきます。

下記をsetBaseURI関数のコードブロック直下に追加しましょう。

function mintNFTs(uint _count) public payable {
uint totalMinted = _tokenIds.current();
require(
// 1つ目のチェック
totalMinted.add(_count) <= MAX_SUPPLY,
"Not enough NFTs!"
);
require(
// 2つ目のチェック
_count > 0 && _count <= MAX_PER_MINT,
"Cannot mint specified number of NFTs."
);
require(
// 3つ目のチェック
msg.value >= PRICE.mul(_count),
"Not enough ether to purchase NFTs."
);
for (uint i = 0; i < _count; i++) {
// すべてのチェックが終わったら、_count 個の NFT をユーザーに Mint する
_mintSingleNFT();
}
}

ユーザーは、あなたのコレクションからNFTを購入してMintしたいときにこの関数を呼び出します。

ユーザーはこの関数にETHを送るので、payable修飾子を関数に付与する必要があります。

下記を参考に、Mintが実行される前に以下3点のチェックを行います。

uint public constant MAX_SUPPLY = 30;
uint public constant PRICE = 0.01 ether;
uint public constant MAX_PER_MINT = 3;

1. ユーザーがMintを希望するNFTの数がコレクションに残っていること。

2. ユーザーが0以上、トランザクションごとに許可されるNFTの最大数(MAX_PER_MINT)未満のMintを実行しようとしていること。

3. ユーザーはNFTをMintするのに十分なETHを送金していること。

🌱 _mintSingleNFT()関数を実装する

最後に、ユーザーがNFTをMintするときに呼び出される_mintSingleNFT()関数を実装していきましょう。

下記をmintNFTs関数のコードブロック直下に追加しましょう。

function _mintSingleNFT() private {
uint newTokenID = _tokenIds.current();
_safeMint(msg.sender, newTokenID);
_tokenIds.increment();
}

まず、uint newTokenID = _tokenIds.current();で、まだMintされていないNFTのIDを取得します。

次に、_safeMint(msg.sender, newTokenID);で、OpenZeppelinですでに定義されている_safeMint()関数を使用して、ユーザー(関数を呼び出したアドレス)にNFT IDを割り当てます。

最後に、_tokenIds.increment();で、tokenIdのカウンタを +1しています。

_mintSingleNFT関数が初めて呼び出されたとき、newTokenID0であり、関数を呼び出したユーザーに、tokenId 0番が与えられます。

  • Mintが完了したら、カウンタが1にインクリメントされます。

次にこの関数が呼ばれたとき、_newTokenIDの値は1になります。

各NFTのメタデータを明示的に設定する必要はないことに注意してください。

Token Base URIを設定することで、各NFTにIPFSに格納された正しいメタデータが自動的に割り当てられます。

👀 特定のアカウントが所有する NFT について知る

NFT保有者に何らかの実用性を提供する場合、各ユーザーがどのNFTを保有しているかを知れると便利です。

ここでは、特定の保有者が所有するすべてのtokenIdを返す簡単な関数tokensOfOwnerを作成します。

下記を_mintSingleNFT関数のコードブロック直下に追加しましょう。

function tokensOfOwner(
address _owner
) external view returns (uint[] memory) {
uint tokenCount = balanceOf(_owner);
uint[] memory tokensId = new uint256[](tokenCount);
for (uint i = 0; i < tokenCount; i++) {
tokensId[i] = tokenOfOwnerByIndex(_owner, i);
}

return tokensId;
}

ERC721 EnumerableのbalanceOftokenOfOwnerByIndex関数を使用していきます。

  • balanceOf : 特定の所有者がいくつのトークンを保持しているかを示す関数。

  • tokenOfOwnerByIndex : 所有者がindex番目に所有するtokenIdを取得する関数。

🏧 残高引き出し機能を実装する

コントラクトに送られたETHを引き出せないのでは、これまでの苦労が水の泡です。

そこで、コントラクトの残高をすべて引き出すことができるwithdraw関数を作成してみましょう。

  • onlyOwner修飾子をつけていきます。
function withdraw() public payable onlyOwner {
uint balance = address(this).balance;
require(balance > 0, "No ether left to withdraw");
(bool success, ) = (msg.sender).call{value: balance}("");
require(success, "Transfer failed.");
}

👏 コントラクトの完成

NFTCollectible.solが完成しました。

下記が最終的なコードです。

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

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;

uint public constant MAX_SUPPLY = 30;
uint public constant PRICE = 0.01 ether;
uint public constant MAX_PER_MINT = 3;

string public baseTokenURI;

constructor(string memory baseURI) ERC721("NFT Collectible", "NFTC") {
setBaseURI(baseURI);
}

function reserveNFTs() public onlyOwner {
uint totalMinted = _tokenIds.current();

require(totalMinted.add(10) < MAX_SUPPLY, "Not enough NFTs left to reserve");

for (uint i = 0; i < 10; i++) {
_mintSingleNFT();
}
}

function _baseURI() internal view virtual override returns (string memory) {
return baseTokenURI;
}

function setBaseURI(string memory _baseTokenURI) public onlyOwner {
baseTokenURI = _baseTokenURI;
}

function mintNFTs(uint _count) public payable {
uint totalMinted = _tokenIds.current();

require(totalMinted.add(_count) <= MAX_SUPPLY, "Not enough NFTs left!");
require(_count >0 && _count <= MAX_PER_MINT, "Cannot mint specified number of NFTs.");
require(msg.value >= PRICE.mul(_count), "Not enough ether to purchase NFTs.");

for (uint i = 0; i < _count; i++) {
_mintSingleNFT();
}
}

function _mintSingleNFT() private {
uint newTokenID = _tokenIds.current();
_safeMint(msg.sender, newTokenID);
_tokenIds.increment();
}

function tokensOfOwner(address _owner) external view returns (uint[] memory) {

uint tokenCount = balanceOf(_owner);
uint[] memory tokensId = new uint256[](tokenCount);

for (uint i = 0; i < tokenCount; i++) {
tokensId[i] = tokenOfOwnerByIndex(_owner, i);
}
return tokensId;
}

function withdraw() public payable onlyOwner {
uint balance = address(this).balance;
require(balance > 0, "No ether left to withdraw");

(bool success, ) = (msg.sender).call{value: balance}("");
require(success, "Transfer failed.");
}

}

🙋‍♂️ 質問する

ここまでの作業で何かわからないことがある場合は、Discordの#polygonで質問をしてください。

ヘルプをするときのフローが円滑になるので、エラーレポートには下記の3点を記載してください ✨

1. 質問が関連しているセクション番号とレッスン番号
2. 何をしようとしていたか
3. エラー文をコピー&ペースト
4. エラー画面のスクリーンショット

コントラクトが完成したら、次のレッスンに進んでイーサリアムネットワークにデプロイしましょう 🎉