つい先日、OpenSeaがNFTの二次流通に対するCreator Feeを強制ではなく取引するユーザーの任意にすることを発表しました。
Changes to creator fees on OpenSea
これに対して世間ではさまざまな反応が見られます。
Yuga LabsはOpenSeaのコントラクトをサポートしない方針を示しました。
![Image](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Ffrqcsxz2%2Fproduction%2F9daa0da148f97ece4888d92ac3910b2cd4391e04-1266x1508.png%3Fw%3D1266%26auto%3Dformat&w=3840&q=75)
今回はNFTのロイヤリティについて改めて整理したいと思います。
NFTのロイヤリティ問題とOpenSeaの奮闘の歴史
NFTが登場した初期には、共通のロイヤリティ規格は存在していませんでした。各マーケットプレイスが独自の分配の仕組みを実装しており、クリエイターへの報酬の流れは一貫していませんでした。
NFTコレクションのコントラクトにクリエイターフィーの情報を含めるロイヤリティ標準規格「EIP-2981」が登場し、マーケットプレイス側がこの情報に基づいてクリエイターフィーを支払う対応を始めました。しかしOpenSeaの競合となるBlurなどはマーケット手数料を0%に、一時的にクリエイターフィーを0%にしてOpenSeaからシェアを奪っていきました。マーケットプレイスマーケットプレイス
2022年OpenSeaはOpenSea Operator Filterを導入し一部のマーケットプレイスをブロックするコントラクトを含まない場合、OpenSeaでのクリエイターフィーは0%になるとの発表がありました。
この辺りはmiinさんの以下の記事が詳しいです。
いま崩れはじめた「NFTはクリエイターにロイヤリティが還元され続ける」という神話
OpenSea vs Blur、クリエイターフィーの現状について
この頃のOpenSeaの方針(特に既存のコレクションに対する扱い)は二転三転しエンジニアとして対応に混乱したことを覚えています。
ERC2981について
改めてERC2981とそれに対応したマーケットプレイスのコントラクトを見てみます。
以下のrolaytyInfoという関数を実行して返ってくる値が支払われるべきクリエイターフィーということです。
function royaltyInfo(uint256 tokenId, uint256 salePrice) public view virtual returns (address, uint256) {
RoyaltyInfo memory royalty = _tokenRoyaltyInfo[tokenId];
if (royalty.receiver == address(0)) {
royalty = _defaultRoyaltyInfo;
}
uint256 royaltyAmount = (salePrice * royalty.royaltyFraction) / _feeDenominator();
return (royalty.receiver, royaltyAmount);
}
こちらはマーケットのサンプルコントラクトです。
function _deduceRoyalties(uint256 tokenId, uint256 grossSaleValue)
internal returns (uint256 netSaleAmount) {
// Get amount of royalties to pays and recipient
(address royaltiesReceiver, uint256 royaltiesAmount) = token
.royaltyInfo(tokenId, grossSaleValue);
// Deduce royalties from sale value
uint256 netSaleValue = grossSaleValue - royaltiesAmount;
// Transfer royalties to rightholder if not zero
if (royaltiesAmount > 0) {
royaltiesReceiver.call{value: royaltiesAmount}('');
}
// Broadcast royalties payment
emit RoyaltiesPaid(tokenId, royaltiesAmount);
return netSaleValue;
}
今後の流れ予想
- コレクション独自のマーケットプレイス増加
- 二次流通ロイヤリティ以外のマネタイズ増加
以前はOpenSeaでの取引高がコレクションの人気の指標として重要視されていましたが、NFT市場全体の注目度の低下もさることながらOpenSea自体の影響度も落ちているような気がします。コレクション独自のページで独自のマーケットプレイスを持つことも多くなると思います。
収益の柱として考えず、他のマネタイズを狙うほうが健全と言えます。#まいにちDappsではNFTの活用方法をたくさん提供しているので、アイデアのタネを提供できれば幸いです。
コレクション独自のマーケットプレイスを作成する
ThirdwebのコントラクトとReactのSDKを使ってマーケットプレイスを作成してみましょう。
基本はこちらのブログを参考に進めますが、別画面で出品すると少しダサいので、OpenSeaのようにNFTの詳細画面内でリスティングしていきます。
Create Your Own NFT Marketplace with TypeScript and Next.js
まずマーケットのコントラクトをデプロイします。
https://thirdweb.com/thirdweb.eth/MarketplaceV3
今回の目的はNFTのロイヤリティの回収ですので、プラットフォームFeeを設定します。
![Image](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Ffrqcsxz2%2Fproduction%2Fc8bea019814f04f2a63a424d308865586a99a954-2938x1384.png%3Fw%3D2000%26auto%3Dformat&w=3840&q=75)
Next.jsのプロジェクトを立ち上げます。
一気に飛ばしてしまってNFT一覧画面をトップページに、詳細画面を以下のコードで作りました。
"use client";
import {
useContract,
ConnectWallet,
useListings,
useNFT,
useAddress,
} from "@thirdweb-dev/react";
import { useState } from "react";
export default function NFT({ params }: { params: { tokenId: string } }) {
const nftContractAddress = "0xE71eE93Ad57c8b355e1bCDFe435B05673d8B4930";
const { contract } = useContract(
"0xE71eE93Ad57c8b355e1bCDFe435B05673d8B4930"
);
const { contract: marketContract } = useContract(
"0xaEa0d74cEFc8D75Fa42Ab1e49B2a7122620128fC",
"marketplace"
);
const [sellingPrice, setSellingPrice] = useState("");
const { data: nft, isLoading, error } = useNFT(contract, params.tokenId);
const {
data: listings,
isLoading: listingLoding,
error: listingError,
} = useListings(marketContract, {
tokenContract: nftContractAddress,
tokenId: params.tokenId,
start: 0,
count: 100,
});
const address = useAddress();
const handleCancel = async (listingId: string) => {
await marketContract?.direct.cancelListing(listingId);
};
const handleSell = async () => {
const listing = {
assetContractAddress: nftContractAddress,
tokenId: params.tokenId,
startTimestamp: new Date(),
listingDurationInSeconds: 86400,
quantity: 1,
currencyContractAddress: "0x0000000000000000000000000000000000000000",
buyoutPricePerToken: sellingPrice,
};
await marketContract?.direct.createListing(listing);
};
const handleBuy = async (listingId: string) => {
await marketContract?.direct.buyoutListing(listingId, 1);
};
return (
<main>
<div className="bg-white min-h-screen">
<div className="mx-auto px-4 py-16 sm:px-6 sm:py-24 lg:max-w-7xl lg:px-8">
{/* Product */}
<div className="lg:grid lg:grid-cols-7 lg:grid-rows-1 lg:gap-x-8 lg:gap-y-10 xl:gap-x-16">
{/* Product image */}
<div className="lg:col-span-4 lg:row-end-1">
<div className="aspect-h-4 aspect-w-4 overflow-hidden rounded-lg bg-gray-100">
{isLoading ? (
<>...loading</>
) : (
<img
src={nft?.metadata.image!}
alt={nft?.metadata.name as string}
className="object-cover object-center mx-auto"
/>
)}
</div>
</div>
{/* Product details */}
<div className="mx-auto mt-14 max-w-2xl sm:mt-16 lg:col-span-3 lg:row-span-2 lg:row-end-2 lg:mt-0 lg:max-w-none">
<div className="flex flex-col-reverse">
<div className="mt-4">
<h1 className="text-2xl font-bold tracking-tight text-gray-900 sm:text-3xl">
{nft?.metadata.name}
</h1>
<h2 id="information-heading" className="sr-only">
Product information
</h2>
</div>
</div>
<p className="mt-6 text-gray-500">{nft?.metadata.description}</p>
{listings?.length && (
<p className="mt-10 text-2xl font-bold tracking-tight text-gray-900 sm:text-3xl">
{listings![0].buyoutCurrencyValuePerToken.displayValue}{" "}
{listings![0].buyoutCurrencyValuePerToken.symbol}
</p>
)}
<div className="mt-2 grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-2">
{address ? (
<>
{/* listingがあるとき */}
{listings?.length ? (
<>
{listings![0].sellerAddress != address ? (
<>
<button
onClick={() => handleBuy(listings[0].id)}
type="button"
className="flex w-full items-center justify-center rounded-md border border-transparent bg-indigo-600 px-8 py-3 text-base font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:ring-offset-gray-50"
>
{/* listingがあり、売り手アドレスが自分以外の時 */}
Buy
</button>
<button
type="button"
className="flex w-full items-center justify-center rounded-md border border-transparent bg-indigo-50 px-8 py-3 text-base font-medium text-indigo-700 hover:bg-indigo-100 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:ring-offset-gray-50"
>
{/* listingがなく、NFTオーナーが自分以外の時 */}
Offer
</button>
</>
) : (
<button
onClick={() => handleCancel(listings[0].id)}
type="button"
className="flex w-full items-center justify-center rounded-md border border-transparent bg-indigo-600 px-8 py-3 text-base font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:ring-offset-gray-50"
>
{/* listingがあり、売り手アドレスが自分の時 */}
Cancel
</button>
)}
</>
) : (
<>
{/* listingがないとき */}
{nft?.owner == address ? (
<>
<div className="relative mt-2 rounded-md">
<input
type="text"
name="price"
id="price"
className="block w-full rounded-md border-0 py-3 pl-7 pr-12 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
placeholder="0.00"
aria-describedby="price-currency"
onChange={(e) => {
setSellingPrice(e.target.value);
}}
/>
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
<span
className="text-gray-500 sm:text-sm"
id="price-currency"
>
MATIC
</span>
</div>
</div>
<button
onClick={handleSell}
type="button"
className="flex w-full items-center justify-center rounded-md border border-transparent bg-indigo-600 px-8 py-3 text-base font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:ring-offset-gray-50"
>
{/* listingがなく、NFTオーナーが自分の時 */}
Sell
</button>
</>
) : (
<button
type="button"
className="flex w-full items-center justify-center rounded-md border border-transparent bg-indigo-600 px-8 py-3 text-base font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:ring-offset-gray-50"
>
{/* listingがなく、NFTオーナーが自分以外の時 */}
Offer
</button>
)}
</>
)}
</>
) : (
<ConnectWallet></ConnectWallet>
)}
</div>
</div>
</div>
</div>
</div>
</main>
);
}
![Image](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Ffrqcsxz2%2Fproduction%2Ffb25070cb25fa2c4b252b7a971669836373c15ef-2932x1528.png%3Fw%3D2000%26auto%3Dformat&w=3840&q=75)
ロジックとしては
- ウォレットを接続していなければConnect Walletボタンを表示
- 当該NFTのリスティングがない場合で
- 当該NFTのオーナーが自分の場合、Sellボタン
- 当該NFTのオーナーが自分以外の場合、Offerボタン
- 当該NFTのリスティングがある場合で
- 当該NFTのオーナーが自分の場合、Cancelボタン
- 当該NFTのオーナーが自分以外の場合、Buyボタン + Offerボタン
という出し分けになります。(本当はあとOffer Acceptも必要ですね)
マーケットコントラクトのロジックはこちらを参照してください。
また、今回のコードは自前のデータベースを持たず、オンチェーンの情報をフロントエンドから都度参照しているので、パフォーマンスは低いものになっています。
まとめ
今回はNFTのロイヤリティ問題の詳細に触れ、ざっくり独自マーケットプレイスを作成する方法について解説しました。本番用に調整したコードでPontechホームページにも導入しようと考えています。
弊社Pontechはweb3に関わる開発を得意とするテック企業です。サービス開発に関するご相談はこちらのフォームからお願いいたします。
また、受託開発案件に共に取り組むメンバーを募集しています!ご興味のある方はぜひお話させてください!