This standard is an extension of ERC-721. It proposes a way to maintain hierarchical relationship between tokens from different contracts. This standard provides an interface to query the parent tokens of an NFT or whether the parent relation exists between two NFTs.
Motivation
Some NFTs want to generate derivative assets as new NFTs. For example, a 2D NFT image would like to publish its 3D model as a new derivative NFT. An NFT may also be derived from multiple parent NFTs. Such cases include a movie NFT featuring multiple characters from other NFTs. This standard is proposed to record such hierarchical relationship between derivative NFTs.
Existing ERC-6150 introduces a similar feature, but it only builds hierarchy between tokens within the same contract. More than often we need to create a new NFT collection with the derivative tokens, which requires cross-contract relationship establishment. In addition, deriving from multiple parents is very common in the scenario of IP licensing, but the existing standard doesn’t support that either.
/// @notice The struct used to reference a token in an NFT contract
structToken{addresscollection;uint256id;}interfaceIERC7510{/// @notice Emitted when the parent tokens for an NFT is updated
eventUpdateParentTokens(uint256indexedtokenId);/// @notice Get the parent tokens of an NFT
/// @param tokenId The NFT to get the parent tokens for
/// @return An array of parent tokens for this NFT
functionparentTokensOf(uint256tokenId)externalviewreturns(Token[]memory);/// @notice Check if another token is a parent of an NFT
/// @param tokenId The NFT to check its parent for
/// @param otherToken Another token to check as a parent or not
/// @return Whether `otherToken` is a parent of `tokenId`
functionisParentToken(uint256tokenId,TokenmemoryotherToken)externalviewreturns(bool);/// @notice Set the parent tokens for an NFT
/// @param tokenId The NFT to set the parent tokens for
/// @param parentTokens The parent tokens to set
functionsetParentTokens(uint256tokenId,Token[]memoryparentTokens)external;}
Rationale
This standard differs from ERC-6150 in mainly two aspects: supporting cross-contract token reference, and allowing multiple parents. But we try to keep the naming consistent overall.
In addition, we didn’t include child relation in the interface. An original NFT exists before its derivative NFTs. Therefore we know what parent tokens to include when minting derivative NFTs, but we wouldn’t know the children tokens when minting the original NFT. If we have to record the children, that means whenever we mint a derivative NFT, we need to call on its original NFT to add it as a child. However, those two NFTs may belong to different contracts and thus require different write permissions, making it impossible to combine the two operations into a single transaction in practice. As a result, we decide to only record the parent relation from the derivative NFTs.
import{loadFixture}from"@nomicfoundation/hardhat-toolbox/network-helpers";import{expect}from"chai";import{ethers}from"hardhat";constNAME="NAME";constSYMBOL="SYMBOL";constTOKEN_ID=1234;constPARENT_1_COLLECTION="0xDEAdBEEf00000000000000000123456789ABCdeF";constPARENT_1_ID=8888;constPARENT_1_TOKEN={collection:PARENT_1_COLLECTION,id:PARENT_1_ID};constPARENT_2_COLLECTION="0xBaDc0ffEe0000000000000000123456789aBCDef";constPARENT_2_ID=9999;constPARENT_2_TOKEN={collection:PARENT_2_COLLECTION,id:PARENT_2_ID};describe("ERC7510",function(){asyncfunctiondeployContractFixture(){const[deployer,owner]=awaitethers.getSigners();constcontract=awaitethers.deployContract("ERC7510",[NAME,SYMBOL],deployer);awaitcontract.mint(owner,TOKEN_ID);return{contract,owner};}describe("Functions",function(){it("Should not set parent tokens if not owner or approved",asyncfunction(){const{contract}=awaitloadFixture(deployContractFixture);awaitexpect(contract.setParentTokens(TOKEN_ID,[PARENT_1_TOKEN])).to.be.revertedWith("ERC7510: caller is not owner or approved");});it("Should correctly query token without parents",asyncfunction(){const{contract}=awaitloadFixture(deployContractFixture);expect(awaitcontract.parentTokensOf(TOKEN_ID)).to.have.lengthOf(0);expect(awaitcontract.isParentToken(TOKEN_ID,PARENT_1_TOKEN)).to.equal(false);});it("Should set parent tokens and then update",asyncfunction(){const{contract,owner}=awaitloadFixture(deployContractFixture);awaitcontract.connect(owner).setParentTokens(TOKEN_ID,[PARENT_1_TOKEN]);letparentTokens=awaitcontract.parentTokensOf(TOKEN_ID);expect(parentTokens).to.have.lengthOf(1);expect(parentTokens[0].collection).to.equal(PARENT_1_COLLECTION);expect(parentTokens[0].id).to.equal(PARENT_1_ID);expect(awaitcontract.isParentToken(TOKEN_ID,PARENT_1_TOKEN)).to.equal(true);expect(awaitcontract.isParentToken(TOKEN_ID,PARENT_2_TOKEN)).to.equal(false);awaitcontract.connect(owner).setParentTokens(TOKEN_ID,[PARENT_2_TOKEN]);parentTokens=awaitcontract.parentTokensOf(TOKEN_ID);expect(parentTokens).to.have.lengthOf(1);expect(parentTokens[0].collection).to.equal(PARENT_2_COLLECTION);expect(parentTokens[0].id).to.equal(PARENT_2_ID);expect(awaitcontract.isParentToken(TOKEN_ID,PARENT_1_TOKEN)).to.equal(false);expect(awaitcontract.isParentToken(TOKEN_ID,PARENT_2_TOKEN)).to.equal(true);});it("Should burn and clear parent tokens",asyncfunction(){const{contract,owner}=awaitloadFixture(deployContractFixture);awaitcontract.connect(owner).setParentTokens(TOKEN_ID,[PARENT_1_TOKEN,PARENT_2_TOKEN]);awaitcontract.burn(TOKEN_ID);awaitexpect(contract.parentTokensOf(TOKEN_ID)).to.be.revertedWith("ERC7510: query for nonexistent token");awaitexpect(contract.isParentToken(TOKEN_ID,PARENT_1_TOKEN)).to.be.revertedWith("ERC7510: query for nonexistent token");awaitexpect(contract.isParentToken(TOKEN_ID,PARENT_2_TOKEN)).to.be.revertedWith("ERC7510: query for nonexistent token");awaitcontract.mint(owner,TOKEN_ID);expect(awaitcontract.parentTokensOf(TOKEN_ID)).to.have.lengthOf(0);expect(awaitcontract.isParentToken(TOKEN_ID,PARENT_1_TOKEN)).to.equal(false);expect(awaitcontract.isParentToken(TOKEN_ID,PARENT_2_TOKEN)).to.equal(false);});});describe("Events",function(){it("Should emit event when set parent tokens",asyncfunction(){const{contract,owner}=awaitloadFixture(deployContractFixture);awaitexpect(contract.connect(owner).setParentTokens(TOKEN_ID,[PARENT_1_TOKEN,PARENT_2_TOKEN])).to.emit(contract,"UpdateParentTokens").withArgs(TOKEN_ID);});});});
Reference Implementation
Reference implementation available at: ERC7510.sol:
// SPDX-License-Identifier: CC0-1.0
pragmasolidity^0.8.0;import"@openzeppelin/contracts/token/ERC721/ERC721.sol";import"./IERC7510.sol";contractERC7510isERC721,IERC7510{mapping(uint256=>Token[])private_parentTokens;mapping(uint256=>mapping(address=>mapping(uint256=>bool)))private_isParentToken;constructor(stringmemoryname,stringmemorysymbol)ERC721(name,symbol){}functionsupportsInterface(bytes4interfaceId)publicviewvirtualoverridereturns(bool){returninterfaceId==type(IERC7510).interfaceId||super.supportsInterface(interfaceId);}functionparentTokensOf(uint256tokenId)publicviewvirtualoverridereturns(Token[]memory){require(_exists(tokenId),"ERC7510: query for nonexistent token");return_parentTokens[tokenId];}functionisParentToken(uint256tokenId,TokenmemoryotherToken)publicviewvirtualoverridereturns(bool){require(_exists(tokenId),"ERC7510: query for nonexistent token");return_isParentToken[tokenId][otherToken.collection][otherToken.id];}functionsetParentTokens(uint256tokenId,Token[]memoryparentTokens)publicvirtualoverride{require(_isApprovedOrOwner(_msgSender(),tokenId),"ERC7510: caller is not owner or approved");_clear(tokenId);for(uint256i=0;i<parentTokens.length;i++){_parentTokens[tokenId].push(parentTokens[i]);_isParentToken[tokenId][parentTokens[i].collection][parentTokens[i].id]=true;}emitUpdateParentTokens(tokenId);}function_burn(uint256tokenId)internalvirtualoverride{super._burn(tokenId);_clear(tokenId);}function_clear(uint256tokenId)private{Token[]storageparentTokens=_parentTokens[tokenId];for(uint256i=0;i<parentTokens.length;i++){delete_isParentToken[tokenId][parentTokens[i].collection][parentTokens[i].id];}delete_parentTokens[tokenId];}}
Security Considerations
Parent tokens of an NFT may point to invalid data for two reasons. First, parent tokens could be burned later. Second, a contract implementing setParentTokens might not check the validity of parentTokens arguments. For security consideration, applications that retrieve parent tokens of an NFT need to verify they exist as valid tokens.
Ming Jiang (@minkyn), Zheng Han (@hanbsd), Fan Yang (@fayang), "ERC-7510: Cross-Contract Hierarchical NFT [DRAFT]," Ethereum Improvement Proposals, no. 7510, August 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7510.