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 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. Proposing a referable solution for existing NFTs and enabling efficient queries on cross-references make much sense.
By adding the referring indicator, users can mint new NFTs (e.g., C, D, E) by referring to existing NFTs (e.g., A, B), while referred enables the referred NFTs (A, B) to be aware that who has quoted it (e.g., A ← D; C ← E; B ← E, and A ← E). The createdTimestamp is an indicator used to show the creation time of NFTs (A, B, C, D, E).
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.
Relationship: a structure that contains referring, referred, 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 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;
createdTimestamp: a time-based indicator, used to compare the timestamp of mint, which MUST NOT be editable anyhow by callers;
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.
Implementers of this standard MUST have all of the following functions:
pragmasolidity^0.8.4;interfaceIERC_5521{/// 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 essentailly `address(this)`.
functionsetNode(uint256tokenId,address[]memoryaddresses,uint256[][]memorytokenIds)external;/// @notice set the referring list of an rNFT associated with different contract addresses.
/// @param `tokenId` of rNFT being set, `addresses` of the contracts in which rNFTs with `_tokenIds` being referred accordingly.
functionsetNodeReferring(address[]memoryaddresses,uint256tokenId,uint256[][]memory_tokenIds)private;/// @notice set the referred list of an rNFT associated with different contract addresses.
/// @param `_tokenIds` of rNFTs, associated with `addresses`, referred by the rNFT with `tokenId` in `this` contract.
functionsetNodeReferred(address[]memoryaddresses,uint256tokenId,uint256[][]memory_tokenIds)private;/// @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);}interfaceTargetContract{/// @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);}
Rationale
This standard is intended to establish the referable DAG for queries on cross-relationship and accordingly provide the simplest functions. It provides advantages as follows.
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 From March 26th 2023, this standard has been stepping forward by enabling cross-contract references, giving a scalable adoption for the broader public with stronger interoperability.
Backwards Compatibility
This standard can be fully ERC-721 compatible by adding an extension function set.
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;uint256createdTimestamp;// unix timestamp when the rNFT is being created
}mapping(uint256=>Relationship)internal_relationship;addresscontractOwner=address(0);mapping(uint256=>address[])privatereferringKeys;mapping(uint256=>address[])privatereferredKeys;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){referringKeys[tokenId].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++){if(_relationship[_tokenIds[i][j]].referred[addresses[i]].length==0){referredKeys[_tokenIds[i][j]].push(addresses[i]);}// Add the address if it's a new entry
Relationshipstoragerelationship=_relationship[_tokenIds[i][j]];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]);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++){if(_relationship[_tokenIds[i]].referred[_address].length==0){referredKeys[_tokenIds[i]].push(_address);}// Add the address if it's a new entry
Relationshipstoragerelationship=_relationship[_tokenIds[i]];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);(_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);(_referredKeys,_referredValues)=targetContractInstance.referredOf(_address,tokenId);}return(_referredKeys,_referredValues);}/// @dev See {IERC165-supportsInterface}.
functionsupportsInterface(bytes4interfaceId)publicviewvirtualoverridereturns(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=referringKeys[tokenId];returnValues=newuint256[][](returnKeys.length);for(uinti=0;i<returnKeys.length;i++){returnValues[i]=relationship.referring[returnKeys[i]];}}else{returnKeys=referredKeys[tokenId];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 to the aggreement when the NFT was being created regardless of the change of ownership unless specified in the agreement.
Referring a token will not refer its descendants by default. In the case that only a specific child token gets referred, it means the privity of 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 constructured 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 token supply. It is important to recognize that these risks are not considered design flaws but intentional trade-offs, which balances 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.