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

lesson-2_ミント機能

💳 ホワイトリスト内のアドレスのみがミントできる NFT コントラクトを書く

このdAppスマートコントラクトには、BAYCと同じERC 721コントラクトを選択します。コントラクトにホワイトリスト制限機能を追加していきましょう。

    address public owner;

constructor(address[] memory initialAddresses) {
owner = msg.sender;
...
}

function addToWhitelist(address _address) public {
// Check if the user is the owner
require(owner == msg.sender, "Caller is not the owner");
...
}

Whitelistコントラクトでは、オーナーアドレスを設定し、requireメソッドを使用してホワイトリストに追加または削除する機能がコントラクトのデプロイヤーによってのみ呼び出されるようにします。

ここでは、より安全で効率的な方法であるOpenZeppelinスマートコントラクトライブラリを使用します。

OpenZeppelinOwnable.solを使ってオーナー権限機能を実装します。

デフォルトでは、Ownableコントラクトのオーナーはそれをデプロイしたアカウントになります。

  • Ownableは次のことも可能です:
    • オーナーのアカウントの所有権を新しいアカウントに移譲する
    • 所有権を放棄する: オーナーがこの管理権限を放棄する、コントラクトの初期管理フェーズ後の一般的なパターンで、コントラクトをより分散化させます。

さらに、ERC721コントラクトの拡張であるERC721 Enumerableを使用します。これには、ERC721のすべての機能に加えて、追加の実装が含まれています。

  • ERC721 Enumerableは、コントラクト内のすべてのtokenIdsおよびコントラクト内の指定されたアドレスが保持するtokenIdsを追跡するのに役立ちます。
  • これには、tokenOfOwnerByIndexなどの便利な関数 がいくつか実装されています。

それでは、contractsディレクトリの下にinterfacesというフォルダを作成しましょう。

image-20230222235209219

interfacesフォルダ内に、IWhitelist.solというスマートコントラクトファイルを作成します。

注意:インターフェースのみを含む Solidity ファイルは、通常、それらがインターフェースであることを示すための接頭辞Iを持っています。

image-20230222235330497

IWhitelist.solに次のコードを記述します。

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

interface IWhitelist {
function whitelistedAddresses(address) external view returns (bool);
}

これはインタフェースファイルです。他のスマートコントラクトがWhitelist.sol内のwhitelistedAddresses関数を呼び出すのを便利にします。これにより、アドレスがホワイトリストに登録されているかどうかを確認できます。

次に、Shield.solcontractsフォルダ内に作成します。

image-20230223091938319

Shield.solに次のコードを記述します。

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

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "./interfaces/IWhitelist.sol";

contract Shield is ERC721Enumerable, Ownable {
/**
* @dev _baseTokenURI for computing {tokenURI}. If set, the resulting URI for each
* token will be the concatenation of the `baseURI` and the `tokenId`.
*/
string private _baseTokenURI;

// price is the price of one Shield NFT
uint256 public price = 0.01 ether;

// paused is used to pause the contract in case of an emergency
bool public paused;

// max number of Shield NFT
uint256 public maxTokenIds = 4;

// total number of tokenIds minted
uint256 public tokenIds;

// Whitelist contract instance
IWhitelist private _whitelist;

modifier onlyWhenNotPaused {
require(!paused, "Contract currently paused");
_;
}

/**
* @dev ERC721 constructor takes in a `name` and a `symbol` to the token collection.
* name in our case is `Shields` and symbol is `CS`.
* Constructor for Shields takes in the baseURI to set _baseTokenURI for the collection.
* It also initializes an instance of whitelist interface.
*/
constructor (string memory baseURI, address whitelistContract) ERC721("ChainIDE Shields", "CS") Ownable(msg.sender) {
_baseTokenURI = baseURI;
_whitelist = IWhitelist(whitelistContract);
}


/**
* @dev presaleMint allows a user to mint one NFT per transaction during the presale.
*/
function mint() public payable onlyWhenNotPaused {
require(_whitelist.whitelistedAddresses(msg.sender), "You are not whitelisted");
require(tokenIds < maxTokenIds, "Exceeded maximum Shields supply");
require(msg.value >= price, "Ether sent is not correct");
tokenIds += 1;
//_safeMint is a safer version of the _mint function as it ensures that
// if the address being minted to is a contract, then it knows how to deal with ERC721 tokens
// If the address being minted to is not a contract, it works the same way as _mint
_safeMint(msg.sender, tokenIds);
}

/**
* @dev _baseURI overides the Openzeppelin's ERC721 implementation which by default
* returned an empty string for the baseURI
*/
function _baseURI() internal view virtual override returns (string memory) {
return _baseTokenURI;
}

/**
* @dev setPaused makes the contract paused or unpaused
*/
function setPaused(bool val) public onlyOwner {
paused = val;
}

/**
* @dev withdraw sends all the ether in the contract
* to the owner of the contract
*/
function withdraw() public onlyOwner {
address _owner = owner();
uint256 amount = address(this).balance;
(bool sent, ) = _owner.call{value: amount}("");
require(sent, "Failed to send Ether");
}
}

心配いりません、このコントラクトを順に解説していきましょう。

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

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "./interfaces/IWhitelist.sol";

contract Shield is ERC721Enumerable, Ownable {
...
}

ERC721EnumerableOwnableのように、ここでは多くのことが起こっています。まず、コントラクトを宣言する際に、2つのOpenZeppelinのコントラクトを「継承」しています。継承に関して詳しくはこちらで読むことができますが、基本的に、Shieldコントラクトのコードには、ERC721EnumerableとOwnableの2つのコントラクトのコードが含まれています。よって、これら2つの機能を実装するためのコードを再度書く必要がなくなります。

以下のより重要な状態変数について説明しましょう。

    /**
* @dev _baseTokenURI for computing {tokenURI}. If set, the resulting URI for each
* token will be the concatenation of the `baseURI` and the `tokenId`.
*/
string _baseTokenURI;
// total number of tokenIds minted
uint256 public tokenIds;

_baseTokenURIはNFTメタデータのルートリンクで、例えば:ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/(IPFSは分散型ストレージプロトコルです。これについては後述します)や、集中型のアドレス、例えば:https://xxxxxxxxxxxx/などです。

tokenIdsは各NFTの数値IDを表しており、これらのIDはユニークです。_baseTokenURIと組み合わせることで、各NFTのメタデータが形成されます(メタデータについては後ほど説明します。今は、メタデータがあることでNFTをさまざまなNFTプラットフォームで表示できることを覚えておいてください)。

    //  price is the price of one Shield NFT
uint256 public price = 0.01 ether;

// max number of Shield NFT
uint256 public maxTokenIds = 4;

priceは各NFTの価格を設定します。Ethereum(ETH)ではETHそのものを指し、PolygonではMaticを指します。ether以外にも単位があります:1 ether = 10^9 gwei、1 gwei = 10^9 weiです。

maxTokenIdsはNFTの最大数を示しています。ここでは4に設定されているため、4つのNFTのメタデータを準備する必要があります。

    /**
* @dev ERC721 constructor takes in a `name` and a `symbol` to the token collection.
* name in our case is `Shields` and symbol is `CS`.
* Constructor for Shields takes in the baseURI to set _baseTokenURI for the collection.
* It also initializes an instance of whitelist interface.
*/
constructor (string memory baseURI, address whitelistContract) ERC721("ChainIDE Shields", "CS") {
_baseTokenURI = baseURI;
_whitelist = IWhitelist(whitelistContract);
}

コントラクトをデプロイする際には、_baseTokenURIWhitelistコントラクトのアドレスを入力する必要があります。同時に、このNFTの名前を「ChainIDE Shields」、記号を「CS」と設定します。

     /**
* @dev presaleMint allows a user to mint one NFT per transaction during the presale.
*/
function mint() public payable onlyWhenNotPaused {
require(_whitelist.whitelistedAddresses(msg.sender), "You are not whitelisted");
require(tokenIds < maxTokenIds, "Exceeded maximum Shields supply");
require(msg.value >= price, "Ether sent is not correct");
tokenIds += 1;
//_safeMint is a safer version of the _mint function as it ensures that
// if the address being minted to is a contract, then it knows how to deal with ERC721 tokens
// If the address being minted to is not a contract, it works the same way as _mint
_safeMint(msg.sender, tokenIds);
}

mint関数の説明に焦点を当てましょう:

  1. payableというキーワードは、この関数が直接トークンを受け取ることができることを示しており、NFTの価格は0.01 etherです。onlyWhenNotPausedはmodifierが定義されています。pausedfalseのときのみ関数が実行されることを示しています(注:コントラクトはpausedがfalseの状態で開始されるため、ホワイトリストのユーザーはコントラクトのデプロイ後に直接ミントを行うことができます)。
    modifier onlyWhenNotPaused {
require(!paused, "Contract currently paused");
_;
}
  1. require(_whitelist.whitelistedAddresses(msg.sender), "You are not whitelisted");:ミントプロセスへの参加をホワイトリストに載っているユーザーのみに制限します。

  2. require(tokenIds < maxTokenIds, "Exceeded maximum Shields supply");tokenIdsの最大量が設定されたmaxTokenIds(4)を超えないように制限されています。

  3. require(msg.value >= price, "Ether sent is not correct");:送られてくるトークンは0.01 ether以上である必要があります。もし0.01 etherよりも多ければ、それも問題ありません! 😄

  4. tokenIds += 1;:上記すべての条件が満たされた後で、tokenIdsは1増加します。デフォルトのtokenIds値は0なので、tokenIdsの範囲は1, 2, 3, 4となります。

  5. _safeMint(msg.sender, tokenIds);:この機能は"@openzeppelin/contracts/token/ERC721/ERC721.sol"によって実装されています。そのコントラクトを参照することで具体的な機能を確認することができます。今のところ、この関数を呼び出した人にNFTがミントされるということだけ理解しておけば良いです。

    /**
* @dev setPaused makes the contract paused or unpaused
*/
function setPaused(bool val) public onlyOwner {
paused = val;
}

コントラクトのミントを一時停止する機能は、paused変数を通じて実現されています。この変数はbool型で、初期値はfalseです。したがって、ユーザーがミントを開始する前に、オーナーだけがこの関数を呼び出す必要があります。

    /**
* @dev withdraw sends all the ether in the contract
* to the owner of the contract
*/
function withdraw() public onlyOwner {
address _owner = owner();
uint256 amount = address(this).balance;
(bool sent, ) = _owner.call{value: amount}("");
require(sent, "Failed to send Ether");
}

コントラクトからetherを引き出すには、withdraw()関数を使用して行います。このコードの目的は、コントラクト内の資金をownerに転送することです。こちらに示されているように、トークンの転送を処理する方法はいくつかあります。この場合、callメソッドを使用しています。

次に、JS VMを使用してこのスマートコントラクトをコンパイルしてデプロイします(ChainIDEが自動的に選択するコンパイラを使用しても問題ありません)。

image-20230223092112169

DEPLOYセクションを見るとわかるように、baseURI(メタデータのルートリンク)とwhitelistContract(以前のホワイトリストのアドレス)を入力する必要があります。したがって、次はメタデータのルートリンクをどのように生成するか考えましょう。

image-20230223092203406

🙋‍♂️ 質問する

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

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

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