失敗談: NFTを作ったらmintできなくなってしまった
ERC721EnumerableとERC721Burnableを継承してNFTを作って、テストネットにデプロイしたのですが、 mintできなくなってしまいました。
バッドケースとして共有しておきます。
作成したコントラクト
すごくシンプルに書くとこうなります。(他の関数は省略)
contract MyNFT is ERC721Enumerable, ERC721Burnable { function mint() public { uint256 tokenId = totalSupply(); _safeMint(_msgSender(), tokenId); } //...略... }
totalSupply()で総発行数を取得して、それをtokenIdにしています。 mintが呼ばれる度にtokenIdが0,1,2...と増えていく、という単純な仕組みです。
ところが
これをデプロイして、mintを2回して、burnして、もう一度mintしようとすると、
Error: execution reverted: ERC721: token already minted
というエラーが出て、mintできなくなってしまいました。
なんで?
totalSupply()は、burnされた分は除外して総発行数を返すようです。つまり減ることもある。(知らんかった。。。)
- 最初の状態では、 totalSupply = 0
- mint 1回目 (tokenID=0) → totalSupply = 1 に増える
- mint 2回目 (tokenID=1) → totalSupply = 2 に増える
- tokenID=0をburn → totalSupply = 1 に減る
- mint 3回目 (tokenID=1) → エラー
となってしまいます。3回目のmintのときに tokenId=1で発行しようとしますが、1番は発行済のためにエラーになってしまう、というわけです。
(追記:ここの説明が間違ってたので修正しました。)
まとめ
tokenIdにtotalSupply()を使うのは危険。tokenId用の変数を用意しよう。
デプロイ後に気づいたので削除も修正もできず、失敗作が(テストネットとはいえ)ブロックチェーン上に残ることに。。。orz
burnによってtotalSupplyが減るまでの処理を追ってみる
burnというのは単にアドレス(0x0)に送信するもの、というふうに思っていましたが、 実際にどういう処理をしているのか見てみました。
まず、ERC721Burnableのburnからスタートします。多重継承なので、あっちこっち飛んでわかりにくいです。
// ERC721Burnable.sol function burn(uint256 tokenId) public virtual { require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721Burnable: caller is not owner nor approved"); _burn(tokenId); // ← }
ERC721の_burnを呼び出し。
// ERC721.sol function _burn(uint256 tokenId) internal virtual { address owner = ERC721.ownerOf(tokenId); _beforeTokenTransfer(owner, address(0), tokenId); // ← _approve(address(0), tokenId); _balances[owner] -= 1; delete _owners[tokenId]; emit Transfer(owner, address(0), tokenId); }
ERC721Enumerableの_beforeTokenTransferを呼び出し。
// ERC721Enumerable.sol function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal virtual override { super._beforeTokenTransfer(from, to, tokenId); if (from == address(0)) { _addTokenToAllTokensEnumeration(tokenId); } else if (from != to) { _removeTokenFromOwnerEnumeration(from, tokenId); } if (to == address(0)) { _removeTokenFromAllTokensEnumeration(tokenId); // ← } else if (to != from) { _addTokenToOwnerEnumeration(to, tokenId); } }
to==address(0) なので、_removeTokenFromAllTokensEnumeration を呼び出し。
// ERC721Enumerable.sol function _removeTokenFromAllTokensEnumeration(uint256 tokenId) private { uint256 lastTokenIndex = _allTokens.length - 1; uint256 tokenIndex = _allTokensIndex[tokenId]; uint256 lastTokenId = _allTokens[lastTokenIndex]; _allTokens[tokenIndex] = lastTokenId; _allTokensIndex[lastTokenId] = tokenIndex; delete _allTokensIndex[tokenId]; _allTokens.pop(); // ← }
最後の_allTokens.pop()で最後の要素が削除され、_allTokens.lengthが一つ小さくなります。
// ERC721Enumerable.sol function totalSupply() public view virtual override returns (uint256) { return _allTokens.length; }
totalSupply関数は _allTokens.length を返しているので、burnによってtotalSupplyが1つ減ることがわかります。
ただし、burnしたtokenIdは再びmintすることは可能です。
おまけ:今日の運勢