====== OpenZeppelin で ERC721(NFT) トークンの作成 ====== ほぼ OpenZeppelin オフィシャルの[[https://docs.openzeppelin.com/contracts/4.x/erc721|ドキュメント]]通りに NFT スマートコントラクトを実装します。 今回作成する GameItem スマートコントラクトは NFT の生成プログラムです。GameItem の awardItem メソッドを呼び出したときに NFT が生成されます。生成された NFT には NFT の所有者とtokenURIを保持します。tokenURI が指し示す先にゲームアイテムデータの実体があります。 tokenURI は NFT を生成する際に指定されます。その指定された URI の所有者を NFT で記録しているということになります。 URI はブロックチェーンの外にありますので、所有者が所有する物はブロックチェーンの外にあることになります。このようなデータを「オフチェーン」と呼びます。逆に、所有する物もブロックチェーン上に記録することを「オンチェーン」と呼びます。 オフィシャルの[[https://docs.openzeppelin.com/contracts/4.x/erc721|ドキュメント]]より引用。 > Note > you’ll notice that the item’s information is included in the metadata, but that information isn’t on-chain! So a game developer could change the underlying metadata, changing the rules of the game! If you’d like to put all item information on-chain, you can extend ERC721 to do so (though it will be rather costly). You could also leverage IPFS to store the tokenURI information, but these techniques are out of the scope of this overview guide. GameItem スマートコントラクトは ERC721URIStorage を継承しているため「オフチェーン」になります。オンチェーンにしたい場合は ERC721 を継承すればOKです。「オンチェーン」の場合は「オフチェーン」よりもガス代が割高になります。また、tokenURI に IPFS の URI を指定することもできます。 つまり、ethereum ブロックチェーン上の NFT と IPFS を組み合わせれば、非中央集権を保ったまま改竄できない NFT を実現できます。 ===== 前提 ===== npm、Truffle、Ganache をインストールしておいてください。 (参考) [[blockchain:truffleを使ったスマートコントラクト開発|Truffleを使ったスマートコントラクト開発]] (参考) [[blockchain:ethereum構築ハンズオン_ganache編|ethereumブロックチェーンの構築(Ganache編)]] ===== プロジェクト作成 ===== > mkdir nft_project > cd nft_project nft_project> truffle init [[blockchain:truffleを使ったスマートコントラクト開発|Truffleを使ったスマートコントラクト開発]] を参考に truffle-config.js の設定をしてください。(どの ethereum ブロックチェーンに接続するか設定する必要があります。) ===== OpenZeppelin のインストール ===== npm を使用して OpenZeppelin をインストールします。 nft_project> npm init This utility will walk you through creating a package.json file. It only covers the most common items, and tries to guess sensible defaults. See `npm help init` for definitive documentation on these fields and exactly what they do. Use `npm install ` afterwards to install a package and save it as a dependency in the package.json file. Press ^C at any time to quit. package name: (nft_project) version: (1.0.0) description: entry point: (truffle-config.js) test command: git repository: keywords: author: license: (ISC) About to write to C:\Users\miyazato\work\ethereum\nft_project\package.json: { "name": "nft_project", "version": "1.0.0", "description": "", "main": "truffle-config.js", "directories": { "test": "test" }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC" } Is this OK? (yes) nft_project> npm install --save-dev @openzeppelin/contracts ===== NFTの実装 ===== nft_project> truffle create contract GameItem nft_project> code contracts/GameItem.sol // contracts/GameItem.sol // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "../node_modules/@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; import "../node_modules/@openzeppelin/contracts/utils/Counters.sol"; contract GameItem is ERC721URIStorage { using Counters for Counters.Counter; Counters.Counter private _tokenIds; constructor() ERC721("GameItem", "ITM") {} function awardItem(address player, string memory tokenURI) public returns (uint256) { uint256 newItemId = _tokenIds.current(); _mint(player, newItemId); _setTokenURI(newItemId, tokenURI); _tokenIds.increment(); return newItemId; } } ===== マイグレーション ===== nft_project> truffle create migration GameItem nft_project> code .\migrations\1651959757_game_item.js const GameItem = artifacts.require("GameItem"); module.exports = function(_deployer) { // Use deployer to state migration tasks. _deployer.deploy(GameItem) }; ===== デプロイ ===== nft_project> truffle migrate ===== 実行(Mint) ===== 今回作成したスマートコントラクトの awardItem を実行すると、新しい NFT が生成されます。 新たな NFT を生成することを Mint と言います。実際 awardItem のソースコードでは「_mint(player, newItemId);」と _mint メソッドを呼び出しています。ERC721(NFT) トークンだけでなく ERC20 トークンを新たに生成することも Mint と言います。 Mint は英語の「Minting 鋳造(ちゅうぞう)」から来ています。(例: minting authority 造幣局) nft_project> truffle console truffle(development)> let gameItem = await GameItem.deployed(); truffle(development)> let address = await web3.eth.getAccounts(); truffle(development)> let playerAddress = address[0]; truffle(development)> let transaction = await gameItem.awardItem(playerAddress, "https://game.example/item-id-8u5h2m.json"); { tx: '0xf32a1d6dd67167b16537ddc0308cdd5918773aca77f91438be056adf38a212bf', receipt: { transactionHash: '0xf32a1d6dd67167b16537ddc0308cdd5918773aca77f91438be056adf38a212bf', transactionIndex: 0, blockHash: '0x3c932e396c46e28469fa2db720481e5d1814a624061429bd5eab1a135c0382ff', blockNumber: 5, from: '0x9384fc1b3f3cc59e6e30a0ba0267451570bb7aa6', to: '0x36c73d181564e2bc7ef390f7067ffabf4c23d535', gasUsed: 153238, cumulativeGasUsed: 153238, contractAddress: null, logs: [ [Object] ], status: true, logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000020000000000000000000800000000000000000000000010000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000008000000000000000000000000000000000020000000000002000000000000000000000000000000000000000000000000040020000000100000000000000000000000000000000000000000000000000000000000', rawLogs: [ [Object] ] }, logs: [ { logIndex: 0, transactionIndex: 0, transactionHash: '0xf32a1d6dd67167b16537ddc0308cdd5918773aca77f91438be056adf38a212bf', blockHash: '0x3c932e396c46e28469fa2db720481e5d1814a624061429bd5eab1a135c0382ff', blockNumber: 5, address: '0x36c73D181564e2BC7Ef390F7067FfABf4C23d535', type: 'mined', id: 'log_ca6a0b3b', event: 'Transfer', args: [Result] } ] } トランザクションログから Mint した NFT の TOKENID を取得します。 (GameItem スマートコントラクトは内部的に自動インクリメントする TOKENID を生成しています。これが主キーの役割を果たしています。) truffle(development)> let TOKENID = transaction.logs[0].args.tokenId; Mint した NFT の所有者と NFT に保存されている tokenURI を確認します。 truffle(development)> gameItem.ownerOf(TOKENID) '0x9384FC1B3F3CC59e6e30a0BA0267451570bb7AA6' truffle(development)> gameItem.tokenURI(TOKENID) 'https://game.example/item-id-8u5h2m.json'