Skip to content

Commit

Permalink
Update EIP-5521: Test cases added, about to move to review
Browse files Browse the repository at this point in the history
Merged by EIP-Bot.
  • Loading branch information
OniReimu authored Aug 23, 2023
1 parent 9042d5a commit 1394272
Show file tree
Hide file tree
Showing 4 changed files with 458 additions and 65 deletions.
74 changes: 9 additions & 65 deletions EIPS/eip-5521.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
eip: 5521
title: Referable NFT
description: An ERC-721 extension to construct reference relationships among NFTs
author: Saber Yu (@OniReimu), Qin Wang <[email protected]>, Shange Fu <[email protected]>, Shiping Chen <[email protected]>, Sherry Xu <[email protected]>, Jiangshan Yu <[email protected]>
author: Saber Yu (@OniReimu), Qin Wang <[email protected]>, Shange Fu <[email protected]>, Yilin Sai <[email protected]>, Shiping Chen <[email protected]>, Sherry Xu <[email protected]>, Jiangshan Yu <[email protected]>
discussions-to: https://ethereum-magicians.org/t/eip-x-erc-721-referable-nft/10310
status: Draft
type: Standards Track
Expand Down Expand Up @@ -56,73 +56,14 @@ This standard can be fully [ERC-721](./eip-721.md) compatible by adding an exten

## 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, rNFT.address_2], [[1,2], [2,4,5]])
rNFT.referredOf(2)
rNFT.referredOf(3)
rNFT.referringOf(3)

```
Test cases are included in [ERC_5521.test.js](../assets/eip-5521/ERC_5521.test.js)

## 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";
Expand Down Expand Up @@ -160,7 +101,7 @@ contract ERC_5521 is ERC721, IERC_5521, TargetContract {
"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"); }
if (tokenIds[i].length == 0) { revert("ERC_5521: the referring list cannot be empty"); }
}
setNodeReferring(addresses, tokenId, tokenIds);
setNodeReferred(addresses, tokenId, tokenIds);
Expand All @@ -186,9 +127,9 @@ contract ERC_5521 is ERC721, IERC_5521, TargetContract {
/// @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++) {
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
Relationship storage relationship = _relationship[_tokenIds[i][j]];
require(tokenId != _tokenIds[i][j], "ERC_5521: self-reference not allowed");
Expand All @@ -199,7 +140,7 @@ contract ERC_5521 is ERC721, IERC_5521, TargetContract {
}
} else {
TargetContract targetContractInstance = TargetContract(addresses[i]);
targetContractInstance.setNodeReferredExternal(addresses[i], tokenId, _tokenIds[i]);
targetContractInstance.setNodeReferredExternal(address(this), tokenId, _tokenIds[i]);
}
}
}
Expand All @@ -208,9 +149,10 @@ contract ERC_5521 is ERC721, IERC_5521, TargetContract {
/// @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++) {
if (_relationship[_tokenIds[i]].referred[_address].length == 0) { referredKeys[_tokenIds[i]].push(_address); } // Add the address if it's a new entry
Relationship storage relationship = _relationship[_tokenIds[i]];
require(_address == address(this), "ERC_5521: this must be an external contract address");
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);
Expand Down Expand Up @@ -301,6 +243,8 @@ The change of ownership has nothing to do with the reference relationship. Norma

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.

The `safeMint` function has been deliberately designed to allow unrestricted minting and relationship setting, akin to the open referencing system seen in platforms like Google Scholar. This decision facilitates strong flexibility, enabling any user to create and define relationships between tokens 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, balancing 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.

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).
170 changes: 170 additions & 0 deletions assets/eip-5521/ERC_5521.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// 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 (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 (addresses[i] == address(this)) {
for (uint j = 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
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(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++) {
if (_relationship[_tokenIds[i]].referred[_address].length == 0) { referredKeys[_tokenIds[i]].push(_address); } // Add the address if it's a new entry
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);
}
}
Loading

0 comments on commit 1394272

Please sign in to comment.