はじめに

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

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

今回は、NFTのロイヤリティの管理や残高、支払いの管理、取引の機能などの仕組みを提案している規格であるERC4910についてまとめていきます!

以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。

他にも様々なERCについてまとめています。

概要

この提案は、NFT(非代替トークン)とロイヤルティを結びつけ、ERC721標準を拡張したスマートコントラクトの設計です。 主な目的は、中央機関が法的に権利を持つ人々への支払いを不正に操作したり回避したりすることを防ぐことです。

ERC721については以下を参考にしてください。

この提案は、OpenZeppelin Smart Contract Toolboxアーキテクチャをベースに構築され、それを拡張しています。 拡張内容は、ロイヤルティアカウントの管理(CRUD操作)、ロイヤルティ残高と支払いの管理、シンプルな取引機能(リストへの追加、リストからの削除、購入など)を含んでいます。 また、取引所での取引を追跡する機能も提供しています。 ロイヤルティ管理の機能により、ロイヤルティツリーと呼ばれる階層的なロイヤルティ構造を確立できます。 これにより、親NFTを子NFTに論理的に関連付け、再帰的に子NFTがさらに子NFTを持つことができます。

この提案はERC721スマートコントラクトを拡張し、NFTとロイヤルティの結びつきを実現し、支払いを公平に行うことを目指しています。 OpenZeppelin Smart Contract Toolboxアーキテクチャを基にしており、ロイヤルティアカウントの管理、残高と支払いの管理、取引機能、取引所での取引追跡機能を提供しています。

動機

ロイヤルティの管理は、古典的な問題で、コントラクトが複雑で透明性が不足しており、不正行為や詐欺が多く発生する特徴があります。

特に、階層的なロイヤルティ構造が存在する場合、この問題はさらに顕著になります。 階層的なロイヤルティでは、1つまたは複数の資産が元の資産から派生しており、例えば、元の絵画からプリントが作成される場合、あるいは1つの楽曲が別の楽曲の制作に使用される場合、または配信権と報酬が複数の提携者を通じて管理される場合などが考えられます。

以下の例では、元の作品を制作したアーティストは、プリントの販売および再販から得られる収益に対して、ロイヤルティを受け取る権利を有しています。

この「祖先の概念」を利用した階層的なロイヤルティの基本的なアイデアは、以下の図で示されています。

このコンセプトでは、ある資産(例: 原画)から派生した子資産(例: プリント)に対するロイヤルティを、資産の「祖先」となるものに関連づけます。 具体的に言えば、原画を制作したアーティストは、プリントが販売または再販されるたびに、その売上の一部を受け取る権利を持つことになります。 この方法により、資産とその派生物との間に階層的なロイヤルティ関係が確立され、公正な収益の分配が可能になります。

つまり、元の資産に関連するすべての子資産で、売上からロイヤルティが支払われ、これによって制作者や関係者への報酬が透明かつ公平に行われることが保証されます。

ここでの「祖先」とは、それ以前までのNFTの所有者のことを指します。

この提案は、複雑な相続の問題を解決するためのアプローチを紹介しています。 具体的には、階層ツリーの深さがNである場合、この問題を各レイヤーごとに分割し再帰的な問題を簡素化します。 これにより、ツリーを最も効率的に、最下層から最上層に向かってトラバースできるようになります。

トラバース 特定のデータ構造やツリーを移動または走査することを指します。 具体的には、階層的な構造(例: 階層ツリー)内の要素を順番にたどり、特定の目的や操作を遂行することを意味します。 この場合、階層ツリーを最下層から最上層に向かって進んでいき、各レベルで必要な操作や計算を行います。

このアプローチにより、原作から派生したアートの制作者やその配布業者に、クリエイティブプロセスからの収益を確保する機会が提供されます。 これにより、NFTには単なる固有の価値だけでなく、収益を伴う付加価値が生まれます。

具体的な手法として、階層ツリーをレイヤーごとに分割し、各レイヤーごとにロイヤルティの計算や支払いを行います。 これにより、アーティストやアートの配布業者は、アート作品からの収益を各レイヤーごとに管理し、透明性のある収益分配が可能となります。 この結果、NFTの価値が向上し、同時に関連する収益も増加することが期待されます。

仕様

概要

この章では、ERC721標準の拡張として導入される新しい概念について説明します。

  • ロイヤルティアカウント(RA)
    • ロイヤルティアカウントは、各NFTに一意のtokenIdを介して関連付けられます。
    • RAは複数のサブアカウントから成り立ち、これらのサブアカウントは個人または他のロイヤルティアカウントのアカウントで構成されます。
    • 各RAはアカウント識別子で識別されます。
  • アカウントタイプ
    • アカウントタイプは、RAのサブアカウントが個人(ユーザー)に属するか、他のRAに属するかを指定します。
    • もし他のRAがRAのサブアカウントとして存在する場合、そのRAに割り当てられた残高はそのRAを構成するサブアカウント間で再割り当てされる必要があります。
  • ロイヤルティ分割
    • ロイヤルティ分割は、RAの各サブアカウントが関連するNFTの販売から受け取る割合を指定します。
  • ロイヤルティ残高
    • ロイヤルティ残高は、RAに関連するロイヤルティの合計残高を示します。
  • サブアカウントロイヤルティ残高
    • サブアカウントロイヤルティ残高は、各RAサブアカウントに関連するロイヤルティの残高を示します。
    • 個人アカウントのみが支払い可能な残高を持つことができるため、RAサブアカウントが他のRAである場合、その最終サブアカウント残高はゼロである必要があります。
  • トークンタイプ
    • トークンタイプは、支援されているユーティリティトークンのシンボル(例: ETH、DAI)を指定します。
  • アセットID
    • アセットIDは、RAが関連付けられるNFTのtokenIdです。
  • 親
    • 親は、RAが属するtokenIdの直接の親を示します。

データ構造

  • Royalty Accountとサブアカウント
    • Royalty Account(ロイヤルティアカウント)は、NFT(Non-Fungible Token)ごとに割り当てられ、複数のサブアカウントで構成されます。
    • サブアカウントは個人または他のロイヤルティアカウントのアカウントであり、各サブアカウントには一意の識別子が関連付けられます。
  • tokenIdとRoyalty Account識別子の関連付け
    • 各NFTのtokenIdとそれに対応するロイヤルティアカウントの識別子を関連付けるためのデータ構造です。
  • 親から子へのNFT関係をマッピングする構造
    • 親NFTとその子NFT間の関係を記録するデータ構造です。
    • この関係をマッピングすることで、NFTの階層関係が把握できます。
  • トークンタイプと最終確認残高のリスト(取引およびロイヤルティ支払い用)
    • トークンの種類(ETHなど)と最終確認残高をリスト化するデータ構造です。
    • 取引やロイヤルティ支払いに関連する情報を追跡します。
  • 実行される支払いの登録リスト
    • executePayment関数で実行される支払い情報を登録し、safeTransferFrom関数で検証するためのデータ構造です。
    • 一度支払いが受け取られ、分配されると、リストから削除されます。
  • 販売対象のNFTのリスト
    • 承認されたユーザーアドレスが非取引所のNFT購入のためにNFTをリスト化するデータ構造です。

ロイヤルティアカウント関連の関数

  • Royalty AccountのRUD(Read-Update-Delete)関数の定義とインターフェース**
    • ロイヤルティアカウントに関連する読み取り、更新、削除の操作を実行する関数の定義とインターフェースです。
    • RAはminting関数内で作成されるため、ロイヤルティアカウントを別途作成する必要はありません。

ロイヤリティを持つNFTの発行

NFTが発行されると、それに対応するロイヤルティアカウント(RA)が作成され、NFTとNFTの所有者、および必要であれば祖先のRAと関連付けられます。 このため、新しく定義されたmint関数内で_safemint関数を利用し、入力変数にさまざまなビジネスルールを適用します。

NFTの販売リスト化およびリストからの削除

承認されたユーザーアドレスは、非取引所のNFT購入のためにNFTをリスト化し、必要に応じてリストから削除することができます。 これにより、NFTの売買が容易に管理できます。

買い手から売り手への支払い機能

購入者から売り手への支払い手続きについて、ETHまたはERC20トークンに応じて説明します。

ERC20トークンの場合
  1. 購入者は、選択した支払いトークン(ERC20コントラクトアドレス)と購入価格をNFTコントラクトに承認します。
    • これは、NFTコントラクトが購入者のアカウントから必要なトークンを引き出すための許可を与える手順です。
  2. ERC20トークンを使用する場合、購入者はNFTコントラクト内のexecutePayment関数を呼び出します。
    • この関数は、指定されたERC20トークンを使用して支払いを行います。
    • このプロセスにおいて、ERC20トークンは直接関与せず、NFTコントラクトが支払いを処理します。
ERC20トークン以外の場合
  • ERC20トークン以外のトークンを使用する場合、購入者はETH(イーサリアムのネイティブトークン)をNFTコントラクトに送金します。
    • そして、購入者はmsg.dataを購入したNFTのtokenIdのuint256[]としてエンコードし、トランザクションに添付する必要があります。

この方法により、ロイヤルティ回避を防ぎつつ、購入者は常にNFTコントラクトに直接支払いを行います。 売り手はロイヤルティ分配を通じて支払われ、後で支払いをリクエストすることができます。

ロイヤルティを配分するために必要な取引データを含む修正NFTTransfer関数

変更されたNFTTransfer関数には、ロイヤルティが適切に分配された後にNFTを転送できるようにするための要件が含まれています。 さらに、一度に複数のトークンを転送できる能力も考慮されています。 以下はそれに関する詳細な説明です。

入力パラメータの検証

転送が行われる前に、入力パラメータが適切に検証されます。 これにより、無効なデータでの転送を防ぎます。

支払いパラメータの検証

購入者からの支払いが正当であることが確認されます。 支払いはNFTコントラクトに直接行われ、売り手への支払いはロイヤリティ分配を通じて行われます。

ロイヤリティの分配

購入者からの支払いが確認された後、ロイヤリティが適切に分配されます。 これは、トークンとその祖先に関連するロイヤリティアカウントの階層構造を考慮して行われます。

支払い済みのロイヤルティアカウントの所有権を更新し、支払いを行う

ロイヤリティアカウントの所有権が正確に更新され、支払いが行われます。

NFTの所有権のtrasnfer

ロイヤリティが分配された後、NFTの所有権が購入者にtrasnferされます。 これにより、NFTの新しい所有者が確立されます。

成功したtrasnfer後、registeredPayment内の支払いエントリの削除

成功したtrasnferの後、支払いエントリはregisteredPaymentから削除されます。 これにより、支払いが二重に行われることを防ぎます。

最後に、ロイヤリティの分配アプローチは、相互に接続されたロイヤリティアカウントの階層構造をレイヤーに分解し、各トークンとその祖先の関係を利用して、ルートの祖先と関連するRAに到達するまで、1つのレイヤーを処理する方法です。 このアプローチにより、ロイヤリティの正確な分配が実現されます。

NFT所有者へのロイヤルティ支払い - safeTransferFrom関数のアドレスから

NFTの所有者にロイヤリティを支払うための最終部分です。 支払い関数には、パブリック関数と内部関数の2つのバージョンがあります。

パブリック関数のインターフェースは以下の通りです。

javascript
function royaltyPayOut(uint256 tokenId, address RAsubaccount, address payable payoutAccount, uint256 amount) public virtual nonReentrant returns (bool)

この関数には、tokenId(トークンの識別子)、RA Sub Accountアドレス(RAサブアカウントのアドレス)、支払い先のアカウントアドレス(payoutAccount)、支払い額(amount)が必要です。 関数はリエントランシー攻撃を防止するために、nonReentrant修飾子で保護されています。

ロイヤリティ支払いを実行するためには、以下の手順を実行する必要があります。

  1. RAaccountとsubaccountPosに基づいてRAサブアカウントを見つけます。
    • この操作により、特定のRAサブアカウントの残高が取得されます。
  2. サブアカウントからtokenType(トークンの種類)を抽出します。
    • これにより、支払いに使用するトークンの種類が特定されます。
  3. トークンタイプに基づいて、指定された金額(amount)をサブアカウントの残高から引き出し、支払い先アカウント(payoutAccount)に送金します。
    • ただし、利用可能な残高を超えない範囲で支払いが行われます。

この手順により、ロイヤリティがNFTの所有者に適切に支払われます。

データ構造

ロイヤリティアカウントとロイヤリティサブアカウント

ERC721のグローバルデータ構造に、NFT(非代替トークン)とRA(ロイヤリティアカウント)をリンクする相互に接続されたデータ構造を作成し、検索を最適化するために、以下の要素を追加する必要があります。

[R1]

1つ以上のロイヤリティサブアカウントは、ロイヤリティアカウントに関連付ける必要があります。

[R2]

ロイヤリティアカウントのアカウント識別子(raAccountId)は一意である必要があります。 つまり、異なるロイヤリティアカウントは同じ識別子を持つことはできません。

[R3]

NFTのtokenIdは、raAccountIdと関連付ける必要があり、raAccountIdをtokenIdに接続します。 これにより、NFTとその関連するロイヤリティアカウントが識別できます。

(子)NFTの表示

親子NFTの関係を管理し、各レベルでの制約を設ける必要があります。 たとえば、直接の親子関係を持つためのリンクの設定や、NFT親が直接のNFT子にリンクすることが求められます。

[R4]

直接の親子関係にリンクが必要です。

NFT支払いトークン

ロイヤリティを受け取るために、NFTコントラクトはNFT取引に関与する必要があります。 したがって、NFTコントラクトはNFT支払いに関する情報を持ち、どのトークンが取引に使用できるかを知る必要があります。

[R5]

サポートされるトークンタイプのリストが必要です。 NFTコントラクトはどの種類のトークンがサポートされているかをリスト化する必要があります。

NFTコントラクトはロイヤリティの分配、支払い、販売を管理するためコントラクトが所有する許可されたトークンタイプの最後に確認された残高を追跡する必要があります。

[R6]

コントラクトによって所有される許可されたトークンタイプの最後に確認された残高へのリンクが必要です。 コントラクトは各トークンタイプの残高を追跡し、必要に応じてそれを使用できるようにする必要があります。

NFTのリストと支払い

コントラクトは販売プロセスに直接関与するため、1つ以上のNFTを販売リストに掲載する機能が必要です。

[R7]

NFTの販売リストが必要です。 これは、販売対象のNFTをリストアップするための機能です。

[R8]

販売リストには一意の識別子が必要です。 各販売リストが一意の識別子を持つことで、それぞれのリストを区別できます。

販売リストの他に、コントラクトは販売を管理する必要があります。 これには、即時の実行またはオークションなど後での支払いのための支払いを登録する機能も含まれます。

[R9]

登録支払いのリストが必要です。 これは、支払いを登録し、後で実行するためのリストです。

[R10]

登録支払いには一意の識別子が必要です。 各登録支払いに一意の識別子が付けられることで、それぞれの支払いを一意に識別できます。

コントラクトのコンストラクタとグローバル変数、およびそれらを更新する関数

この標準は現在のERC721コンストラクタを拡張し、NFTの作成者の特別な役割、そしてコントラクトが販売とロイヤリティの管理に直接関与することを認識するために、いくつかのグローバル変数を追加します。

[R11]

最小のコントラクトコンストラクタは、以下の入力要素を含まなければなりません。

javascript
///
/// @dev Definition of the contract constructor
///
/// @param name as in ERC-721
/// @param symbol as in ERC-721
/// @param baseTokenURI as in ERC-721
/// @param allowedTokenTypes is the array of allowed tokens for payment

constructor(
    string memory name,
    string memory symbol,
    string memory baseTokenURI,
    address[] memory allowedTokenTypes
) ERC721(name, symbol) {...}

ロイヤリティ・アカウント管理

ロイヤリティアカウントの管理に関する情報です。

ロイヤリティアカウントは、NFTの発行(minting)関数で作成されます。 そのため、ロイヤリティアカウントを作成するための別個の関数は不要です。

ロイヤリティアカウントの取得

ロイヤリティアカウントとそのサブアカウントは、Royalty Accountのancestryフィールド内のtokenIdを使用して取得できます。 このため、取得関数は1つだけで十分です。

[R12]

getRoyaltyAccount関数のインターフェースは以下のようになります。

javascript
/// @dev Function to fetch a Royalty Account for a given tokenId
/// @param tokenId is the identifier of the NFT to which a Royalty Account is attached
/// @param RoyaltyAccount is a data structure containing the royalty account information
/// @param RASubAccount[] is an array of data structures containing the information of the royalty sub accounts associated with the royalty account

function getRoyaltyAccount (uint256 tokenId) public view virtual returns (address,
    RoyaltyAccount memory,
    RASubAccount[] memory);

この関数を使用すると、指定されたtokenIdに関連付けられたロイヤリティアカウントとそのサブアカウントを取得できます。 この機能を通じて、ロイヤリティアカウントの情報を読み取ることができます。

[R13]

getRoyaltyAccount関数には、以下のビジネスルールが適用されます。

  • tokenIdが存在し、burn(破棄)されていない必要があります。

このルールは、getRoyaltyAccount関数を呼び出す時に確認されるべき条件です。 具体的には、指定されたtokenIdが存在し、NFTがすでにburnされていないことを確認する必要があります。 burnされたNFTに関連付けられたロイヤリティアカウントを取得しようとすることはできません。 このルールの遵守は、正当なNFTのロイヤリティアカウントを取得するために重要です。

Royalty Accountの更新

ロイヤリティアカウントの更新には、呼び出し元が「tokenId」とロイヤリティアカウント自体の情報の両方を持っている必要があります。 これらの情報は、既存のロイヤリティアカウントの情報を変更または更新するために使用されます。

[R14]

以下がupdateRoyaltyAccount関数のインターフェースの定義です。

javascript
/// @dev Function to update a Royalty Account and its Sub Accounts
/// @param tokenId is the identifier of the NFT to which the Royalty Account to be updated is attached
/// @param RoyaltyAccount is the Royalty Account and associated Royalty Sub Accounts with updated values  

function updateRoyaltyAccount (uint256 _tokenId, `RoyaltyAccount memory _raAccount) public virtual returns (bool)

この関数の目的は、指定されたNFT(トークン)のロイヤリティアカウント情報を更新することです。 呼び出し元は、更新対象のNFTのtokenId(トークンの識別子)と、変更されたロイヤリティアカウント情報(_raAccount)を提供する必要があります。 この関数は、NFTのロイヤリティ情報を正確に管理および更新するために使用され、NFTの所有者がロイヤリティ情報を変更できるメカニズムを提供します。

[R15]

ロイヤリティアカウントの更新に関するビジネスルールは以下の通りです。

  1. NFTのアセット識別子

    • NFTの識別子(tokenId)は変更できません。
  2. NFTの祖先

    • NFTの祖先情報は変更できません。
  3. NFTで受け入れられる支払い用のトークンタイプ

    • NFTが受け入れる支払いトークンのタイプは変更できません。
  4. ロイヤリティサブアカウントのロイヤリティ残高

    • ロイヤリティサブアカウント内のロイヤリティ残高は変更できません。
  5. NFTの親から子に継承されるロイヤリティ分割

    • NFTの親から子へのロイヤリティ分割情報は変更できません。
  6. 新しいロイヤリティ分割値

    • 新しいロイヤリティ分割値は、設定されたロイヤリティ分割の境界値(存在する場合)より大きいか、または小さいか、または等しい必要があります。
  7. ロイヤリティサブアカウントの数

    • 既存のロイヤリティサブアカウントの数と追加される新しいロイヤリティサブアカウントの数の合計は、設定された境界値(存在する場合)より小さいか等しい必要があります。
  8. ロイヤリティサブアカウント全体のロイヤリティ分割の合計

    • すべてのロイヤリティ分割の合計は常に1またはその数値相当でなければなりません。
  9. msg.sender

    • msg.senderは、変更されるロイヤリティアカウントのロイヤリティサブアカウントのアカウント識別子と等しい必要があり、そのロイヤリティサブアカウントは親NFTに属していないことを示す必要があります。
    • アカウント識別子が属するサブアカウントは削除してはなりません。
    • ロイヤリティ分割は減少するだけであり、既存のサブアカウントのロイヤリティ分割が適切に増加し、すべてのロイヤリティ分割の合計が1またはその数値相当であるように増加しなければなりません。
      • たは、ルール10に従って1つ以上の新しいロイヤリティサブアカウントが追加されなければなりません。
    • ロイヤリティ残高は変更してはなりません。
    • アカウント識別子はNULLではない必要があります。
  10. msg.senderがロイヤリティサブアカウントのアカウント識別子と等しい

    • もしmsg.senderに属するロイヤリティサブアカウントのロイヤリティ分割が減少した場合、新しいロイヤリティサブアカウントのロイヤリティ分割が減少した場合、新しいロイヤリティサブアカウントのロイヤリティ残高はゼロでなければならず、新しいロイヤリティ分割データの合計は、msg.senderのロイヤリティサブアカウントが変更される前のロイヤリティ分割と等しくなければなりません。
    • 新しいアカウント識別子はNULLではない必要があります。
  11. ロイヤリティアカウントの更新の正確性

    • もしロイヤリティアカウントの更新が正確であれば、関数はtrueを返し、それ以外の場合はfalseを返します。

ロイヤルティアカウントの削除

ロイヤルティアカウントを削除することは、場合によっては必要で便利なことがありますがガスの使用料が非常に高くつく機能です。 そのため、以下に列挙された条件が確実に満たされている場合以外は、この機能を使用しないほうが良いです。

[R16]

以下は、deleteRoyaltyAccount関数のインターフェースの定義です。

solidity
/// @dev Function to delete a Royalty Account
/// @param tokenId is the identifier of the NFT to which the Royalty Account to be updated is attached

function deleteRoyaltyAccount (uint256 _tokenId) public virtual returns (bool)
[R17]

この関数のビジネスルールは以下の通りです。

  • _tokenIdはburnされている必要があります。
    • 有者のアドレスが0である必要があります。
  • _tokenId(NFTの識別子)はburnされており、所有者が存在しない必要があります。
    • _tokenIdに関連するすべてのtokenId番号(祖先または子孫)もburnされている必要があります。
  • ロイヤリティサブアカウント内のすべての残高はゼロである必要があります。

これらの条件を満たさない場合、ロイヤリティアカウントを削除することはできません。

NFTのミント

ERC721のミント機能に加えて、NFTのミント時にはロイヤリティアカウントとロイヤリティサブアカウントが作成される必要があります。 また、NFTトークンごとに最大子NFTの数などの制約をサポートするためのデータ構造が確立されます。

[R18]

新しいNFTがミントされる時には、1つ以上のロイヤリティアカウントとロイヤリティサブアカウントがNFTとその所有者に関連付けられます。 さらに、祖先が存在する場合、祖先のロイヤリティアカウントにも関連付けられます。 これにより、ロイヤリティ情報が効果的に管理され、配分されることが保証されます。

[D1]

mint関数は、1回の呼び出しで複数のNFTを同時に#### できる機能を持つべきです。 これにより、効率的なバッチミンティングが可能となります。

[R19]

新しいNFTの所有者は、NFTコントラクト自体である必要があります。 これは、NFTコントラクトが支払いとロイヤルティのエスクローとして機能するためです。

[R20]

NFTコントラクトの非所有者は、isApprovedとして設定される必要があります。 これにより、非コントラクト所有者もNFTに関する操作を行えるようになります。 これは支払いとロイヤリティの追跡と関連付けを可能にするための措置です。

[R21]

入力の簡潔さと、トークンメタデータがトークンごとに異なることを考慮し最小限のデータ構造が必要です。 これにより、ミントプロセスが効率的に行えます。

javascript
/// @param parent is the parent tokenId of the (child) token, and if set to 0 then there is no parent.
/// @param canBeParent indicates if a tokenId can have children or not.
/// @param maxChildren defines how many children an NFT can have.
/// @param royaltySplitForItsChildren is the royalty percentage split that a child has to pay to its parent.
/// @param uri is the unique token URI of the NFT
[R22]

mint関数インターフェースは以下のように定義されています。

javascript
/// @dev Function creates one or more new NFTs with its relevant meta data necessary for royalties, and a Royalty Account with its associated met data for `to` address. The tokenId(s) will be automatically assigned (and available on the emitted {IERC-721-Transfer} event).
/// @param to is the address to which the NFT(s) are minted
/// @param nfttoken is an array of struct type NFTToken for the meta data of the minted NFT(s)
/// @param tokenType is the type of allowed payment token for the NFT

function mint(address to, NFTToken[] memory nfttoken, address tokenType) public virtual
[R23]

mint関数の入力データのビジネスルールは、NFTをミンティングする際のルールと条件を示すものです。 以下にそれぞれのルールをわかりやすく説明します。

  • ミントされるトークン数
    • ミントされるトークンの数は0であってはなりません。
    • 少なくとも1つ以上のNFTをミンティングする必要があります。
  • ロールについて
    • NFTをミントするために、msg.senderは、MINTER_ROLEまたは最初のNFTのクリエイターを識別するCREATOR_ROLEのいずれかを持っている必要があります。
  • toアドレス
    • toアドレスはゼロアドレスではない必要があります。
    • NFTを受け取るアドレスは、ゼロアドレスではない必要があります。
  • toアドレスがコントラクト
    • NFTをコントラクトアドレスに送信する場合、そのコントラクトはホワイトリストに登録されている必要があります。
  • tokenType
    • tokenTypeは、コントラクトでサポートされているトークンタイプである必要があります。
    • 指定されたトークンタイプは、コントラクトでサポートされている必要があります。
  • royaltySplitForItsChildren
    • royaltySplitForItsChildrenは、100%またはその数値相当(プラットフォーム手数料などの制約を考慮)以下である必要があります。
  • 新しいNFTが子供を持てない場合
    • 新しいNFTが子供を持てない場合、royaltySplitForItsChildrenは0にして子供へのロイヤリティ分割はゼロにする必要があります。
  • 新しいNFTが親を持つ場合
    • 新しいNFTが親を持つ場合、親NFTのtokenIdは存在している必要があります。
  • 親の祖先レベル
    • 親の祖先レベルは、指定された最大NFT世代数よりも小さくなければなりません。
    • 新しいNFTが親を持つ場合、親NFTの祖先レベルは指定された最大NFT世代数よりも小さくなければなりません。
  • ミントされるNFTの許可される子供の数
    • ミンティングされるNFTの許可される子供の数は、指定された最大許可子供数よりも小さくなければなりません。

これらのルールに従って、mint関数へのデータが正当かどうかを確認することが必要です。

NFTの直接販売のリストと非リスト

販売プロセスにおいて、最低限2つの取引タイプを区別する必要があります。

  1. 取引所を介した販売(Exchange-mediated sales)
  2. 直接販売(Direct sales)

最初の取引タイプでは、スマートコントラクトが販売リストを認識する必要はありません。 取引所コントラクトが支払いとNFTコントラクトへの直接の転送トランザクションを開始します。 しかし、後者の取引タイプでは、スマートコントラクトによる仲介が各ステップで必要です。

[R24]

直接販売の場合、NFTのリスティングと非リスティングのトランザクションはNFTスマートコントラクトを介して実行される必要があります。

直接販売では、承認されたユーザーアドレスがNFTを販売リストに登録できます。 以下は、そのビジネスルールです。

[R25] listNFT関数のインターフェースは、以下の定義に従う必要があります。
javascript
/// @dev Function to list one or more NFTs for direct sales
/// @param tokenIds is the array of tokenIds to be included in the listing
/// @param price is the price set by the owner for the listed NFT(s)
/// @param tokenType is the payment token type allowed for the listing

function listNFT (uint256[] calldata tokenIds, uint256 price, address tokenType) public virtual returns (bool)

boolの戻り値は、関数の実行が成功した場合はtrue、失敗した場合はfalseです。

[R26]

listNFT関数のビジネスルールは以下の通りです。

  • 提案されたリストに1つ以上のNFTのリスティングがすでに存在していてはいけません。
    • すでに同じNFTがリストに存在していないことを確認します。
  • 提案されたリスト内のすべてのNFTに対して、seller(出品者)はgetApproved(tokenId[i])と等しくなければなりません。
    • 提案されたNFTリスト内の各NFTについて、出品者が正当な権限を持っていることを確認します。
  • tokenTypeは、スマートコントラクトでサポートされている必要があります。
    • 指定された支払いトークンがコントラクトでサポートされていることを確認します。
  • priceは0より大きい値でなければなりません。
    • 出品価格が0以上であることを確認します。
[R27]

[R26]の条件が満たされた場合、NFTの販売リストは更新されなければなりません。

[R28]

removeNFTListing関数のインターフェースは、以下の定義に従う必要があります。

solidity
/// @dev Function to de-list one or more NFTs for direct sales
/// @param listingId is the identifier of the NFT listing

function removeNFTListing (uint256 listingId) public virtual returns (bool)

boolの戻り値は、関数の実行が成功した場合はtrue、失敗した場合はfalseです。

[R29] removeNFTListing関数のビジネスルールは以下の通りです。
  • 登録された支払いエントリはNULLである必要があります。
    • 登録された支払い情報が存在しないことを確認します。
  • msg.sender(呼び出し元のアドレス)はNFTリストの対象となるNFTのgetApproved(tokenId)と等しくなければなりません。
    • 削除しようとしているNFTリストに対する操作権限を持つことを確認します。
[R30]

[R29]の条件が満たされた場合、NFTの販売リストは削除されなければなりません。

NFTセールの支払い

[R31]

購入者はいつもNFTコントラクトに直接支払いを行い、売り手には支払いません。 売り手はロイヤルティの分配を通じて支払われ、後でウォレットへの支払いをリクエストできます。

支払いプロセスには、1つまたは2つのステップが必要です。

  • ERC20トークンの場合
    • 購入者は選択した支払いトークンタイプの購入価格をNFT契約に承認する必要があります。
    • 購入者はexecutePayment関数を呼び出す必要があります。
  • プロトコルトークンの場合
    • 購入者はmsg.dataがNULLでない支払いフォールバック関数を呼び出す必要があります。
[R32]

ERC20トークンタイプの場合、必要なexecutePayment関数のインターフェースは以下の定義に従う必要があります。

javascript
/// @dev Function to make a NFT direct sales or exchange-mediate sales payment
/// @param receiver is the address of the receiver of the payment
/// @param seller is the address of the NFT seller 
/// @param tokenIds are the tokenIds of the NFT to be bought
/// @param payment is the amount of that payment to be made
/// @param tokenType is the type of payment token
/// @param trxnType is the type of payment transaction -- minimally direct sales or exchange-mediated

function executePayment (address receiver, address seller, uint 256[] tokenIds, uint256 payment, string tokenType, int256 trxnType) public virtual nonReentrant returns (bool)

bool値の戻り値は、関数の実行が成功した場合はtrue、失敗した場合はfalseとなります。

[R33]

トランザクションタイプ(trxnType)に関係なく、入力データのビジネスルールは以下の通りです。µ

  1. tokenIdsの配列内のすべての購入済みNFTは存在し、burned(破棄)されていてはいけません。
  2. tokenTypeはサポートされているトークンである必要があります。
  3. trxnTypeは0(直接販売)、1(取引所を介した販売)、または他のサポートされているタイプに設定されている必要があります。
  4. receiverはNULLであっても構いませんが、ゼロアドレスであってはいけません。
  5. sellerは対応するリスティング内のアドレスである必要があります。
  6. msg.senderはコントラクトであってはいけません(ただし、NFTコントラクトでホワイトリストに登録されている場合を除く)。
[R34]

trxnType = 0の場合、支払いデータは以下のルールに基づいてリスティングに対して検証される必要があります。

  1. NFTはリストに載っている必要があります。
  2. 支払いはリスト価格以上である必要があります。
  3. リストされたNFTは支払いデータ内のNFTと一致している必要があります。
  4. リストされたNFTはsellerによってコントロールされている必要があります。
[R35]

[R33]および[R34]のすべてのチェックが合格した場合、executePayment関数はtokenTypeで指定されたERC20コントラクト内のtransfer関数を呼び出し、recipient = address(this)およびamount = paymentとして実行されます。

注意 NFTコントラクトはbuyerからの承認トランザクションで設定された利用可能な承認を使用して自身に支払います。

[R36]

trxnType = 1の場合、成功した支払いに対してregisteredPaymentマッピングは支払いと一致するように更新されなければならず、成功した場合は関数の戻り値としてtrueが返され、それ以外の場合はfalseが返されなければなりません。

[R37]

trxnType = 0の場合、NFTのトランスファーは内部バージョンのsafeTransferFrom関数が呼び出され、成功した場合、buyerにMINTER_ROLEが与えられなければなりません(buyerがすでにそのロールを持っていない場合に限る)。

注意 _safeTransferFrom関数は、safeTransferFromと同じ構造を持っていますが、入力データの検証をスキップします。

[R38]

trxnType = 0の場合、NFTのtransferが成功した場合、NFTのリスティングは削除されなければなりません。

[R39]

支払いトークンとしてプロトコルトークンが使用される場合、trxnTypeにかかわらずbuyerはNFTコントラクトに対してエスクローとしてプロトコルトークンを送信しなければならず、msg.dataは支払われたNFTの配列をエンコードする必要があります(uint256[] tokenIds)。

[R40]

NFTコントラクトがプロトコルトークンを受け取るために、支払い可能なフォールバック関数(fallback() external payable)を実装する必要があります。

注意 支払いがどのNFTに対して行われたかの情報を渡す必要があるため、シンプルなreceive()、fallback関数は許可されていません(トランザクションと一緒にmsg.dataを送信できないため)。

[R41]

fallback関数のmsg.dataには、最低限以下のデータが含まれている必要があります。

  • address seller
  • uint256[] _tokenId
  • address receiver
  • int256 trxnType
[R42]

trxnTypeが「0」または「1」または他のサポートされているタイプと等しくない場合、fallback関数はrevertしなければなりません。

[R43]

trxnTypeが「0」または「1」の場合、fallback関数が正常に実行されるには、[R33]から[R38]までの要件を満たす必要があり、それ以外の場合、fallback関数はrevertしなければなりません。

[R44]

トランザクションの失敗(直接販売の場合、trxnType = 0)またはNFTリスティングの購入者が考えを変えた場合(取引所を介した販売の場合、trxnType = 1)、提出された支払いは、reversePayment関数を使用してrevertできる必要があります。 関数インターフェースは以下の通りです。

javascript
/// @dev Definition of the function enabling the reversal of a payment before the sale is complete
/// @param paymentId is the unique identifier for which a payment was made
/// @param tokenType is the type of payment token used in the payment
function reversePayment(uint256 paymentId, string memory tokenType) public virtual returns (bool)
[R45]

reversePayment関数のビジネスルールは以下の通りです。

  • 特定のpaymentIdとtokenTypeに対して登録された支払いが存在する必要があります。
    • reversePayment関数を実行する場合は、対象となる支払いが記録されている必要があります。
  • msg.senderは登録された支払い内のbuyerのアドレスである必要があります。
    • これは、reversePayment関数が要求できるのは支払いを行ったbuyerだけであることを確認するルールです。
  • 支払い金額は0より大きい必要があります。
    • 逆の支払いは金額が0以上である必要があります。
    • 支払いが行われていない場合や金額が0の場合はreversePayment関数の実行は失敗します。
  • 支払いが正常にrevertされた場合、登録された支払いは削除されなければなりません。
    • reversePayment関数が成功した場合、その支払い情報はデータベースから削除されます。
    • reversePayment関数が失敗した場合、支払い情報は変更されません。

reversePayment関数は資金が関与しているため、再入攻撃から保護するために、Open ZeppelinライブラリなどのnonReentrantなどの再入防止措置を強くお勧めします。

NFT Transfer関数の修正

この文書では、ERC721のsafeTransferFrom関数について説明していて、以下の部分を強調しています。

javascript
function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory _data) external virtual override
要件の満たし方

NFT(トークン)は、ロイヤルティが適切に配布された後にtransferされる必要があります。 また、一度に複数のトークンを転送できる能力が必要ですが、ERC721標準では1つのトークンしかtransferできません。 そのため、この文書では最初のNFTに関してはtokenIdを使用し、その他のトランザクション関連データを_dataにエンコードします。

高レベルの要件

transferに関する高レベルの要件は次のとおりです。

  • _dataにエンコードされた取引の支払いパラメータを検証する必要があります。
  • 販売者と販売対象のNFTトークンは存在し、販売者はそのトークンの所有者でなければなりません。
  • msg.senderは販売者のアドレスであるか、承認されたアドレスである必要があります。
  • NFTコントラクトが受け取った取引の支払いが、すべてのロイヤルティサブアカウントの所有者に正しく支払われる必要があります。
  • NFTトークンは、NFTトークンに関連するすべてのロイヤルティサブアカウントとその所有者が正しくクレジットされた後に転送されなければなりません。
ロイヤルティの回避攻撃対策

ロイヤルティを回避する攻撃を防ぐために、NFTトークンのtransferには1つの関数しか提供されていません。 これにより、ロイヤルティの正当な分配が確保されます。

[R46]

dataなしでのtransferFromおよびsafeTransferFromは無効にする必要があります。 これを実現するには、例えばoverride関数内でrevertステートメントを使用します。 これにより、データなしでのトークンの移動が禁止されます。

[R47]

関数の入力パラメータに関する要件は以下の通りです。

  • fromはaddress(0)であってはいけません。
    • fromはゼロアドレスではない有効なアドレスである必要があります。
  • fromはtokenIdおよび_dataで含まれる他のトークンの所有者であるか、それらのトークンに対して承認されている必要があります。
  • fromはホワイトリストに登録されていない限り、コントラクトであってはいけません。
    • fromがコントラクトである場合、そのコントラクトはホワイトリストに登録されている必要があります。
  • tokenIdおよび_dataで含まれる他のトークンに対してロイヤルティアカウントが関連付けられている必要があります。
  • _dataはNULLであってはいけません。
    • _dataには有効なデータが含まれている必要があります。
  • msg.senderはfromと等しいか、承認されたアドレスであるか、ホワイトリストに登録されたコントラクトである必要があります。

注意 この文書の文脈では、呼び出し元のコントラクトがまだ作成中であるシナリオのみが攻撃ベクトルとして考えられ、transferシナリオに注意深く対処する必要があります。

[R48]

_dataオブジェクトには、次の支払いに関するパラメータが少なくとも含まれている必要があります。

  • 販売者のアドレス(seller)
    • address型
  • 購入者のアドレス(buyer)
    • address型
  • 受信者のアドレス(receiver)
    • address型
  • トークン識別子(tokenId)
    • uint256[]型
  • 支払いに使用されるトークンのタイプ(tokenType)
    • uint256型
  • NFTコントラクトに支払われる支払い額(payment)
    • uint256型
  • 登録された支払いの識別子(registered payment identifier)
  • 下層ブロックチェーンのブロックチェーンID(block.chainid)
[R49]

_data内の支払いデータは以下のビジネスルールを満たす必要があります。

  • sellerはfromと同じである必要があります。
  • tokenId[0]はtokenIdと同じである必要があります。
  • _tokenId内の各トークンには関連するロイヤルティアカウントが存在する必要があります。
  • chainidはblock.chainidと同じである必要があります。
  • buyerは指定されたpaymentIdに関連する登録された支払いのbuyer addressと同じである必要があります。
  • receiverはtoと同じである必要があります。
  • トークンの受信者は販売者ではない必要があります。
  • トークンの受信者はコントラクトでないか、またはホワイトリストに登録されたコントラクトである必要があります。
  • 支払いに含まれるすべてのNFTに対して、tokenId[i]はregisteredPayment[paymentId].boughtTokens[i]と同じである必要があります。
  • tokenTypeはコントラクトでサポートされている必要があります。
  • allowedToken[tokenType]はNULLでない必要があります。
  • tokenTypeはregisteredPayment[paymentId].tokenTypeと同じである必要があります。
  • paymentはlastBalanceAllowedToken[allowedToken[listingId]]より大きい必要があります。
  • paymentはregisteredPayment[paymentId].paymentと同じである必要があります。

これらのビジネスルールに従うことにより、支払いデータが正確であり、NFTの取引が適切に処理されます。

transfer関数におけるロイヤリティの分配

[R50]

distributePayment関数の役割は、ロイヤルティアカウントの階層的な構造を分解し、階層ごとにロイヤルティを処理することです。

javascript
/// @dev Function to distribute a payment as royalties to a chain of Royalty Accounts
/// @param tokenId is a tokenId included in the sale and used to look up the associated Royalty Account
/// @param payment is the payment (portion) to be distributed as royalties

function distributePayment (uint256 tokenId, uint265 payment) internal virtual returns (bool)

各NFTとその祖先の関連性を活用し、ロイヤルティアカウントチェーンをルート祖先とそれに関連するロイヤルティアカウントまでトラバースします。

この関数は、与えられたトークンID(tokenId)を使用して、関連するロイヤルティアカウントを特定し、支払い(payment)をそのロイヤルティアカウントに分配します。 この関数は内部で呼び出され、その結果に応じてtrueまたはfalseのbool値を返します。 成功した場合はtrueを、失敗した場合はfalseを返します。

この関数では、支払い額とロイヤルティ分割率(パーセント)を計算する必要があります。 支払い額は、支払いの一部であり、この額をロイヤルティ分割率と乗算し、結果を100%で割って正確なロイヤルティを計算します。 この時、数値のオーバーフローやアンダーフローなどの問題に十分に注意する必要があります。 これにより、正確なロイヤルティが支払いに適用され、適切に分配されます。

この関数は、NFT取引におけるロイヤルティの処理を行う重要なステップであり、数値処理に関する正確さと安全性が要求されます。

[R51]

distributePayment 関数の処理ロジックは以下の通りです。

  1. まず、渡されたトークンIDを使用して、ロイヤルティアカウント(RA)およびそれに関連するロイヤルティサブアカウントを読み込みます。

  2. 次に、RA内の各ロイヤルティサブアカウントに対して、以下のルールを適用します。

    • もしRA内のロイヤルティサブアカウントがisIndividualをtrueに設定している場合、以下の手順を実行します。
      1. そのロイヤルティサブアカウントのロイヤルティ率を支払い額に適用し、計算された金額(例: royaltyAmountTemp)をそのロイヤルティサブアカウントの royaltybalance に追加します。
      2. また、支払いの通知として、そのロイヤルティサブアカウントの accountIdに対応する情報(assetId、accountId、tokenType、royaltybalance`)を含むイベントを発行します。
      3. 最後に、RAのbalanceにroyaltyAmountTempの金額を追加します。
    • もしRA内のロイヤルティサブアカウントがisIndividualをfalseに設定している場合、以下の手順を実行します。
      1. そのロイヤルティサブアカウントのロイヤルティ率を支払い額に適用し、計算結果を一時的な変数(例: RApaymenttemp)に保存しますが、そのロイヤルティサブアカウントのroyaltybalanceは更新しません(0のままです)。
      2. 次に、祖先(ancestor)を使用して、新しいRAにアクセスし、たとえばRoyalty Accountマッピングを介して接続されたRAを取得します。
      • 新しいRAを読み込みます。
    • もし新しいRA内のロイヤルティサブアカウントのisIndividualがtrueに設定されている場合、再帰的にそのRAのロイヤルティサブアカウントに対してもisIndividual = trueのルールを適用します。
    • もし新しいRA内のロイヤルティサブアカウントのisIndividualがfalseに設定されている場合、再帰的にそのRAのロイヤルティサブアカウントに対してもisIndividual = falseのルールを適用します。
  3. 上記の手順を、最終的に祖先が存在せずすべてのロイヤルティサブアカウントがisIndividual = trueに設定されているRAに到達するまで繰り返します。

    • この段階で、isIndividual = trueのルールをこのRA内のすべてのロイヤルティサブアカウントに適用します。

このプロセスにより、複雑なロイヤルティアカウントの階層構造が適切に処理され、ロイヤルティが正しく分配されます。

ロイヤルティサブアカウントの所有権を更新し、承認されたアドレスに支払う

[R52]

所有権の移転を簡略化するため、繰り返しになりますが最初に承認されたアドレス(コントラクトではないNFTの所有者)であるfromに対して、そのロイヤルティの割合を支払います。 その後、ロイヤルティサブアカウントは新しい所有者であるtoに更新されます。 この手順は、transferされる各トークンに対して繰り返されます。

[R52]のビジネスルールは以下の通りです。

  • royaltyPayOut関数の内部バージョンは、fromアドレスが所有するロイヤルティサブアカウントのロイヤルティ残高全額をfromアドレスに支払わなければなりません。
    • これにより、ロイヤルティが元の所有者に支払われます。
  • ロイヤルティサブアカウントは、支払い関数が正常に完了し、そのロイヤルティサブアカウントの royaltybalanceが0になった後に、新しい所有者であるtoにのみ更新されます。
    • これにより、ロイヤルティが正確に新しい所有者に移転されます。
[R53]

プロセスチェーンの最後のステップは、購入したNFTをtoアドレスにtransferすることです。

[R53]のビジネスルールは以下の通りです。

  • 購入される各NFT(バッチ内の各NFT)に対して、所有権を完全に移転させるために、toアドレスは approved(ERC721の機能)である必要があります。
    • これは、所有権の移転を完了するために必要な承認手続きです。
  • 技術的なNFTの所有者はNFTコントラクトのままであり、変更されません。
    • これは、NFT自体の所有権が変わるが、NFTがコントラクトであること自体は変わらないことを意味します。

送金成功後の支払いエントリーの削除

NFTの実際の所有者は、承認されたアドレスが正常に更新された後、支払いレジストリエントリを削除し、transferされたNFTを再び販売できるようにします。

[R54]

承認関係が新しい所有者であるtoアドレスに正常に更新された後、登録された支払い情報は削除されなければなりません。 これにより、所有権の変更が確定し、新しい所有者に対する支払いが正しく関連付けられるため、transferされたNFTは再び販売できるようになります。 支払い情報を削除することで、以前の所有者との支払い関連のトラッキングが終了し、新しい所有者によるNFTの完全な管理が可能になります。

safeTransferFrom関数の送信元アドレスへのロイヤリティの支払い

[R55]
  • royaltyPayOut関数は、ロイヤルティ支払いの公開インターフェースを提供します。
  • この関数は、NFTの所有者から受け取り、指定されたアドレスにロイヤルティ支払いを行うためのものです。
  • 関数は再入防止保護が施されており、正常に実行されるとtrueを返します。
    • 失敗時にはfalseを返します。
  • 入力パラメータには、NFTの識別子 (tokenId)、ロイヤルティサブアカウントのアドレス (RAsubaccount)、支払いを受け取るアカウントのアドレス (payoutAccount)、支払い金額 (amount) が含まれます。
  • 関数の実行には特定の条件があり、これらの条件が満たされない場合、関数は失敗します。
[R56]
  • royaltyPayOut関数の入力パラメータに関する詳細な要件があります。
  • msg.senderはRAsubaccountと一致する必要があります。
  • tokenIdは存在し、burnされていない必要があります。
  • tokenIdはロイヤルティアカウントに関連付けられている必要があります。
  • RAsubaccountはtokenIdのロイヤルティアカウント内の有効なaccountIdである必要があります。
  • RAsubaccountのロイヤルティサブアカウントのisIndividualはtrueである必要があります。
  • amountはRAsubaccountのロイヤルティサブアカウントのroyaltybalance以下である必要があります。
  • これらの条件が満たされない場合、関数は失敗します。
[R57]
  • _royaltyPayOut関数は内部インターフェースで、同様のパラメータを受け取りますが、公開関数 royaltyPayOutの実装に使用されます。
  • 再入防止保護などが適用され、正常に実行されるとtrueを返します。
    • 失敗時にはfalseを返します。
[R58]

内部の_royaltyPayOut関数は、以下のアクションを実行します。

  1. payoutAccountに支払いを送信します。
  2. トランザクションが成功した場合、RAsubaccountのロイヤルティサブアカウントのroyaltybalanceを更新します。
[R59]

ロイヤルティ支払いを受け取るためには、以下の手順が実行されます。

  1. ロイヤルティサブアカウントを特定します。
  2. ロイヤルティサブアカウントからtokenTypeを取得します。
  3. トークンのタイプに基づいて、支払いをpayoutAccountに送信します。
    • トークンのタイプに応じて、'ETH'や関連するプロトコルトークン、またはトークンのタイプに基づいた別のトークンを送信することがあります。
  4. 支払いトランザクションが成功した場合、RAsubaccountのロイヤルティサブアカウントのroyaltybalanceからamountを差し引き、関数の戻り値としてtrueを返します。
    • トランザクションが成功しなかった場合は、falseを返します。

これらの手順により、ロイヤルティ支払いが正確に受信者に送信され、支払いの処理が確実に実行されます。

補足

NFTのロイヤルティは、基本的にはライセンスの配布に関する問題です。 購入者は資産/コンテンツの権利を取得しますが、それが再生可能か、変更可能か、購入者または購入者の代理人によって再生可能かどうかは不明です。 そのため、包括的な仕様は、1つまたは複数のアセットが元のアセットから派生する階層のロイヤルティに対処する必要があります。 このため、設計は多階層の継承と再帰の問題を解決する必要があります。

この複雑な継承の問題を解決するために、この提案の設計はまず再帰の問題を深さNのツリーに分割し、その後、ツリー構造をNの異なる問題に分割します。 この設計により、ツリーを最下層から最上層に効率的にトラバースできるようになります。 これは、distributePayment関数の設計と、NFTのデータ構造がツリー構造をサポートしているため実現されています。 例えば、ancestry、royaltyAccount、RAsubaccountなどがあります。

さらに、ロイヤルティの支払いに伴うガスコストが大きくなることを防ぐために、大きなロイヤルティツリーでブロックガス制限を超える可能性があるため、設計は受取人のロイヤルティ残高を維持するためのロイヤルティ会計システムを作成する必要がありました。 これは、royaltyAccount、RAsubaccount データ構造とそれに関連するCRUD操作を含み、ロイヤルティ支払いは個別または要求に基づいて行われる必要があり、royaltyPayout 関数の設計によって実現されています。

さらに、ロイヤルティの支払いを行うためには、NFTの購入と売却に関するすべての情報をスマートコントラクトが把握する必要がありました。 これらの購入と売却は、NFT契約を介して直接行われるか、中間業者を介して行われる場合があります。 これは中央集権的な要素です。 購入方法の設計は、これらの2つのモードを考慮しています。

購入プロセスの開始時にNFTコントラクトが情報を把握するためには、認可されたユーザーアドレスが直接販売のためにNFTをリストアップできる必要があります。 取引が中間業者を介して行われる場合、購入が完了する前にNFTコントラクトに支払い情報を登録する必要があります。

設計は購入プロセス中にロイヤルティの回避を防ぐ必要があり、そのためにはNFTは必ずNFTコントラクトに直接支払いを行わなければならず、販売者に支払いを行わないようにする必要がありました。 その結果、NFTコントラクトはERC721に準拠し、実際の所有者は承認されたアドレスである必要があります。

また、仕様の設計は、支払いプロセスがETHまたはERC20トークンで受け取られるかに応じて異なることを考慮する必要がありました:

ERC20トークンの場合、購入者は支払いを行うためにNFTコントラクトを承認し、その後、NFTコントラクト内のexecutePaymentを呼び出す必要があります。 ERC20トークンは直接関与しません。

非ERC20トークンの場合、購入者はプロトコルトークン(ETH)をNFTコントラクトに送信し、エンコードされたリスティング情報と支払い情報を送信する必要があります。

さらに、executePayment関数は、NFTの直接販売と取引所を介した販売の両方を処理できるように設計されました。 このため、購入が直接販売か取引所を介した販売かを示す指標が導入されました。

executePayment関数は、NFTの転送と購入のクリーンアップを担当しています。 これには、リスティングの削除、登録された支払いの削除、ロイヤルティの分配、販売者への支払い、販売者へのtransferが含まれます。

ERC721の設計に準拠し、ロイヤルティ回避を防ぎつつすべてのtransfer関数を無効にする必要があります。 そのため、追加情報を関数と一緒に提供できる唯一の関数であるsafeTransferFromの使用が許可されています。 安全性を確保するために、入力パラメータがNFTが適切に分配された後でないとtransferされないように、いくつかの要件を満たす必要があります。 設計は、直接販売と取引所を介した販売に対して異なる転送方法を考慮しています。

最後に、仕様には、ERC721の仕様に準拠しながら、NFTを作成およびburnできる必要がありました。 そのため、NFTが作成される時に、NFTに関連するロイヤルティアカウントを作成し、NFTとNFTの所有者に関連付ける必要があります。 また、NFTの祖先が存在する場合、祖先のロイヤルティアカウントも設定する必要があります。 このため、仕様は新しく定義されたmint関数でERC721の_safemint関数を使用し、適切なセットアップを確実にするために入力変数にさまざまなビジネスルールを適用します。

ロイヤルティアカウントを持つNFTはburnできますが、特定の条件を満たす必要があります。 これには、NFTおよびその子孫(存在する場合)のすべてのロイヤルティを支払う必要があります。 また、子孫が存在する場合、祖先がburnされる前に子孫をburnする必要があります。 これらのルールが適切に守られない場合、ツリーの一部で階層的なロイヤルティ構造が崩れ、未払いのロイヤルティなどの問題が発生する可能性があります。

後方互換性

このEIPは、ERC721標準に新しいインターフェースと機能を導入する一方で、ERC721標準の基本的なインターフェースと機能を変更せずに維持します。 既存のERC721トークンやスマートコントラクトは、このEIPによる変更に対して後方互換性があります。

テスト

以下のGithubに格納されています。

参考実装

以下のGithubに格納されています。

セキュリティ考慮事項

以下では、最も重要な攻撃ベクトルのカテゴリとそれらの緩和策について説明します。

専門的なセキュリティ分析ツールを使用することに加え、各インプリメンテーションがそのインプリメンテーションのセキュリティ監査を実施することも推奨されます。

支払いと支払い

リエントラント攻撃

すべての支払い関数にリエントラント保護を適用することで対処します。 例えば、Open Zeppelinのリファレンス実装を参照してください。

認可されていないアカウントからの支払い

ロイヤルティサブアカウントは、少なくともmsg.senderがロイヤルティサブアカウントの所有者であることを要求します。

executePayment関数の失敗

取引中介の販売では、executePayment関数が失敗した場合、購入者はreversePaymentを使用して支払いを元に戻すことができます。 直接の販売では、executePayment関数内で直接reversePaymentがトリガーされます。

ロイヤルティの回避

オフチェーンでのキー交換

  • 問題
    • ユーザーがNFTのプライベートキーをオフチェーンでお金と交換する可能性がある。
  • 対策
    • この行為はどの場面でも完全に防げないが、セキュリティの向上に努め、不正なキー交換を監視することが必要。

スマートコントラクトウォレットがNFTを所有

  • 問題
    • 複数のアドレスによって制御されるスマートコントラクトウォレットがNFTを所有し、オフチェーンで資産をやり取りすることができる。
  • 対策
    • スマートコントラクトがNFTを所有することを特別なケース(コレクションなど)を考慮して明示的に許可されていない限り禁止する。

ロイヤルティの支払いの拒否

  • 問題
    • 攻撃者が低いロイヤルティ分割率を持つ多数のロイヤルティサブアカウントを追加し、NFTの再印刷を行うことでコントラクトにガス切れエラーやランタイムエラーを引き起こす可能性がある。
  • 対策
    • 各NFTごとにロイヤルティサブアカウントの数を制限し、ロイヤルティ分割率の上限を設定する。

addListNFT関数の利用

  • 問題
    • 攻撃者がaddListNFT関数を悪用し、多数のNFTを低価格でリストし、別のアカウントから購入を試み、executePayment関数でエラーを引き起こす可能性がある。
  • 対策
    • 1つのリスティングに含めることができるNFTの数を制限する。

世代数の設定

  • 問題
    • NFTファミリーの作成者が非常に高い世代数を設定すると、ロイヤルティ分配関数の再帰性により、ガス切れエラーやランタイムエラーが発生する可能性がある。
  • 対策
    • 作成者によるmaxNumberGenerationの制限を設ける。

一般的な考慮事項

NFTファミリーの作成者は、ビジネスモデルに合わせてパラメータを慎重に設定する必要があります。 これには、最大世代数、ロイヤルティサブアカウントの数、プリントごとの印刷数、リストされるNFTの数、ロイヤルティ分割率の範囲などが含まれます。

フィッシング攻撃

NFTフィッシング攻撃は、NFTの所有者をだまして、攻撃者のアカウントを被害者の1つまたはすべてのNFTの承認アカウントとして追加するトランザクションに署名させようとするものです。

対策

このコントラクトは、この種のフィッシング攻撃に対して脆弱ではありません。 なぜなら、すべてのNFTの転送が販売となり、NFTコントラクト自体がすべてのNFTの所有者であるからです。 これは、購入後の転送が_approve関数内で新しい所有者を設定することによって実現されるためです。 悪意のあるトランザクションのmsg.senderはNFTの所有者である必要がないため、public approve関数の呼び出しはエラーになります。

NFTフィッシング攻撃は、addListNFT関数を標的とし、被害者を1つ以上のNFTを非常に低価格でリストするようにだまし、攻撃者がすぐに支払いを登録しそれを即座に実行しようとします。

対策

購入に影響を与えることができる場合、被害者がremoveListNFT関数を呼び出す時間を与えるための待機期間を実装できます さらに、実装者は、コントラクトに組み込むかウォレットソフトウェアに組み込まれたGoogle Authenticatorなどの認証アプリを利用して、二要素認証を要求することができます。

これらの対策は、NFTフィッシング攻撃に対するセキュリティ向上策です。

引用

Andreas Freund (@Therecanbeonlyone1969), "ERC-4910: Royalty Bearing NFTs," Ethereum Improvement Proposals, no. 4910, March 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-4910.

最後に

今回は「NFTのロイヤリティの管理や残高、支払いの管理、取引の機能などの仕組みを提案している規格であるERC4910」についてまとめてきました! いかがだったでしょうか?

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

他の媒体でも情報発信しているのでぜひ他も見ていってください!