📡

【Solidity/NFT】ERC721URIStorageを使わないERC721実装

2022/12/05に公開

はじめに

NFTコントラクト実装の入門記事では、OpenZeppelinのERC721URIStorage._setTokenURIを使っていることが多く、恥ずかしながらこれしか手段知らなかった😇
BAYCのコントラクトを読んでいると_setTokenURIを使ってなくて、IPFSへの配置方法を工夫すればERC721._safeMintだけでもいけることに気づいたのでそのメモ

要約

https://twitter.com/yagi_eng/status/1597532121132072960

ありがちな入門コード

ERC721URIStorageを使うことが悪いとかではない
普通にこれ使うことも多いだろうし、むしろ入門ではこっちの方が手軽に試せる

//Contract based on [https://docs.openzeppelin.com/contracts/3.x/erc721](https://docs.openzeppelin.com/contracts/3.x/erc721)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

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

contract MyNFT is ERC721URIStorage, Ownable {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;

    constructor() ERC721("MyNFT", "NFT") {}

    function mintNFT(address recipient, string memory tokenURI)
        public onlyOwner
        returns (uint256)
    {
        _tokenIds.increment();

        uint256 newItemId = _tokenIds.current();
        _mint(recipient, newItemId);
        _setTokenURI(newItemId, tokenURI);

        return newItemId;
    }
}

引用; https://ethereum.org/en/developers/tutorials/how-to-write-and-deploy-an-nft/

OpenZeppelin/ERC721だけで済ませる方法

IPFSで一手間ありますが、以下を満たすのであればこっちの方がシンプル

  • IPFSに配置するmetadataの数・内容に変更がない

理由は、IPFSに一度アップロードすると基本的に後から変更出来ないためです
(IPFSのCIDを上書きすれば可能だけど)

IPFS

必要な手順は以下の通り

  • metadataファイルを全て同じフォルダに格納
  • ファイル名は1, 2, 3, ...とする
  • フォルダ毎IPFSへアップロード

このファイル名がtoken idと紐付きます
token idが1のNFTは名前1のファイルを参照することになります

参考; BAYCのIPFS; https://ipfs.io/ipfs/QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/

コントラクト

こんな感じ。以下ポイント

  • _baseURI()の返り値をベタがきしているが、実際にはコンストラクタに入れたり、setterを良いした方が良さそう
  • 上記コードとは異なり、mintNFT()の引数にtokenURIを渡す必要はない
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

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

contract myNFT is ERC721, Ownable {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;

    constructor() ERC721("myNFT", "MN") {}

    function _baseURI() internal view virtual override returns (string memory) {
        // TODO 自分でアップしたIPFSのCIDを使う
        return "ipfs://QmR9gMXXoC3RRnZVWGgJcHkRLjq2UqEkA7ifQqiMc7QGPN/";
    }

    function mintNFT(address recipient) external returns (uint256) {
        _tokenIds.increment();

        uint256 newTokenId = _tokenIds.current();
        _safeMint(recipient, newTokenId);

        return newTokenId;
    }
}

解説

OpenZeppelinのERC721.sol
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol

/**
    * @dev See {IERC721Metadata-tokenURI}.
    */
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
    _requireMinted(tokenId);

    string memory baseURI = _baseURI();
    return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : "";
}

/**
    * @dev Base URI for computing {tokenURI}. If set, the resulting URI for each
    * token will be the concatenation of the `baseURI` and the `tokenId`. Empty
    * by default, can be overridden in child contracts.
    */
function _baseURI() internal view virtual returns (string memory) {
    return "";
}

上記コードより、_baseURI()を子コントラクトでoverrideしておけば、tokenURI()の返り値が_baseURI + tokenIdとなることがわかります
簡単な話ですね。。

なので、例えば、

  • _baseURI()の返り値が"ipfs://QmR9gMXXoC3RRnZVWGgJcHkRLjq2UqEkA7ifQqiMc7QGPN/"
  • tokenId1

の時は、tokenURIの返り値は"ipfs://QmR9gMXXoC3RRnZVWGgJcHkRLjq2UqEkA7ifQqiMc7QGPN/1となり、名前1のファイルが参照されます

サンプル

ちなみに、例えばmetadataを2つだけ配置しておくと、3つ目以降の画像は当然表示されない

さいごに

ユースケース限定的かも知れませんが、誰かの参考になればと!

Twitterの方でも、モダンな技術習得やサービス開発の様子を発信したりしているので良かったらチェックしてみてください!

https://twitter.com/yagi_eng/status/1599706395137351681

また、個人開発したdAppsの解説記事もありますので、良かったらそちらもご覧ください!

https://twitter.com/yagi_eng/status/1503144502034563074

Discussion