はじめに
初めまして。
CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。
代表的なゲームはクリプトスペルズというブロックチェーンゲームです。
今回は、ERC721規格のNFTの参照関係を表現する仕組みを提案しているERC5521についてまとめていきます!
以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。
5521は現在(2023年9月20日)では「Draft」段階です。
概要
この規格では、ERC721を拡張して2つの参照可能なインジケーター (referingとreferred) と時間ベースのインジケーターであるcreatedTimestamp
提案しています。
各NFT間の関係は有向非巡回グラフ(DAG)を形成します。
この標準により、ユーザーは関係を照会、追跡、分析できます。
ERC721については以下の記事を参考にしてください。
有向非巡回グラフ(DAG)について
有向非巡回グラフ (Directed Acyclic Graph, DAG) は、グラフ理論の1つで、以下の特徴を持つ構造です。
-
有向性 (Directed)
- 各エッジ(頂点間の線)が特定の方向を持っている。
- つまり、頂点 $A$ から頂点 $B$ へのエッジがあり、片方行もしくは両方向へのエッジが存在する。
-
非巡回性 (Acyclic)
- 任意の頂点から出発してその頂点に戻る閉路(サイクル)が存在しない。
- つまり、どの頂点からも元の頂点に戻ることができない構造です。
例1: タスクの依存関係
DAGは、プロジェクト管理でタスクの依存関係を表現するのに役立ちます。
-
タスクA
- プロジェクトの初期設定
-
タスクB
- データ収集
-
タスクC
- データ分析
-
タスクD
- レポート作成
これらのタスクの依存関係をDAGで表現すると以下のようになります。
- タスクA → タスクB
- タスクA → タスクC
- タスクB → タスクD
- タスクC → タスクD
このDAGでは、タスクAが完了した後にタスクBとタスクCが始まり、タスクBとタスクCが完了した後にタスクDが始まることを示しています。
このようにDAGは、タスクの実行順序を明確にし、効率的なプロジェクト管理を可能にします。
例2: バージョン管理システム
Gitのような分散型バージョン管理システムもDAGを利用しています。
各コミットは1つの頂点として表され、コミット間の依存関係(親子関係)がエッジとして表現されます。
-
コミットA
- プロジェクトの初期コミット
-
コミットB
- コミットAからの変更
-
コミットC
- コミットAからの別の変更
-
コミットD
- コミットBとコミットCをマージしたもの
このDAGでは、コミットAから始まり、コミットBとコミットCに分岐し、それがコミットDで再び統合されることを示しています。
このように、DAGは複数の変更履歴を追跡して統合するのに役立ちます。
例3: rNFTの関係性
提案されたERC5521標準では、rNFTの関係性もDAGで表現されます。
各NFTは頂点として扱われ、参照関係がエッジとして表現されます。例えば:
- NFT A: オリジナルのアート作品
- NFT B: NFT Aを参照して作成されたアート作品
- NFT C: NFT Aを参照して作成された別のアート作品
- NFT D: NFT BとNFT Cを参照して作成されたアート作品
このDAGでは、NFT Aから始まり、NFT BとNFT Cが参照され、それがNFT Dで再び統合されることを示しています。このように、DAGはNFTの参照関係を明確にし、複雑な参照構造を管理しやすくします。
DAGの利点
-
明確な依存関係
- タスクやバージョンの依存関係を視覚的に表現しやすく、順序や依存関係を直感的に理解できます。
-
効率的な管理
- タスクの順序やバージョン管理を効率的に行うための基盤を提供します。
-
データの整合性
- 閉路が存在しないため、データの一貫性を保ちながら依存関係を管理できます。
以上のように、DAGは複雑な依存関係を持つシステムやプロジェクトを管理するのに非常に有効なツールです。
ERC5521標準でも、このDAGの特性を活かしてNFT間の関係性を効果的に管理し、追跡することが可能です。
上記は、rNFTの関係性を表す有向非巡回グラフ(DAG)の図です。
それぞれのrNFTには番号が振られていて、それ以前のrNFTを参照しています。
また、参照しているrNFTには収益分配がされます。
図のように、rNFT 26から発生した収益分配額のうち、以下のように参照したrNFTに分配されます。
- rNFT 11
- 60%
- rNFT 21
- 20%
- rNFT 25
- 20%
この収益分配には、外部ユーザーとコミュニティ、rNFTの発行者、規制当局などのステークホルダー間での合意形成プロセスに基づいて行われます。
動機
既存のNFTでは、以前作成したNFTに基づいてNFTを作成したり、2つのレコードをリミックスしても、新しい作品と元になった作品の作者との関係性がなくなります。
これは、各NFTの販売が一度気になったり、時間経過とともに知的財産の価値を向上させることができなくなります。
NFT間の参照関係を導入することで、持続可能な経済モデルが確立され、NFTの作成、使用、プロモーションに対する継続的なエンゲージメントが促進されます。
参照可能NFT(rNFT)の導入により、動的に拡張可能なネットワークを構築できます。
参照と被参照の関係を形成し、有向非巡回グラフ(DAG)ベースのNFTネットワークが構築され、ユーザーが各NFTの関係をクエリ、追跡、分析することが可能になります。
重要なポイント
ここでは以下の点がポイントになります。
- 明確な所有権の継承
- 所有権を継承することにより、既存の作品をベースにした作品を作成できます。
- これにより車輪の再発明を回避できます。
- インセンティブの互換性
- 参照元のNFT作成者と新しいNFTの作成者の両方に対するインセンティブモデルの統合が容易になります。
- 簡単な統合
- 既存トークン標準や第三者プロトコルとの統合を容易にします。
- 例えば、rNFTは賃貸などのシナリオでも活用できます。
- ERC5006などの規格を使用して、複数のユーザーが同じNFTを同時にレンタルしたり、1人のユーザーが複数のNFTを同じ期間にレンタル可能です。
ERC5006については以下の記事を参考にしてください。
- スケーラブルな相互運用性
- 異なるコントラクト間(クロスコントラクト)での参照を可能にし、相互運用性が強化されます。
仕様
「referring」(参照元)
この属性は、特定のNFTが他のNFTを参照していることを示します。
つまり、あるNFTが他のNFTに「リンク」していることを示します。
この属性は、外向きのリンクの数を追跡します。
つまり、このNFTが他のNFTをどれだけ参照しているかを数えます。
「referred」(参照先)
この属性は、特定のNFTが他のNFTから参照されていることを示します。
つまり、あるNFTが他のNFTに「リンク」されていることを示します。
この属性は、内向きのリンクの数を追跡します。
つまり、このNFTが他のNFTからどれだけ参照されているかを数えます。
「createdTimestamp」(作成日時)
この属性は、NFTが作成された具体的な日時を示します。
これを使用すると、NFTがいつ作成されたかを比較できるなど時間に関する情報を提供します。
この規格を実装するコントラクトでは、以下の機能を備えている必要があります。
pragma solidity ^0.8.4;
import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
interface IERC_5521 is IERC165 {
/// Logged when a node in the rNFT gets referred and changed.
/// @notice Emitted when the `node` (i.e., an rNFT) is changed.
event UpdateNode(uint256 indexed tokenId,
address indexed owner,
address[] _address_referringList,
uint256[][] _tokenIds_referringList,
address[] _address_referredList,
uint256[][] _tokenIds_referredList
);
/// @notice set the referred list of an rNFT associated with different contract addresses and update the referring list of each one in the referred list. Checking the duplication of `addresses` and `tokenIds` is **RECOMMENDED**.
/// @param `tokenId` of rNFT being set. `addresses` of the contracts in which rNFTs with `tokenIds` being referred accordingly.
/// @requirement
/// - the size of `addresses` **MUST** be the same as that of `tokenIds`;
/// - once the size of `tokenIds` is non-zero, the inner size **MUST** also be non-zero;
/// - the `tokenId` **MUST** be unique within the same contract;
/// - the `tokenId` **MUST NOT** be the same as `tokenIds[i][j]` if `addresses[i]` is essentially `address(this)`.
function setNode(uint256 tokenId, address[] memory addresses, uint256[][] memory tokenIds) external;
/// @notice get the referring list of an rNFT.
/// @param `tokenId` of the rNFT being focused, `_address` of contract address associated with the focused rNFT.
/// @return the referring mapping of the rNFT.
function referringOf(address _address, uint256 tokenId) external view returns(address[] memory, uint256[][] memory);
/// @notice get the referred list of an rNFT.
/// @param `tokenId` of the rNFT being focused, `_address` of contract address associated with the focused rNFT.
/// @return the referred mapping of the rNFT.
function referredOf(address _address, uint256 tokenId) external view returns(address[] memory, uint256[][] memory);
/// @notice get the timestamp of an rNFT when is being created.
/// @param `tokenId` of the rNFT being focused, `_address` of contract address associated with the focused rNFT.
/// @return the timestamp of the rNFT when is being created with uint256 format.
function createdTimestampOf(address _address, uint256 tokenId) external view returns(uint256);
/// @notice check supported interfaces, adhereing to ERC165.
function supportsInterface(bytes4 interfaceId) external view returns (bool);
}
interface TargetContract is IERC165 {
/// @notice set the referred list of an rNFT associated with external contract addresses.
/// @param `_tokenIds` of rNFTs associated with the contract address `_address` being referred by the rNFT with `tokenId`.
/// @requirement
/// - `_address` **MUST NOT** be the same as `address(this)` where `this` is executed by an external contract where `TargetContract` interface is implemented.
function setNodeReferredExternal(address _address, uint256 tokenId, uint256[] memory _tokenIds) external;
function referringOf(address _address, uint256 tokenId) external view returns(address[] memory, uint256[][] memory);
function referredOf(address _address, uint256 tokenId) external view returns(address[] memory, uint256[][] memory);
function createdTimestampOf(address _address, uint256 tokenId) external view returns(uint256);
function supportsInterface(bytes4 interfaceId) external view returns (bool);
}
-
UpdateNode
-
setNode
が呼び出されたときに発生されるイベント。
-
-
safeMint
- 新しいrNFTを作成。
-
setNode
- 特定のrNFTの参照リストを設定し、そのrNFTが参照しているNFTのリストを更新。
- NFT間の関係性を設定。
-
setNodeReferring
- 特定のrNFTの参照リストを設定。
- 外向きのリンクを設定。
-
setNodeReferred
- 指定されたrNFTに対する参照先リストを設定。
- 内向きのリンクを設定。
-
setNodeReferredExternal
- 他のコントラクトから提供されたrNFTに対する参照先リストを設定。
- 外部からの内向きのリンクを設定。
-
referringOf
- 特定のrNFTの参照リストを取得。
- 外向きのリンクを取得。
-
referredOf
- 特定のrNFTの参照先リストを取得。
- 内向きのリンクを取得。
-
createdTimestampOf
- rNFT作成時のタイムスタンプを取得。
補足
UpdateNodeが十分な情報を提供できているか
UpdateNode
は以下の情報を提供します。
- rNFTのID
- rNFTの所有者
- 特定のrNFTを参照している、または参照されているコントラクトアドレスとIDのリスト
このデータセットにより、関係者はrNFTエコシステムに内在する複雑な関係性を効率的に管理し、ナビゲートすることができます。
こられのデータの実装方法に関してはさまざま存在するが(構造体、mapping配列など)、どのメカニズムを選択しても問題ないです。
createdTimestampOf
が必要な理由
rNFT同士は参照を行います。
そのため、rNFTのグローバルタイムスタンプが存在することで、rNFTの時間ベースのシーケンスが可能になります。
クロスコントラクト参照はどのように実行されるか?
setNodeReferredExternal
関数を使用して、外部コントラクトのインターフェース検証を行い、既存コントラクトの互換性を確認して実行されます。
互換性
この規格は、拡張機能セットを追加することでERC721と完全に互換性を持たせることができます。
テスト
以下にコードがあります。
参考実装
参考実装コード
pragma solidity ^0.8.4;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "./IERC_5521.sol";
contract ERC_5521 is ERC721, IERC_5521, TargetContract {
struct Relationship {
mapping (address => uint256[]) referring;
mapping (address => uint256[]) referred;
address[] referringKeys;
address[] referredKeys;
uint256 createdTimestamp; // unix timestamp when the rNFT is being created
// extensible parameters
// ...
}
mapping (uint256 => Relationship) internal _relationship;
address contractOwner = address(0);
constructor(string memory name_, string memory symbol_) ERC721(name_, symbol_) {
contractOwner = msg.sender;
}
function safeMint(uint256 tokenId, address[] memory addresses, uint256[][] memory _tokenIds) public {
// require(msg.sender == contractOwner, "ERC_rNFT: Only contract owner can mint");
_safeMint(msg.sender, tokenId);
setNode(tokenId, addresses, _tokenIds);
}
/// @notice set the referred list of an rNFT associated with different contract addresses and update the referring list of each one in the referred list
/// @param tokenIds array of rNFTs, recommended to check duplication at the caller's end
function setNode(uint256 tokenId, address[] memory addresses, uint256[][] memory tokenIds) public virtual override {
require(
addresses.length == tokenIds.length,
"Addresses and TokenID arrays must have the same length"
);
for (uint i = 0; i < tokenIds.length; i++) {
if (tokenIds[i].length == 0) { revert("ERC_5521: the referring list cannot be empty"); }
}
setNodeReferring(addresses, tokenId, tokenIds);
setNodeReferred(addresses, tokenId, tokenIds);
}
/// @notice set the referring list of an rNFT associated with different contract addresses
/// @param _tokenIds array of rNFTs associated with addresses, recommended to check duplication at the caller's end
function setNodeReferring(address[] memory addresses, uint256 tokenId, uint256[][] memory _tokenIds) private {
require(_isApprovedOrOwner(msg.sender, tokenId), "ERC_5521: transfer caller is not owner nor approved");
Relationship storage relationship = _relationship[tokenId];
for (uint i = 0; i < addresses.length; i++) {
if (relationship.referring[addresses[i]].length == 0) { relationship.referringKeys.push(addresses[i]); } // Add the address if it's a new entry
relationship.referring[addresses[i]] = _tokenIds[i];
}
relationship.createdTimestamp = block.timestamp;
emitEvents(tokenId, msg.sender);
}
/// @notice set the referred list of an rNFT associated with different contract addresses
/// @param _tokenIds array of rNFTs associated with addresses, recommended to check duplication at the caller's end
function setNodeReferred(address[] memory addresses, uint256 tokenId, uint256[][] memory _tokenIds) private {
for (uint i = 0; i < addresses.length; i++) {
if (addresses[i] == address(this)) {
for (uint j = 0; j < _tokenIds[i].length; j++) {
Relationship storage relationship = _relationship[_tokenIds[i][j]];
if (relationship.referred[addresses[i]].length == 0) { relationship.referredKeys.push(addresses[i]); } // Add the address if it's a new entry
require(tokenId != _tokenIds[i][j], "ERC_5521: self-reference not allowed");
if (relationship.createdTimestamp >= block.timestamp) { revert("ERC_5521: the referred rNFT needs to be a predecessor"); } // Make sure the reference complies with the timing sequence
relationship.referred[address(this)].push(tokenId);
emitEvents(_tokenIds[i][j], ownerOf(_tokenIds[i][j]));
}
} else {
TargetContract targetContractInstance = TargetContract(addresses[i]);
bool isSupports = targetContractInstance.supportsInterface(type(TargetContract).interfaceId);
if (isSupports) {
// The target contract supports the interface, safe to call functions of the interface.
targetContractInstance.setNodeReferredExternal(address(this), tokenId, _tokenIds[i]);
}
}
}
}
/// @notice set the referred list of an rNFT associated with different contract addresses
/// @param _tokenIds array of rNFTs associated with addresses, recommended to check duplication at the caller's end
function setNodeReferredExternal(address _address, uint256 tokenId, uint256[] memory _tokenIds) external {
for (uint i = 0; i < _tokenIds.length; i++) {
Relationship storage relationship = _relationship[_tokenIds[i]];
if (relationship.referred[_address].length == 0) { relationship.referredKeys.push(_address); } // Add the address if it's a new entry
require(_address != address(this), "ERC_5521: this must be an external contract address");
if (relationship.createdTimestamp >= block.timestamp) { revert("ERC_5521: the referred rNFT needs to be a predecessor"); } // Make sure the reference complies with the timing sequence
relationship.referred[_address].push(tokenId);
emitEvents(_tokenIds[i], ownerOf(_tokenIds[i]));
}
}
/// @notice Get the referring list of an rNFT
/// @param tokenId The considered rNFT, _address The corresponding contract address
/// @return The referring mapping of an rNFT
function referringOf(address _address, uint256 tokenId) external view virtual override(IERC_5521, TargetContract) returns (address[] memory, uint256[][] memory) {
address[] memory _referringKeys;
uint256[][] memory _referringValues;
if (_address == address(this)) {
require(_exists(tokenId), "ERC_5521: token ID not existed");
(_referringKeys, _referringValues) = convertMap(tokenId, true);
} else {
TargetContract targetContractInstance = TargetContract(_address);
require(targetContractInstance.supportsInterface(type(TargetContract).interfaceId), "ERC_5521: target contract not supported");
(_referringKeys, _referringValues) = targetContractInstance.referringOf(_address, tokenId);
}
return (_referringKeys, _referringValues);
}
/// @notice Get the referred list of an rNFT
/// @param tokenId The considered rNFT, _address The corresponding contract address
/// @return The referred mapping of an rNFT
function referredOf(address _address, uint256 tokenId) external view virtual override(IERC_5521, TargetContract) returns (address[] memory, uint256[][] memory) {
address[] memory _referredKeys;
uint256[][] memory _referredValues;
if (_address == address(this)) {
require(_exists(tokenId), "ERC_5521: token ID not existed");
(_referredKeys, _referredValues) = convertMap(tokenId, false);
} else {
TargetContract targetContractInstance = TargetContract(_address);
require(targetContractInstance.supportsInterface(type(TargetContract).interfaceId), "ERC_5521: target contract not supported");
(_referredKeys, _referredValues) = targetContractInstance.referredOf(_address, tokenId);
}
return (_referredKeys, _referredValues);
}
/// @notice Get the timestamp of an rNFT when is being created.
/// @param `tokenId` of the rNFT being focused, `_address` of contract address associated with the focused rNFT.
/// @return The timestamp of the rNFT when is being created with uint256 format.
function createdTimestampOf(address _address, uint256 tokenId) external view returns(uint256) {
uint256 memory createdTimestamp;
if (_address == address(this)) {
require(_exists(tokenId), "ERC_5521: token ID not existed");
Relationship storage relationship = _relationship[tokenId];
createdTimestamp = relationship.createdTimestamp;
} else {
TargetContract targetContractInstance = TargetContract(_address);
require(targetContractInstance.supportsInterface(type(TargetContract).interfaceId), "ERC_5521: target contract not supported");
createdTimestamp = targetContractInstance.createdTimestampOf(_address, tokenId);
}
return createdTimestamp;
}
/// @dev See {IERC165-supportsInterface}.
function supportsInterface(bytes4 interfaceId) public view virtual override (ERC721, IERC_5521, TargetContract) returns (bool) {
return interfaceId == type(IERC_5521).interfaceId
|| interfaceId == type(TargetContract).interfaceId
|| super.supportsInterface(interfaceId);
}
// @notice Emit an event of UpdateNode
function emitEvents(uint256 tokenId, address sender) private {
(address[] memory _referringKeys, uint256[][] memory _referringValues) = convertMap(tokenId, true);
(address[] memory _referredKeys, uint256[][] memory _referredValues) = convertMap(tokenId, false);
emit UpdateNode(tokenId, sender, _referringKeys, _referringValues, _referredKeys, _referredValues);
}
// @notice Convert a specific `local` token mapping to a key array and a value array
function convertMap(uint256 tokenId, bool isReferring) private view returns (address[] memory, uint256[][] memory) {
Relationship storage relationship = _relationship[tokenId];
address[] memory returnKeys;
uint256[][] memory returnValues;
if (isReferring) {
returnKeys = relationship.referringKeys;
returnValues = new uint256[][](returnKeys.length);
for (uint i = 0; i < returnKeys.length; i++) {
returnValues[i] = relationship.referring[returnKeys[i]];
}
} else {
returnKeys = relationship.referredKeys;
returnValues = new uint256[][](returnKeys.length);
for (uint i = 0; i < returnKeys.length; i++) {
returnValues[i] = relationship.referred[returnKeys[i]];
}
}
return (returnKeys, returnValues);
}
}
推奨される実装
以下の実装が推奨されています。
Relationship 構造体
Relationshipは、rNFT(参照可能なNFT)の関係性を管理するための構造体です。
この構造体には以下の属性が含まれます。
-
referring
- 他のNFTの参照データ。
-
referred
- 他のNFTからの参照データ。
-
referringKeys
-
referring
の情報を効率的に管理するための補助データ。
-
-
referredKeys
-
referred
の情報を効率的に管理するための補助データ。
-
-
createdTimestamp
- rNFTが作成された時刻を示すタイムスタンプで、外部から編集できません。
その他、カスタマイズ可能なオプションの属性として以下のようなものがあります。
-
privityOfAgreement
- rNFTが作成された時点で参照しているNFTの所有権。
-
profitSharing
- 参照関係に基づく収益分配。
referringOf と referredOf
referringOfとreferredOfは、rNFTの参照関係を取得するための関数です。
これらの関数はクロスコントラクト(異なるスマートコントラクト間)での参照を可能にします。
これは、直接**_relationship**にアクセスすることで実現できません。
プライバシーが問題とならない場合は、_relationshipをpublic
にすることでコントラクトを簡素化できます。
しかし、データの可視性を制御する必要がある場合は、状態変数をプライベートにして、そのデータを取得できる関数を実装する方が良いです。
例えば、_relationshipに特定ユーザーの取引や相互作用の詳細が含まれている場合、常にこのデータを公開すると、ユーザーの行動パターンや好みが明らかになる可能性があります。
これによりプライバシーの侵害が生じる可能性があります。
convertMap
convertMap関数は、構造体内の完全なマッピング内容を取得するために必要です。
_relationshipが公開されていても、データの取得には特定のキーに対する個別の値の取得しか許可しません。
すべての保存されたアドレスに包括的にアクセスする必要があるため、イベントの発行要件を満たすためにconvertMapが必要です。
セキュリティ考慮事項
createdTimestamp(作成日時)
ブロックヘッダーに基づいた時間情報を提供しますが、トランザクションごとの細かい時間情報は提供しないため、より詳細な時間情報を得ることはできません。
所有権と参照
所有権の変更はトークンの参照関係に影響しません。
通常、NFTが作成されたときの設定値に従って、所有権の変更に関係なく利益が分配されます。
rNFTを参照しても、参照先のrNFTが参照しているrNFTは参照しません。
特定のrNFTの子トークンのみ参照されますが、ルートトークンから特定の子トークンまでの参照チェーン(ルートからリーフまで)を構築して記録することも可能です。
オープンミントと関係リスク
safeMint
関数は、制限のないミンティングを許可するように設計されています。
これは、Google Scholarのようなオープンな参照システムに似ています。
この設計により、ユーザーは中央集権的な制御なしにNFT間の関係を作成および定義することができます。
一方、不正確な参照や未承認の参照が作成される可能性があります(これは伝統的な学術的参照の課題に似ています)。
悪意のあるアクターが関係を操作したり、トークン供給を膨らませるリスクもあります。
これらのリスクは設計上の欠陥ではなく、システムの柔軟性と信頼性のバランスを取るための意図的なトレードオフとして考慮されています。
また、オンチェーンデータの整合性はブロックチェーンに記録された内容のみを保証され、オフチェーンのエラーや操作の可能性を排除できません。
引用
Saber Yu (@OniReimu), Qin Wang qin.wang@data61.csiro.au, Shange Fu shange.fu@monash.edu, Yilin Sai yilin.sai@data61.csiro.au, Shiping Chen shiping.chen@data61.csiro.au, Sherry Xu xiwei.xu@data61.csiro.au, Jiangshan Yu jiangshan.yu@monash.edu, "ERC-5521: Referable NFT [DRAFT]," Ethereum Improvement Proposals, no. 5521, August 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-5521.
最後に
今回は「ERC721規格のNFTの参照関係を表現する仕組みを提案しているERC5521」についてまとめてきました!
いかがだったでしょうか?
質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!
他の媒体でも情報発信しているのでぜひ他も見ていってください!