ERC-6059 Parent-governed nestable NFT (Parent-Governed Nestable Non-Fungible Tokens)

Final · NFTs & multi-token · created 2022-11-15

A standard that allows an NFT to own other NFTs. Anyone can propose a child token, but the owner of the parent token decides whether to accept it.

Why it is needed

Under the existing NFT standard (ERC-721), the owner of a token must be a regular account (EOA, Externally Owned Account) or a smart contract. As a result, there was no standardized way to bundle NFTs from several collections and sell them at once, to organize NFTs into groups by one's own criteria, or to attach bonus NFTs directly to a membership NFT. Also, if any token could be forcibly attached, there is a risk of being flooded with spam or malicious tokens. This standard defines a parent-child relationship in which an NFT owns NFTs, and a child token first enters a pending list and becomes a proper child only when the root owner of the parent token accepts it. This makes uses such as bundle sales, collection organization, membership rewards and vote delegation possible in a safe way.

What to implement

In depth

Ownership structure: root owner and direct owner

ownerOf is the same function as in ERC-721, but if the token is owned by another NFT, it must recursively follow parents until an address that is not an NFT is reached and return the root owner. directOwnerOf returns the nearest owner as three values: the owner address, the parent token ID and a flag indicating whether the owner is an NFT. If the direct owner is not an NFT, the token ID must be 0. Internally it is stored as a DirectOwner struct (tokenId, ownerAddress), and if the owner is another token, ownerAddress must be the address of the parent token's collection contract. For this reason a token ID must never be 0. 0 is used as the marker that the owner or destination is not an NFT.

Propose-commit pattern

Adding a child token must follow the propose-accept pattern. A child token first enters the parent's pending array and becomes a proper child only when the root owner of the parent token moves it to the active array. The pending array SHOULD be limited to 128 slots to prevent spam and griefing. Minting malicious or spam tokens is cheap and easy, so limiting the pending array keeps legitimate tokens from being buried under spam. The pending array is bounded without pagination because it is not a storage buffer but a candidate list that must be managed by accepting and rejecting regularly. The active array has no limit. Only the root owner can accept, so no one can force an unwanted child onto another owner.

Parent Governed pattern and permission rules

The true owner of a nested token is the parent NFT and its root owner. The root owner or an approved party MUST be able to execute acceptChild, rejectAllChildren and transferChild on a child. On the other hand, transferFrom, safeTransferFrom, nestTransferFrom and burn can be executed by the root owner or an approved party only when the token is not owned by an NFT. If the token is owned by an NFT, only the parent NFT can execute these operations, and transfers must go through the parent's transferChild. transferChild is recommended to call the child contract's nestTransferFrom or safeTransferFrom depending on whether the destination is an NFT. This restriction exists to prevent inconsistent state in the parent contract, because only transferChild handles removing the child from the parent. To burn a nested token, it must first be taken out to an EOA and then burned.

The 5 state transitions possible with transferChild

A single transferChild handles 5 state transitions of a child token. 1) Reject: removes a child from the pending array. 2) Abandon: removes the child from the array without updating the owner (when to is the 0x0 address). 3) Unnest: takes the child out of the parent. 4) Transfer to an EOA or ERC721Receiver. 5) Transfer to a new parent token: in this case the child enters the new parent's pending array and must be accepted by the new parent's root owner before it moves to the active array. The isPending parameter specifies whether the child is in the pending array or the active array.

Index-based access and race condition prevention

The reason for using an index (childIndex) instead of a token ID when accepting or transferring a child is to save gas. Looking up by token ID requires iterating the array, so the cost is proportional to the array size, while an index has a fixed cost. However, to prevent race conditions where the index changes, the expected child's collection address (childAddress) and token ID (childId) are passed together to verify that the token at the index position is the expected one. The maxRejections of rejectAllChildren serves the same purpose: if a new token slips into the pending array while the call is being prepared, the transaction is reverted. An implementation that manages indexes internally with a mapping was also tried, but it raised the cost of accepting a child by more than 20% and the cost of minting by more than 15%, so it was not included in the standard.

Recursive burn and the ban on circular nesting

burn(tokenId, maxRecursiveBurns) recursively burns child tokens as well when burning a token. If there are more children to burn than maxRecursiveBurns, execution MUST revert, and if the value is 0 only the specified token is attempted to be burned, and it must revert if there is even one child. Also, nesting a token into its own child or any descendant of that child is prohibited. If it were allowed, an ownership loop would form and no token in the loop could be managed. The reason there is no separate safe nested transfer function is that nestTransfer must always check the destination's IERC6059 compatibility, so it is always safe.

ERC-721 compatibility and interface identifier

This standard extends ERC-721 and is designed to be compatible with ERC-721 so that existing ERC-721 tooling and infrastructure can be used as is. Backwards compatibility is provided only for ERC-721 to keep the interface simple. Nesting is not confined to a single contract and can be performed between completely separate NFT collections. The ERC-165 interface identifier is 0x42b0e56f. This proposal is a standard finalized with Final status.

Things to watch out for

Original ERC · ← ERCs