ERC-721とSolidityで見るNFTのミントの仕組み

NFTをミントするスマートコントラクトを作ろうと思い立ったはいいのですが、
そもそもNFTはどういった仕組みで作り出されているんだ？という疑問が浮かびました。

この記事は、その疑問が解決に至るまでをまとめた内容になります。
ERC-721の規格の概要、スマートコントラクトの実装とデプロイ、NFTのミント、OpenSeaでの確認の過程を一通りまとめました。

## NFTは規格に沿って実装されている
調べてみるとNFTはERC-721規格またはその派生規格に従って実装されていることが分かりました。

ERC-721の派生にはERC-998、ERC-1156もNFT規格として存在していて適材適所で使われる規格であることも分かりました。しかし、今は基本的なミントの仕組みを理解することが目的であることを思い出し、ここではそれらの調査は見送りました。

ということで引き続きERC-721を見ていきます。

## ERC-721はミントの方法を定義していない
ERC-721を読んで以下のことが分かりました。しかし、期待していたNFTをミントするための`mint()`のような機能については記載がありませんでした。。

- NFTを追跡、送信するための基本的な機能を定めた規格であること
    >This standard provides basic functionality to track and transfer NFTs.
- ERC-20では実現できない非代替性を実現するための規格であること
    >This standard is inspired by the ERC-20 token standard and builds on two years of experience since EIP-20 was created. EIP-20 is insufficient for tracking NFTs because each asset is distinct (non-fungible) whereas each of a quantity of tokens is identical (fungible).
- 決められたInterfaceを必ず実装しなければならないこと
    >Every ERC-721 compliant contract must implement the ERC721 and ERC165 interfaces (subject to “caveats” below):

    - 主に、保有数や保有しているかのチェック機能、NFTを送信する機能、NFTを第三者に承認する機能、ERC-721に対応しているかチェックする機能（ERC-165）を実装する必要がある
    - 送信機能では、バリデーションを行った上で送信する安全な送信と、単純な送信の２種類がある
    - ERC-721対応チェック機能におけるERC721のinterfaceIDは`0x80ac58cd`
- NFTを受信する側が必ず実装しなければならない機能をInterfaceとして定義している
    >A wallet/broker/auction application MUST implement the wallet interface if it will accept safe transfers.
- オプションとして、メタデータ（コントラクトの名前、シンボル、NFTデータ）についてのInterfaceを定義している
    >The metadata extension is OPTIONAL for ERC-721 smart contracts (see “caveats”, below). This allows your smart contract to be interrogated for its name and for details about the assets which your NFTs represent.
- オプションとして、NFTの発行総数やIDで検索する機能についてのInterfaceを定義している。
- solidityバージョン0.4.20を使っている人への注意事項が書いてある
    >The 0.4.20 Solidity interface grammar is not expressive enough to document the ERC-721 standard. A contract which complies with ERC-721 MUST also abide by the following:
- "NFT"という名称についてや、NFTのIDを変えるべきではないことなど諸々がRationaleに書いてある
- ERC-20の互換性についても言及されている

## ミントの方法は自分で決めてOK
ミントの方法が定義されていないということは、自分で決めて良いということになります。
また言うまでもないですが、上記ERC-721で定義されているInterfaceの中身も自分で実装する必要があります。

ということで次は、みんなどういうふうに実装しているのか？という疑問が浮かんだので調べました。

## スマートコントラクト（ERC-721+ミント機能）
特別な理由がない限りはライブラリを使って実装するのが一般的なようです。自前でゼロから実装するよりも早くかつ信頼できるというのが理由だと思います。

またそのライブラリの中でもデファクトスタンダードとも言えるOpenZeppelinが、GUI上でスマートコントラクトコードを生成するサービスを提供していました。コミュニティの方々には本当に頭が下がります。

https://wizard.openzeppelin.com/#erc721

早速作りました。「Mintable（ミント機能）」にチェックしています。
![Screenshot 2023-09-18 at 22.05.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3559834/ecf00dfc-dede-4879-e1e3-669faf5bfe6b.png)

<details><summary>スクショのコード</summary>

```javascript
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract MyToken is ERC721, Ownable {
    using Counters for Counters.Counter;

    Counters.Counter private _tokenIdCounter;

    constructor() ERC721("MyToken", "MTK") {}

    function safeMint(address to) public onlyOwner {
        uint256 tokenId = _tokenIdCounter.current();
        _tokenIdCounter.increment();
        _safeMint(to, tokenId);
    }
}
```

</details>

## OpenSeaなどで画像を表示させるにはURI Storage機能が必要
これをgoerliテストネットにデプロイして`safeMint()`しました。スクショの通りミント先アドレス`to`を指定するだけです。
![Screenshot 2023-09-18 at 23.37.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3559834/953e6ef3-b0b7-5963-2a8c-d8ccd318839f.png)
そしてOpenSea TestNetでNFTがミントされているのが確認できました。しかし、何も表示されていません。。
![Screenshot 2023-09-18 at 22.18.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3559834/cbd79aef-e7a2-4722-a10f-95e064ef94d8.png)
理由は簡単。今の`safeMint()`はNFTに画像などのデータ（メタデータ）を紐づける機能を持っていないためです。このままでは使いものになりません。

そこでこの問題を解決するために、ERC-721でオプションとして定義されていたメタデータInterface、つまり、**URI Storage機能**を使います。

## スマートコントラクト（ERC-721+ミント機能+URI Storage機能）
先ほどのGUIに戻って、「URI Storage」をチェックします。
![Screenshot 2023-09-18 at 22.01.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3559834/19375c61-11c7-dc98-eb58-9da0cb276360.png)
`safeMint(address to, string memory uri)`の第２引数にメタデータを紐づけるための`uri`が追加されています。

<details><summary>スクショのコード</summary>

```javascript
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract MyToken is ERC721, ERC721URIStorage, Ownable {
    using Counters for Counters.Counter;

    Counters.Counter private _tokenIdCounter;

    constructor() ERC721("MyToken", "MTK") {}

    function safeMint(address to, string memory uri) public onlyOwner {
        uint256 tokenId = _tokenIdCounter.current();
        _tokenIdCounter.increment();
        _safeMint(to, tokenId);
        _setTokenURI(tokenId, uri);
    }

    // The following functions are overrides required by Solidity.

    function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage) {
        super._burn(tokenId);
    }

    function tokenURI(uint256 tokenId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (string memory)
    {
        return super.tokenURI(tokenId);
    }

    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
}
```

</details>

## URIにはJSONファイルのリンクを設定する
最初、`safeMint(address to, string memory uri)`の`uri`に画像のリンクを設定してみたのですがそれだとOpenSeaでは表示されませんでした。。

画像のリンクのままでも規格に違反している訳ではありませんが、NFTの画像をOpenSeaで表示させたいと誰もが思うでしょう。

これを実現するためには、ERC-721にも記載がある通り先ほどの`uri`に**JSONファイルのリンク**を設定する必要があるということが分かりました。
ということで、[OpenSeaのJSON書式](https://docs.opensea.io/docs/metadata-standards)を参考にして以下のJSONファイルを作成しました。そしてこのJSONファイルのURIが欲しいので[Pinata](https://www.pinata.cloud/)にアップロードしておきます。

```json
{
    "name": "My NFT 1",
    "description": "This is my test NFT.",
    "image": "[画像のURL]",
    "attributes": [
        {
            "trait_type": "Style", 
            "value": "Elegant"
        },
        {
            "display_type": "boost_number", 
            "trait_type": "Energy", 
            "value": 40
        }, 
        {
            "display_type": "number", 
            "trait_type": "Generation", 
            "value": 2
        }

    ]
}
```
あとはデプロイして、先ほどと同様にミント先アドレス（`to`）とJSONファイルのURI（`uri`）を設定して実行するだけです。
![Screenshot 2023-09-18 at 23.28.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3559834/03cc870f-d397-aa97-5522-c1c3b5838612.png)
OpenSea TestNetで確認するときちんと画像が表示されていました。また、DescriptionやTraitsなども指定した通り表示されていました。成功です！
![Screenshot 2023-09-18 at 23.07.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3559834/5abf2ab8-f5a5-9b6e-00fe-463971782e29.png)

以上です。