Skip to content

Commit 5955dbb

Browse files
authored
Merge 5070713 into dc7a45f
2 parents dc7a45f + 5070713 commit 5955dbb

File tree

7 files changed

+508
-0
lines changed

7 files changed

+508
-0
lines changed

EIPS/eip-7510.md

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
---
2+
eip: 7510
3+
title: Cross-Contract Hierarchical NFT
4+
description: An extension of ERC-721 to maintain hierarchical relationship between tokens from different contracts.
5+
author: Ming Jiang (@minkyn), Zheng Han (@hanbsd), Fan Yang (@fayang)
6+
discussions-to: https://ethereum-magicians.org/t/eip-7510-cross-contract-hierarchical-nft/15687
7+
status: Draft
8+
type: Standards Track
9+
category: ERC
10+
created: 2023-08-24
11+
requires: 721
12+
---
13+
14+
## Abstract
15+
16+
This standard is an extension of [ERC-721](./eip-721.md). 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.
17+
18+
## Motivation
19+
20+
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.
21+
22+
Existing [ERC-6150](./eip-6150.md) 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.
23+
24+
## Specification
25+
26+
Solidity interface available at [IERC7510.sol](../assets/eip-7510/contracts/IERC7510.sol):
27+
28+
```solidity
29+
/// @notice The struct used to reference a token in an NFT contract
30+
struct Token {
31+
address collection;
32+
uint256 id;
33+
}
34+
35+
interface IERC7510 {
36+
37+
/// @notice Emitted when the parent tokens for an NFT is updated
38+
event UpdateParentTokens(uint256 indexed tokenId);
39+
40+
/// @notice Get the parent tokens of an NFT
41+
/// @param tokenId The NFT to get the parent tokens for
42+
/// @return An array of parent tokens for this NFT
43+
function parentTokensOf(uint256 tokenId) external view returns (Token[] memory);
44+
45+
/// @notice Check if another token is a parent of an NFT
46+
/// @param tokenId The NFT to check its parent for
47+
/// @param otherToken Another token to check as a parent or not
48+
/// @return Whether `otherToken` is a parent of `tokenId`
49+
function isParentToken(uint256 tokenId, Token memory otherToken) external view returns (bool);
50+
51+
}
52+
```
53+
54+
## Rationale
55+
56+
This standard differs from [ERC-6150](./eip-6150.md) in mainly two aspects: supporting cross-contract token reference, and allowing multiple parents. But we try to keep the naming consistent overall.
57+
58+
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.
59+
60+
## Backwards Compatibility
61+
62+
No backwards compatibility issues found.
63+
64+
## Test Cases
65+
66+
Test cases available at: [ERC7510.test.ts](../assets/eip-7510/test/ERC7510.test.ts):
67+
68+
```typescript
69+
import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers";
70+
import { expect } from "chai";
71+
import { ethers } from "hardhat";
72+
73+
const NAME = "NAME";
74+
const SYMBOL = "SYMBOL";
75+
const TOKEN_ID = 1234;
76+
77+
const PARENT_1_COLLECTION = "0xDEAdBEEf00000000000000000123456789ABCdeF";
78+
const PARENT_1_ID = 8888;
79+
const PARENT_1_TOKEN = { collection: PARENT_1_COLLECTION, id: PARENT_1_ID };
80+
81+
const PARENT_2_COLLECTION = "0xBaDc0ffEe0000000000000000123456789aBCDef";
82+
const PARENT_2_ID = 9999;
83+
const PARENT_2_TOKEN = { collection: PARENT_2_COLLECTION, id: PARENT_2_ID };
84+
85+
describe("ERC7510", function () {
86+
87+
async function deployContractFixture() {
88+
const [deployer, owner] = await ethers.getSigners();
89+
90+
const contract = await ethers.deployContract("ERC7510", [NAME, SYMBOL], deployer);
91+
await contract.mint(owner, TOKEN_ID);
92+
93+
return { contract, owner };
94+
}
95+
96+
describe("Functions", function () {
97+
it("Should not set parent tokens if not owner or approved", async function () {
98+
const { contract } = await loadFixture(deployContractFixture);
99+
100+
await expect(contract.setParentTokens(TOKEN_ID, [PARENT_1_TOKEN]))
101+
.to.be.revertedWith("ERC7510: caller is not owner or approved");
102+
});
103+
104+
it("Should correctly query token without parents", async function () {
105+
const { contract } = await loadFixture(deployContractFixture);
106+
107+
expect(await contract.parentTokensOf(TOKEN_ID)).to.have.lengthOf(0);
108+
109+
expect(await contract.isParentToken(TOKEN_ID, PARENT_1_TOKEN)).to.equal(false);
110+
});
111+
112+
it("Should set parent tokens and then update", async function () {
113+
const { contract, owner } = await loadFixture(deployContractFixture);
114+
115+
await contract.connect(owner).setParentTokens(TOKEN_ID, [PARENT_1_TOKEN]);
116+
117+
let parentTokens = await contract.parentTokensOf(TOKEN_ID);
118+
expect(parentTokens).to.have.lengthOf(1);
119+
expect(parentTokens[0].collection).to.equal(PARENT_1_COLLECTION);
120+
expect(parentTokens[0].id).to.equal(PARENT_1_ID);
121+
122+
expect(await contract.isParentToken(TOKEN_ID, PARENT_1_TOKEN)).to.equal(true);
123+
expect(await contract.isParentToken(TOKEN_ID, PARENT_2_TOKEN)).to.equal(false);
124+
125+
await contract.connect(owner).setParentTokens(TOKEN_ID, [PARENT_2_TOKEN]);
126+
127+
parentTokens = await contract.parentTokensOf(TOKEN_ID);
128+
expect(parentTokens).to.have.lengthOf(1);
129+
expect(parentTokens[0].collection).to.equal(PARENT_2_COLLECTION);
130+
expect(parentTokens[0].id).to.equal(PARENT_2_ID);
131+
132+
expect(await contract.isParentToken(TOKEN_ID, PARENT_1_TOKEN)).to.equal(false);
133+
expect(await contract.isParentToken(TOKEN_ID, PARENT_2_TOKEN)).to.equal(true);
134+
});
135+
136+
it("Should burn and clear parent tokens", async function () {
137+
const { contract, owner } = await loadFixture(deployContractFixture);
138+
139+
await contract.connect(owner).setParentTokens(TOKEN_ID, [PARENT_1_TOKEN, PARENT_2_TOKEN]);
140+
await contract.burn(TOKEN_ID);
141+
142+
await expect(contract.parentTokensOf(TOKEN_ID)).to.be.revertedWith("ERC7510: query for nonexistent token");
143+
await expect(contract.isParentToken(TOKEN_ID, PARENT_1_TOKEN)).to.be.revertedWith("ERC7510: query for nonexistent token");
144+
await expect(contract.isParentToken(TOKEN_ID, PARENT_2_TOKEN)).to.be.revertedWith("ERC7510: query for nonexistent token");
145+
146+
await contract.mint(owner, TOKEN_ID);
147+
148+
expect(await contract.parentTokensOf(TOKEN_ID)).to.have.lengthOf(0);
149+
expect(await contract.isParentToken(TOKEN_ID, PARENT_1_TOKEN)).to.equal(false);
150+
expect(await contract.isParentToken(TOKEN_ID, PARENT_2_TOKEN)).to.equal(false);
151+
});
152+
});
153+
154+
describe("Events", function () {
155+
it("Should emit event when set parent tokens", async function () {
156+
const { contract, owner } = await loadFixture(deployContractFixture);
157+
158+
await expect(contract.connect(owner).setParentTokens(TOKEN_ID, [PARENT_1_TOKEN, PARENT_2_TOKEN]))
159+
.to.emit(contract, "UpdateParentTokens").withArgs(TOKEN_ID);
160+
});
161+
});
162+
163+
});
164+
```
165+
166+
## Reference Implementation
167+
168+
Reference implementation available at: [ERC7510.sol](../assets/eip-7510/contracts/ERC7510.sol):
169+
170+
```solidity
171+
// SPDX-License-Identifier: CC0-1.0
172+
173+
pragma solidity ^0.8.0;
174+
175+
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
176+
177+
import "./IERC7510.sol";
178+
179+
contract ERC7510 is ERC721, IERC7510 {
180+
181+
mapping(uint256 => Token[]) private _parentTokens;
182+
mapping(uint256 => mapping(address => mapping(uint256 => bool))) private _isParentToken;
183+
184+
constructor(
185+
string memory name, string memory symbol
186+
) ERC721(name, symbol) {}
187+
188+
function supportsInterface(
189+
bytes4 interfaceId
190+
) public view virtual override returns (bool) {
191+
return interfaceId == type(IERC7510).interfaceId || super.supportsInterface(interfaceId);
192+
}
193+
194+
function parentTokensOf(
195+
uint256 tokenId
196+
) public view virtual override returns (Token[] memory) {
197+
require(_exists(tokenId), "ERC7510: query for nonexistent token");
198+
return _parentTokens[tokenId];
199+
}
200+
201+
function isParentToken(
202+
uint256 tokenId,
203+
Token memory otherToken
204+
) public view virtual override returns (bool) {
205+
require(_exists(tokenId), "ERC7510: query for nonexistent token");
206+
return _isParentToken[tokenId][otherToken.collection][otherToken.id];
207+
}
208+
209+
function setParentTokens(
210+
uint256 tokenId, Token[] memory parentTokens
211+
) public virtual {
212+
require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC7510: caller is not owner or approved");
213+
_clear(tokenId);
214+
for (uint256 i = 0; i < parentTokens.length; i++) {
215+
_parentTokens[tokenId].push(parentTokens[i]);
216+
_isParentToken[tokenId][parentTokens[i].collection][parentTokens[i].id] = true;
217+
}
218+
emit UpdateParentTokens(tokenId);
219+
}
220+
221+
function _burn(
222+
uint256 tokenId
223+
) internal virtual override {
224+
super._burn(tokenId);
225+
_clear(tokenId);
226+
}
227+
228+
function _clear(
229+
uint256 tokenId
230+
) private {
231+
Token[] storage parentTokens = _parentTokens[tokenId];
232+
for (uint256 i = 0; i < parentTokens.length; i++) {
233+
delete _isParentToken[tokenId][parentTokens[i].collection][parentTokens[i].id];
234+
}
235+
delete _parentTokens[tokenId];
236+
}
237+
238+
}
239+
```
240+
241+
## Security Considerations
242+
243+
No security considerations found.
244+
245+
## Copyright
246+
247+
Copyright and related rights waived via [CC0](../LICENSE.md).
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// SPDX-License-Identifier: CC0-1.0
2+
3+
pragma solidity ^0.8.0;
4+
5+
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
6+
7+
import "./IERC7510.sol";
8+
9+
contract ERC7510 is ERC721, IERC7510 {
10+
11+
mapping(uint256 => Token[]) private _parentTokens;
12+
mapping(uint256 => mapping(address => mapping(uint256 => bool))) private _isParentToken;
13+
14+
constructor(
15+
string memory name, string memory symbol
16+
) ERC721(name, symbol) {}
17+
18+
function supportsInterface(
19+
bytes4 interfaceId
20+
) public view virtual override returns (bool) {
21+
return interfaceId == type(IERC7510).interfaceId || super.supportsInterface(interfaceId);
22+
}
23+
24+
function parentTokensOf(
25+
uint256 tokenId
26+
) public view virtual override returns (Token[] memory) {
27+
require(_exists(tokenId), "ERC7510: query for nonexistent token");
28+
return _parentTokens[tokenId];
29+
}
30+
31+
function isParentToken(
32+
uint256 tokenId,
33+
Token memory otherToken
34+
) public view virtual override returns (bool) {
35+
require(_exists(tokenId), "ERC7510: query for nonexistent token");
36+
return _isParentToken[tokenId][otherToken.collection][otherToken.id];
37+
}
38+
39+
function setParentTokens(
40+
uint256 tokenId, Token[] memory parentTokens
41+
) public virtual {
42+
require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC7510: caller is not owner or approved");
43+
_clear(tokenId);
44+
for (uint256 i = 0; i < parentTokens.length; i++) {
45+
_parentTokens[tokenId].push(parentTokens[i]);
46+
_isParentToken[tokenId][parentTokens[i].collection][parentTokens[i].id] = true;
47+
}
48+
emit UpdateParentTokens(tokenId);
49+
}
50+
51+
function _burn(
52+
uint256 tokenId
53+
) internal virtual override {
54+
super._burn(tokenId);
55+
_clear(tokenId);
56+
}
57+
58+
function _clear(
59+
uint256 tokenId
60+
) private {
61+
Token[] storage parentTokens = _parentTokens[tokenId];
62+
for (uint256 i = 0; i < parentTokens.length; i++) {
63+
delete _isParentToken[tokenId][parentTokens[i].collection][parentTokens[i].id];
64+
}
65+
delete _parentTokens[tokenId];
66+
}
67+
68+
// For test only
69+
function mint(
70+
address to, uint256 tokenId
71+
) public virtual {
72+
_mint(to, tokenId);
73+
}
74+
75+
// For test only
76+
function burn(
77+
uint256 tokenId
78+
) public virtual {
79+
_burn(tokenId);
80+
}
81+
82+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// SPDX-License-Identifier: CC0-1.0
2+
3+
pragma solidity ^0.8.0;
4+
5+
/// @notice The struct used to reference a token in an NFT contract
6+
struct Token {
7+
address collection;
8+
uint256 id;
9+
}
10+
11+
interface IERC7510 {
12+
13+
/// @notice Emitted when the parent tokens for an NFT is updated
14+
event UpdateParentTokens(uint256 indexed tokenId);
15+
16+
/// @notice Get the parent tokens of an NFT
17+
/// @param tokenId The NFT to get the parent tokens for
18+
/// @return An array of parent tokens for this NFT
19+
function parentTokensOf(uint256 tokenId) external view returns (Token[] memory);
20+
21+
/// @notice Check if another token is a parent of an NFT
22+
/// @param tokenId The NFT to check its parent for
23+
/// @param otherToken Another token to check as a parent or not
24+
/// @return Whether `otherToken` is a parent of `tokenId`
25+
function isParentToken(uint256 tokenId, Token memory otherToken) external view returns (bool);
26+
27+
}

assets/eip-7510/hardhat.config.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { HardhatUserConfig } from "hardhat/config";
2+
import "@nomicfoundation/hardhat-toolbox";
3+
4+
const config: HardhatUserConfig = {
5+
solidity: {
6+
version: "0.8.21",
7+
settings: {
8+
optimizer: {
9+
enabled: true,
10+
runs: 200,
11+
},
12+
},
13+
},
14+
};
15+
16+
export default config;

0 commit comments

Comments
 (0)