LoginSignup
15
4
お題は不問!Qiita Engineer Festa 2023で記事投稿!

[ERC2981]NFTのロイヤリティ強制化!? ERC2981の仕組みについて理解しよう!

Last updated at Posted at 2023-07-04

はじめに

初めまして。
CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。

代表的なゲームはクリプトスペルズというブロックチェーンゲームです。

今回は、NFTの二次流通時に発生するロイヤリティをマーケットプレイスに依存せずに受け取れるようにする規格である、「ERC2981」の解説をしていきます!

基本的に以下の提案をもとに説明していきます。

では早速見ていきましょう!

前提

NFTを販売する時マーケットプレイスという、NFTの売買ができるプラットフォームに出品することがほとんどです。
このマーケットプレイスで二次販売を行うとき、ロイヤリティを受け取れるかがマーケットプレイス次第であることが問題となっています。
よくわからない言葉が並んでいると思うので説明していきます。

例えばクリエイターがNFTを作成したとします。
作成した作品をマーケットプレイスで販売し、誰かが購入してその売り上げがクリエイターの手元に入ります。

img1.png

ここまでは問題ありません。
問題はここからです。

購入した人がマーケットプレイスにて販売(これが二次販売です)をして、別の人が購入したとします(二次流通)。

img2.png

クリエイターが販売するときに「ロイヤリティ」というものを設定することができます。
ロイヤリティ」とは、二次流通した時の売上の一部がクリエイターにも入るというものです。
この「ロイヤリティ」というものがあると、クリエイターは二次流通が起こるたびに資金を還元してもらえます。
ただ、これはクリエイターにとっては嬉しいことですが、作品を二次流通で販売した人からすると売り上げの一部が減るため嫌がる人もいます。
そして、NFTの販売所であるマーケットプレイスによっては「ロイヤリティ」を0にすることができてしまいます。
こうなると先ほどの作品を二次流通で販売したい人は、「ロイヤリティ」が0になったマーケットプレイスを積極的に使うようになり、クリエイターには資金の還元がされなくなります。
これを問題だと考えた人たちは、スマートコントラクト上で「ロイヤリティ」を設定できないのかあれやこれや議論・実装をしていました。
その中で提案されたのが今回取り上げる「ERC2981」です。
前提を確認できたので早速中身を見ていきましょう!

要約

NFTのロイヤリティ情報を取得するための標準規格です。
すべてのNFTマーケットプレイスでロイヤリティ支払いを可能にします。

標準規格とは設計図のようなもので、この規格に沿うことで異なるプロジェクト間での互換性や、標準規格に沿って開発することで効率性が向上したりなどメリットがあります。

動機

NFTのマーケットプレイスは数多く存在し、マーケットプレイスごとにロイヤリティを設定できてしまい互換性を持たず標準化されていません。
前提の部分で話したように、現在はマーケットプレイスごとにすきなようロイヤリティを設定できてしまい、二次販売を行うユーザーはできる限りロイヤリティが少ないマーケットプレイスで販売しようとします。

このERCでは、すべてのマーケットプレイスがNFTの標準化されたロイヤリティ情報を取得できるようにします。
これにより、マーケットプレイスがこのERCを実装しない場合、ERC-721およびERC-1155規格のNFTのカンダリセールでは資金が支払われなくなります(焦点をERC-721とERC-1155に当てていますが、それ以外の規格に対しても有効です)。

アーティストやクリエイター、プロジェクトがロイヤリティを受け取れなくなると、継続的な資金を受け取れなくなりNFTという分野から離れていきかねません。
これによりNFTの成長と普及が妨げられることを問題視しています。
しかし、ロイヤリティ支払いの標準規格を用意することで、NFTエコシステム全体に利益をもたらします。

以下は「ERC2981」の中にあった、ロイヤリティ支払いにおける現在の状況を表した会話です。

アーティスト:「あなたのプラットフォームではロイヤリティ支払いをサポートしていますか?」
マーケットプレイス:「はい、ロイヤリティ支払いはありますが、あなたのNFTが他のマーケットプレイスで販売された場合、この支払いを強制することはできません。」
アーティスト:「ロイヤリティをサポートする他のマーケットプレイスでは、私のロイヤリティ情報を共有して機能させることはできませんか?」
マーケットプレイス:「いいえ、ロイヤリティ情報を共有しません。」

仕様

ERC-721およびERC-1155に準拠したコントラクトにおいて、ロイヤリティ支払い情報を取得する実装のための仕様を説明しています。

まず、マーケットプレイスは、royaltyInfo()関数に渡される_salePriceと同じ取引単位でロイヤリティを支払わなければならない。
_salePriceの取引単位がETHであればETH、USDCであればUSDCでロイヤリティを支払う。

このために、ロイヤリティ額を計算する際に_salePriceの割合を計算する必要がある。

仕様.png

royaltyInfo()関数は、売価とロイヤリティ支払いの単位を認識していないため、_salePriceと取引単位を合わせたroyaltyAmountを決定しないといけない。
よって、割合の値は売価に対して独立している必要がある(割合の値が10%であれば、_salePriceがどんな値であれ10%を適用する)。
また、ロイヤリティが10%で、_salePrice999である場合、切り上げるか切り捨てるか好きな方を選択しroyaltyAmount99または100の好きな方を返すことができる。

割合の値は予測可能な変数によって変更することも可能。
例えばNFTの所有者が変わる回数やNFTのトークンIDごとに割合の値を変化して、ロイヤリティの値を変更することもできる。

royaltyAmount0の場合ガス代が無駄になることからトランザクションを送信しないようにする。

これでロイヤリティを矯正できる!

提案されているInterfaceのコードは以下です。

Interfaceについては以下の記事を参考にしてください。

pragma solidity ^0.6.0;
import "./IERC165.sol";

interface IERC2981 is IERC165 {
    function royaltyInfo(
        uint256 _tokenId,
        uint256 _salePrice
    ) external view returns (
        address receiver,
        uint256 royaltyAmount
    );
}

interface IERC165 {
    function supportsInterface(bytes4 interfaceID) external view returns (bool);
}

royaltyInfo

ロイヤリティ情報をクエリするために、売価が指定された時に呼び出される。

  • _tokenId
    • NFTのトークンID。
  • _salePrice
    • _tokenIdのNFTの売価。
  • receiver
    • ロイヤリティを受け取るアドレス。
  • royaltyAmount
    • _salePriceに対するロイヤリティ額。

実装例(ERC721)

constructor (string memory name, string memory symbol, string memory baseURI) {
    _name = name;
    _symbol = symbol;
    _setBaseURI(baseURI);
    _registerInterface(_INTERFACE_ID_ERC721);
    _registerInterface(_INTERFACE_ID_ERC721_METADATA);
    _registerInterface(_INTERFACE_ID_ERC721_ENUMERABLE);
    _registerInterface(_INTERFACE_ID_ERC2981);
}

ERC721コントラクトのconstructorです。
constructorとは、コントラクトをデプロイする時に一度だけ実行される関数です。
このconstructorの中に**ERC2981**のインターフェースを追加します。

bytes4 private constant _INTERFACE_ID_ERC2981 = 0x2a55205a;

function checkRoyalties(address _contract) internal returns (bool) {
    (bool success) = IERC165(_contract).supportsInterface(_INTERFACE_ID_ERC2981);
    return success;
 }

マーケットプレイスで販売されているNFTがロイヤリティを実装しているかどうかをチェックしています。
もしサポートしていた場合trueを返します。

補足

オプションのロイヤリティ支払い

NFTを販売しているのか、それともウォレットからウォレットへNFTを移動しているのかを判断することはできません。
そのため全てのTransfer関連関数にロイヤリティ支払いを実装するわけにはいきません(ウォレットから移動したいだけなのにロイヤリティの支払いが発生するため)。
どこにロイヤリティ強制の機能を実装するかは実装者に委ねられています。

単一のアドレスへのシンプルなロイヤリティ支払い

この提案ではロイヤリティを受け取る方法は指定されていません。
手数料の分割や受け取れるアドレスが複数あったり、税金、会計の関係からロジックが複雑にな理、全てのユースケースをカバーできないからです。
この部分も実装者に委ねられています。

ロイヤリティ支払いの割合の計算

仕様の部分で、「割合の値は予測可能な変数によって変更することも可能」と述べたが、block.timestamp(現在のブロックのタイムスタンプ)などの予測不能な変数を計算に含めるのは避けるべきです。
避けるべき具体例としては以下になります。

  • マーケットプレイスでNFTを販売し、royaltyInfo()を呼び出した後、支払い完了までX日の遅延が発生する。
  • マーケットプレイスがYのroyaltyAmountを受け取り、遅延がなかった場合にX日前に計算されたroyaltyAmountとは大幅に値が異なるっている。

上記を避けるために、売価に応じてマーケットプレイスがロイヤリティ金額を計算するのではなく、与えられた売価に対してroyaltyAmountを計算する必要があります。

全てのマーケットプレイス、オンチェーンおよびオフチェーンを含む通貨に依存しないロイヤリティ支払い

このERCでは、セールやロイヤリティ支払いに使用される通貨またはトークンは指定されていません。
そのため、通貨やトークンに関係なくロイヤリティを支払わなければいけません。

普遍的なロイヤリティ支払い

NFTを特に考慮して設計されているものの、他のどのコントラクトでも、このインターフェースを使用してロイヤリティ支払い情報を返すことができます。

実装

実装コードは以下になります。

Openzeppelinでの実装コードは以下になります。

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (token/common/ERC2981.sol)

pragma solidity ^0.8.0;

import "../../interfaces/IERC2981.sol";
import "../../utils/introspection/ERC165.sol";

/**
 * @dev Implementation of the NFT Royalty Standard, a standardized way to retrieve royalty payment information.
 *
 * Royalty information can be specified globally for all token ids via {_setDefaultRoyalty}, and/or individually for
 * specific token ids via {_setTokenRoyalty}. The latter takes precedence over the first.
 *
 * Royalty is specified as a fraction of sale price. {_feeDenominator} is overridable but defaults to 10000, meaning the
 * fee is specified in basis points by default.
 *
 * IMPORTANT: ERC-2981 only specifies a way to signal royalty information and does not enforce its payment. See
 * https://eips.ethereum.org/EIPS/eip-2981#optional-royalty-payments[Rationale] in the EIP. Marketplaces are expected to
 * voluntarily pay royalties together with sales, but note that this standard is not yet widely supported.
 *
 * _Available since v4.5._
 */
abstract contract ERC2981 is IERC2981, ERC165 {
    struct RoyaltyInfo {
        address receiver;
        uint96 royaltyFraction;
    }

    RoyaltyInfo private _defaultRoyaltyInfo;
    mapping(uint256 => RoyaltyInfo) private _tokenRoyaltyInfo;

    /**
     * @dev See {IERC165-supportsInterface}.
     */
    function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165) returns (bool) {
        return interfaceId == type(IERC2981).interfaceId || super.supportsInterface(interfaceId);
    }

    /**
     * @inheritdoc IERC2981
     */
    function royaltyInfo(uint256 tokenId, uint256 salePrice) public view virtual override 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);
    }

    /**
     * @dev The denominator with which to interpret the fee set in {_setTokenRoyalty} and {_setDefaultRoyalty} as a
     * fraction of the sale price. Defaults to 10000 so fees are expressed in basis points, but may be customized by an
     * override.
     */
    function _feeDenominator() internal pure virtual returns (uint96) {
        return 10000;
    }

    /**
     * @dev Sets the royalty information that all ids in this contract will default to.
     *
     * Requirements:
     *
     * - `receiver` cannot be the zero address.
     * - `feeNumerator` cannot be greater than the fee denominator.
     */
    function _setDefaultRoyalty(address receiver, uint96 feeNumerator) internal virtual {
        require(feeNumerator <= _feeDenominator(), "ERC2981: royalty fee will exceed salePrice");
        require(receiver != address(0), "ERC2981: invalid receiver");

        _defaultRoyaltyInfo = RoyaltyInfo(receiver, feeNumerator);
    }

    /**
     * @dev Removes default royalty information.
     */
    function _deleteDefaultRoyalty() internal virtual {
        delete _defaultRoyaltyInfo;
    }

    /**
     * @dev Sets the royalty information for a specific token id, overriding the global default.
     *
     * Requirements:
     *
     * - `receiver` cannot be the zero address.
     * - `feeNumerator` cannot be greater than the fee denominator.
     */
    function _setTokenRoyalty(uint256 tokenId, address receiver, uint96 feeNumerator) internal virtual {
        require(feeNumerator <= _feeDenominator(), "ERC2981: royalty fee will exceed salePrice");
        require(receiver != address(0), "ERC2981: Invalid parameters");

        _tokenRoyaltyInfo[tokenId] = RoyaltyInfo(receiver, feeNumerator);
    }

    /**
     * @dev Resets royalty information for the token id back to the global default.
     */
    function _resetTokenRoyalty(uint256 tokenId) internal virtual {
        delete _tokenRoyaltyInfo[tokenId];
    }
}

構造体

RoyaltyInfo

ロイヤリティ情報を格納するための構造体。
以下のフィールドがあります。

  • receiver
    • ロイヤリティを受け取るアドレス。
  • royaltyFraction
    • ロイヤリティを表す分数。

変数

_defaultRoyaltyInfo

すべてのトークンIDに適用されるデフォルトのロイヤリティ情報を保持する変数。

_tokenRoyaltyInfo

トークンIDごとに指定されたロイヤリティ情報を保持するマッピング配列。

関数

supportsInterface

ERC165のインターフェースをサポートしているかどうかを返す関数。

ERC165については以下を参照してください。

royaltyInfo

与えられたトークンIDと売却価格に基づいて、ロイヤリティ情報を取得するための関数。

ロイヤリティ情報はreceiver(ロイヤリティを受け取るアドレス)とroyaltyAmount(ロイヤリティの金額)の2つの値が返されます。

_feeDenominator

ロイヤリティを表す分数の分母として使用されるデフォルトの値を返す内部関数。

_setDefaultRoyalty

すべてのトークンIDに適用されるデフォルトのロイヤリティ情報を設定する内部関数。

_deleteDefaultRoyalty

デフォルトのロイヤリティ情報を削除する内部関数。

_resetTokenRoyalty

特定のトークンIDに対するロイヤリティ情報をデフォルトにリセットする内部関数。

テストコード

テストコードは以下になります。

後方互換性

ERC721およびERC1155規格と互換性がある。

考察

そもそもほんとにロイヤリティを強制化できるのでしょうか?
結論としては、「ロイヤリティの強制化はできない」です。
理由としては、マーケットプレイスに依存してしまうからです。
実装部分を見ていただけるとわかりますが、ロイヤリティの計算を行っている関数はありますが、NFTの送付(transfer)を実行する関数にその機能が盛り込まれていません。
また、NFTの送付(transfer)が実行される前に呼び出される、_beforeTokenTransfer関数も上書きしていないため、NFTの送付(transfer)時にデフォルトの実装ではロイヤリティの強制化を実装できていません。
マーケットプレイスでのNFTの購入フローは以下のようになっています。

  1. NFTに価格をつけて、マーケットプレイスに送付(transfer)を与え販売。
  2. 購入者が自身のアドレスから、購入価格分の料金をマーケットプレイスに送付。
  3. マーケットプレイスは手数料を受け取り、残りの購入価格を販売者、クリエイターに分配。
  4. マーケットプレイスから購入者のアドレスへNFTを送付(transfer)。

ブログERC & EIP.png

上記のフローでは、「3」でクリエイターに購入価格が分配されていますが、マーケットプレイス次第で分配者を販売者のみにすることができてしまいます。

では、「NFTを送付(transfer)するときに、ロイヤリティを支払う機能を入れてしまえば良いのでは?」と思うかもしれないです。
ただ、これには以下の2つの懸念点があります。

  1. ERC721およびERC1155との互換性がなくなる。
  2. NFTの所有者自身の2つのアドレス間でNFTを送付(transfer)するときに、手数料を支払わなければいけない。

1つずつ詳しく説明します。

ERC721ERC1155との互換性がなくなる

互換性とは、「ERC721ERC1155と認識して使用できるか」ということです。
もし、NFTを送付(transfer)するときに別の処理を入れてしまうと、ERC721ERC1155と同じだと思って使用できなくなってしまいます。
これでは全く別の新しい規格となってしまいます。

NFTを送付(transfer)時に手数料を支払わなければいけない

NFTの所有者がなんらかの理由で、自分の2つのウォレット間でNFTを送付(transfer)したいとします。
このとき、ロイヤリティが強制化されていると、自分から自分に送っているのにも関わらず、ロイヤリティ分の料金が徴収されてしまいます。
これでは、NFTを使用する上でだいぶ不便になってしまいます。

これらの理由から、NFTを送付(transfer)するときにロイヤリティを強制化する機能が入れられていません。
先ほどの2つのデメリットを許容できるようであれば、NFTの送付(transfer)時にロイヤリティを強制化する実装を盛り込んでも良いかもしれないです。

このように、仕組みやメリット・デメリットを理解することは大切なので、個人的にも大変勉強になりました。

引用

Zach Burks (@vexycats), James Morgan (@jamesmorgan), Blaine Malone (@blmalone), James Seibel (@seibelj), "ERC-2981: NFT Royalty Standard," Ethereum Improvement Proposals, no. 2981, September 2020. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-2981.

最後に

今回はNFTの二次流通時に発生するロイヤリティをマーケットプレイスに依存せずに受け取れるようにする規格である、「ERC2981」についてまとめてきました!'

いかがだったでしょうか?

質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!

Twitter @cardene777

採用強化中!

CryptoGamesでは一緒に働く仲間を大募集中です。

この記事で書いた自分の経験からもわかるように、裁量権を持って働くことができて一気に成長できる環境です。
「ブロックチェーンやWeb3、NFTに興味がある」、「スマートコントラクトの開発に携わりたい」など、少しでも興味を持っている方はまずはお話ししましょう!

15
4
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
4