ERC-7401 Parent-governed NFT nesting standard (Parent-Governed Non-Fungible Tokens Nesting)

Final · NFTs & multi-token · created 2023-07-26

Standardizes a parent-child nesting relationship, where an NFT owns other NFTs, as an ERC-721 extension. Anyone can propose a child token, but only the owner of the parent token can accept and manage it.

Why it is needed

In ERC-721, the owner of a token can only be an externally owned account (EOA) or a smart contract, so a token cannot own a token. As a result, there was no standard way to implement uses such as bundles that combine NFTs from multiple collections and sell them in a single transaction, collections that gather related tokens under a specific NFT, minting a bonus NFT directly into a membership token for its holder, and delegation by placing a voting NFT inside another member's NFT. ERC-6059, which addressed the same problem earlier, had a defect: as the proposal was refined, one parameter was dropped from the specification but remained in the interface ID, so the interface ID and the actual specification did not match. This standard defines a framework for parent-child relationships between NFTs and is a functionally equivalent replacement for ERC-6059 that corrects that mismatch.

What to implement

In depth

Root owner and direct owner

Unlike ERC-721, ownerOf recursively looks up parents while the owner is an NFT, until it reaches a non-NFT address, and returns the root owner. directOwnerOf returns the immediate owner as three values: the owner address, the parent token ID, and a boolean indicating whether the owner is an NFT. If the direct owner is not an NFT, the token ID must be 0. Internally, a DirectOwner struct (tokenId, ownerAddress) holds this information, and if the owner is another token, ownerAddress must be the address of the parent token's collection contract. Because a 0 value is used as the marker for "not an NFT", a token ID can never be 0.

Propose-commit pattern

Adding a child token always follows a propose-accept pattern. A new child first enters the pending array, and the root owner of the parent token must move it to the active array. The pending array SHOULD be limited to 128 slots to prevent spam and harassment. addChild must not be called directly. It must only be called on an IERC7059 instance as part of nestTransfer or transferChild, and the child contract's directOwnerOf must point to the calling contract. In addition to the index, acceptChild takes the expected contract address and token ID, which prevents accepting the wrong token in a race condition where indexes shift. maxRejections in rejectAllChildren works on the same principle: if children that arrived while the call was being prepared would also be rejected, the transaction is reverted.

Parent-driven permission model

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. 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. When the token is owned by an NFT, only the parent NFT can execute these operations, and a transfer must go through the parent token's transferChild, which calls the child contract's nestTransferFrom or safeTransferFrom depending on the destination. Only transferChild handles removing the child from the parent, so this restriction prevents inconsistent state in the parent contract. To burn a nested token, it must first be moved out to an EOA and then burned.

Five state transitions of transferChild

A single transferChild handles five state transitions: rejecting a child token, abandoning it, unnesting it, transferring it to an EOA or an ERC721Receiver, and transferring it to a new parent token. The parameter combination distinguishes them. destinationId must be 0 if the destination is not a token, and isPending indicates whether the child is in the pending array (true) or the active array (false). If to is the 0x0 address, the child token's ownership information is not updated. When a child is sent to a new parent token, it enters the new parent's pending array, so the new parent's root owner must accept it again before it is placed in the active array.

Recursive burn and maxRecursiveBurns

burn(tokenId, maxRecursiveBurns) burns a token and recursively burns its child tokens as well. If there are more children to burn than the specified maximum, execution MUST revert. If maxRecursiveBurns is set to 0, only that token should be burned (SHOULD), and if it has any child at all, execution MUST revert. The return value is the number of child tokens burned recursively. Without this mechanism, burning a deeply nested token could destroy more tokens than intended, so the caller is given explicit control over the burn scope.

Using indexes and forbidding circular nesting

Indexes are used instead of token IDs when accepting or rejecting a child because of gas cost. Looking up by token ID requires iterating the array, so the cost grows with array size, while an index has a fixed cost. An implementation that tracks 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 left out of the standard and kept as an extension. Also, nesting a parent token inside its own child or any descendant is forbidden. If allowed, an ownership loop would form and no token in the loop could be managed. There is no separate safe nested-transfer function because nestTransfer must always check the destination's IERC7059 compatibility, which makes it safe on its own.

Superseding ERC-6059 and ERC-721 compatibility

This standard supersedes ERC-6059. As that proposal evolved, one parameter was dropped from the interface specification but remained in the interface ID, so the two did not match. This standard is a functionally equivalent proposal that fixes only that. The ERC-165 identifier of this interface is 0x42b0e56f. The only incompatibility with ERC-721 is that token ID 0 cannot be used, and ownerOf differs in behavior by returning the root owner. When a token is owned by an EOA or an ERC-721 Receiver, ownerOf behaves the same as in ERC-721. This proposal is a standard finalized in Final status.

Things to watch out for

Original ERC · ← ERCs