ERC-6059: Parent-Governed Nestable NFTs
ERC-6059 lets NFTs own other NFTs with the parent governing transfer semantics of children. Used for:
- Game inventories — character NFT contains weapon NFTs that move with the character.
- Composable assets — house NFT contains room NFTs that contain furniture NFTs.
- Bundle NFTs — single token represents a bundle of constituent tokens transferable as a group.
- DAO-owned NFTs — DAO NFT owns governance NFTs of its members.
The "parent-governed" part: children can be added to a parent (with parent's accept), but children cannot leave without parent approval (or owner approval if the parent is held by a regular EOA). This makes nesting a strong-ownership relationship rather than a weak-reference one.
Required Interface (abridged)
interface IERC6059 {
struct Child { uint256 tokenId; address contractAddress; }
event ChildProposed(uint256 indexed tokenId, uint256 indexed childIndex,
address indexed childAddress, uint256 childId);
event ChildAccepted(uint256 indexed tokenId, uint256 indexed childIndex,
address indexed childAddress, uint256 childId);
event ChildTransferred(uint256 indexed tokenId, uint256 indexed childIndex,
address indexed childAddress, uint256 childId, bool fromPending);
function nestTransferFrom(address from, address to, uint256 tokenId,
uint256 destinationId, bytes calldata data) external;
function addChild(uint256 parentId, uint256 childId) external;
function acceptChild(uint256 parentId, uint256 childIndex,
address childAddress, uint256 childId) external;
function rejectAllChildren(uint256 parentId, uint256 maxRejections) external;
function transferChild(uint256 tokenId, address to, uint256 destinationId,
uint256 childIndex, address childAddress, uint256 childId,
bool isPending, bytes calldata data) external;
function childrenOf(uint256 parentId) external view returns (Child[] memory);
function pendingChildrenOf(uint256 parentId) external view returns (Child[] memory);
function directOwnerOf(uint256 tokenId) external view returns (address, uint256, bool);
}The directOwnerOf returns either an EOA owner (isNft = false) or a parent NFT (isNft = true); to find the root EOA owner, walk up the parent chain.
Neo Equivalent: NEP-11 + Parent-Child Storage with Accept Flow
public static void NestTransferFrom(UInt160 from, UInt160 destinationContract,
ByteString tokenId, ByteString destinationId)
{
if (!Runtime.CheckWitness(from)) throw new Exception("NEP11:NoAuth");
// Move the NFT to the destination contract; record parent linkage.
SetParent(tokenId, destinationContract, destinationId);
Contract.Call(destinationContract, "addChild", CallFlags.All,
new object[] { destinationId, Runtime.ExecutingScriptHash, tokenId });
}
public static void AddChild(ByteString parentId, UInt160 childContract, ByteString childId)
{
// Called by the child's contract on transfer-into-parent.
var pending = GetPendingChildren(parentId);
pending.Add(new Map<string, object>
{
["contract"] = childContract,
["tokenId"] = childId,
});
SetPendingChildren(parentId, pending);
OnChildProposed(parentId, pending.Count - 1, childContract, childId);
}
public static void AcceptChild(ByteString parentId, BigInteger pendingIndex,
UInt160 childContract, ByteString childId)
{
if (!Runtime.CheckWitness(OwnerOf(parentId))) throw new Exception("NEP11:NotOwner");
var pending = GetPendingChildren(parentId);
var children = GetChildren(parentId);
children.Add(pending[(int)pendingIndex]);
pending.RemoveAt((int)pendingIndex);
SetChildren(parentId, children);
SetPendingChildren(parentId, pending);
OnChildAccepted(parentId, children.Count - 1, childContract, childId);
}| ERC-6059 (Ethereum) | Neo Equivalent | Notes |
|---|---|---|
nestTransferFrom(...) | NestTransferFrom(...) triggers the cross-contract child registration | Atomic transfer + addChild |
acceptChild(...) / rejectAllChildren(...) | AcceptChild(...) / RejectAllChildren(...) parent-owner-checked | Same flow |
directOwnerOf(tokenId) | DirectOwnerOf(tokenId) returning (owner, parentTokenId, isNft) | Direct port |
childrenOf / pendingChildrenOf | ChildrenOf / PendingChildrenOf views | |
| Walk up parent chain for root owner | Same recursion or off-chain helper | NeoVM iterators |
Composition
- ERC-5773 — multi-asset. Often paired: a child can be one of many assets on a parent.
- ERC-6220 — composable equippable parts (not yet mirrored). Builds the equip-slot layer on top of nesting.
- ERC-7066 — lockable. Locks on the parent propagate to children (children can't be transferred out while parent is locked).
- ERC-2981 — royalties. Distinct royalty splits per parent and child contracts.
Migration Notes
For Solidity collections using RMRK / EIP-6059:
- Store children as serialised arrays per parent tokenId.
- Implement
NestTransferFromas a two-step (transfer + addChild) atomic operation — abort the transfer if the child contract refuses. - The
directOwnerOfrecursion can be expensive; consider caching the root-owner per tokenId for hot lookups. - Lock semantics — when a parent is locked (per ERC-7066), block
transferChilduntil unlock.
The on-chain footprint per nest-deep tree is roughly proportional to the children count; deep nesting (>10 levels) is expensive to recursively resolve owner — design accordingly.
