306 lines
16 KiB
Markdown
306 lines
16 KiB
Markdown
|
---
|
||
|
eip: 5521
|
||
|
title: Referable NFT
|
||
|
description: An ERC-721 extension to construct reference relationships among NFTs
|
||
|
author: Saber Yu (@OniReimu), Qin Wang <qin.wang@data61.csiro.au>, Shange Fu <shange.fu@monash.edu>, Shiping Chen <shiping.chen@data61.csiro.au>, Sherry Xu <xiwei.xu@data61.csiro.au>, Jiangshan Yu <jiangshan.yu@monash.edu>
|
||
|
discussions-to: https://ethereum-magicians.org/t/eip-x-erc-721-referable-nft/10310
|
||
|
status: Draft
|
||
|
type: Standards Track
|
||
|
category: ERC
|
||
|
created: 2022-08-10
|
||
|
requires: 165, 721
|
||
|
---
|
||
|
|
||
|
## Abstract
|
||
|
|
||
|
This standard is an extension of [ERC-721](./eip-721.md). It proposes two referrable 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 attributes such as `mapping (uint256 => address) privityOfAgreement` recording the ownerships of referred NFTs at the time the rNFTs were being created.
|
||
|
`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.
|
||
|
|
||
|
`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;
|
||
|
`setNodeReferredExternal`: set the referred list of the given rNFTs sourced from other contracts;
|
||
|
`referringOf`: Get the referring list of an rNFT;
|
||
|
`referredOf`: Get the referred list of an rNFT.
|
||
|
|
||
|
## 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 collaborating with the Top-down composible NFT (cf. [ERC-998](./eip-998.md) to build a finer-grained reference relationship, where the `Relationship` structure and the interface `IERC_rNFT` can be seamlessly stored and updated when invoking the `mint` function). Another example is that the rNFT can be applied to rentable scenarios (cf. [ERC-5006](./eip-5006.md) 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](./eip-721.md) compatible by adding an extension function set.
|
||
|
|
||
|
## Test Cases
|
||
|
|
||
|
Truffle and Openzeppelin are required to run the following in a test network.
|
||
|
|
||
|
```node
|
||
|
|
||
|
truffle develop
|
||
|
rNFT = await ERC_rNFT.new("ERC_5521","ERC_5521")
|
||
|
rNFT.safeMint(1,[],[])
|
||
|
rNFT.referredOf(1)
|
||
|
rNFT.referringOf(1)
|
||
|
|
||
|
rNFT.safeMint(2,[rNFT.address],[1])
|
||
|
rNFT.referredOf(2)
|
||
|
rNFT.referringOf(2)
|
||
|
|
||
|
rNFT.safeMint(3,[rNFT.address],[1,2])
|
||
|
rNFT.referredOf(2)
|
||
|
rNFT.referredOf(3)
|
||
|
rNFT.referringOf(3)
|
||
|
|
||
|
```
|
||
|
|
||
|
## Reference Implementation
|
||
|
|
||
|
```solidity
|
||
|
|
||
|
// SPDX-License-Identifier: MIT
|
||
|
pragma solidity ^0.8.4;
|
||
|
|
||
|
interface IERC_5521 {
|
||
|
|
||
|
/// 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
|
||
|
/// @param tokenIds array of rNFTs, recommended to check duplication at the caller's end
|
||
|
function setNode(uint256 tokenId, address[] memory addresses, uint256[][] memory tokenIds) external;
|
||
|
|
||
|
/// @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 returns(address[] memory, uint256[][] memory);
|
||
|
|
||
|
/// @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 returns(address[] memory, uint256[][] memory);
|
||
|
}
|
||
|
|
||
|
interface TargetContract {
|
||
|
function setNodeReferredExternal(address successor, 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);
|
||
|
}
|
||
|
|
||
|
```
|
||
|
|
||
|
```solidity
|
||
|
|
||
|
// SPDX-License-Identifier: MIT
|
||
|
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;
|
||
|
uint256 createdTimestamp; // unix timestamp when the rNFT is being created
|
||
|
}
|
||
|
|
||
|
mapping (uint256 => Relationship) internal _relationship;
|
||
|
address contractOwner = address(0);
|
||
|
|
||
|
mapping (uint256 => address[]) private referringKeys;
|
||
|
mapping (uint256 => address[]) private referredKeys;
|
||
|
|
||
|
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 (contractOwner != msg.sender && 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) { 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
|
||
|
function setNodeReferred(address[] memory addresses, uint256 tokenId, uint256[][] memory _tokenIds) private {
|
||
|
for (uint i = 0; i < addresses.length; i++) {
|
||
|
if (_relationship[tokenId].referred[addresses[i]].length == 0) { referredKeys[tokenId].push(addresses[i]); } // Add the address if it's a new entry
|
||
|
if (addresses[i] == address(this)) {
|
||
|
for (uint j = 0; j < _tokenIds[i].length; j++) {
|
||
|
Relationship storage relationship = _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 {
|
||
|
TargetContract targetContractInstance = TargetContract(addresses[i]);
|
||
|
targetContractInstance.setNodeReferredExternal(addresses[i], 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]];
|
||
|
|
||
|
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);
|
||
|
(_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);
|
||
|
(_referredKeys, _referredValues) = targetContractInstance.referredOf(_address, tokenId);
|
||
|
}
|
||
|
return (_referredKeys, _referredValues);
|
||
|
}
|
||
|
|
||
|
/// @dev See {IERC165-supportsInterface}.
|
||
|
function supportsInterface(bytes4 interfaceId) public view virtual override 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 = referringKeys[tokenId];
|
||
|
returnValues = new uint256[][](returnKeys.length);
|
||
|
for (uint i = 0; i < returnKeys.length; i++) {
|
||
|
returnValues[i] = relationship.referring[returnKeys[i]];
|
||
|
}
|
||
|
} else {
|
||
|
returnKeys = referredKeys[tokenId];
|
||
|
returnValues = new uint256[][](returnKeys.length);
|
||
|
for (uint i = 0; i < returnKeys.length; i++) {
|
||
|
returnValues[i] = relationship.referred[returnKeys[i]];
|
||
|
}
|
||
|
}
|
||
|
return (returnKeys, returnValues);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
```
|
||
|
|
||
|
## Security Considerations
|
||
|
|
||
|
The `createdTimestamp` only covers the block-level timestamp (based on block headers), which does not support fine-grained comparisons such as transaction-level.
|
||
|
|
||
|
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.
|
||
|
|
||
|
In the context of collaborating with [ERC-998](./eip-998.md), 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.
|
||
|
|
||
|
## Copyright
|
||
|
|
||
|
Copyright and related rights waived via [CC0](../LICENSE.md).
|