Ethereum で最近流行りの NFT を作って売り出そう!

1. NFTとは何かを考えてみる

最近テック関連のニュースで話題になっている「NFT」とは(Non-Fungible Token、非代替性トークン)の略です。 トークンと付くので暗号資産の一種です。

Ethereum の ERC20 に代表されるトークン(NFTに対して Fungible Token、代替可能トークンと呼ばれる)は銘柄が一緒ならどれも同一と見なされ、 例えば、あなたの持つ 1 トークンと私の持つ 1 トークンを交換しても、交換は「発生しなかった」事になります。

これに対して、NFTは全てのトークンが固有であり「替えがきかない」物です。 言い換えれば「発行数1枚の仮想通貨」とも言えます。

NFTは非代替性である為、トークン1つ1つに対してユニークな特性が持たされます。 特性は何であってもいいのですが、最終的にはURLやリソースID、アドレス、シリアル番号と言った、具体的な物の所在を特定する記号へ置き換えられ、 トークン内に埋め込まれます。

皆さんの中には NFT に大きな値段が付くのを疑問に思う方もいるでしょう。筆者もその一人です。

そもそも、NFTを所有している事が即ち、物を所有している事になるのでしょうか?

例えばこう考えてみましょう。 突然NFT発行者が「要望が多いから、同じ物に新たなトークンを発行することにしたよ」と言い出したら、 最初に大枚をはたいてNFTを購入した人はどうするでしょうか。

NFTに関連付けられた物が「複製の効かない物理的な物品」で「NFT所有者が物品の所有者」と定義されるなら、単なる二重契約になるでしょう。 購入者はブロックチェーンの記録を基に、自身が正当な所有者であることを証明できます。

しかしデジタルデータならどうでしょうか。デジタルデータは簡単にコピーでき、オリジナルとコピー品の区別は一切つきません。 コピーしたデジタルデータへ別のNFTを関連付けて売りに出す事は、技術的に容易です。

デジタルデータをコピーしたNFTが売りに出された場合であっても、最初の購入者はブロックチェーンの記録を基に、 オリジナルの所有者が自分であり、自分の物が正しくオリジナルである事を証明できます。

更に、NFT発行者自身が「別ロットとして」新たなNFTを発行することはあり得ますし、違法とも言えません。 突然「二つ目、三つ目」を繰り出して、同じ見た目の別NFTが市場へ出回る事になるかもしれません。 しかしブロックチェーンには改竄できない履歴が残っているので、所有者は「初期ロットを所有するのは自分」と常に証明できます。 これは発行者自身にも改編できない事実になります。

NFTの本質とは、関連付けされた物の所有を意味しません。 「ブロックチェーンという改編不可能な履歴へ、物がn番目にリンクされた証拠」の所有であると言えます。 人間の本能的な価値観から、n=1 が最も価値を持つのは言うまでもありませんし、n が複数でも十分小さければレアリティが付くでしょう (*1)。

NFTになぜ値段が付くのか理解できたでしょうか?

それを踏まえた上で、NFTアートを見ると、 アーティストが有名人なら、あるいは「将来(良くも悪くも)話題になりそうな人物」なら、 大きく値を上げる可能性がある為、購入する人がいるのも頷けますね (*2)。

NFTに関連付けられた「現物」をどのように扱って良いのかはトークンによってまちまちだと思うので、購入する前に確認する必要があるでしょう。 そういう点曖昧なのも多いと思われますし、購入者の捉え方もまちまちなのではと思います。 今後問題になるんじゃないかな?


(*1)これはあらゆるID付けされたデータや資産、歴史的出来事や、その他ありとあらゆる「固有の概念」がNFTとしてマーケットプレースに並ぶ可能性を示しています。 素数ですらNFTとして流通するかもしれません。NFTコミュニティの価値観が変態の領域に突入すれば、あり得るかもしれないですね

(*2) あくまでもアートはビジュアルありきなので、価値があるからと言って、感性に合わないビジュアルのNFTを無理に買う必要は全くありません。

2. NFT を作ってみよう

NFTを作る前に、どのブロックチェーンで作るかを決める必要があります。 また、ブロックチェーンは NFT に対応しているものを選ばなければなりません。 例えば、独自トークンを作れてメタデータが登録できる、あるいはスマートコントラクトが使える物が該当します。

NFTの作成に最も多く使用されているのはご存じ Ethereum です。 主要なマーケットプレースで取引ができるので、デファクトスタンダードと言っても過言ではないです (*)。

という事で今回は Ethereum で NFT を作る方法を見ていきたいと思います。

更に、販売することを考えるとマーケットプレースも選ぶ必要がありますが、今回は一番有名な OpenSea を想定します。

GitHub にボイラープレートを置いたので、めんどくさい方はこちらをダウンロードして使ってください。 以下の手順の大部分を省略できます。


(*) しかし Ethereum は現在スケーラビリティとガス代問題が存在するので、別のプラットフォームを採用するケースもあるそうです。

2.1. スマートコントラクトの作成

NFTはERC721という規格を使用してスマートコントラクトを作成します。

とりあえず、hardhat の環境を構築しましょう。 Node.jsyarn は導入済みの前提とします。 Ethereum の利用には必須のウォレットアプリ MetaMask も導入しておいてください。

mkdir mynft
cd mynft
yarn init
yarn add -D hardhat @nomiclabs/hardhat-waffle ethereum-waffle @nomiclabs/hardhat-ethers hardhat-deploy hardhat-deploy-ethers ethers chai dotenv
yarn add -D ts-node typescript @types/node @types/mocha @types/chai
npx hardhat

Hardhat のインストーラーでは、「Create an empty hardhat.config.js」を選択してください。

プロジェクトルートに hardhat.config.js が作成されるので、 hardhat.config.ts にリネームして以下の様に編集してください。 Solidity のバージョンを 0.8.0 に指定します。 これをしないと、後述のライブラリが使用できないです。

import dotenv from "dotenv";
dotenv.config();

import { HardhatUserConfig } from "hardhat/config";
import "@nomiclabs/hardhat-waffle";
import "@nomiclabs/hardhat-ethers";
import "hardhat-deploy-ethers";
import "hardhat-deploy";
import assert from "assert";

assert(process.env.NODE_URL);
assert(process.env.WALLET_PRIVATE_KEY);

const config: HardhatUserConfig = {
  defaultNetwork: "hardhat",
  networks: {
    online: {
      url: process.env.NODE_URL,
      accounts: [
        process.env.WALLET_PRIVATE_KEY,
      ]
    },
  },
  namedAccounts: {
    deployer: {
      default: 0,
    },
    tester1: {
      default: 1,
    },
    tester2: {
      default: 2,
    },
  },
  solidity: "0.8.0",
}

export default config;

プロジェクトルートに tsconfig.json を作成し、以下の内容を入力して保存してください。

{
  "compilerOptions": {
    "target": "es2018",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "outDir": "dist"
  },
  "include": ["./deploy", "./test"],
  "files": ["./hardhat.config.ts"]
}

作成したいスマートコントラクトの大部分は、 実装内容が共通しているので Open Zeppelin のライブラリを継承することで、 工数を大幅に減らせます。

ライブラリをプロジェクトに追加しましょう。

$ yarn add -D @openzeppelin/contracts

プロジェクトルートに contracts ディレクトリを作成し、MyERC721.sol というファイルを作成してください。 内容は下記の通りになります。

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

import "@openzeppelin/contracts/token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol";

contract MyERC721 is ERC721PresetMinterPauserAutoId {
    address proxyRegistryAddress;

    constructor(
        address _proxyRegistryAddress,
        string memory _name,
        string memory _symbol,
        string memory _baseTokenURI
    )
        ERC721PresetMinterPauserAutoId(_name, _symbol, _baseTokenURI)
    {
        proxyRegistryAddress = _proxyRegistryAddress;
    }

    function isApprovedForAll(address _owner, address _operator)
        public override view returns (bool)
    {
        ProxyRegistry proxyRegistry = ProxyRegistry(proxyRegistryAddress);
        if (address(proxyRegistry.proxies(_owner)) == _operator) {
            return true;
        }

        return super.isApprovedForAll(_owner, _operator);
    }
}

コンストラクターの引数(「トークンの名前 _name」「シンボル _symbol」等)は、デプロイ時に指定します。

_baseTokenURI は、トークンのメタデータを定義する json へアクセスするための URI になります。 メタデータと併せて後ほど説明します。

関数 isApprovedForAll をオーバーライドしているのはマーケットプレースの「OpenSea」が推奨している実装で、 マーケットプレースにリストする際にネットワーク手数料(ガス代)を削減することが出来ます。 コンストラクターの _proxyRegistryAddress に OpenSea が指定するアドレスを設定します。

コントラクトが出来たらコンパイルしてみましょう。

$ npx hardhat compile

2.2. メタデータの作成とアップロード

今回用意した ERC721 コントラクトでブロックチェーン内に含められる情報は、トークン名とシンボル、そしてメタデータ json ファイルの URL だけです。 従って、その他のディテール(説明文、画像等)は、URLで外部からアクセス可能な Web ストレージに json 形式で格納しなければなりません。

URL は最終的に http://www.url.to/metadata/{tokenId} というフォーマットになります(.json という拡張子がつかないことに注意) http://www.url.to/metadata の部分はデプロイ時に設定します。 {tokenId} は発行の度に 0 からカウントアップされる ID に置換されます。一個目のトークンは 0 になります。 つまりコントラクト1つでいくつも NFT を作ることが出来ます。

json ファイルの中身は下記の通りです。

{
    "name": "My special item",
    "description": "This is most rare item in the world.",
    "image": "https://www.url.to/picture-of-item.png"
}
  • name 名前
  • description 説明
  • image 画像ファイルURL

内容はご自身が作るNFTの内容に置き換えてください。 json ファイルは任意にカスタマイズが可能で、好きなデータを追加できます。

トークンに設定される URLは NFT に関連付けられる物の所在で、json ファイルは物のルックスを定義する訳です。

格納する Web ストーレジは URL でアクセスできて公開設定できればなんでもいいですし、容量も大して消費しませんが、 Google Cloud Storage が手軽に使えて便利と思います。

2.3. デプロイスクリプトの作成

デプロイスクリプトを作成します。 プロジェクトルートに deploy ディレクトリを作成し、MyERC721.ts を作ってください。 内容は下記の通り。

import {HardhatRuntimeEnvironment} from 'hardhat/types';
import {DeployFunction} from 'hardhat-deploy/types';

const deploy: DeployFunction = async function ({
    getNamedAccounts,
    deployments,
    getChainId,
    getUnnamedAccounts
}: HardhatRuntimeEnvironment) {
    const { deploy, execute } = deployments;
    const { deployer } = await getNamedAccounts();

    const MyERC721 = await deploy("MyERC721", {
        from: deployer,
        args: [process.env.PROXY_REGISTRY_ADDRESS, 'My special item', 'MSI', 'http://www.url.to/metadata/'],
    })

    console.log('MyERC721: ' + MyERC721.address);

    await execute('MyERC721', {from: deployer}, 'mint', deployer);
}

export default deploy;
  • My special item トークンの名前
  • MSI トークンのシンボル
  • http://www.url.to/metadata/ json ファイルの格納場所

上記をご自身が作るNFTの内容に置き換えてください。

デプロイ時に、自分自身に対して 1 個目の NFT を Mint(発行)します。この NFT の tokenId は 0 になります。

デプロイ後も Mint だけを実行すれば、同じコントラクトで新たなNFTを作り出すことが出来ます。

最後にプロジェクトルートに .env というファイルを作って内容を以下の様にします。 Replace your ... の部分はご自身の設定に置換してください。

# Test network
PROXY_REGISTRY_ADDRESS=0xf57b2c51ded3a29e6891aba85459d600256cf317
NODE_URL=Replace your API URL
WALLET_PRIVATE_KEY=Replace your private key

# Mainnet
#PROXY_REGISTRY_ADDRESS=0xa5409ec958c83c3f309868babaca7c86dcb077c1
#NODE_URL=Replace your API URL
#WALLET_PRIVATE_KEY=Replace your private key

Test network にテストの設定、Mainnet に本番の設定を入れます。 # で始まる行はコメントですから、使う方の # を消して、使わない方に # を付け足してください。

  • PROXY_REGISTRY_ADDRESS マーケットプレース OpenSea から指定されるアドレスで、コントラクトのデプロイ時に使用されます。
  • NODE_URL デプロイ用に用意したノードの API URI です。Alchemy から無料で借りられますので、 CREATE APP を使って、「Network」にテストの場合は「Rinkeby」、本番の場合は「Mainnet」を選択してノードを作成してください。 APP ダッシュボードから VIEW KEY → HTTP の URL をコピー&ペーストすればOKです。
  • WALLET_PRIVATE_KEY NFTのオーナーとなるアドレスの秘密鍵を指定します。デプロイ時のガス代もこのアドレスから支払われます。

え?こんなんで物売るっていうレベルなの? と思った方は 1 章を読み返してみてください。

2.4. 単体テストの実施

スマートコントラクトは一度デプロイしたら後から修正が利きませんから、必ずテストを行います。 プロジェクトルートに test ディレクトリを作成し、MyERC721.test.js を作ってください。

import {expect} from "chai";
import hre from "hardhat";
import {TransactionResponse} from "@ethersproject/abstract-provider";
import {BigNumber} from "ethers";

describe('MyERC721', () => {
    beforeEach(async () => {
        await hre.deployments.fixture();
    })

    const getContracts = async () => {
        const { deployer, tester1, tester2 } = await hre.getNamedAccounts();
        const MyERC721 = await hre.ethers.getContract('MyERC721', deployer);

        return { MyERC721, deployer, tester1, tester2 };
    }

    it('Transfer', async () => {
        const { MyERC721, deployer, tester1 } = await getContracts();

        expect(await MyERC721.totalSupply()).equal(BigNumber.from(1));
        expect(await MyERC721.balanceOf(deployer)).equal(BigNumber.from(1));

        await MyERC721.transferFrom(deployer, tester1, 0).then((tx: TransactionResponse) => tx.wait());

        expect(await MyERC721.balanceOf(tester1)).equal(BigNumber.from(1));
        expect(await MyERC721.balanceOf(deployer)).equal(BigNumber.from(0));
        expect(await MyERC721.ownerOf(0)).equal(tester1);
    })

    it('TransferFrom', async () => {
        const { MyERC721, deployer, tester1, tester2 } = await getContracts();

        expect(await MyERC721.getApproved(0)).equal('0x0000000000000000000000000000000000000000');

        await MyERC721.approve(tester1, 0).then((tx: TransactionResponse) => tx.wait());

        expect(await MyERC721.getApproved(0)).equal(tester1);

        await MyERC721.transferFrom(deployer, tester2, 0).then((tx: TransactionResponse) => tx.wait());

        expect(await MyERC721.balanceOf(tester2)).equal(BigNumber.from(1));
        expect(await MyERC721.balanceOf(deployer)).equal(BigNumber.from(0));
        expect(await MyERC721.ownerOf(0)).equal(tester2);
    })

    it('Transfer Error', async () => {
        const { MyERC721, deployer, tester1, tester2 } = await getContracts();
        const MyERC721_2 = await hre.ethers.getContract('MyERC721', tester1);

        expect(await MyERC721.transferFrom(tester1, tester2, 0).catch((e: Error) => e.message))
            .to.have.string('ERC721: transfer of token that is not own');
        expect(await MyERC721_2.transferFrom(deployer, tester2, 0).catch((e: Error) => e.message))
            .to.have.string('ERC721: transfer caller is not owner nor approved');
    })
})

単体テストコードを起動するには npx hardhat test を実行します。

ethers.getContract() が無いとエラーが出る場合は、 package.json"@nomiclabs/hardhat-ethers": "^2.0.2""@nomiclabs/hardhat-ethers": "npm:hardhat-deploy-ethers" に書き換えてください。

また、書き換えた後は yarn.lock を一旦削除して yarn を実行してください。

hardhat-deploy-ethers の Issue より

2.5. Test Network へのデプロイと OpenSea での確認

Rinkeby Test Network へデプロイするためには、アカウントに Ether が必要になります。 こちらを参照して Ether を入手してください。3 ETH もあれば足ります。

Ether を入手したら以下のコマンドでデプロイ出来ます。

$ npx hardhat deploy --network online

デプロイはうまくいきましたか? 成功した際にコントラクトのアドレスが表示されますから、メモしてください。

https://rinkeby.etherscan.io/address/コントラクトのアドレス

をブラウザで開くと、正常にデプロイされたことが確認できます。 かかったガス代が「Transaction Fee」に表示されています。 Rinkeby は Gas Price = 1 Gwei でトランザクションが実行されているので、 現在の Mainnet へデプロイした場合にかかるガス代を知りたければ 100 倍程度してください。

ちなみに、今回かかったガス代は Mainnet 価格換算で、デプロイに 4,384,932 Unit = 0.4384932 ETH(約99,000円)、 Mint に 127,944 Unit = 0.0127944 ETH(約2,900円)です。

首尾よくデプロイできたら、 OpenSea で表示の確認をしましょう。 実際に売りに出した時、どのように見えるかを確認できます。

https://testnets.opensea.io/assets/コントラクトのアドレス/0

にブラウザでアクセスすると、以下の様に確認できます。

https://testnets-api.opensea.io/asset/コントラクトのアドレス/0/validate

にアクセスすると、json の書式が問題ないかを検証してくれます。

OpenSea には テストサイト がありますので、実際に販売するところまでやってみましょう。

ウォレットアプリの MetaMask は導入済みの前提で進めます。 まず、画面右上のアイコンをクリックして、MetaMask と接続します。

次に Create → Submit NFTs → add an existing contract を開き

「Live on testnet」を選んで、あなたの NFT コントラクトアドレスを入力して、コレクションに加えます。

コレクションから販売したい NFT を選び

「Sell」を押します。

販売条件を設定して「Post Your Listing」をクリックします。 特定の値段で販売したり、オークションにかけたりと言った設定が出来ます。 初回の販売開始時に MetaMask でガス代を支払う必要がありますので注意してください。

また、OpenSeaでの販売は手数料として成約した金額の2.5%が掛かります。

テストサイトでやってみたところ、初回の出品が完了するまで、現在のレートで約8,747円程度のガス代がかかりました。 同じ NFT であれば2回目からはガス代が発生しません。 販売取り下げには約1,647円のガス代でした。 これらは Ethereum のネットワークに支払う手数料です(OpenSeaではありません)。

こちらで、「販売中」としてマーケットプレースにリストされます。

キャンセルしたければ「Cancel Listing」をクリックしてください。

2.6. Mainnet へのデプロイと OpenSea での販売開始

Mainnet へのデプロイは実際のお金がかかります。実行する前に専門家と相談し、良く検討とテストを実施した上で、自己責任のもと行ってください

NFTを本番のマーケットプレースで販売するには Mainnet へコントラクトをデプロイしなければなりません。 デプロイするにはガス代があなたの本物の資産から差し引かれますから注意してください。

.env の設定を Mainnet の物へと書き換えた後、npx hardhat deploy --network online を実行してください。

本番にデプロイしたら後は OpenSea で売りに出しましょう。 今度は 本番の OpenSea にログイン(MetaMask で OpenSea と接続)して、 テストと同様に Create → Submit NFTs → add on existing contract でコントラクトアドレスを入力して NFT を追加し、 詳細画面で「Sell」をクリックすれば値段をつけて販売(あるいはオークションを開始)することが出来ます。

3. 売れる NFT とは?

流行りの NFT だからって何でも売れるわけではないと思います。 基本的には制作者が有名人であり、誰でも知ってる出来事や事件に関連したデータ(歴史的資産)や、 将来有望なクリエイターの作品に需要が集まるんじゃないでしょうか。

我々にとって一番分かり易いのはゲーム・アプリのアイテムで、 ゲーム・アプリ内で使用出来る実用性があり、かつゲーム・アプリによってそのレアリティが保証されているものは高値が付くでしょう。

つまり、ただグラフィックアートを Web ストレージにアップロードして NFT にリンクするのではなく、 リンク先が Web アプリになっていて、一定のロジックで合成(クラフティング)された物にするといった、創意工夫を加える物が人気を集めていると思います。 合成される確率が低いものは正しく「レアアイテム」という事になるわけです。

誰ぞ知らない人が書いた単なる落書きはそもそも売れないですし、売りたくないですね・・・。

4. NFT の問題点

4.1. 所有権があいまいである

第1章で説明した通り、NFTを購入したからと言って物の所有権が得られるとは限らないので、対象の NFT がどういった性質の物かは事前によく確認するべきでしょう。

NFT が必ずしも独占的なアクセス権を認める物ではないですし、当然、著作権も移譲されません。 NFT でグラフィックアートを買ったからと言って、作品を何にでも自由に使える訳ではないかもしれません(素材の販売とは違うもの)。 NFT で動画を購入しても、独占的な動画視聴の権利を買っているわけではないかもしれません。

マーケットプレースを眺めている限り、NFT を購入した事により、リンクされたデータに対してどの様な権利が発生するのかあいまいな NFT も多いです。

4.2. (一部を除いて)実用性がほぼゼロである

ゲームやアプリで生成された固有のアイテムなら、ゲーム・アプリ内で使える為分かりやすいですが、 そうでないものは、購入しようとしている NFT が一体何なのかを考えるべきでしょう。

特に「Twitter CEOの初回ツイート」みたいな、第1章で述べたような価値観で高値が付いている NFT は、 人によって価値が全く理解できない領域だと思いますし、物としての実用性はほぼゼロです。

これに限らず、ゲームやアプリのアイテムでないものは実用性がほぼゼロですから、実用性を求めて購入する物では有りません。

4.3. 盗作し放題である

例えば NFT アートの場合、NFT に関連付けられた作品が、本当に発行者が制作した物なのか確認しなければなりません。 勝手に他人の作品を NFT に関連付けて販売している人がいるかもしれません。と、言うか確実にいると思います。 未だ聡明期な故、権利者に気付かれにくい状況でもあるため、悪質な行為が横行していると思った方がいいでしょう。 そういった盗作 NFT は、いずれ判明すれば価値が無くなる恐れがあります。

有名なクリエイターの NFT は、大抵本人の Web サイトや SNS アカウントで発売をアナウンスしているはずですから、 アナウンスに記載された URL やアドレスが、マーケットプレース上と一致するか確認すべきでしょう。

偽物を掴まされるリスクは常にあります。

また、クリエイターとしても、必ず本人確認ができるメディアを使い、正確な URL やアドレスを明記して告知してください。

4.4. メタデータのホストは、突然サービスが終了するかもしれない

メタデータ json をホストしている Web ストレージが、何らかの理由でサービス終了する可能性があります。 サービス終了した場合でも、ブロックチェーン上の履歴とメタデータは維持されますが、ルックスが失われる事になります。

従って、NFT の発行者はメタデータ json のホストを維持し続けなければなりません。 無料で利用でき、そうそう潰れる心配がないサービスでホストするのが良いでしょう (Google Cloud Storage をお勧めするのはその為です)

ゲーム・アプリのアイテムも、ゲーム・アプリ自体のサービスが終了する可能性はあります。 その場合、ブロックチェーン上のメタデータは維持されますが、ルックスおよび機能が失われる事になります。

4.5. 規制される可能性がある

マネーロンダリングの手段になったり、詐欺が横行したりすれば、相応の規制がかかる可能性が高いです。 話題になっている以上、今まさに当局が注目している分野だと思います。

5. まとめ

NFT はいわば、ブロックチェーンの「コレクションアイテム」で、暗号資産という新しい価値観の中に生まれた物であり、 コミュニティの外から見ると、にわかには理解できない物だと思います。

かのイーロン・マスク氏が「正しいこととは思えない」として、 自身が作った NFT 売却をキャンセルしたという ニュース もありました。

筆者自身、色々調べた上でも、ゲーム・アプリのアイテム以外は、未だその価値を完全には理解出来ないでいます。

しかし、実用性もなく、芸術を独占できる訳でもない NFT に大枚をはたく人々がいるのは確かです。 確かに、有名人や芸術家がリリースする「特別な物」にお金を払うというのは、 必ずしも物質的な価値やビジュアルや実用性に対してではないとするならば、 「不変的かつ特別な物」であるならば払っても良いという人はいるでしょう。 と、頭では理解しているのですが、余りにも価値観が抽象化されすぎて、なかなか実感できないものではあります。

ブログをご覧の Youtuber の皆さんプロゲーマーの皆さん、NFT 作ってみませんか? 弊社はいつでもお手伝いできますよ!

と、いうわけで今回は世間を騒がせている NFT についてでした。 それではまた次回!