// SPDX-License-Identifier: CC0-1.0 //Generally all interactions should propagate downstream pragma solidity ^0.8.16; import "./IERC6059.sol"; import "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; import "@openzeppelin/contracts/utils/Address.sol"; import "@openzeppelin/contracts/utils/Context.sol"; import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; error ChildAlreadyExists(); error ChildIndexOutOfRange(); error ERC721AddressZeroIsNotaValidOwner(); error ERC721ApprovalToCurrentOwner(); error ERC721ApproveCallerIsNotOwnerNorApprovedForAll(); error ERC721ApproveToCaller(); error ERC721InvalidTokenId(); error ERC721MintToTheZeroAddress(); error ERC721NotApprovedOrOwner(); error ERC721TokenAlreadyMinted(); error ERC721TransferFromIncorrectOwner(); error ERC721TransferToNonReceiverImplementer(); error ERC721TransferToTheZeroAddress(); error IdZeroForbidden(); error IsNotContract(); error MaxPendingChildrenReached(); error MaxRecursiveBurnsReached(address childContract, uint256 childId); error MintToNonNestableImplementer(); error NestableTooDeep(); error NestableTransferToDescendant(); error NestableTransferToNonNestableImplementer(); error NestableTransferToSelf(); error NotApprovedOrDirectOwner(); error PendingChildIndexOutOfRange(); error UnexpectedChildId(); error UnexpectedNumberOfChildren(); /** * @title NestableToken * @author RMRK team * @notice Smart contract of the Nestable module. * @dev This contract is hierarchy agnostic and can support an arbitrary number of nested levels up and down, as long as * gas limits allow it. */ contract NestableToken is Context, IERC165, IERC721, IERC6059 { using Address for address; uint256 private constant _MAX_LEVELS_TO_CHECK_FOR_INHERITANCE_LOOP = 100; // Mapping owner address to token count mapping(address => uint256) private _balances; // Mapping from token ID to approver address to approved address // The approver is necessary so approvals are invalidated for nested children on transfer // WARNING: If a child NFT returns to a previous root owner, old permissions would be active again mapping(uint256 => mapping(address => address)) private _tokenApprovals; // Mapping from owner to operator approvals mapping(address => mapping(address => bool)) private _operatorApprovals; // ------------------- NESTABLE -------------- // Mapping from token ID to DirectOwner struct mapping(uint256 => DirectOwner) private _directOwners; // Mapping of tokenId to array of active children structs mapping(uint256 => Child[]) private _activeChildren; // Mapping of tokenId to array of pending children structs mapping(uint256 => Child[]) private _pendingChildren; // Mapping of child token address to child token ID to whether they are pending or active on any token // We might have a first extra mapping from token ID, but since the same child cannot be nested into multiple tokens // we can strip it for size/gas savings. mapping(address => mapping(uint256 => uint256)) private _childIsInActive; // -------------------------- MODIFIERS ---------------------------- /** * @notice Used to verify that the caller is either the owner of the token or approved to manage it by its owner. * @dev If the caller is not the owner of the token or approved to manage it by its owner, the execution will be * reverted. * @param tokenId ID of the token to check */ function _onlyApprovedOrOwner(uint256 tokenId) private view { if (!_isApprovedOrOwner(_msgSender(), tokenId)) revert ERC721NotApprovedOrOwner(); } /** * @notice Used to verify that the caller is either the owner of the token or approved to manage it by its owner. * @param tokenId ID of the token to check */ modifier onlyApprovedOrOwner(uint256 tokenId) { _onlyApprovedOrOwner(tokenId); _; } /** * @notice Used to verify that the caller is approved to manage the given token or it its direct owner. * @dev This does not delegate to ownerOf, which returns the root owner, but rater uses an owner from DirectOwner * struct. * @dev The execution is reverted if the caller is not immediate owner or approved to manage the given token. * @dev Used for parent-scoped transfers. * @param tokenId ID of the token to check. */ function _onlyApprovedOrDirectOwner(uint256 tokenId) private view { if (!_isApprovedOrDirectOwner(_msgSender(), tokenId)) revert NotApprovedOrDirectOwner(); } /** * @notice Used to verify that the caller is approved to manage the given token or is its direct owner. * @param tokenId ID of the token to check */ modifier onlyApprovedOrDirectOwner(uint256 tokenId) { _onlyApprovedOrDirectOwner(tokenId); _; } // ------------------------------- ERC721 --------------------------------- /** * @inheritdoc IERC165 */ function supportsInterface( bytes4 interfaceId ) public view virtual returns (bool) { return interfaceId == type(IERC165).interfaceId || interfaceId == type(IERC721).interfaceId || interfaceId == type(IERC721Metadata).interfaceId || interfaceId == type(IERC6059).interfaceId; } /** * @inheritdoc IERC721 */ function balanceOf(address owner) public view virtual returns (uint256) { if (owner == address(0)) revert ERC721AddressZeroIsNotaValidOwner(); return _balances[owner]; } //////////////////////////////////////// // TRANSFERS //////////////////////////////////////// /** * @inheritdoc IERC721 */ function transferFrom( address from, address to, uint256 tokenId ) public virtual onlyApprovedOrDirectOwner(tokenId) { _transfer(from, to, tokenId); } /** * @inheritdoc IERC721 */ function safeTransferFrom( address from, address to, uint256 tokenId ) public virtual { safeTransferFrom(from, to, tokenId, ""); } /** * @inheritdoc IERC721 */ function safeTransferFrom( address from, address to, uint256 tokenId, bytes memory data ) public virtual onlyApprovedOrDirectOwner(tokenId) { _safeTransfer(from, to, tokenId, data); } /** * @notice Used to transfer the token into another token. * @dev The destination token MUST NOT be a child token of the token being transferred or one of its downstream * child tokens. * @param from Address of the direct owner of the token to be transferred * @param to Address of the receiving token's collection smart contract * @param tokenId ID of the token being transferred * @param destinationId ID of the token to receive the token being transferred */ function nestTransferFrom( address from, address to, uint256 tokenId, uint256 destinationId, bytes memory data ) public virtual onlyApprovedOrDirectOwner(tokenId) { _nestTransfer(from, to, tokenId, destinationId, data); } /** * @notice Used to safely transfer the token form `from` to `to`. * @dev The function checks that contract recipients are aware of the ERC721 protocol to prevent tokens from being * forever locked. * @dev This internal function is equivalent to {safeTransferFrom}, and can be used to e.g. implement alternative * mechanisms to perform token transfer, such as signature-based. * @dev Requirements: * * - `from` cannot be the zero address. * - `to` cannot be the zero address. * - `tokenId` token must exist and be owned by `from`. * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. * @dev Emits a {Transfer} event. * @param from Address of the account currently owning the given token * @param to Address to transfer the token to * @param tokenId ID of the token to transfer * @param data Additional data with no specified format, sent in call to `to` */ function _safeTransfer( address from, address to, uint256 tokenId, bytes memory data ) internal virtual { _transfer(from, to, tokenId); if (!_checkOnERC721Received(from, to, tokenId, data)) revert ERC721TransferToNonReceiverImplementer(); } /** * @notice Used to transfer the token from `from` to `to`. * @dev As opposed to {transferFrom}, this imposes no restrictions on msg.sender. * @dev Requirements: * * - `to` cannot be the zero address. * - `tokenId` token must be owned by `from`. * @dev Emits a {Transfer} event. * @param from Address of the account currently owning the given token * @param to Address to transfer the token to * @param tokenId ID of the token to transfer */ function _transfer( address from, address to, uint256 tokenId ) internal virtual { (address immediateOwner, uint256 parentId, ) = directOwnerOf(tokenId); if (immediateOwner != from) revert ERC721TransferFromIncorrectOwner(); if (to == address(0)) revert ERC721TransferToTheZeroAddress(); _beforeTokenTransfer(from, to, tokenId); _beforeNestedTokenTransfer(immediateOwner, to, parentId, 0, tokenId); _balances[from] -= 1; _updateOwnerAndClearApprovals(tokenId, 0, to, false); _balances[to] += 1; emit Transfer(from, to, tokenId); emit NestTransfer(immediateOwner, to, parentId, 0, tokenId); _afterTokenTransfer(from, to, tokenId); _afterNestedTokenTransfer(immediateOwner, to, parentId, 0, tokenId); } /** * @notice Used to transfer a token into another token. * @dev Attempting to nest a token into `0x0` address will result in reverted transaction. * @dev Attempting to nest a token into itself will result in reverted transaction. * @param from Address of the account currently owning the given token * @param to Address of the receiving token's collection smart contract * @param tokenId ID of the token to transfer * @param destinationId ID of the token receiving the given token * @param data Additional data with no specified format, sent in the addChild call */ function _nestTransfer( address from, address to, uint256 tokenId, uint256 destinationId, bytes memory data ) internal virtual { (address immediateOwner, uint256 parentId, ) = directOwnerOf(tokenId); if (immediateOwner != from) revert ERC721TransferFromIncorrectOwner(); if (to == address(0)) revert ERC721TransferToTheZeroAddress(); if (to == address(this) && tokenId == destinationId) revert NestableTransferToSelf(); // Destination contract checks: // It seems redundant, but otherwise it would revert with no error if (!to.isContract()) revert IsNotContract(); if (!IERC165(to).supportsInterface(type(IERC6059).interfaceId)) revert NestableTransferToNonNestableImplementer(); _checkForInheritanceLoop(tokenId, to, destinationId); _beforeTokenTransfer(from, to, tokenId); _beforeNestedTokenTransfer( immediateOwner, to, parentId, destinationId, tokenId ); _balances[from] -= 1; _updateOwnerAndClearApprovals(tokenId, destinationId, to, true); _balances[to] += 1; // Sending to NFT: _sendToNFT(immediateOwner, to, parentId, destinationId, tokenId, data); } /** * @notice Used to send a token to another token. * @dev If the token being sent is currently owned by an externally owned account, the `parentId` should equal `0`. * @dev Emits {Transfer} event. * @dev Emits {NestTransfer} event. * @param from Address from which the token is being sent * @param to Address of the collection smart contract of the token to receive the given token * @param parentId ID of the current parent token of the token being sent * @param destinationId ID of the tokento receive the token being sent * @param tokenId ID of the token being sent * @param data Additional data with no specified format, sent in the addChild call */ function _sendToNFT( address from, address to, uint256 parentId, uint256 destinationId, uint256 tokenId, bytes memory data ) private { IERC6059 destContract = IERC6059(to); destContract.addChild(destinationId, tokenId, data); _afterTokenTransfer(from, to, tokenId); _afterNestedTokenTransfer(from, to, parentId, destinationId, tokenId); emit Transfer(from, to, tokenId); emit NestTransfer(from, to, parentId, destinationId, tokenId); } /** * @notice Used to check if nesting a given token into a specified token would create an inheritance loop. * @dev If a loop would occur, the tokens would be unmanageable, so the execution is reverted if one is detected. * @dev The check for inheritance loop is bounded to guard against too much gas being consumed. * @param currentId ID of the token that would be nested * @param targetContract Address of the collection smart contract of the token into which the given token would be * nested * @param targetId ID of the token into which the given token would be nested */ function _checkForInheritanceLoop( uint256 currentId, address targetContract, uint256 targetId ) private view { for (uint256 i; i < _MAX_LEVELS_TO_CHECK_FOR_INHERITANCE_LOOP; ) { ( address nextOwner, uint256 nextOwnerTokenId, bool isNft ) = IERC6059(targetContract).directOwnerOf(targetId); // If there's a final address, we're good. There's no loop. if (!isNft) { return; } // Ff the current nft is an ancestor at some point, there is an inheritance loop if (nextOwner == address(this) && nextOwnerTokenId == currentId) { revert NestableTransferToDescendant(); } // We reuse the parameters to save some contract size targetContract = nextOwner; targetId = nextOwnerTokenId; unchecked { ++i; } } revert NestableTooDeep(); } //////////////////////////////////////// // MINTING //////////////////////////////////////// /** * @notice Used to safely mint a token to a specified address. * @dev Requirements: * * - `tokenId` must not exist. * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. * @dev Emits a {Transfer} event. * @param to Address to which to safely mint the gven token * @param tokenId ID of the token to mint to the specified address */ function _safeMint(address to, uint256 tokenId) internal virtual { _safeMint(to, tokenId, ""); } /** * @notice Used to safely mint the token to the specified address while passing the additional data to contract * recipients. * @param to Address to which to mint the token * @param tokenId ID of the token to mint * @param data Additional data to send with the tokens */ function _safeMint( address to, uint256 tokenId, bytes memory data ) internal virtual { _mint(to, tokenId); if (!_checkOnERC721Received(address(0), to, tokenId, data)) revert ERC721TransferToNonReceiverImplementer(); } /** * @notice Used to mint a specified token to a given address. * @dev WARNING: Usage of this method is discouraged, use {_safeMint} whenever possible. * @dev Requirements: * * - `tokenId` must not exist. * - `to` cannot be the zero address. * @dev Emits a {Transfer} event. * @param to Address to mint the token to * @param tokenId ID of the token to mint */ function _mint(address to, uint256 tokenId) internal virtual { _innerMint(to, tokenId, 0); emit Transfer(address(0), to, tokenId); emit NestTransfer(address(0), to, 0, 0, tokenId); _afterTokenTransfer(address(0), to, tokenId); _afterNestedTokenTransfer(address(0), to, 0, 0, tokenId); } /** * @notice Used to mint a child token to a given parent token. * @param to Address of the collection smart contract of the token into which to mint the child token * @param tokenId ID of the token to mint * @param destinationId ID of the token into which to mint the new child token * @param data Additional data with no specified format, sent in the addChild call */ function _nestMint( address to, uint256 tokenId, uint256 destinationId, bytes memory data ) internal virtual { // It seems redundant, but otherwise it would revert with no error if (!to.isContract()) revert IsNotContract(); if (!IERC165(to).supportsInterface(type(IERC6059).interfaceId)) revert MintToNonNestableImplementer(); _innerMint(to, tokenId, destinationId); _sendToNFT(address(0), to, 0, destinationId, tokenId, data); } /** * @notice Used to mint a child token into a given parent token. * @dev Requirements: * * - `to` cannot be the zero address. * - `tokenId` must not exist. * - `tokenId` must not be `0`. * @param to Address of the collection smart contract of the token into which to mint the child token * @param tokenId ID of the token to mint * @param destinationId ID of the token into which to mint the new token */ function _innerMint( address to, uint256 tokenId, uint256 destinationId ) private { if (to == address(0)) revert ERC721MintToTheZeroAddress(); if (_exists(tokenId)) revert ERC721TokenAlreadyMinted(); if (tokenId == 0) revert IdZeroForbidden(); _beforeTokenTransfer(address(0), to, tokenId); _beforeNestedTokenTransfer(address(0), to, 0, destinationId, tokenId); _balances[to] += 1; _directOwners[tokenId] = DirectOwner({ ownerAddress: to, tokenId: destinationId, isNft: destinationId != 0 }); } //////////////////////////////////////// // Ownership //////////////////////////////////////// /** * @notice Used to retrieve the root owner of the given token. * @dev Root owner is always the externally owned account. * @dev If the given token is owned by another token, it will recursively query the parent tokens until reaching the * root owner. * @param tokenId ID of the token for which the root owner is being retrieved * @return address Address of the root owner of the given token */ function ownerOf( uint256 tokenId ) public view virtual override(IERC6059, IERC721) returns (address) { (address owner, uint256 ownerTokenId, bool isNft) = directOwnerOf( tokenId ); if (isNft) { owner = IERC6059(owner).ownerOf(ownerTokenId); } return owner; } /** * @notice Used to retrieve the immediate owner of the given token. * @dev In the event the NFT is owned by an externally owned account, `tokenId` will be `0` and `isNft` will be * `false`. * @param tokenId ID of the token for which the immediate owner is being retrieved * @return address Address of the immediate owner. If the token is owned by an externally owned account, its address * will be returned. If the token is owned by another token, the parent token's collection smart contract address * is returned * @return uint256 Token ID of the immediate owner. If the immediate owner is an externally owned account, the value * should be `0` * @return bool A boolean value signifying whether the immediate owner is a token (`true`) or not (`false`) */ function directOwnerOf( uint256 tokenId ) public view virtual returns (address, uint256, bool) { DirectOwner memory owner = _directOwners[tokenId]; if (owner.ownerAddress == address(0)) revert ERC721InvalidTokenId(); return (owner.ownerAddress, owner.tokenId, owner.isNft); } //////////////////////////////////////// // BURNING //////////////////////////////////////// /** * @notice Used to burn a given token. * @param tokenId ID of the token to burn */ function burn(uint256 tokenId) public virtual { burn(tokenId, 0); } /** * @notice Used to burn a token. * @dev When a token is burned, its children are recursively burned as well. * @dev The approvals are cleared when the token is burned. * @dev Requirements: * * - `tokenId` must exist. * @dev Emits a {Transfer} event. * @param tokenId ID of the token to burn * @param maxChildrenBurns Maximum children to recursively burn * @return uint256 The number of recursive burns it took to burn all of the children */ function burn( uint256 tokenId, uint256 maxChildrenBurns ) public virtual onlyApprovedOrDirectOwner(tokenId) returns (uint256) { return _burn(tokenId, maxChildrenBurns); } /** * @notice Used to burn a token. * @dev When a token is burned, its children are recursively burned as well. * @dev The approvals are cleared when the token is burned. * @dev Requirements: * * - `tokenId` must exist. * @dev Emits a {Transfer} event. * @dev Emits a {NestTransfer} event. * @param tokenId ID of the token to burn * @param maxChildrenBurns Maximum children to recursively burn * @return uint256 The number of recursive burns it took to burn all of the children */ function _burn( uint256 tokenId, uint256 maxChildrenBurns ) internal virtual returns (uint256) { (address immediateOwner, uint256 parentId, ) = directOwnerOf(tokenId); address owner = ownerOf(tokenId); _balances[immediateOwner] -= 1; _beforeTokenTransfer(owner, address(0), tokenId); _beforeNestedTokenTransfer( immediateOwner, address(0), parentId, 0, tokenId ); _approve(address(0), tokenId); _cleanApprovals(tokenId); Child[] memory children = childrenOf(tokenId); delete _activeChildren[tokenId]; delete _pendingChildren[tokenId]; delete _tokenApprovals[tokenId][owner]; uint256 pendingRecursiveBurns; uint256 totalChildBurns; uint256 length = children.length; //gas savings for (uint256 i; i < length; ) { if (totalChildBurns >= maxChildrenBurns) revert MaxRecursiveBurnsReached( children[i].contractAddress, children[i].tokenId ); delete _childIsInActive[children[i].contractAddress][ children[i].tokenId ]; unchecked { // At this point we know pendingRecursiveBurns must be at least 1 pendingRecursiveBurns = maxChildrenBurns - totalChildBurns; } // We substract one to the next level to count for the token being burned, then add it again on returns // This is to allow the behavior of 0 recursive burns meaning only the current token is deleted. totalChildBurns += IERC6059(children[i].contractAddress).burn( children[i].tokenId, pendingRecursiveBurns - 1 ) + 1; unchecked { ++i; } } // Can't remove before burning child since child will call back to get root owner delete _directOwners[tokenId]; _afterTokenTransfer(owner, address(0), tokenId); _afterNestedTokenTransfer( immediateOwner, address(0), parentId, 0, tokenId ); emit Transfer(owner, address(0), tokenId); emit NestTransfer(immediateOwner, address(0), parentId, 0, tokenId); return totalChildBurns; } //////////////////////////////////////// // APPROVALS //////////////////////////////////////// /** * @inheritdoc IERC721 */ function approve(address to, uint256 tokenId) public virtual { address owner = ownerOf(tokenId); if (to == owner) revert ERC721ApprovalToCurrentOwner(); if (_msgSender() != owner && !isApprovedForAll(owner, _msgSender())) revert ERC721ApproveCallerIsNotOwnerNorApprovedForAll(); _approve(to, tokenId); } /** * @inheritdoc IERC721 */ function getApproved( uint256 tokenId ) public view virtual returns (address) { _requireMinted(tokenId); return _tokenApprovals[tokenId][ownerOf(tokenId)]; } /** * @inheritdoc IERC721 */ function setApprovalForAll(address operator, bool approved) public virtual { if (_msgSender() == operator) revert ERC721ApproveToCaller(); _operatorApprovals[_msgSender()][operator] = approved; emit ApprovalForAll(_msgSender(), operator, approved); } /** * @inheritdoc IERC721 */ function isApprovedForAll( address owner, address operator ) public view virtual returns (bool) { return _operatorApprovals[owner][operator]; } /** * @notice Used to grant an approval to manage a given token. * @dev Emits an {Approval} event. * @param to Address to which the approval is being granted * @param tokenId ID of the token for which the approval is being granted */ function _approve(address to, uint256 tokenId) internal virtual { address owner = ownerOf(tokenId); _tokenApprovals[tokenId][owner] = to; emit Approval(owner, to, tokenId); } /** * @notice Used to update the owner of the token and clear the approvals associated with the previous owner. * @dev The `destinationId` should equal `0` if the new owner is an externally owned account. * @param tokenId ID of the token being updated * @param destinationId ID of the token to receive the given token * @param to Address of account to receive the token * @param isNft A boolean value signifying whether the new owner is a token (`true`) or externally owned account * (`false`) */ function _updateOwnerAndClearApprovals( uint256 tokenId, uint256 destinationId, address to, bool isNft ) internal { _directOwners[tokenId] = DirectOwner({ ownerAddress: to, tokenId: destinationId, isNft: isNft }); // Clear approvals from the previous owner _approve(address(0), tokenId); _cleanApprovals(tokenId); } /** * @notice Used to remove approvals for the current owner of the given token. * @param tokenId ID of the token to clear the approvals for */ function _cleanApprovals(uint256 tokenId) internal virtual {} //////////////////////////////////////// // UTILS //////////////////////////////////////// /** * @notice Used to check whether the given account is allowed to manage the given token. * @dev Requirements: * * - `tokenId` must exist. * @param spender Address that is being checked for approval * @param tokenId ID of the token being checked * @return bool The boolean value indicating whether the `spender` is approved to manage the given token */ function _isApprovedOrOwner( address spender, uint256 tokenId ) internal view virtual returns (bool) { address owner = ownerOf(tokenId); return (spender == owner || isApprovedForAll(owner, spender) || getApproved(tokenId) == spender); } /** * @notice Used to check whether the account is approved to manage the token or its direct owner. * @param spender Address that is being checked for approval or direct ownership * @param tokenId ID of the token being checked * @return bool The boolean value indicating whether the `spender` is approved to manage the given token or its * direct owner */ function _isApprovedOrDirectOwner( address spender, uint256 tokenId ) internal view virtual returns (bool) { (address owner, uint256 parentId, ) = directOwnerOf(tokenId); // When the parent is an NFT, only it can do operations if (parentId != 0) { return (spender == owner); } // Otherwise, the owner or approved address can return (spender == owner || isApprovedForAll(owner, spender) || getApproved(tokenId) == spender); } /** * @notice Used to enforce that the given token has been minted. * @dev Reverts if the `tokenId` has not been minted yet. * @dev The validation checks whether the owner of a given token is a `0x0` address and considers it not minted if * it is. This means that both tokens that haven't been minted yet as well as the ones that have already been * burned will cause the transaction to be reverted. * @param tokenId ID of the token to check */ function _requireMinted(uint256 tokenId) internal view virtual { if (!_exists(tokenId)) revert ERC721InvalidTokenId(); } /** * @notice Used to check whether the given token exists. * @dev Tokens start existing when they are minted (`_mint`) and stop existing when they are burned (`_burn`). * @param tokenId ID of the token being checked * @return bool The boolean value signifying whether the token exists */ function _exists(uint256 tokenId) internal view virtual returns (bool) { return _directOwners[tokenId].ownerAddress != address(0); } /** * @notice Used to invoke {IERC721Receiver-onERC721Received} on a target address. * @dev The call is not executed if the target address is not a contract. * @param from Address representing the previous owner of the given token * @param to Yarget address that will receive the tokens * @param tokenId ID of the token to be transferred * @param data Optional data to send along with the call * @return bool Boolean value signifying whether the call correctly returned the expected magic value */ function _checkOnERC721Received( address from, address to, uint256 tokenId, bytes memory data ) private returns (bool) { if (to.isContract()) { try IERC721Receiver(to).onERC721Received( _msgSender(), from, tokenId, data ) returns (bytes4 retval) { return retval == IERC721Receiver.onERC721Received.selector; } catch (bytes memory reason) { if (reason.length == 0) { revert ERC721TransferToNonReceiverImplementer(); } else { /// @solidity memory-safe-assembly assembly { revert(add(32, reason), mload(reason)) } } } } else { return true; } } //////////////////////////////////////// // CHILD MANAGEMENT PUBLIC //////////////////////////////////////// /** * @notice Used to add a child token to a given parent token. * @dev This adds the iichild token into the given parent token's pending child tokens array. * @dev You MUST NOT call this method directly. To add a a child to an NFT you must use either * `nestTransfer`, `nestMint` or `transferChild` to the NFT. * @dev Requirements: * * - `ownerOf` on the child contract must resolve to the called contract. * - The pending array of the parent contract must not be full. * @param parentId ID of the parent token to receive the new child token * @param childId ID of the new proposed child token * @param data Additional data with no specified format */ function addChild( uint256 parentId, uint256 childId, bytes memory data ) public virtual { _requireMinted(parentId); address childAddress = _msgSender(); if (!childAddress.isContract()) revert IsNotContract(); Child memory child = Child({ contractAddress: childAddress, tokenId: childId }); _beforeAddChild(parentId, childAddress, childId); uint256 length = pendingChildrenOf(parentId).length; if (length < 128) { _pendingChildren[parentId].push(child); } else { revert MaxPendingChildrenReached(); } // Previous length matches the index for the new child emit ChildProposed(parentId, length, childAddress, childId); _afterAddChild(parentId, childAddress, childId); } /** * @notice @notice Used to accept a pending child token for a given parent token. * @dev This moves the child token from parent token's pending child tokens array into the active child tokens * array. * @param parentId ID of the parent token for which the child token is being accepted * @param childIndex Index of a child tokem in the given parent's pending children array * @param childAddress Address of the collection smart contract of the child token expected to be located at the * specified index of the given parent token's pending children array * @param childId ID of the child token expected to be located at the specified index of the given parent token's * pending children array */ function acceptChild( uint256 parentId, uint256 childIndex, address childAddress, uint256 childId ) public virtual onlyApprovedOrOwner(parentId) { _acceptChild(parentId, childIndex, childAddress, childId); } /** * @notice Used to accept a pending child token for a given parent token. * @dev This moves the child token from parent token's pending child tokens array into the active child tokens * array. * @dev Requirements: * * - `tokenId` must exist * - `index` must be in range of the pending children array * @param parentId ID of the parent token for which the child token is being accepted * @param childIndex Index of a child tokem in the given parent's pending children array * @param childAddress Address of the collection smart contract of the child token expected to be located at the * specified index of the given parent token's pending children array * @param childId ID of the child token expected to be located at the specified index of the given parent token's * pending children array */ function _acceptChild( uint256 parentId, uint256 childIndex, address childAddress, uint256 childId ) internal virtual { if (pendingChildrenOf(parentId).length <= childIndex) revert PendingChildIndexOutOfRange(); Child memory child = pendingChildOf(parentId, childIndex); _checkExpectedChild(child, childAddress, childId); if (_childIsInActive[childAddress][childId] != 0) revert ChildAlreadyExists(); _beforeAcceptChild(parentId, childIndex, childAddress, childId); // Remove from pending: _removeChildByIndex(_pendingChildren[parentId], childIndex); // Add to active: _activeChildren[parentId].push(child); _childIsInActive[childAddress][childId] = 1; // We use 1 as true emit ChildAccepted(parentId, childIndex, childAddress, childId); _afterAcceptChild(parentId, childIndex, childAddress, childId); } /** * @notice Used to reject all pending children of a given parent token. * @dev Removes the children from the pending array mapping. * @dev This does not update the ownership storage data on children. If necessary, ownership can be reclaimed by the * rootOwner of the previous parent. * @param tokenId ID of the parent token for which to reject all of the pending tokens */ function rejectAllChildren(uint256 tokenId, uint256 maxRejections) public virtual onlyApprovedOrOwner(tokenId) { _rejectAllChildren(tokenId, maxRejections); } /** * @notice Used to reject all pending children of a given parent token. * @dev Removes the children from the pending array mapping. * @dev This does not update the ownership storage data on children. If necessary, ownership can be reclaimed by the * rootOwner of the previous parent. * @dev Requirements: * * - `tokenId` must exist * @param tokenId ID of the parent token for which to reject all of the pending tokens. * @param maxRejections Maximum number of expected children to reject, used to prevent from * rejecting children which arrive just before this operation. */ function _rejectAllChildren(uint256 tokenId, uint256 maxRejections) internal virtual { if (_pendingChildren[tokenId].length > maxRejections) revert UnexpectedNumberOfChildren(); _beforeRejectAllChildren(tokenId); delete _pendingChildren[tokenId]; emit AllChildrenRejected(tokenId); _afterRejectAllChildren(tokenId); } /** * @notice Used to transfer a child token from a given parent token. * @param tokenId ID of the parent token from which the child token is being transferred * @param to Address to which to transfer the token to * @param destinationId ID of the token to receive this child token (MUST be 0 if the destination is not a token) * @param childIndex Index of a token we are transferring, in the array it belongs to (can be either active array or * pending array) * @param childAddress Address of the child token's collection smart contract. * @param childId ID of the child token in its own collection smart contract. * @param isPending A boolean value indicating whether the child token being transferred is in the pending array of the * parent token (`true`) or in the active array (`false`) * @param data Additional data with no specified format, sent in call to `_to` */ function transferChild( uint256 tokenId, address to, uint256 destinationId, uint256 childIndex, address childAddress, uint256 childId, bool isPending, bytes memory data ) public virtual onlyApprovedOrOwner(tokenId) { _transferChild( tokenId, to, destinationId, childIndex, childAddress, childId, isPending, data ); } /** * @notice Used to transfer a child token from a given parent token. * @dev When transferring a child token, the owner of the token is set to `to`, or is not updated in the event of `to` * being the `0x0` address. * @dev Requirements: * * - `tokenId` must exist. * @dev Emits {ChildTransferred} event. * @param tokenId ID of the parent token from which the child token is being transferred * @param to Address to which to transfer the token to * @param destinationId ID of the token to receive this child token (MUST be 0 if the destination is not a token) * @param childIndex Index of a token we are transferring, in the array it belongs to (can be either active array or * pending array) * @param childAddress Address of the child token's collection smart contract. * @param childId ID of the child token in its own collection smart contract. * @param isPending A boolean value indicating whether the child token being transferred is in the pending array of the * parent token (`true`) or in the active array (`false`) * @param data Additional data with no specified format, sent in call to `_to` */ function _transferChild( uint256 tokenId, address to, uint256 destinationId, // newParentId uint256 childIndex, address childAddress, uint256 childId, bool isPending, bytes memory data ) internal virtual { Child memory child; if (isPending) { child = pendingChildOf(tokenId, childIndex); } else { child = childOf(tokenId, childIndex); } _checkExpectedChild(child, childAddress, childId); _beforeTransferChild( tokenId, childIndex, childAddress, childId, isPending ); if (isPending) { _removeChildByIndex(_pendingChildren[tokenId], childIndex); } else { delete _childIsInActive[childAddress][childId]; _removeChildByIndex(_activeChildren[tokenId], childIndex); } if (to != address(0)) { if (destinationId == 0) { IERC721(childAddress).safeTransferFrom( address(this), to, childId, data ); } else { // Destination is an NFT IERC6059(child.contractAddress).nestTransferFrom( address(this), to, child.tokenId, destinationId, data ); } } emit ChildTransferred( tokenId, childIndex, childAddress, childId, isPending ); _afterTransferChild( tokenId, childIndex, childAddress, childId, isPending ); } function _checkExpectedChild( Child memory child, address expectedAddress, uint256 expectedId ) private pure { if ( expectedAddress != child.contractAddress || expectedId != child.tokenId ) revert UnexpectedChildId(); } //////////////////////////////////////// // CHILD MANAGEMENT GETTERS //////////////////////////////////////// /** * @notice Used to retrieve the active child tokens of a given parent token. * @dev Returns array of Child structs existing for parent token. * @dev The Child struct consists of the following values: * [ * tokenId, * contractAddress * ] * @param parentId ID of the parent token for which to retrieve the active child tokens * @return struct[] An array of Child structs containing the parent token's active child tokens */ function childrenOf( uint256 parentId ) public view virtual returns (Child[] memory) { Child[] memory children = _activeChildren[parentId]; return children; } /** * @notice Used to retrieve the pending child tokens of a given parent token. * @dev Returns array of pending Child structs existing for given parent. * @dev The Child struct consists of the following values: * [ * tokenId, * contractAddress * ] * @param parentId ID of the parent token for which to retrieve the pending child tokens * @return struct[] An array of Child structs containing the parent token's pending child tokens */ function pendingChildrenOf( uint256 parentId ) public view virtual returns (Child[] memory) { Child[] memory pendingChildren = _pendingChildren[parentId]; return pendingChildren; } /** * @notice Used to retrieve a specific active child token for a given parent token. * @dev Returns a single Child struct locating at `index` of parent token's active child tokens array. * @dev The Child struct consists of the following values: * [ * tokenId, * contractAddress * ] * @param parentId ID of the parent token for which the child is being retrieved * @param index Index of the child token in the parent token's active child tokens array * @return struct A Child struct containing data about the specified child */ function childOf( uint256 parentId, uint256 index ) public view virtual returns (Child memory) { if (childrenOf(parentId).length <= index) revert ChildIndexOutOfRange(); Child memory child = _activeChildren[parentId][index]; return child; } /** * @notice Used to retrieve a specific pending child token from a given parent token. * @dev Returns a single Child struct locating at `index` of parent token's active child tokens array. * @dev The Child struct consists of the following values: * [ * tokenId, * contractAddress * ] * @param parentId ID of the parent token for which the pending child token is being retrieved * @param index Index of the child token in the parent token's pending child tokens array * @return struct A Child struct containting data about the specified child */ function pendingChildOf( uint256 parentId, uint256 index ) public view virtual returns (Child memory) { if (pendingChildrenOf(parentId).length <= index) revert PendingChildIndexOutOfRange(); Child memory child = _pendingChildren[parentId][index]; return child; } /** * @notice Used to verify that the given child tokwn is included in an active array of a token. * @param childAddress Address of the given token's collection smart contract * @param childId ID of the child token being checked * @return bool A boolean value signifying whether the given child token is included in an active child tokens array * of a token (`true`) or not (`false`) */ function childIsInActive( address childAddress, uint256 childId ) public view virtual returns (bool) { return _childIsInActive[childAddress][childId] != 0; } // HOOKS /** * @notice Hook that is called before any token transfer. This includes minting and burning. * @dev Calling conditions: * * - When `from` and `to` are both non-zero, ``from``'s `tokenId` will be transferred to `to`. * - When `from` is zero, `tokenId` will be minted to `to`. * - When `to` is zero, ``from``'s `tokenId` will be burned. * - `from` and `to` are never zero at the same time. * * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. * @param from Address from which the token is being transferred * @param to Address to which the token is being transferred * @param tokenId ID of the token being transferred */ function _beforeTokenTransfer( address from, address to, uint256 tokenId ) internal virtual {} /** * @notice Hook that is called after any transfer of tokens. This includes minting and burning. * @dev Calling conditions: * * - When `from` and `to` are both non-zero. * - `from` and `to` are never zero at the same time. * * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. * @param from Address from which the token has been transferred * @param to Address to which the token has been transferred * @param tokenId ID of the token that has been transferred */ function _afterTokenTransfer( address from, address to, uint256 tokenId ) internal virtual {} /** * @notice Hook that is called before nested token transfer. * @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. * @param from Address from which the token is being transferred * @param to Address to which the token is being transferred * @param fromTokenId ID of the token from which the given token is being transferred * @param toTokenId ID of the token to which the given token is being transferred * @param tokenId ID of the token being transferred */ function _beforeNestedTokenTransfer( address from, address to, uint256 fromTokenId, uint256 toTokenId, uint256 tokenId ) internal virtual {} /** * @notice Hook that is called after nested token transfer. * @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. * @param from Address from which the token was transferred * @param to Address to which the token was transferred * @param fromTokenId ID of the token from which the given token was transferred * @param toTokenId ID of the token to which the given token was transferred * @param tokenId ID of the token that was transferred */ function _afterNestedTokenTransfer( address from, address to, uint256 fromTokenId, uint256 toTokenId, uint256 tokenId ) internal virtual {} /** * @notice Hook that is called before a child is added to the pending tokens array of a given token. * @dev The Child struct consists of the following values: * [ * tokenId, * contractAddress * ] * @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. * @param tokenId ID of the token that will receive a new pending child token * @param childAddress Address of the collection smart contract of the child token expected to be located at the * specified index of the given parent token's pending children array * @param childId ID of the child token expected to be located at the specified index of the given parent token's * pending children array */ function _beforeAddChild( uint256 tokenId, address childAddress, uint256 childId ) internal virtual {} /** * @notice Hook that is called after a child is added to the pending tokens array of a given token. * @dev The Child struct consists of the following values: * [ * tokenId, * contractAddress * ] * @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. * @param tokenId ID of the token that has received a new pending child token * @param childAddress Address of the collection smart contract of the child token expected to be located at the * specified index of the given parent token's pending children array * @param childId ID of the child token expected to be located at the specified index of the given parent token's * pending children array */ function _afterAddChild( uint256 tokenId, address childAddress, uint256 childId ) internal virtual {} /** * @notice Hook that is called before a child is accepted to the active tokens array of a given token. * @dev The Child struct consists of the following values: * [ * tokenId, * contractAddress * ] * @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. * @param parentId ID of the token that will accept a pending child token * @param childIndex Index of the child token to accept in the given parent token's pending children array * @param childAddress Address of the collection smart contract of the child token expected to be located at the * specified index of the given parent token's pending children array * @param childId ID of the child token expected to be located at the specified index of the given parent token's * pending children array */ function _beforeAcceptChild( uint256 parentId, uint256 childIndex, address childAddress, uint256 childId ) internal virtual {} /** * @notice Hook that is called after a child is accepted to the active tokens array of a given token. * @dev The Child struct consists of the following values: * [ * tokenId, * contractAddress * ] * @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. * @param parentId ID of the token that has accepted a pending child token * @param childIndex Index of the child token that was accpeted in the given parent token's pending children array * @param childAddress Address of the collection smart contract of the child token that was expected to be located * at the specified index of the given parent token's pending children array * @param childId ID of the child token that was expected to be located at the specified index of the given parent * token's pending children array */ function _afterAcceptChild( uint256 parentId, uint256 childIndex, address childAddress, uint256 childId ) internal virtual {} /** * @notice Hook that is called before a child is transferred from a given child token array of a given token. * @dev The Child struct consists of the following values: * [ * tokenId, * contractAddress * ] * @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. * @param tokenId ID of the token that will transfer a child token * @param childIndex Index of the child token that will be transferred from the given parent token's children array * @param childAddress Address of the collection smart contract of the child token that is expected to be located * at the specified index of the given parent token's children array * @param childId ID of the child token that is expected to be located at the specified index of the given parent * token's children array * @param isPending A boolean value signifying whether the child token is being transferred from the pending child * tokens array (`true`) or from the active child tokens array (`false`) */ function _beforeTransferChild( uint256 tokenId, uint256 childIndex, address childAddress, uint256 childId, bool isPending ) internal virtual {} /** * @notice Hook that is called after a child is transferred from a given child token array of a given token. * @dev The Child struct consists of the following values: * [ * tokenId, * contractAddress * ] * @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. * @param tokenId ID of the token that has transferred a child token * @param childIndex Index of the child token that was transferred from the given parent token's children array * @param childAddress Address of the collection smart contract of the child token that was expected to be located * at the specified index of the given parent token's children array * @param childId ID of the child token that was expected to be located at the specified index of the given parent * token's children array * @param isPending A boolean value signifying whether the child token was transferred from the pending child tokens * array (`true`) or from the active child tokens array (`false`) */ function _afterTransferChild( uint256 tokenId, uint256 childIndex, address childAddress, uint256 childId, bool isPending ) internal virtual {} /** * @notice Hook that is called before a pending child tokens array of a given token is cleared. * @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. * @param tokenId ID of the token that will reject all of the pending child tokens */ function _beforeRejectAllChildren(uint256 tokenId) internal virtual {} /** * @notice Hook that is called after a pending child tokens array of a given token is cleared. * @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. * @param tokenId ID of the token that has rejected all of the pending child tokens */ function _afterRejectAllChildren(uint256 tokenId) internal virtual {} // HELPERS /** * @notice Used to remove a specified child token form an array using its index within said array. * @dev The caller must ensure that the length of the array is valid compared to the index passed. * @dev The Child struct consists of the following values: * [ * tokenId, * contractAddress * ] * @param array An array od Child struct containing info about the child tokens in a given child tokens array * @param index An index of the child token to remove in the accompanying array */ function _removeChildByIndex(Child[] storage array, uint256 index) private { array[index] = array[array.length - 1]; array.pop(); } }