From 1394272b8dafa75f18c19ab40d8f21c9a138d18d Mon Sep 17 00:00:00 2001 From: OniReimu Date: Wed, 23 Aug 2023 11:18:48 +1000 Subject: [PATCH] Update EIP-5521: Test cases added, about to move to review Merged by EIP-Bot. --- EIPS/eip-5521.md | 74 ++-------- assets/eip-5521/ERC_5521.sol | 170 +++++++++++++++++++++ assets/eip-5521/ERC_5521.test.js | 244 +++++++++++++++++++++++++++++++ assets/eip-5521/IERC_5521.sol | 35 +++++ 4 files changed, 458 insertions(+), 65 deletions(-) create mode 100644 assets/eip-5521/ERC_5521.sol create mode 100644 assets/eip-5521/ERC_5521.test.js create mode 100644 assets/eip-5521/IERC_5521.sol diff --git a/EIPS/eip-5521.md b/EIPS/eip-5521.md index d5b7be9dc166a..4bf16cae95435 100644 --- a/EIPS/eip-5521.md +++ b/EIPS/eip-5521.md @@ -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 , Shange Fu , Shiping Chen , Sherry Xu , Jiangshan Yu +author: Saber Yu (@OniReimu), Qin Wang , Shange Fu , Yilin Sai , Shiping Chen , Sherry Xu , Jiangshan Yu 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). diff --git a/assets/eip-5521/ERC_5521.sol b/assets/eip-5521/ERC_5521.sol new file mode 100644 index 0000000000000..aee6a3aee5084 --- /dev/null +++ b/assets/eip-5521/ERC_5521.sol @@ -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); + } +} \ No newline at end of file diff --git a/assets/eip-5521/ERC_5521.test.js b/assets/eip-5521/ERC_5521.test.js new file mode 100644 index 0000000000000..43c5f4083e22f --- /dev/null +++ b/assets/eip-5521/ERC_5521.test.js @@ -0,0 +1,244 @@ +// Right click on the script name and hit "Run" to execute +const { expect } = require("chai"); +const { ethers } = require("hardhat"); + +const TOKEN_NAME = "ERC_5521_NAME"; +const TOKEN_SYMBOL = "ERC_5521_SYMBOL"; +const TOKEN_NAME1 = "ERC_5521_NAME1"; +const TOKEN_SYMBOL1 = "ERC_5521_SYMBOL1"; +const TOKEN_NAME2 = "ERC_5521_NAME2"; +const TOKEN_SYMBOL2 = "ERC_5521_SYMBOL2"; + +function tokenIds2Number(tokenIds) { + return tokenIds.map(tIds => tIds.map(tId => tId.toNumber())); +} + +function assertRelationship(rel, tokenAddresses, tokenIds) { + expect(rel[0]).to.deep.equal(tokenAddresses); + expect(tokenIds2Number(rel[1])).to.deep.equal(tokenIds); +} + +describe("ERC_5521 - single token contract scenario", function () { + let tokenContract1; + + beforeEach(async () => { + const RNFT = await ethers.getContractFactory("ERC_5521"); + const rNFT = await RNFT.deploy(TOKEN_NAME,TOKEN_SYMBOL); + await rNFT.deployed(); + console.log('ERC_5521 deployed at:'+ rNFT.address); + tokenContract1 = rNFT; + }); + + it("should report correct token name and symbol", async function () { + expect((await tokenContract1.symbol())).to.equal(TOKEN_SYMBOL); + expect((await tokenContract1.name())).to.equal(TOKEN_NAME); + }); + + it("can mint a token with empty referredOf and referringOf", async function () { + await tokenContract1.safeMint(1, [], []); + assertRelationship(await tokenContract1.referredOf(tokenContract1.address, 1), [], []); + assertRelationship(await tokenContract1.referringOf(tokenContract1.address, 1), [], []); + }) + + it("cannot query relationships of a non-existent token", async function () { + const mintToken1Tx = await tokenContract1.safeMint(1, [], []); + // mint tx of token 1 must be mined before it can be referred to + await mintToken1Tx.wait(); + // wait 1 sec to ensure that token 2 is minted at a later block timestamp (block timestamp is in second) + await new Promise(r => setTimeout(r, 1000)); + await tokenContract1.safeMint(2, [tokenContract1.address], [[1]]); + + // tokenContract1 didn't mint any token with id 3 + await expect(tokenContract1.referringOf(tokenContract1.address, 3)).to.be.revertedWith("token ID not existed"); + await expect(tokenContract1.referredOf(tokenContract1.address, 3)).to.be.revertedWith("token ID not existed"); + }) + + it("must not mint two tokens with the same token id", async function () { + await tokenContract1.safeMint(1, [], []); + await expect(tokenContract1.safeMint(1, [], [])).to.be.revertedWith("ERC721: token already minted"); + }) + + it("can mint a token referring to another minted token", async function () { + const mintToken1Tx = await tokenContract1.safeMint(1, [], []); + // mint tx of token 1 must be mined before it can be referred to + await mintToken1Tx.wait(); + // wait 1 sec to ensure that token 2 is minted at a later block timestamp (block timestamp is in second) + await new Promise(r => setTimeout(r, 1000)); + await tokenContract1.safeMint(2, [tokenContract1.address], [[1]]); + + const referringOfT2 = await tokenContract1.referringOf(tokenContract1.address, 2) + assertRelationship(referringOfT2, [tokenContract1.address], [[1]]); + + const referredOfT2 = await tokenContract1.referredOf(tokenContract1.address, 2) + assertRelationship(referredOfT2, [], []); + + const referringOfT1 = await tokenContract1.referringOf(tokenContract1.address, 1) + assertRelationship(referringOfT1, [], []); + + const referredOfT1 = await tokenContract1.referredOf(tokenContract1.address, 1) + assertRelationship(referredOfT1, [tokenContract1.address], [[2]]); + }) + + it("cannot mint a token referring to a token that is not yet minted", async function () { + await expect(tokenContract1.safeMint(2, [tokenContract1.address], [[1]])).to.be.revertedWith("invalid token ID"); + }) + + it("can mint 3 tokens forming a simple DAG", async function () { + const mintToken1Tx = await tokenContract1.safeMint(1, [], []); + // mint tx of token 1 must be mined before it can be referred to + await mintToken1Tx.wait(); + // wait 1 sec to ensure that token 2 is minted at a later block timestamp (block timestamp is in second) + await new Promise(r => setTimeout(r, 1000)); + const mintToken2Tx = await tokenContract1.safeMint(2, [tokenContract1.address], [[1]]); + await mintToken2Tx.wait(); + await new Promise(r => setTimeout(r, 1000)); + const mintToken3Tx = await tokenContract1.safeMint(3, [tokenContract1.address], [[1, 2]]); + await mintToken3Tx.wait(); + + const referringOfT2 = await tokenContract1.referringOf(tokenContract1.address, 2) + assertRelationship(referringOfT2, [tokenContract1.address], [[1]]); + + const referredOfT2 = await tokenContract1.referredOf(tokenContract1.address, 2) + assertRelationship(referredOfT2, [tokenContract1.address], [[3]]); + + const referringOfT1 = await tokenContract1.referringOf(tokenContract1.address, 1) + assertRelationship(referringOfT1, [], []); + + const referredOfT1 = await tokenContract1.referredOf(tokenContract1.address, 1) + assertRelationship(referredOfT1, [tokenContract1.address], [[2, 3]]); + + const referringOfT3 = await tokenContract1.referringOf(tokenContract1.address, 3) + assertRelationship(referringOfT3, [tokenContract1.address], [[1, 2]]); + + const referredOfT3 = await tokenContract1.referredOf(tokenContract1.address, 3) + assertRelationship(referredOfT3, [], []); + }) + + it("should revert when trying to create a cycle in the relationship DAG", async function () { + const mintToken1Tx = await tokenContract1.safeMint(1, [], []); + // mint tx of token 1 must be mined before it can be referred to + await mintToken1Tx.wait(); + // wait 1 sec to ensure that token 2 is minted at a later block timestamp (block timestamp is in second) + await new Promise(r => setTimeout(r, 1000)); + await tokenContract1.safeMint(2, [tokenContract1.address], [[1]]); + await expect(tokenContract1.safeMint(1, [tokenContract1.address], [[2]])).to.be.reverted; + }) + + it("should revert when attempting to create an invalid relationship", async function () { + const mintToken1Tx = await tokenContract1.safeMint(1, [], []); + // mint tx of token 1 must be mined before it can be referred to + await mintToken1Tx.wait(); + // wait 1 sec to ensure that token 2 is minted at a later block timestamp (block timestamp is in second) + await new Promise(r => setTimeout(r, 1000)); + // Intentionally creating an invalid relationship + await expect(tokenContract1.safeMint(2, [tokenContract1.address], [[1, 2, 3]])).to.be.revertedWith("ERC_5521: self-reference not allowed"); + await expect(tokenContract1.safeMint(2, [tokenContract1.address], [[1, 3]])).to.be.revertedWith("invalid token ID"); + await expect(tokenContract1.safeMint(2, [tokenContract1.address], [])).to.be.revertedWith("Addresses and TokenID arrays must have the same length"); + await expect(tokenContract1.safeMint(2, [tokenContract1.address], [[]])).to.be.revertedWith("the referring list cannot be empty"); + }); +}); + +describe("ERC_5521 - multi token contracts scenario", function () { + let tokenContract1; + let tokenContract2; + + beforeEach(async () => { + const RNFT = await ethers.getContractFactory("ERC_5521"); + + const rNFT1 = await RNFT.deploy(TOKEN_NAME1,TOKEN_SYMBOL1); + await rNFT1.deployed(); + console.log('ERC_5521 deployed at:'+ rNFT1.address); + tokenContract1 = rNFT1; + + const rNFT2 = await RNFT.deploy(TOKEN_NAME2,TOKEN_SYMBOL2); + await rNFT2.deployed(); + console.log('ERC_5521 deployed at:'+ rNFT2.address); + tokenContract2 = rNFT2; + }); + + it("should revert when referring and referred lists have mismatched lengths", async function () { + await expect(tokenContract1.safeMint(1, [tokenContract1.address], [[1], [2]])).to.be.reverted; + }); + + it("can mint a token referring to another minted token", async function () { + const mintToken1Tx = await tokenContract1.safeMint(1, [], []); + // mint tx of token 1 must be mined before it can be referred to + await mintToken1Tx.wait(); + // wait 1 sec to ensure that token 2 is minted at a later block timestamp (block timestamp is in second) + await new Promise(r => setTimeout(r, 1000)); + await tokenContract2.safeMint(2, [tokenContract1.address], [[1]]); + + // relationships of token 2 can be queried using any ERC5521 contract, not necessarily the contract that minted token 2 + const referringOfT2QueriedByC1 = await tokenContract1.referringOf(tokenContract2.address, 2) + const referringOfT2QueriedByByC2 = await tokenContract2.referringOf(tokenContract2.address, 2) + assertRelationship(referringOfT2QueriedByC1, [tokenContract1.address], [[1]]); + assertRelationship(referringOfT2QueriedByByC2, [tokenContract1.address], [[1]]); + + const referredOfT2QueriedByC1 = await tokenContract1.referredOf(tokenContract2.address, 2) + const referredOfT2QueriedByC2 = await tokenContract2.referredOf(tokenContract2.address, 2) + assertRelationship(referredOfT2QueriedByC1, [], []); + assertRelationship(referredOfT2QueriedByC2, [], []); + + const referringOfT1QueriedByC1 = await tokenContract1.referringOf(tokenContract1.address, 1) + const referringOfT1QueriedByC2 = await tokenContract2.referringOf(tokenContract1.address, 1) + assertRelationship(referringOfT1QueriedByC1, [], []); + assertRelationship(referringOfT1QueriedByC2, [], []); + + const referredOfT1QueriedByC1 = await tokenContract1.referredOf(tokenContract1.address, 1) + const referredOfT1QueriedByC2 = await tokenContract2.referredOf(tokenContract1.address, 1) + assertRelationship(referredOfT1QueriedByC1, [tokenContract2.address], [[2]]); + assertRelationship(referredOfT1QueriedByC2, [tokenContract2.address], [[2]]); + }) + + it("cannot query relationships of a non-existent token", async function () { + const mintToken1Tx = await tokenContract1.safeMint(1, [], []); + // mint tx of token 1 must be mined before it can be referred to + await mintToken1Tx.wait(); + // wait 1 sec to ensure that token 2 is minted at a later block timestamp (block timestamp is in second) + await new Promise(r => setTimeout(r, 1000)); + await tokenContract2.safeMint(2, [tokenContract1.address], [[1]]); + + // tokenContract1 didn't mint any token with id 2 + await expect(tokenContract1.referringOf(tokenContract1.address, 2)).to.be.revertedWith("token ID not existed"); + await expect(tokenContract1.referredOf(tokenContract1.address, 2)).to.be.revertedWith("token ID not existed"); + }) + + it("cannot mint a token referring to a token that is not yet minted", async function () { + await expect(tokenContract2.safeMint(2, [tokenContract1.address], [[1]])).to.be.revertedWith("invalid token ID"); + }) + + it("can mint 3 tokens forming a simple DAG", async function () { + const mintToken1Tx = await tokenContract1.safeMint(1, [], []); + // mint tx of token 1 must be mined before it can be referred to + await mintToken1Tx.wait(); + // wait 1 sec to ensure that token 2 is minted at a later block timestamp (block timestamp is in second) + await new Promise(r => setTimeout(r, 1000)); + const mintToken2Tx = await tokenContract2.safeMint(2, [tokenContract1.address], [[1]]); + await mintToken2Tx.wait(); + await new Promise(r => setTimeout(r, 1000)); + const mintToken3Tx = await tokenContract2.safeMint(3, [tokenContract1.address, tokenContract2.address], [[1], [2]]); + await mintToken3Tx.wait(); + + const referringOfT2 = await tokenContract1.referringOf(tokenContract2.address, 2) + assertRelationship(referringOfT2, [tokenContract1.address], [[1]]); + + const referredOfT2 = await tokenContract1.referredOf(tokenContract2.address, 2) + assertRelationship(referredOfT2, [tokenContract2.address], [[3]]); + + const referringOfT1 = await tokenContract1.referringOf(tokenContract1.address, 1) + assertRelationship(referringOfT1, [], []); + + const referredOfT1 = await tokenContract1.referredOf(tokenContract1.address, 1) + assertRelationship(referredOfT1, [tokenContract2.address], [[2, 3]]); + + const referringOfT3 = await tokenContract1.referringOf(tokenContract2.address, 3) + assertRelationship(referringOfT3, [tokenContract1.address, tokenContract2.address], [[1], [2]]); + + const referringOfT3fromContract2 = await tokenContract2.referringOf(tokenContract2.address, 3) + assertRelationship(referringOfT3fromContract2, [tokenContract1.address, tokenContract2.address], [[1], [2]]); + + const referredOfT3 = await tokenContract1.referredOf(tokenContract2.address, 3) + assertRelationship(referredOfT3, [], []); + }) + +}); \ No newline at end of file diff --git a/assets/eip-5521/IERC_5521.sol b/assets/eip-5521/IERC_5521.sol new file mode 100644 index 0000000000000..ae9c3fbb86811 --- /dev/null +++ b/assets/eip-5521/IERC_5521.sol @@ -0,0 +1,35 @@ +// 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); +}