diff --git a/contracts/RMRK/equippable/RMRKMinifiedEquippableChildAutoAccept.sol b/contracts/RMRK/equippable/RMRKMinifiedEquippableChildAutoAccept.sol new file mode 100644 index 00000000..b147ad8d --- /dev/null +++ b/contracts/RMRK/equippable/RMRKMinifiedEquippableChildAutoAccept.sol @@ -0,0 +1,2049 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.21; + +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {IERC5773} from "../multiasset/IERC5773.sol"; +import {IERC6220} from "../equippable/IERC6220.sol"; +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import { + IERC721Receiver +} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; +import {IERC7401} from "../nestable/IERC7401.sol"; +import {Context} from "@openzeppelin/contracts/utils/Context.sol"; +import {IRMRKCatalog} from "../catalog/IRMRKCatalog.sol"; +import {RMRKCore} from "../core/RMRKCore.sol"; +import {RMRKLib} from "../library/RMRKLib.sol"; +import {ReentrancyGuard} from "../security/ReentrancyGuard.sol"; +import "../library/RMRKErrors.sol"; + +/** + * @title RMRKMinifiedEquippableChildAutoAccept + * @author RMRK team + * @notice Smart contract of the RMRK Equippable module, without utility internal functions. + * @dev This includes all the code for MultiAsset, Nestable and Equippable. + * @dev Most of the code is duplicated from the other legos, this version is created to save size. + * @dev Children added go directly to the active children array. + */ +contract RMRKMinifiedEquippableChildAutoAccept is + ReentrancyGuard, + Context, + IERC165, + IERC721, + IERC7401, + IERC6220, + RMRKCore +{ + using RMRKLib for uint64[]; + + 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 _RMRKOwners; + + // Mapping of tokenId to array of active children structs + mapping(uint256 => Child[]) internal _activeChildren; + + // -------------------------- 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) internal view { + address owner = ownerOf(tokenId); + if ( + !(_msgSender() == owner || + isApprovedForAll(owner, _msgSender()) || + getApproved(tokenId) == _msgSender()) + ) 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) internal view { + (address owner, uint256 parentId, ) = directOwnerOf(tokenId); + // When the parent is an NFT, only it can do operations. Otherwise, the owner or approved address can + if ( + (parentId != 0 && _msgSender() != owner) || + !(_msgSender() == owner || + isApprovedForAll(owner, _msgSender()) || + getApproved(tokenId) == _msgSender()) + ) { + revert RMRKNotApprovedOrDirectOwner(); + } + } + + /** + * @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 --------------------------------- + + /** + * @notice Used to retrieve the number of tokens in `owner`'s account. + * @param owner Address of the account being checked + * @return balance The balance of the given account + */ + function balanceOf( + address owner + ) public view virtual returns (uint256 balance) { + if (owner == address(0)) revert ERC721AddressZeroIsNotaValidOwner(); + balance = _balances[owner]; + } + + //////////////////////////////////////// + // TRANSFERS + //////////////////////////////////////// + + /** + * @notice Transfers a given token from `from` to `to`. + * @dev Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must be owned by `from`. + * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. + * @dev Emits a {Transfer} event. + * @param from Address from which to transfer the token from + * @param to Address to which to transfer the token to + * @param tokenId ID of the token to transfer + */ + function transferFrom( + address from, + address to, + uint256 tokenId + ) public virtual onlyApprovedOrDirectOwner(tokenId) { + _transfer(from, to, tokenId, ""); + } + + /** + * @notice Used to safely transfer a given token token from `from` to `to`. + * @dev Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must exist and be owned by `from`. + * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. + * - 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 to transfer the tokens from + * @param to Address to transfer the tokens to + * @param tokenId ID of the token to transfer + */ + function safeTransferFrom( + address from, + address to, + uint256 tokenId + ) public virtual { + safeTransferFrom(from, to, tokenId, ""); + } + + /** + * @notice Used to safely transfer a given token token from `from` to `to`. + * @dev Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must exist and be owned by `from`. + * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. + * - 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 to transfer the tokens from + * @param to Address to transfer the tokens to + * @param tokenId ID of the token to transfer + * @param data Additional data without a specified format to be sent along with the token transaction + */ + function safeTransferFrom( + address from, + address to, + uint256 tokenId, + bytes memory data + ) public virtual onlyApprovedOrDirectOwner(tokenId) { + _transfer(from, to, tokenId, data); + if (!_checkOnERC721Received(from, to, tokenId, data)) + revert ERC721TransferToNonReceiverImplementer(); + } + + /** + * @inheritdoc IERC7401 + */ + function nestTransferFrom( + address from, + address to, + uint256 tokenId, + uint256 destinationId, + bytes memory data + ) public virtual onlyApprovedOrDirectOwner(tokenId) { + (address immediateOwner, uint256 parentId, ) = directOwnerOf(tokenId); + if (immediateOwner != from) revert ERC721TransferFromIncorrectOwner(); + if (to == address(this) && tokenId == destinationId) + revert RMRKNestableTransferToSelf(); + + _checkDestination(to); + _checkForInheritanceLoop(tokenId, to, destinationId); + + _beforeTokenTransfer(from, to, tokenId); + _beforeNestedTokenTransfer( + immediateOwner, + to, + parentId, + destinationId, + tokenId, + data + ); + _balances[from] -= 1; + _updateOwnerAndClearApprovals(tokenId, destinationId, to); + _balances[to] += 1; + + // Sending to NFT: + _sendToNFT(immediateOwner, to, parentId, destinationId, tokenId, data); + } + + /** + * @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 + * @param data Additional data with no specified format, sent in call to `to` + */ + function _transfer( + address from, + address to, + uint256 tokenId, + bytes memory data + ) internal virtual { + (address immediateOwner, uint256 parentId, ) = directOwnerOf(tokenId); + if (immediateOwner != from) revert ERC721TransferFromIncorrectOwner(); + if (to == address(0)) revert ERC721TransferToTheZeroAddress(); + + _beforeTokenTransfer(from, to, tokenId); + _beforeNestedTokenTransfer(from, to, parentId, 0, tokenId, data); + + _balances[from] -= 1; + _updateOwnerAndClearApprovals(tokenId, 0, to); + _balances[to] += 1; + + emit Transfer(from, to, tokenId); + emit NestTransfer(from, to, parentId, 0, tokenId); + + _afterTokenTransfer(from, to, tokenId); + _afterNestedTokenTransfer(from, to, parentId, 0, 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 { + IERC7401 destContract = IERC7401(to); + destContract.addChild(destinationId, tokenId, data); + + emit Transfer(from, to, tokenId); + emit NestTransfer(from, to, parentId, destinationId, tokenId); + + _afterTokenTransfer(from, to, tokenId); + _afterNestedTokenTransfer( + from, + to, + parentId, + destinationId, + tokenId, + data + ); + } + + /** + * @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 + ) = IERC7401(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 RMRKNestableTransferToDescendant(); + } + // We reuse the parameters to save some contract size + targetContract = nextOwner; + targetId = nextOwnerTokenId; + unchecked { + ++i; + } + } + revert RMRKNestableTooDeep(); + } + + //////////////////////////////////////// + // MINTING + //////////////////////////////////////// + + /** + * @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 { + _innerMint(to, tokenId, 0, data); + + 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, data); + if (!_checkOnERC721Received(address(0), to, tokenId, data)) + revert ERC721TransferToNonReceiverImplementer(); + } + + /** + * @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 { + _checkDestination(to); + _innerMint(to, tokenId, destinationId, data); + _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 + * @param data Additional data with no specified format, sent in call to `to` + */ + function _innerMint( + address to, + uint256 tokenId, + uint256 destinationId, + bytes memory data + ) private { + if (to == address(0)) revert ERC721MintToTheZeroAddress(); + if (_exists(tokenId)) revert ERC721TokenAlreadyMinted(); + if (tokenId == uint256(0)) revert RMRKIdZeroForbidden(); + + _beforeTokenTransfer(address(0), to, tokenId); + _beforeNestedTokenTransfer( + address(0), + to, + 0, + destinationId, + tokenId, + data + ); + + _balances[to] += 1; + _RMRKOwners[tokenId] = DirectOwner({ + ownerAddress: to, + tokenId: destinationId + }); + } + + //////////////////////////////////////// + // Ownership + //////////////////////////////////////// + + /** + * @inheritdoc IERC7401 + */ + function ownerOf( + uint256 tokenId + ) public view virtual override(IERC7401, IERC721) returns (address owner_) { + (address owner, uint256 ownerTokenId, bool isNft) = directOwnerOf( + tokenId + ); + if (isNft) { + owner = IERC7401(owner).ownerOf(ownerTokenId); + } + owner_ = owner; + } + + /** + * @inheritdoc IERC7401 + */ + function directOwnerOf( + uint256 tokenId + ) + public + view + virtual + returns (address owner_, uint256 parentId, bool isNFT) + { + DirectOwner memory owner = _RMRKOwners[tokenId]; + if (owner.ownerAddress == address(0)) revert ERC721InvalidTokenId(); + + owner_ = owner.ownerAddress; + parentId = owner.tokenId; + isNFT = owner.tokenId != 0; + } + + //////////////////////////////////////// + // BURNING + //////////////////////////////////////// + + /** + * @notice Used to burn a given token. + * @dev In case the token has any child tokens, the execution will be reverted. + * @param tokenId ID of the token to burn + */ + function burn(uint256 tokenId) public virtual { + burn(tokenId, 0); + } + + /** + * @inheritdoc IERC7401 + */ + function burn( + uint256 tokenId, + uint256 maxChildrenBurns + ) + public + virtual + onlyApprovedOrDirectOwner(tokenId) + returns (uint256 burnedChildren) + { + (address immediateOwner, uint256 parentId, ) = directOwnerOf(tokenId); + address rootOwner = ownerOf(tokenId); + + _beforeTokenTransfer(immediateOwner, address(0), tokenId); + _beforeNestedTokenTransfer( + immediateOwner, + address(0), + parentId, + 0, + tokenId, + "" + ); + + _balances[immediateOwner] -= 1; + _approve(address(0), tokenId); + _approveForAssets(address(0), tokenId); + + Child[] memory children = childrenOf(tokenId); + + delete _activeChildren[tokenId]; + delete _tokenApprovals[tokenId][rootOwner]; + + uint256 pendingRecursiveBurns; + + uint256 length = children.length; //gas savings + for (uint256 i; i < length; ) { + if (burnedChildren >= maxChildrenBurns) + revert RMRKMaxRecursiveBurnsReached( + children[i].contractAddress, + children[i].tokenId + ); + unchecked { + // At this point we know pendingRecursiveBurns must be at least 1 + pendingRecursiveBurns = maxChildrenBurns - burnedChildren; + } + // 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. + burnedChildren += + IERC7401(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 _RMRKOwners[tokenId]; + + emit Transfer(immediateOwner, address(0), tokenId); + emit NestTransfer(immediateOwner, address(0), parentId, 0, tokenId); + + _afterTokenTransfer(immediateOwner, address(0), tokenId); + _afterNestedTokenTransfer( + immediateOwner, + address(0), + parentId, + 0, + tokenId, + "" + ); + } + + //////////////////////////////////////// + // APPROVALS + //////////////////////////////////////// + + /** + * @notice Used to grant a one-time approval to manage one's token. + * @dev Gives permission to `to` to transfer `tokenId` token to another account. + * @dev The approval is cleared when the token is transferred. + * @dev Only a single account can be approved at a time, so approving the zero address clears previous approvals. + * @dev Requirements: + * + * - The caller must own the token or be an approved operator. + * - `tokenId` must exist. + * @dev Emits an {Approval} event. + * @param to Address receiving the approval + * @param tokenId ID of the token for which the approval is being granted + */ + 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); + } + + /** + * @notice Used to retrieve the account approved to manage given token. + * @dev Requirements: + * + * - `tokenId` must exist. + * @param tokenId ID of the token to check for approval + * @return approved Address of the account approved to manage the token + */ + function getApproved( + uint256 tokenId + ) public view virtual returns (address approved) { + _requireMinted(tokenId); + + approved = _tokenApprovals[tokenId][ownerOf(tokenId)]; + } + + /** + * @notice Used to approve or remove `operator` as an operator for the caller. + * @dev Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller. + * @dev Requirements: + * + * - The `operator` cannot be the caller. + * @dev Emits an {ApprovalForAll} event. + * @param operator Address of the operator being managed + * @param approved A boolean value signifying whether the approval is being granted (`true`) or (`revoked`) + */ + function setApprovalForAll(address operator, bool approved) public virtual { + if (_msgSender() == operator) revert ERC721ApproveToCaller(); + _operatorApprovals[_msgSender()][operator] = approved; + emit ApprovalForAll(_msgSender(), operator, approved); + } + + /** + * @notice Used to check if the given address is allowed to manage the tokens of the specified address. + * @param owner Address of the owner of the tokens + * @param operator Address being checked for approval + * @return isApproved A boolean value signifying whether the *operator* is allowed to manage the tokens of the *owner* (`true`) + * or not (`false`) + */ + function isApprovedForAll( + address owner, + address operator + ) public view virtual returns (bool isApproved) { + isApproved = _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 + */ + function _updateOwnerAndClearApprovals( + uint256 tokenId, + uint256 destinationId, + address to + ) internal { + _RMRKOwners[tokenId] = DirectOwner({ + ownerAddress: to, + tokenId: destinationId + }); + + // Clear approvals from the previous owner + _approve(address(0), tokenId); + _approveForAssets(address(0), tokenId); + } + + //////////////////////////////////////// + // UTILS + //////////////////////////////////////// + + /** + * @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 exists A boolean value signifying whether the token exists + */ + function _exists( + uint256 tokenId + ) internal view virtual returns (bool exists) { + exists = _RMRKOwners[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 valid 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 valid) { + if (to.code.length != 0) { + try + IERC721Receiver(to).onERC721Received( + _msgSender(), + from, + tokenId, + data + ) + returns (bytes4 retval) { + valid = retval == IERC721Receiver.onERC721Received.selector; + } catch (bytes memory reason) { + if (reason.length == uint256(0)) { + revert ERC721TransferToNonReceiverImplementer(); + } else { + /// @solidity memory-safe-assembly + assembly { + revert(add(32, reason), mload(reason)) + } + } + } + } else { + valid = true; + } + } + + //////////////////////////////////////// + // CHILD MANAGEMENT PUBLIC + //////////////////////////////////////// + + /** + * @inheritdoc IERC7401 + */ + function addChild( + uint256 parentId, + uint256 childId, + bytes memory data + ) public virtual { + _requireMinted(parentId); + + address childAddress = _msgSender(); + if (childAddress.code.length == 0) revert RMRKIsNotContract(); + + Child memory child = Child({ + contractAddress: childAddress, + tokenId: childId + }); + + _beforeAddChild(parentId, childAddress, childId, data); + + uint256 length = pendingChildrenOf(parentId).length; + + // Previous length matches the index for the new child + emit ChildProposed(parentId, length, childAddress, childId); + + // Add to active: + _activeChildren[parentId].push(child); + + emit ChildAccepted(parentId, 0, childAddress, childId); + + _afterAddChild(parentId, childAddress, childId, data); + } + + /** + * @inheritdoc IERC7401 + */ + 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 + * @dev Emits ***ChildAccepted*** event. + * @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 {} + + /** + * @inheritdoc IERC7401 + */ + function rejectAllChildren( + uint256 tokenId, + uint256 maxRejections + ) public virtual onlyApprovedOrOwner(tokenId) {} + + /** + * @inheritdoc IERC7401 + */ + function transferChild( + uint256 tokenId, + address to, + uint256 destinationId, + uint256 childIndex, + address childAddress, + uint256 childId, + bool isPending, + bytes memory data + ) public virtual onlyApprovedOrOwner(tokenId) { + Child memory child; + if (isPending) { + child = pendingChildOf(tokenId, childIndex); + } else { + if (isChildEquipped(tokenId, childAddress, childId)) + revert RMRKMustUnequipFirst(); + child = childOf(tokenId, childIndex); + } + _checkExpectedChild(child, childAddress, childId); + + _beforeTransferChild( + tokenId, + childIndex, + childAddress, + childId, + isPending, + data + ); + + _removeChild(tokenId, childIndex); + + if (to != address(0)) { + if (destinationId == uint256(0)) { + IERC721(childAddress).safeTransferFrom( + address(this), + to, + childId, + data + ); + } else { + // Destination is an NFT + IERC7401(child.contractAddress).nestTransferFrom( + address(this), + to, + child.tokenId, + destinationId, + data + ); + } + } + + emit ChildTransferred( + tokenId, + childIndex, + childAddress, + childId, + isPending, + to == address(0) + ); + _afterTransferChild( + tokenId, + childIndex, + childAddress, + childId, + isPending, + data + ); + } + + function _removeChild( + uint256 tokenId, + uint256 childIndex + ) internal virtual { + uint256 lastIndex = _activeChildren[tokenId].length - 1; + if (childIndex != lastIndex) { + _activeChildren[tokenId][childIndex] = _activeChildren[tokenId][ + lastIndex + ]; + } + _activeChildren[tokenId].pop(); + } + + /** + * @notice Used to verify that the child being accessed is the intended child. + * @dev The Child struct consists of the following values: + * [ + * tokenId, + * contractAddress + * ] + * @param child A Child struct of a child being accessed + * @param expectedAddress The address expected to be the one of the child + * @param expectedId The token ID expected to be the one of the child + */ + function _checkExpectedChild( + Child memory child, + address expectedAddress, + uint256 expectedId + ) private pure { + if ( + expectedAddress != child.contractAddress || + expectedId != child.tokenId + ) revert RMRKUnexpectedChildId(); + } + + //////////////////////////////////////// + // CHILD MANAGEMENT GETTERS + //////////////////////////////////////// + + /** + * @inheritdoc IERC7401 + */ + + function childrenOf( + uint256 parentId + ) public view virtual returns (Child[] memory children) { + children = _activeChildren[parentId]; + } + + /** + * @inheritdoc IERC7401 + */ + + function pendingChildrenOf( + uint256 parentId + ) public view virtual returns (Child[] memory children) {} + + /** + * @inheritdoc IERC7401 + */ + function childOf( + uint256 parentId, + uint256 index + ) public view virtual returns (Child memory child) { + if (childrenOf(parentId).length <= index) + revert RMRKChildIndexOutOfRange(); + child = _activeChildren[parentId][index]; + } + + /** + * @inheritdoc IERC7401 + */ + function pendingChildOf( + uint256, + uint256 + ) public view virtual returns (Child memory) { + revert RMRKPendingChildIndexOutOfRange(); + } + + // 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 + * @param data Additional data with no specified format, sent in the addChild call + */ + function _beforeNestedTokenTransfer( + address from, + address to, + uint256 fromTokenId, + uint256 toTokenId, + uint256 tokenId, + bytes memory data + ) 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 + * @param data Additional data with no specified format, sent in the addChild call + */ + function _afterNestedTokenTransfer( + address from, + address to, + uint256 fromTokenId, + uint256 toTokenId, + uint256 tokenId, + bytes memory data + ) 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 + * @param data Additional data with no specified format + */ + function _beforeAddChild( + uint256 tokenId, + address childAddress, + uint256 childId, + bytes memory data + ) 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 + * @param data Additional data with no specified format + */ + function _afterAddChild( + uint256 tokenId, + address childAddress, + uint256 childId, + bytes memory data + ) 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`) + * @param data Additional data with no specified format, sent in the addChild call + */ + function _beforeTransferChild( + uint256 tokenId, + uint256 childIndex, + address childAddress, + uint256 childId, + bool isPending, + bytes memory data + ) 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`) + * @param data Additional data with no specified format, sent in the addChild call + */ + function _afterTransferChild( + uint256 tokenId, + uint256 childIndex, + address childAddress, + uint256 childId, + bool isPending, + bytes memory data + ) 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 + + /// Mapping of uint64 Ids to asset metadata + mapping(uint64 => string) internal _assets; + + /// Mapping of tokenId to new asset, to asset to be replaced + mapping(uint256 => mapping(uint64 => uint64)) internal _assetReplacements; + + /// Mapping of tokenId to an array of active assets + /// @dev Active recurses is unbounded, getting all would reach gas limit at around 30k items + /// so we leave this as internal in case a custom implementation needs to implement pagination + mapping(uint256 => uint64[]) internal _activeAssets; + + /// Mapping of tokenId to an array of pending assets + mapping(uint256 => uint64[]) internal _pendingAssets; + + /// Mapping of tokenId to an array of priorities for active assets + mapping(uint256 => uint64[]) internal _activeAssetPriorities; + + /// Mapping of tokenId to assetId to whether the token has this asset assigned + mapping(uint256 => mapping(uint64 => bool)) internal _tokenAssets; + + /// Mapping from owner to operator approvals for assets + mapping(address => mapping(address => bool)) + private _operatorApprovalsForAssets; + + /** + * @inheritdoc IERC5773 + */ + function getAssetMetadata( + uint256 tokenId, + uint64 assetId + ) public view virtual override returns (string memory metadata) { + // Allow to get the asset metadata without a token having it: + if (tokenId != 0 && !_tokenAssets[tokenId][assetId]) + revert RMRKTokenDoesNotHaveAsset(); + metadata = _assets[assetId]; + } + + /** + * @inheritdoc IERC5773 + */ + function getActiveAssets( + uint256 tokenId + ) public view virtual returns (uint64[] memory assetIds) { + assetIds = _activeAssets[tokenId]; + } + + /** + * @inheritdoc IERC5773 + */ + function getPendingAssets( + uint256 tokenId + ) public view virtual returns (uint64[] memory assetIds) { + assetIds = _pendingAssets[tokenId]; + } + + /** + * @inheritdoc IERC5773 + */ + function getActiveAssetPriorities( + uint256 tokenId + ) public view virtual returns (uint64[] memory priorities) { + priorities = _activeAssetPriorities[tokenId]; + } + + /** + * @inheritdoc IERC5773 + */ + function getAssetReplacements( + uint256 tokenId, + uint64 newAssetId + ) public view virtual returns (uint64 replacedAssetId) { + replacedAssetId = _assetReplacements[tokenId][newAssetId]; + } + + /** + * @inheritdoc IERC5773 + */ + function isApprovedForAllForAssets( + address owner, + address operator + ) public view virtual returns (bool isApproved) { + isApproved = _operatorApprovalsForAssets[owner][operator]; + } + + /** + * @inheritdoc IERC5773 + */ + function setApprovalForAllForAssets( + address operator, + bool approved + ) public virtual { + if (_msgSender() == operator) + revert RMRKApprovalForAssetsToCurrentOwner(); + + _operatorApprovalsForAssets[_msgSender()][operator] = approved; + emit ApprovalForAllForAssets(_msgSender(), operator, approved); + } + + /** + * @notice Used to validate the index on the pending assets array + * @dev The call is reverted if the index is out of range or the asset Id is not present at the index. + * @param tokenId ID of the token that the asset is validated from + * @param index Index of the asset in the pending array + * @param assetId Id of the asset expected to be in the index + */ + function _validatePendingAssetAtIndex( + uint256 tokenId, + uint256 index, + uint64 assetId + ) private view { + if (index >= _pendingAssets[tokenId].length) + revert RMRKIndexOutOfRange(); + if (assetId != _pendingAssets[tokenId][index]) + revert RMRKUnexpectedAssetId(); + } + + /** + * @notice Used to remove the asset at the index on the pending assets array + * @param tokenId ID of the token that the asset is being removed from + * @param index Index of the asset in the pending array + * @param assetId Id of the asset expected to be in the index + */ + function _removePendingAsset( + uint256 tokenId, + uint256 index, + uint64 assetId + ) private { + _pendingAssets[tokenId].removeItemByIndex(index); + delete _assetReplacements[tokenId][assetId]; + } + + /** + * @notice Used to add an asset entry. + * @dev If the specified ID is already used by another asset, the execution will be reverted. + * @dev This internal function warrants custom access control to be implemented when used. + * @dev Emits ***AssetSet*** event. + * @param id ID of the asset to assign to the new asset + * @param metadataURI Metadata URI of the asset + */ + function _addAssetEntry( + uint64 id, + string memory metadataURI + ) internal virtual { + if (id == uint64(0)) revert RMRKIdZeroForbidden(); + if (bytes(_assets[id]).length > 0) revert RMRKAssetAlreadyExists(); + + _beforeAddAsset(id, metadataURI); + _assets[id] = metadataURI; + + emit AssetSet(id); + _afterAddAsset(id, metadataURI); + } + + /** + * @notice Used to add an asset to a token. + * @dev If the given asset is already added to the token, the execution will be reverted. + * @dev If the asset ID is invalid, the execution will be reverted. + * @dev If the token already has the maximum amount of pending assets (128), the execution will be + * reverted. + * @dev Emits ***AssetAddedToTokens*** event. + * @param tokenId ID of the token to add the asset to + * @param assetId ID of the asset to add to the token + * @param replacesAssetWithId ID of the asset to replace from the token's list of active assets + */ + function _addAssetToToken( + uint256 tokenId, + uint64 assetId, + uint64 replacesAssetWithId + ) internal virtual { + if (_tokenAssets[tokenId][assetId]) revert RMRKAssetAlreadyExists(); + + if (bytes(_assets[assetId]).length == uint256(0)) + revert RMRKNoAssetMatchingId(); + + if (_pendingAssets[tokenId].length >= 128) + revert RMRKMaxPendingAssetsReached(); + + _beforeAddAssetToToken(tokenId, assetId, replacesAssetWithId); + _tokenAssets[tokenId][assetId] = true; + _pendingAssets[tokenId].push(assetId); + + if (replacesAssetWithId != uint64(0)) { + _assetReplacements[tokenId][assetId] = replacesAssetWithId; + } + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + emit AssetAddedToTokens(tokenIds, assetId, replacesAssetWithId); + _afterAddAssetToToken(tokenId, assetId, replacesAssetWithId); + } + + /** + * @notice Hook that is called before an asset is added. + * @param id ID of the asset + * @param metadataURI Metadata URI of the asset + */ + function _beforeAddAsset( + uint64 id, + string memory metadataURI + ) internal virtual {} + + /** + * @notice Hook that is called after an asset is added. + * @param id ID of the asset + * @param metadataURI Metadata URI of the asset + */ + function _afterAddAsset( + uint64 id, + string memory metadataURI + ) internal virtual {} + + /** + * @notice Hook that is called before adding an asset to a token's pending assets array. + * @dev If the asset doesn't intend to replace another asset, the `replacesAssetWithId` value should be `0`. + * @param tokenId ID of the token to which the asset is being added + * @param assetId ID of the asset that is being added + * @param replacesAssetWithId ID of the asset that this asset is attempting to replace + */ + function _beforeAddAssetToToken( + uint256 tokenId, + uint64 assetId, + uint64 replacesAssetWithId + ) internal virtual {} + + /** + * @notice Hook that is called after an asset has been added to a token's pending assets array. + * @dev If the asset doesn't intend to replace another asset, the `replacesAssetWithId` value should be `0`. + * @param tokenId ID of the token to which the asset is has been added + * @param assetId ID of the asset that is has been added + * @param replacesAssetWithId ID of the asset that this asset is attempting to replace + */ + function _afterAddAssetToToken( + uint256 tokenId, + uint64 assetId, + uint64 replacesAssetWithId + ) internal virtual {} + + /** + * @notice Hook that is called before an asset is accepted to a token's active assets array. + * @param tokenId ID of the token for which the asset is being accepted + * @param index Index of the asset in the token's pending assets array + * @param assetId ID of the asset expected to be located at the specified `index` + */ + function _beforeAcceptAsset( + uint256 tokenId, + uint256 index, + uint64 assetId + ) internal virtual {} + + /** + * @notice Hook that is called after an asset is accepted to a token's active assets array. + * @param tokenId ID of the token for which the asset has been accepted + * @param index Index of the asset in the token's pending assets array + * @param assetId ID of the asset expected to have been located at the specified `index` + * @param replacedAssetId ID of the asset that has been replaced by the accepted asset + */ + function _afterAcceptAsset( + uint256 tokenId, + uint256 index, + uint64 assetId, + uint64 replacedAssetId + ) internal virtual {} + + /** + * @notice Hook that is called before rejecting an asset. + * @param tokenId ID of the token from which the asset is being rejected + * @param index Index of the asset in the token's pending assets array + * @param assetId ID of the asset expected to be located at the specified `index` + */ + function _beforeRejectAsset( + uint256 tokenId, + uint256 index, + uint64 assetId + ) internal virtual {} + + /** + * @notice Hook that is called after rejecting an asset. + * @param tokenId ID of the token from which the asset has been rejected + * @param index Index of the asset in the token's pending assets array + * @param assetId ID of the asset expected to have been located at the specified `index` + */ + function _afterRejectAsset( + uint256 tokenId, + uint256 index, + uint64 assetId + ) internal virtual {} + + /** + * @notice Hook that is called before rejecting all assets of a token. + * @param tokenId ID of the token from which all of the assets are being rejected + */ + function _beforeRejectAllAssets(uint256 tokenId) internal virtual {} + + /** + * @notice Hook that is called after rejecting all assets of a token. + * @param tokenId ID of the token from which all of the assets have been rejected + */ + function _afterRejectAllAssets(uint256 tokenId) internal virtual {} + + /** + * @notice Hook that is called before the priorities for token's assets is set. + * @param tokenId ID of the token for which the asset priorities are being set + * @param priorities[] An array of priorities for token's active resources + */ + function _beforeSetPriority( + uint256 tokenId, + uint64[] memory priorities + ) internal virtual {} + + /** + * @notice Hook that is called after the priorities for token's assets is set. + * @param tokenId ID of the token for which the asset priorities have been set + * @param priorities[] An array of priorities for token's active resources + */ + function _afterSetPriority( + uint256 tokenId, + uint64[] memory priorities + ) internal virtual {} + + // ------------------- ASSETS -------------- + + // ------------------- ASSET APPROVALS -------------- + + /** + * @notice Mapping from token ID to approver address to approved address for assets. + * @dev The approver is necessary so approvals are invalidated for nested children on transfer. + * @dev WARNING: If a child NFT returns the original root owner, old permissions would be active again. + */ + mapping(uint256 => mapping(address => address)) + private _tokenApprovalsForAssets; + + // ------------------- EQUIPPABLE -------------- + /// Mapping of uint64 asset ID to corresponding catalog address. + mapping(uint64 => address) internal _catalogAddresses; + /// Mapping of uint64 ID to asset object. + mapping(uint64 => uint64) internal _equippableGroupIds; + /// Mapping of assetId to catalog parts applicable to this asset, both fixed and slot + mapping(uint64 => uint64[]) internal _partIds; + + /// Mapping of token ID to catalog address to slot part ID to equipment information. Used to compose an NFT. + mapping(uint256 => mapping(address => mapping(uint64 => Equipment))) + internal _equipments; + + /// Mapping of token ID to child (nestable) address to child ID to count of equipped items. Used to check if equipped. + mapping(uint256 => mapping(address => mapping(uint256 => uint256))) + internal _equipCountPerChild; + + /// Mapping of `equippableGroupId` to parent contract address and valid `slotId`. + mapping(uint64 => mapping(address => uint64)) internal _validParentSlots; + + /** + * @notice Used to verify that the caller is either the owner of the given token or approved to manage the token's assets + * of the owner. + * @param tokenId ID of the token that we are checking + */ + function _onlyApprovedForAssetsOrOwner(uint256 tokenId) internal view { + address owner = ownerOf(tokenId); + if ( + !(_msgSender() == owner || + isApprovedForAllForAssets(owner, _msgSender()) || + getApprovedForAssets(tokenId) == _msgSender()) + ) revert RMRKNotApprovedForAssetsOrOwner(); + } + + /** + * @notice Used to ensure that the caller is either the owner of the given token or approved to manage the token's assets + * of the owner. + * @dev If that is not the case, the execution of the function will be reverted. + * @param tokenId ID of the token that we are checking + */ + modifier onlyApprovedForAssetsOrOwner(uint256 tokenId) { + _onlyApprovedForAssetsOrOwner(tokenId); + _; + } + + /** + * @inheritdoc IERC165 + */ + function supportsInterface( + bytes4 interfaceId + ) public view virtual override returns (bool) { + return + interfaceId == type(IERC165).interfaceId || + interfaceId == type(IERC721).interfaceId || + interfaceId == type(IERC7401).interfaceId || + interfaceId == type(IERC5773).interfaceId || + interfaceId == type(IERC6220).interfaceId; + } + + // ------------------------------- ASSETS ------------------------------ + + // --------------------------- ASSET HANDLERS ------------------------- + + /** + * @notice Accepts a asset at from the pending array of given token. + * @dev Migrates the asset from the token's pending asset array to the token's active asset array. + * @dev Active assets cannot be removed by anyone, but can be replaced by a new asset. + * @dev Requirements: + * + * - The caller must own the token or be approved to manage the token's assets + * - `tokenId` must exist. + * - `index` must be in range of the length of the pending asset array. + * @dev Emits an {AssetAccepted} event. + * @param tokenId ID of the token for which to accept the pending asset + * @param index Index of the asset in the pending array to accept + * @param assetId ID of the asset that is being accepted + */ + function acceptAsset( + uint256 tokenId, + uint256 index, + uint64 assetId + ) public virtual onlyApprovedForAssetsOrOwner(tokenId) { + _acceptAsset(tokenId, index, assetId); + } + + /** + * @notice Used to accept a pending asset. + * @dev The call is reverted if there is no pending asset at a given index. + * @dev Emits ***AssetAccepted*** event. + * @param tokenId ID of the token for which to accept the pending asset + * @param index Index of the asset in the pending array to accept + * @param assetId ID of the asset to accept in token's pending array + */ + function _acceptAsset( + uint256 tokenId, + uint256 index, + uint64 assetId + ) internal virtual { + _validatePendingAssetAtIndex(tokenId, index, assetId); + _beforeAcceptAsset(tokenId, index, assetId); + + uint64 replacesId = _assetReplacements[tokenId][assetId]; + uint256 replaceIndex; + bool replacefound; + if (replacesId != uint64(0)) + (replaceIndex, replacefound) = _activeAssets[tokenId].indexOf( + replacesId + ); + + if (replacefound) { + // We don't want to remove and then push a new asset. + // This way we also keep the priority of the original asset + _activeAssets[tokenId][replaceIndex] = assetId; + delete _tokenAssets[tokenId][replacesId]; + } else { + // We use the current size as next priority, by default priorities would be [0,1,2...] + _activeAssetPriorities[tokenId].push( + uint64(_activeAssets[tokenId].length) + ); + _activeAssets[tokenId].push(assetId); + replacesId = uint64(0); + } + _removePendingAsset(tokenId, index, assetId); + + emit AssetAccepted(tokenId, assetId, replacesId); + _afterAcceptAsset(tokenId, index, assetId, replacesId); + } + + /** + * @notice Rejects a asset from the pending array of given token. + * @dev Removes the asset from the token's pending asset array. + * @dev Requirements: + * + * - The caller must own the token or be approved to manage the token's assets + * - `tokenId` must exist. + * - `index` must be in range of the length of the pending asset array. + * @dev Emits a {AssetRejected} event. + * @param tokenId ID of the token that the asset is being rejected from + * @param index Index of the asset in the pending array to be rejected + * @param assetId ID of the asset that is being rejected + */ + function rejectAsset( + uint256 tokenId, + uint256 index, + uint64 assetId + ) public virtual onlyApprovedForAssetsOrOwner(tokenId) { + _validatePendingAssetAtIndex(tokenId, index, assetId); + _beforeRejectAsset(tokenId, index, assetId); + + _removePendingAsset(tokenId, index, assetId); + delete _tokenAssets[tokenId][assetId]; + + emit AssetRejected(tokenId, assetId); + _afterRejectAsset(tokenId, index, assetId); + } + + /** + * @notice Rejects all assets from the pending array of a given token. + * @dev Effecitvely deletes the pending array. + * @dev Requirements: + * + * - The caller must own the token or be approved to manage the token's assets + * - `tokenId` must exist. + * @dev Emits a {AssetRejected} event with assetId = 0. + * @param tokenId ID of the token of which to clear the pending array. + * @param maxRejections Maximum number of expected assets to reject, used to prevent from rejecting assets which + * arrive just before this operation. + */ + function rejectAllAssets( + uint256 tokenId, + uint256 maxRejections + ) public virtual onlyApprovedForAssetsOrOwner(tokenId) { + uint256 len = _pendingAssets[tokenId].length; + if (len > maxRejections) revert RMRKUnexpectedNumberOfAssets(); + + _beforeRejectAllAssets(tokenId); + + for (uint256 i; i < len; ) { + uint64 assetId = _pendingAssets[tokenId][i]; + delete _assetReplacements[tokenId][assetId]; + unchecked { + ++i; + } + } + delete (_pendingAssets[tokenId]); + + emit AssetRejected(tokenId, uint64(0)); + _afterRejectAllAssets(tokenId); + } + + /** + * @notice Sets a new priority array for a given token. + * @dev The priority array is a non-sequential list of `uint64`s, where the lowest value is considered highest + * priority. + * @dev Value `0` of a priority is a special case equivalent to unitialized. + * @dev Requirements: + * + * - The caller must own the token or be approved to manage the token's assets + * - `tokenId` must exist. + * - The length of `priorities` must be equal the length of the active assets array. + * @dev Emits a {AssetPrioritySet} event. + * @param tokenId ID of the token to set the priorities for + * @param priorities An array of priority values + */ + function setPriority( + uint256 tokenId, + uint64[] memory priorities + ) public virtual onlyApprovedForAssetsOrOwner(tokenId) { + uint256 length = priorities.length; + if (length != _activeAssets[tokenId].length) + revert RMRKBadPriorityListLength(); + + _beforeSetPriority(tokenId, priorities); + _activeAssetPriorities[tokenId] = priorities; + + emit AssetPrioritySet(tokenId); + _afterSetPriority(tokenId, priorities); + } + + // --------------------------- ASSET INTERNALS ------------------------- + + /** + * @notice Used to add a asset entry. + * @dev This internal function warrants custom access control to be implemented when used. + * @param id ID of the asset being added + * @param equippableGroupId ID of the equippable group being marked as equippable into the slot associated with + * `Parts` of the `Slot` type + * @param catalogAddress Address of the `Catalog` associated with the asset + * @param metadataURI The metadata URI of the asset + * @param partIds An array of IDs of fixed and slot parts to be included in the asset + */ + function _addAssetEntry( + uint64 id, + uint64 equippableGroupId, + address catalogAddress, + string memory metadataURI, + uint64[] memory partIds + ) internal virtual { + _addAssetEntry(id, metadataURI); + + if (catalogAddress == address(0) && partIds.length != 0) + revert RMRKCatalogRequiredForParts(); + + _catalogAddresses[id] = catalogAddress; + _equippableGroupIds[id] = equippableGroupId; + _partIds[id] = partIds; + } + + // ----------------------- ASSET APPROVALS ------------------------ + + /** + * @notice Used to grant approvals for specific tokens to a specified address. + * @dev This can only be called by the owner of the token or by an account that has been granted permission to + * manage all of the owner's assets. + * @param to Address of the account to receive the approval to the specified token + * @param tokenId ID of the token for which we are granting the permission + */ + function approveForAssets(address to, uint256 tokenId) public virtual { + address owner = ownerOf(tokenId); + if (to == owner) revert RMRKApprovalForAssetsToCurrentOwner(); + + if ( + _msgSender() != owner && + !isApprovedForAllForAssets(owner, _msgSender()) + ) revert RMRKApproveForAssetsCallerIsNotOwnerNorApprovedForAll(); + _approveForAssets(to, tokenId); + } + + /** + * @notice Used to get the address of the user that is approved to manage the specified token from the current + * owner. + * @param tokenId ID of the token we are checking + * @return approved Address of the account that is approved to manage the token + */ + function getApprovedForAssets( + uint256 tokenId + ) public view virtual returns (address approved) { + _requireMinted(tokenId); + approved = _tokenApprovalsForAssets[tokenId][ownerOf(tokenId)]; + } + + /** + * @notice Internal function for granting approvals for a specific token. + * @param to Address of the account we are granting an approval to + * @param tokenId ID of the token we are granting the approval for + */ + function _approveForAssets(address to, uint256 tokenId) internal virtual { + address owner = ownerOf(tokenId); + _tokenApprovalsForAssets[tokenId][owner] = to; + emit ApprovalForAssets(owner, to, tokenId); + } + + // ------------------------------- EQUIPPING ------------------------------ + + /** + * @inheritdoc IERC6220 + */ + function equip( + IntakeEquip memory data + ) public virtual onlyApprovedForAssetsOrOwner(data.tokenId) nonReentrant { + address catalogAddress = _catalogAddresses[data.assetId]; + uint64 slotPartId = data.slotPartId; + if ( + _equipments[data.tokenId][catalogAddress][slotPartId] + .childEquippableAddress != address(0) + ) revert RMRKSlotAlreadyUsed(); + + // Check from parent's asset perspective: + _checkAssetAcceptsSlot(data.assetId, slotPartId); + + IERC7401.Child memory child = childOf(data.tokenId, data.childIndex); + + // Check from child perspective intention to be used in part + // We add reentrancy guard because of this call, it happens before updating state + if ( + !IERC6220(child.contractAddress) + .canTokenBeEquippedWithAssetIntoSlot( + address(this), + child.tokenId, + data.childAssetId, + slotPartId + ) + ) revert RMRKTokenCannotBeEquippedWithAssetIntoSlot(); + + // Check from catalog perspective + if ( + !IRMRKCatalog(catalogAddress).checkIsEquippable( + slotPartId, + child.contractAddress + ) + ) revert RMRKEquippableEquipNotAllowedByCatalog(); + + _beforeEquip(data); + Equipment memory newEquip = Equipment({ + assetId: data.assetId, + childAssetId: data.childAssetId, + childId: child.tokenId, + childEquippableAddress: child.contractAddress + }); + + _equipments[data.tokenId][catalogAddress][slotPartId] = newEquip; + _equipCountPerChild[data.tokenId][child.contractAddress][ + child.tokenId + ] += 1; + + emit ChildAssetEquipped( + data.tokenId, + data.assetId, + slotPartId, + child.tokenId, + child.contractAddress, + data.childAssetId + ); + _afterEquip(data); + } + + /** + * @notice Private function to check if a given asset accepts a given slot or not. + * @dev Execution will be reverted if the `Slot` does not apply for the asset. + * @param assetId ID of the asset + * @param slotPartId ID of the `Slot` + */ + function _checkAssetAcceptsSlot( + uint64 assetId, + uint64 slotPartId + ) private view { + (, bool found) = _partIds[assetId].indexOf(slotPartId); + if (!found) revert RMRKTargetAssetCannotReceiveSlot(); + } + + /** + * @inheritdoc IERC6220 + */ + function unequip( + uint256 tokenId, + uint64 assetId, + uint64 slotPartId + ) public virtual onlyApprovedForAssetsOrOwner(tokenId) { + address targetCatalogAddress = _catalogAddresses[assetId]; + Equipment memory equipment = _equipments[tokenId][targetCatalogAddress][ + slotPartId + ]; + if (equipment.childEquippableAddress == address(0)) + revert RMRKNotEquipped(); + _beforeUnequip(tokenId, assetId, slotPartId); + + delete _equipments[tokenId][targetCatalogAddress][slotPartId]; + _equipCountPerChild[tokenId][equipment.childEquippableAddress][ + equipment.childId + ] -= 1; + + emit ChildAssetUnequipped( + tokenId, + assetId, + slotPartId, + equipment.childId, + equipment.childEquippableAddress, + equipment.childAssetId + ); + _afterUnequip(tokenId, assetId, slotPartId); + } + + /** + * @inheritdoc IERC6220 + */ + function isChildEquipped( + uint256 tokenId, + address childAddress, + uint256 childId + ) public view virtual returns (bool isEquipped) { + isEquipped = _equipCountPerChild[tokenId][childAddress][childId] != 0; + } + + // --------------------- ADMIN VALIDATION --------------------- + + /** + * @notice Internal function used to declare that the assets belonging to a given `equippableGroupId` are + * equippable into the `Slot` associated with the `partId` of the collection at the specified `parentAddress`. + * @dev Emits ***ValidParentEquippableGroupIdSet*** event. + * @param equippableGroupId ID of the equippable group + * @param parentAddress Address of the parent into which the equippable group can be equipped into + * @param slotPartId ID of the `Slot` that the items belonging to the equippable group can be equipped into + */ + function _setValidParentForEquippableGroup( + uint64 equippableGroupId, + address parentAddress, + uint64 slotPartId + ) internal virtual { + if (equippableGroupId == uint64(0) || slotPartId == uint64(0)) + revert RMRKIdZeroForbidden(); + _validParentSlots[equippableGroupId][parentAddress] = slotPartId; + emit ValidParentEquippableGroupIdSet( + equippableGroupId, + slotPartId, + parentAddress + ); + } + + /** + * @inheritdoc IERC6220 + */ + function canTokenBeEquippedWithAssetIntoSlot( + address parent, + uint256 tokenId, + uint64 assetId, + uint64 slotId + ) public view virtual returns (bool canBeEquipped) { + uint64 equippableGroupId = _equippableGroupIds[assetId]; + uint64 equippableSlot = _validParentSlots[equippableGroupId][parent]; + if (equippableSlot == slotId) { + (, bool found) = getActiveAssets(tokenId).indexOf(assetId); + canBeEquipped = found; + } + } + + // --------------------- Getting Extended Assets --------------------- + + /** + * @inheritdoc IERC6220 + */ + function getAssetAndEquippableData( + uint256 tokenId, + uint64 assetId + ) + public + view + virtual + returns ( + string memory metadataURI, + uint64 equippableGroupId, + address catalogAddress, + uint64[] memory partIds + ) + { + metadataURI = getAssetMetadata(tokenId, assetId); + equippableGroupId = _equippableGroupIds[assetId]; + catalogAddress = _catalogAddresses[assetId]; + partIds = _partIds[assetId]; + } + + //////////////////////////////////////// + // UTILS + //////////////////////////////////////// + + /** + * @inheritdoc IERC6220 + */ + function getEquipment( + uint256 tokenId, + address targetCatalogAddress, + uint64 slotPartId + ) public view virtual returns (Equipment memory equipment) { + equipment = _equipments[tokenId][targetCatalogAddress][slotPartId]; + } + + /** + * @notice Checks the destination is valid for a Nest Transfer/Mint. + * @dev The destination must be a contract that implements the IERC7401 interface. + * @param to Address of the destination + */ + function _checkDestination(address to) internal view { + // Checking if it is a contract before calling it seems redundant, but otherwise it would revert with no error + if (to.code.length == 0) revert RMRKIsNotContract(); + if (!IERC165(to).supportsInterface(type(IERC7401).interfaceId)) + revert RMRKNestableTransferToNonRMRKNestableImplementer(); + } + + // HOOKS + + /** + * @notice A hook to be called before a equipping a asset to the token. + * @dev The `IntakeEquip` struct consist of the following data: + * [ + * tokenId, + * childIndex, + * assetId, + * slotPartId, + * childAssetId + * ] + * @param data The `IntakeEquip` struct containing data of the asset that is being equipped + */ + function _beforeEquip(IntakeEquip memory data) internal virtual {} + + /** + * @notice A hook to be called after equipping a asset to the token. + * @dev The `IntakeEquip` struct consist of the following data: + * [ + * tokenId, + * childIndex, + * assetId, + * slotPartId, + * childAssetId + * ] + * @param data The `IntakeEquip` struct containing data of the asset that was equipped + */ + function _afterEquip(IntakeEquip memory data) internal virtual {} + + /** + * @notice A hook to be called before unequipping a asset from the token. + * @param tokenId ID of the token from which the asset is being unequipped + * @param assetId ID of the asset being unequipped + * @param slotPartId ID of the slot from which the asset is being unequipped + */ + function _beforeUnequip( + uint256 tokenId, + uint64 assetId, + uint64 slotPartId + ) internal virtual {} + + /** + * @notice A hook to be called after unequipping a asset from the token. + * @param tokenId ID of the token from which the asset was unequipped + * @param assetId ID of the asset that was unequipped + * @param slotPartId ID of the slot from which the asset was unequipped + */ + function _afterUnequip( + uint256 tokenId, + uint64 assetId, + uint64 slotPartId + ) internal virtual {} +} diff --git a/contracts/mocks/RMRKMinifiedEquippableChildAutoAcceptMock.sol b/contracts/mocks/RMRKMinifiedEquippableChildAutoAcceptMock.sol new file mode 100644 index 00000000..4ae7934b --- /dev/null +++ b/contracts/mocks/RMRKMinifiedEquippableChildAutoAcceptMock.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.21; + +import { + RMRKMinifiedEquippableChildAutoAccept +} from "../RMRK/equippable/RMRKMinifiedEquippableChildAutoAccept.sol"; + +//Minimal public implementation of RMRKEquippable for testing. +contract RMRKMinifiedEquippableChildAutoAcceptMock is + RMRKMinifiedEquippableChildAutoAccept +{ + function safeMint(address to, uint256 tokenId) public { + _safeMint(to, tokenId, ""); + } + + function safeMint(address to, uint256 tokenId, bytes memory _data) public { + _safeMint(to, tokenId, _data); + } + + function mint(address to, uint256 tokenId) external { + _safeMint(to, tokenId, ""); + } + + function nestMint( + address to, + uint256 tokenId, + uint256 destinationId + ) external { + _nestMint(to, tokenId, destinationId, ""); + } + + function addAssetToToken( + uint256 tokenId, + uint64 assetId, + uint64 replacesAssetWithId + ) external { + _addAssetToToken(tokenId, assetId, replacesAssetWithId); + } + + function addEquippableAssetEntry( + uint64 id, + uint64 equippableGroupId, + address catalogAddress, + string memory metadataURI, + uint64[] calldata partIds + ) external { + _addAssetEntry( + id, + equippableGroupId, + catalogAddress, + metadataURI, + partIds + ); + } + + function setValidParentForEquippableGroup( + uint64 equippableGroupId, + address parentAddress, + uint64 partId + ) external { + _setValidParentForEquippableGroup( + equippableGroupId, + parentAddress, + partId + ); + } +}