This standard is an extension of ERC-721. It proposes two referable indicators, referring and referred, and a time-based indicator createdTimestamp. The relationship between each NFT forms a directed acyclic graph (DAG). The standard allows users to query, track and analyze their relationships.
Motivation
Many scenarios require the inheritance, reference, and extension of NFTs. For instance, an artist may develop his NFT work based on a previous NFT, or a DJ may remix his record by referring to two pop songs, etc. A gap in existing NFT standards is the absence of established relationships between an NFT and its original creator. This void isolates NFTs, rendering the sale of each one a one-off transaction, thereby obstructing creators from accruing the full value of their intellectual property over time.
In this sense, proposing a referable solution for existing NFTs that enables efficient queries on cross-references is necessary. By introducing a reference relationship between NFTs, a sustainable economic model can be established to incentivize continued engagement in creating, using, and promoting NFTs.
This standard accordingly introduces a new concept, referable NFT (rNFT), which can transform static NFTs into a dynamically extensible network. We embed reference information, including referring and referred relationships, aiding in the formation of a Direct Acyclic Graph (DAG)-based NFT network. This structure provides a transparent graphical historical record and allows users to query, trace, and analyze relationships. It can enable NFT creators to build upon existing works without the need to start anew.
An intuitive example: users can create new NFTs (C, D, E) by referencing existing ones (A, B), while the referred function informs the original NFTs (A, B) about their citations (e.g., A ← D; C ← E; B ← E, and A ← E). Here, the createdTimestamp (block-level) serves as an indicator for the creation time of NFTs (A, B, C, D, E).
Key Takeaways
This standard provides several advantages:
Clear ownership inheritance: This standard extends the static NFT into a virtually extensible NFT network. Artists do not have to create work isolated from others. The ownership inheritance avoids reinventing the same wheel.
Incentive Compatibility: This standard clarifies the referable relationship across different NFTs, helping to integrate multiple up-layer incentive models for both original NFT owners and new creators.
Easy Integration: This standard makes it easier for the existing token standards or third-party protocols. For instance, the rNFT can be applied to rentable scenarios (cf. ERC-5006 to build a hierarchical rental market, where multiple users can rent the same NFT during the same time or one user can rent multiple NFTs during the same duration).
Scalable Interoperability: This standard enables cross-contract references, giving a scalable adoption for the broader public with stronger interoperability.
Specification
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.
UpdateNode: event emitted when setNode is invoked;
safeMint: mint a new rNFT;
setNode: set the referring list of an rNFT and update the referred list of each one in the referring list;
setNodeReferring: set the referring list of an rNFT;
setNodeReferred: set the referred list of the given rNFTs sourced from different contracts;
setNodeReferredExternal: set the referred list of the given rNFTs sourced from external contracts;
referringOf: get the referring list of an rNFT;
referredOf: get the referred list of an rNFT;
createdTimestampOf: get the timestamp of an rNFT when it is being created.
Implementers of this standard MUST have all of the following functions:
pragmasolidity^0.8.4;import"@openzeppelin/contracts/utils/introspection/IERC165.sol";interfaceIERC_5521isIERC165{/// Logged when a node in the rNFT gets referred and changed.
/// @notice Emitted when the `node` (i.e., an rNFT) is changed.
eventUpdateNode(uint256indexedtokenId,addressindexedowner,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)`.
functionsetNode(uint256tokenId,address[]memoryaddresses,uint256[][]memorytokenIds)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.
functionreferringOf(address_address,uint256tokenId)externalviewreturns(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.
functionreferredOf(address_address,uint256tokenId)externalviewreturns(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.
functioncreatedTimestampOf(address_address,uint256tokenId)externalviewreturns(uint256);/// @notice check supported interfaces, adhereing to ERC165.
functionsupportsInterface(bytes4interfaceId)externalviewreturns(bool);}interfaceTargetContractisIERC165{/// @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.
functionsetNodeReferredExternal(address_address,uint256tokenId,uint256[]memory_tokenIds)external;functionreferringOf(address_address,uint256tokenId)externalviewreturns(address[]memory,uint256[][]memory);functionreferredOf(address_address,uint256tokenId)externalviewreturns(address[]memory,uint256[][]memory);functioncreatedTimestampOf(address_address,uint256tokenId)externalviewreturns(uint256);functionsupportsInterface(bytes4interfaceId)externalviewreturns(bool);}
Rationale
Is this event informative enough?
UpdateNode: This event disseminates crucial information, including the rNFT ID, its owner, and lists of contract addresses/IDs with rNFTs referring to or referred by the subject rNFT. This data set enables stakeholders to efficiently manage and navigate the complex web of relationships inherent in the rNFT ecosystem.
Implementers are free to choose to use a struct (a recommended struct is given in the Reference Implementation), or several separate mappings, or whatever other storage mechanism. Whichever mechanism chosen has no observable effect on the behaviour of the contract, as long as its output can fulfill the UpdateNode event.
Why createdTimestampOf?
createdTimestamp: A key principle of this standard is that an rNFT should reference content already accepted by the community (a time-based sequence known by participants). Global timestamps for rNFTs are thus essential, serving to prevent conflicting states (akin to concurrency issues in transaction processing and block organization). We define a block-level timestamp where createdTimestamp = block.timestamp Note that, given that the granularity of references is tied to the block timestamp, it is impractical to discern the order of two rNFTs within the same block.
How is cross-contract reference performed?
setNodeReferredExternal: This function operates conditionally, dependent on successful interface verification in external contracts. Such selective invocation ensures backward compatibility and integration with existing contracts, provided they adhere to specified interfaces.
Backwards Compatibility
This standard can be fully ERC-721 compatible by adding an extension function set.
The recommended implementation is demonstrated as follows:
Relationship: a structure that contains referring, referred, referringKeys, referredKeys, createdTimestamp, and other customized and optional attributes (i.e., not necessarily included in the standard) such as privityOfAgreement recording the ownerships of referred NFTs at the time the Referable NFTs (rNFTs) were being created or profitSharing recording the profit sharing of referring.
referring: an out-degree indicator, used to show the users this NFT refers to;
referred: an in-degree indicator, used to show the users who have refereed this NFT;
referringKeys: a helper for mapping conversion of out-degree indicators, used for events;
referredKeys: a helper for mapping conversion of in-degree indicators, used for events;
createdTimestamp: a time-based indicator, used to compare the timestamp of mint, which should not be editable anyhow by callers.
referringOf and referredOf: First, the current referringOf and referredOf allow cross-contract looking up, while this cannot be done by directly accessing _relationship. Secondly, only if privacy is not a concern, making _relationship public simplifies the contract by relying on Solidity’s automatically generated getters. However, if you need to control the visibility of the data, keeping the state variable private and providing specific getter functions would be the best approach. For example, if _relationship includes details about specific users’ interactions or transactions or some private extensible parameters (in the updated version, we specifically highlight the Relationship can be extended to meet different requirements), always making this data public could reveal users’ behavior patterns or preferences, leading to potential privacy breaches.
convertMap: This function is essential for retrieving the full mapping contents within a struct. Even if _relationship is public, The getters only allow retrieval of individual values for specific keys. Since we need comprehensive access to all stored addresses, convertMap is necessary to fulfill our event emission requirements.
pragmasolidity^0.8.4;import"@openzeppelin/contracts/token/ERC721/ERC721.sol";import"./IERC_5521.sol";contractERC_5521isERC721,IERC_5521,TargetContract{structRelationship{mapping(address=>uint256[])referring;mapping(address=>uint256[])referred;address[]referringKeys;address[]referredKeys;uint256createdTimestamp;// unix timestamp when the rNFT is being created
// extensible parameters
// ...
}mapping(uint256=>Relationship)internal_relationship;addresscontractOwner=address(0);constructor(stringmemoryname_,stringmemorysymbol_)ERC721(name_,symbol_){contractOwner=msg.sender;}functionsafeMint(uint256tokenId,address[]memoryaddresses,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
functionsetNode(uint256tokenId,address[]memoryaddresses,uint256[][]memorytokenIds)publicvirtualoverride{require(addresses.length==tokenIds.length,"Addresses and TokenID arrays must have the same length");for(uinti=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
functionsetNodeReferring(address[]memoryaddresses,uint256tokenId,uint256[][]memory_tokenIds)private{require(_isApprovedOrOwner(msg.sender,tokenId),"ERC_5521: transfer caller is not owner nor approved");Relationshipstoragerelationship=_relationship[tokenId];for(uinti=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
functionsetNodeReferred(address[]memoryaddresses,uint256tokenId,uint256[][]memory_tokenIds)private{for(uinti=0;i<addresses.length;i++){if(addresses[i]==address(this)){for(uintj=0;j<_tokenIds[i].length;j++){Relationshipstoragerelationship=_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{TargetContracttargetContractInstance=TargetContract(addresses[i]);boolisSupports=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
functionsetNodeReferredExternal(address_address,uint256tokenId,uint256[]memory_tokenIds)external{for(uinti=0;i<_tokenIds.length;i++){Relationshipstoragerelationship=_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
functionreferringOf(address_address,uint256tokenId)externalviewvirtualoverride(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{TargetContracttargetContractInstance=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
functionreferredOf(address_address,uint256tokenId)externalviewvirtualoverride(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{TargetContracttargetContractInstance=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.
functioncreatedTimestampOf(address_address,uint256tokenId)externalviewreturns(uint256){uint256memorycreatedTimestamp;if(_address==address(this)){require(_exists(tokenId),"ERC_5521: token ID not existed");Relationshipstoragerelationship=_relationship[tokenId];createdTimestamp=relationship.createdTimestamp;}else{TargetContracttargetContractInstance=TargetContract(_address);require(targetContractInstance.supportsInterface(type(TargetContract).interfaceId),"ERC_5521: target contract not supported");createdTimestamp=targetContractInstance.createdTimestampOf(_address,tokenId);}returncreatedTimestamp;}/// @dev See {IERC165-supportsInterface}.
functionsupportsInterface(bytes4interfaceId)publicviewvirtualoverride(ERC721,IERC_5521,TargetContract)returns(bool){returninterfaceId==type(IERC_5521).interfaceId||interfaceId==type(TargetContract).interfaceId||super.supportsInterface(interfaceId);}// @notice Emit an event of UpdateNode
functionemitEvents(uint256tokenId,addresssender)private{(address[]memory_referringKeys,uint256[][]memory_referringValues)=convertMap(tokenId,true);(address[]memory_referredKeys,uint256[][]memory_referredValues)=convertMap(tokenId,false);emitUpdateNode(tokenId,sender,_referringKeys,_referringValues,_referredKeys,_referredValues);}// @notice Convert a specific `local` token mapping to a key array and a value array
functionconvertMap(uint256tokenId,boolisReferring)privateviewreturns(address[]memory,uint256[][]memory){Relationshipstoragerelationship=_relationship[tokenId];address[]memoryreturnKeys;uint256[][]memoryreturnValues;if(isReferring){returnKeys=relationship.referringKeys;returnValues=newuint256[][](returnKeys.length);for(uinti=0;i<returnKeys.length;i++){returnValues[i]=relationship.referring[returnKeys[i]];}}else{returnKeys=relationship.referredKeys;returnValues=newuint256[][](returnKeys.length);for(uinti=0;i<returnKeys.length;i++){returnValues[i]=relationship.referred[returnKeys[i]];}}return(returnKeys,returnValues);}}
Security Considerations
Timestamp
The createdTimestamp only covers the block-level timestamp (based on block headers), which does not support fine-grained comparisons such as transaction-level.
Ownership and Reference
The change of ownership has nothing to do with the reference relationship. Normally, the distribution of profits complies with the agreement when the NFT was being created regardless of the change of ownership unless specified in the agreement.
Referring a token will not refer to its descendants by default. In the case that only a specific child token gets referred, it means the privity of the contract will involve nobody other than the owner of this specific child token. Alternatively, a chain-of-reference all the way from the root token to a specific very bottom child token (from root to leaf) can be constructed and recorded in the referring to explicitly define the distribution of profits.
Open Minting and Relationship Risks
The safeMint function has been deliberately designed to allow unrestricted minting and relationship setting, akin to the open referencing system seen in platforms such as Google Scholar. This decision facilitates strong flexibility, enabling any user to create and define relationships between NFTs without centralized control. While this design aligns with the intended openness of the system, it inherently carries certain risks. Unauthorized or incorrect references can be created, mirroring the challenges faced in traditional scholarly referencing, where erroneous citations may occur. Additionally, the open nature may expose the system to potential abuse by malicious actors, who might manipulate relationships or inflate the token supply. It is important to recognize that these risks are not considered design flaws but intentional trade-offs, which balance the system’s flexibility against potential reliability concerns.
Stakeholders should be aware that the on-chain data integrity guarantees extend only to what has been recorded on the blockchain and do not preclude the possibility of off-chain errors or manipulations. Thus, users and integrators should exercise caution and judgment in interpreting and using the relationships and other data provided by this system.