-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Update EIP-5521: Test cases added, about to move to review
Merged by EIP-Bot.
- Loading branch information
Showing
4 changed files
with
458 additions
and
65 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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"; | ||
|
@@ -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); | ||
|
@@ -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"); | ||
|
@@ -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]); | ||
} | ||
} | ||
} | ||
|
@@ -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); | ||
|
@@ -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). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.