From 67d5853fa12eb7bc882cacdd97c5be3c631863db Mon Sep 17 00:00:00 2001 From: steven2308 Date: Fri, 19 Apr 2024 11:18:11 -0500 Subject: [PATCH 1/4] Implements BulkWriter.transferAndEquip. The NFT to transfer can be equipped, nested or free. The destination must be an NFT. The NFT can be equipped into the destination. If destination slot is occupied, it will be unequipped. All these operations happen under a single transactions, given the right approvals. --- contracts/RMRK/utils/RMRKBulkWriter.sol | 128 +++++++++++++ docs/RMRK/utils/RMRKBulkWriter.md | 21 +++ test/bulkWriter.ts | 235 +++++++++++++++++++++++- 3 files changed, 382 insertions(+), 2 deletions(-) diff --git a/contracts/RMRK/utils/RMRKBulkWriter.sol b/contracts/RMRK/utils/RMRKBulkWriter.sol index a9ac0385..2ea39223 100644 --- a/contracts/RMRK/utils/RMRKBulkWriter.sol +++ b/contracts/RMRK/utils/RMRKBulkWriter.sol @@ -27,6 +27,14 @@ contract RMRKBulkWriter is Context { uint64 slotPartId; } + struct ParentData { + address parentCollection; + uint256 parentTokenId; + uint256 childIndexInParent; + uint64 parentEquippedAssetId; + uint64 parentEquippedSlotPartId; + } + /** * @notice Reverts if the caller is not the owner of the token. * @param collection Address of the collection that this contract is managing @@ -216,6 +224,126 @@ contract RMRKBulkWriter is Context { } } + /** + * @notice Transfers an NFT to and NFT and optionally equips it. + * @dev If the NFT is currently equipped, you MUST set full `parentData` and it will be unequipped before transfer. + * @dev If the NFT is currently nested, you MUST set `parentData` with `parentCollection`, `parentTokenId` and `childIndexInParent`. The child will be transferred directly to the destination NFT via `transferChild`. + * @dev If the NFT is not currently nested you MUST send empty `parentData` and the transfer will be done via `nestTransferFrom`. + * @dev If the destination contract does not have an auto accept mechanism for children, the child will be auto accepted. + * @dev If the `equipData` is set, the NFT will be equipped after the transfer. Mind that the child index equip data is the index after the transfer, so it MUST be set to the number of children the destination NFT has. + * @dev If equipping into destination, the `equipData.tokenId` will match the `destinationTokenId`. + * @dev This contract MUST have approval on the parent collection, if any, to transfer the NFT. + * @dev This contract MUST have approval for assets on the parent collection, if any, to unequip the NFT. + * @dev This contract MUST have approval on the NFT collection, if not nested, to transfer the NFT. + * @dev This contract MUST have approval on the destination collection to accept the NFT. + * @dev This contract MUST have approval for assets on the destination collection to equip the NFT. + * @param collection Address of the collection that this contract is managing + * @param tokenId ID of the token we are managing + * @param destinationCollection Address of the destination collection + * @param destinationTokenId ID of the destination token + * @param parentData A `ParentData` struct specifying the parent data + * @param equipData An `IntakeEquip` struct specifying the equip data + */ + function transferAndEquip( + address collection, + uint256 tokenId, + address destinationCollection, + uint256 destinationTokenId, + ParentData memory parentData, + IERC6220.IntakeEquip memory equipData + ) public onlyTokenOwner(collection, tokenId) { + if (parentData.parentCollection != address(0)) { + if ( + parentData.parentEquippedAssetId != 0 && + parentData.parentEquippedSlotPartId != 0 + ) { + IERC6220(parentData.parentCollection).unequip( + parentData.parentTokenId, + parentData.parentEquippedAssetId, + parentData.parentEquippedSlotPartId + ); + } + IERC7401(parentData.parentCollection).transferChild( + parentData.parentTokenId, + destinationCollection, + destinationTokenId, + parentData.childIndexInParent, + collection, + tokenId, + false, + "" + ); + } else { + IERC7401(collection).nestTransferFrom( + _msgSender(), + destinationCollection, + tokenId, + destinationTokenId, + "" + ); + } + + _acceptChildIfNecessary( + destinationCollection, + destinationTokenId, + collection, + tokenId + ); + + if (equipData.assetId == 0) return; + + _unequipSlotIfNecessary( + destinationCollection, + destinationTokenId, + equipData.assetId, + equipData.slotPartId + ); + + IERC6220(destinationCollection).equip(equipData); + } + + function _acceptChildIfNecessary( + address parentCollection, + uint256 parentId, + address childCollection, + uint256 childId + ) private { + IERC7401 destination = IERC7401(parentCollection); + IERC7401.Child[] memory children = destination.pendingChildrenOf( + parentId + ); + uint256 length = children.length; + for (uint256 i; i < length; ) { + IERC7401.Child memory child = children[i]; + if ( + child.contractAddress == childCollection && + child.tokenId == childId + ) { + destination.acceptChild(parentId, i, childCollection, childId); + return; + } + unchecked { + ++i; + } + } + } + + function _unequipSlotIfNecessary( + address collection, + uint256 tokenId, + uint64 assetId, + uint64 slotPartId + ) private { + IERC6220 targetCollection = IERC6220(collection); + (, , address catalogAddress, ) = targetCollection + .getAssetAndEquippableData(tokenId, assetId); + if ( + targetCollection + .getEquipment(tokenId, catalogAddress, slotPartId) + .childId != 0 + ) targetCollection.unequip(tokenId, assetId, slotPartId); + } + /** * @notice Validates that the caller is the owner of the token. * @dev Reverts if the caller is not the owner of the token. diff --git a/docs/RMRK/utils/RMRKBulkWriter.md b/docs/RMRK/utils/RMRKBulkWriter.md index 9cd65f99..f586bff6 100644 --- a/docs/RMRK/utils/RMRKBulkWriter.md +++ b/docs/RMRK/utils/RMRKBulkWriter.md @@ -85,6 +85,27 @@ function replaceEquip(address collection, IERC6220.IntakeEquip data) external no | collection | address | undefined | | data | IERC6220.IntakeEquip | undefined | +### transferAndEquip + +```solidity +function transferAndEquip(address collection, uint256 tokenId, address destinationCollection, uint256 destinationTokenId, RMRKBulkWriter.ParentData parentData, IERC6220.IntakeEquip equipData) external nonpayable +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| collection | address | undefined | +| tokenId | uint256 | undefined | +| destinationCollection | address | undefined | +| destinationTokenId | uint256 | undefined | +| parentData | RMRKBulkWriter.ParentData | undefined | +| equipData | IERC6220.IntakeEquip | undefined | + diff --git a/test/bulkWriter.ts b/test/bulkWriter.ts index 6ae0c85e..c7da1b55 100644 --- a/test/bulkWriter.ts +++ b/test/bulkWriter.ts @@ -26,7 +26,6 @@ import { setUpKanariaAsset, setUpGemAssets, } from './kanariaUtils'; -import { IERC6454 } from './interfaces'; // --------------- FIXTURES ----------------------- @@ -84,12 +83,13 @@ async function bulkWriterFixture() { }; } -describe('Advanced Equip Render Utils', async function () { +describe('Bulk writer', async function () { let owner: SignerWithAddress; let catalog: RMRKCatalogImpl; let kanaria: RMRKEquippableMock; let kanariaAddress: string; let gem: RMRKEquippableMock; + let gemAddress: string; let bulkWriter: RMRKBulkWriter; let bulkWritterAddress: string; let bulkWriterPerCollection: RMRKBulkWriterPerCollection; @@ -112,6 +112,7 @@ describe('Advanced Equip Render Utils', async function () { gemId3, } = await loadFixture(bulkWriterFixture)); kanariaAddress = await kanaria.getAddress(); + gemAddress = await gem.getAddress(); bulkWritterAddress = await bulkWriter.getAddress(); }); @@ -401,6 +402,236 @@ describe('Advanced Equip Render Utils', async function () { ).to.be.revertedWithCustomError(bulkWriter, 'RMRKCanOnlyDoBulkOperationsOnOwnedTokens'); }); }); + + describe('Unequip+Transfer+Accept+Unequip+Equip', async function () { + let kanariaId2: bigint; + let gemId4: bigint; + let defaultEquipData = { + tokenId: 0n, + childIndex: 0n, + assetId: 0n, + slotPartId: 0n, + childAssetId: 0n, + }; + beforeEach(async function () { + // Mint and configure a second kanaria + kanariaId2 = await mintFromMock(kanaria, owner.address); + await kanaria.addAssetToToken(kanariaId2, assetForKanariaFull, 0); + await kanaria.acceptAsset(kanariaId2, 0, assetForKanariaFull); + + // Mint and configure a gem into the second kanaria + gemId4 = await nestMintFromMock(gem, kanariaAddress, kanariaId2); + await gem.addAssetToToken(gemId4, assetForGemALeft, 0); + await gem.acceptAsset(gemId4, 0, assetForGemALeft); + + // Approve the bulk writer for all operations + await kanaria.setApprovalForAllForAssets(bulkWritterAddress, true); // For equip / unequip + await kanaria.setApprovalForAll(bulkWritterAddress, true); // For transfer and accepting children + }); + + it('can transfer and accept child', async function () { + // We unnest it first so it starts from EoA + await kanaria.transferChild( + kanariaId, + owner.address, + 0, + 0, + gemAddress, + gemId1, + false, + ethers.ZeroHash, + ); + await gem.setApprovalForAll(bulkWritterAddress, true); // For transfer and accepting children + + // Parent data must be empty in this case + const parentData = { + parentCollection: ethers.ZeroAddress, + parentTokenId: 0, + childIndexInParent: 0, + parentEquippedAssetId: 0, + parentEquippedSlotPartId: 0, + }; + + await bulkWriter.transferAndEquip( + gemAddress, + gemId1, + kanariaAddress, + kanariaId2, + parentData, + defaultEquipData, + ); + + expect(await gem.directOwnerOf(gemId1)).to.eql([kanariaAddress, kanariaId2, true]); + expect(await kanaria.childrenOf(kanariaId2)).to.eql([[gemId1, gemAddress]]); + }); + + it('cannot transfer and accept child if not owned', async function () { + const [, notOwner] = await ethers.getSigners(); + const parentData = { + parentCollection: kanariaAddress, + parentTokenId: kanariaId, + childIndexInParent: 0, + parentEquippedAssetId: 0, + parentEquippedSlotPartId: 0, + }; + + await expect( + bulkWriter + .connect(notOwner) + .transferAndEquip( + gemAddress, + gemId1, + kanariaAddress, + kanariaId2, + parentData, + defaultEquipData, + ), + ).to.be.revertedWithCustomError(bulkWriter, 'RMRKCanOnlyDoBulkOperationsOnOwnedTokens'); + }); + + it('can nest transfer and accept child', async function () { + const parentData = { + parentCollection: kanariaAddress, + parentTokenId: kanariaId, + childIndexInParent: 0, + parentEquippedAssetId: 0, + parentEquippedSlotPartId: 0, + }; + + await bulkWriter.transferAndEquip( + gemAddress, + gemId1, + kanariaAddress, + kanariaId2, + parentData, + defaultEquipData, + ); + + expect(await gem.directOwnerOf(gemId1)).to.eql([kanariaAddress, kanariaId2, true]); + expect(await kanaria.childrenOf(kanariaId2)).to.eql([[gemId1, gemAddress]]); + }); + + it('can unequip, nest transfer and accept child', async function () { + // The token we are moving is equipped + await kanaria.equip({ + tokenId: kanariaId, + childIndex: 0, + assetId: assetForKanariaFull, + slotPartId: slotIdGemLeft, + childAssetId: assetForGemALeft, + }); + + const parentData = { + parentCollection: kanariaAddress, + parentTokenId: kanariaId, + childIndexInParent: 0, + parentEquippedAssetId: assetForKanariaFull, + parentEquippedSlotPartId: slotIdGemLeft, + }; + + await bulkWriter.transferAndEquip( + gemAddress, + gemId1, + kanariaAddress, + kanariaId2, + parentData, + defaultEquipData, + ); + + expect(await gem.directOwnerOf(gemId1)).to.eql([kanariaAddress, kanariaId2, true]); + expect(await kanaria.childrenOf(kanariaId2)).to.eql([[gemId1, gemAddress]]); + }); + + it('can unequip, nest transfer, accept child and equip in new parent', async function () { + // The token we are moving is equipped + await kanaria.equip({ + tokenId: kanariaId, + childIndex: 0, + assetId: assetForKanariaFull, + slotPartId: slotIdGemLeft, + childAssetId: assetForGemALeft, + }); + + const parentData = { + parentCollection: kanariaAddress, + parentTokenId: kanariaId, + childIndexInParent: 0, + parentEquippedAssetId: assetForKanariaFull, + parentEquippedSlotPartId: slotIdGemLeft, + }; + const newEquipData = { + tokenId: kanariaId2, + childIndex: 0, + assetId: assetForKanariaFull, + slotPartId: slotIdGemLeft, + childAssetId: assetForGemALeft, + }; + + await bulkWriter.transferAndEquip( + gemAddress, + gemId1, + kanariaAddress, + kanariaId2, + parentData, + newEquipData, + ); + + expect(await gem.directOwnerOf(gemId1)).to.eql([kanariaAddress, kanariaId2, true]); + expect(await kanaria.childrenOf(kanariaId2)).to.eql([[gemId1, gemAddress]]); + expect(await kanaria.isChildEquipped(kanariaId2, gemAddress, gemId1)).to.be.true; + }); + + it('can unequip, nest transfer, accept child, unequip destination slot and equip in new parent', async function () { + // The token we are moving is equipped + await kanaria.equip({ + tokenId: kanariaId, + childIndex: 0, + assetId: assetForKanariaFull, + slotPartId: slotIdGemLeft, + childAssetId: assetForGemALeft, + }); + // The destination slot is also equipped, the bulk writer should unequip it first + await kanaria.acceptChild(kanariaId2, 0, gemAddress, gemId4); + await kanaria.equip({ + tokenId: kanariaId2, + childIndex: 0, + assetId: assetForKanariaFull, + slotPartId: slotIdGemLeft, + childAssetId: assetForGemALeft, + }); + + const parentData = { + parentCollection: kanariaAddress, + parentTokenId: kanariaId, + childIndexInParent: 0, + parentEquippedAssetId: assetForKanariaFull, + parentEquippedSlotPartId: slotIdGemLeft, + }; + const newEquipData = { + tokenId: kanariaId2, + childIndex: 1, + assetId: assetForKanariaFull, + slotPartId: slotIdGemLeft, + childAssetId: assetForGemALeft, + }; + + await bulkWriter.transferAndEquip( + gemAddress, + gemId1, + kanariaAddress, + kanariaId2, + parentData, + newEquipData, + ); + + expect(await gem.directOwnerOf(gemId1)).to.eql([kanariaAddress, kanariaId2, true]); + expect(await kanaria.childrenOf(kanariaId2)).to.eql([ + [gemId4, gemAddress], + [gemId1, gemAddress], + ]); + expect(await kanaria.isChildEquipped(kanariaId2, gemAddress, gemId1)).to.be.true; + }); + }); }); describe('With Bulk Writer Per Collection', async function () { From 85f9e55ba6182af4d809ae4d5edf18764570f96e Mon Sep 17 00:00:00 2001 From: steven2308 Date: Mon, 29 Apr 2024 11:24:58 -0500 Subject: [PATCH 2/4] Fix on catalog factory, transfer ownership to deployer. --- contracts/implementations/catalog/RMRKCatalogFactory.sol | 1 + test/implementations/catalogFactory.ts | 7 +++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/implementations/catalog/RMRKCatalogFactory.sol b/contracts/implementations/catalog/RMRKCatalogFactory.sol index 5887344d..5ae6b749 100644 --- a/contracts/implementations/catalog/RMRKCatalogFactory.sol +++ b/contracts/implementations/catalog/RMRKCatalogFactory.sol @@ -26,6 +26,7 @@ contract RMRKCatalogFactory { ) public returns (address) { RMRKCatalogImpl catalog = new RMRKCatalogImpl(metadataURI, type_); _deployerCatalogs[msg.sender].push(address(catalog)); + catalog.transferOwnership(msg.sender); emit CatalogDeployed(msg.sender, address(catalog)); return address(catalog); } diff --git a/test/implementations/catalogFactory.ts b/test/implementations/catalogFactory.ts index f874e2aa..c2045a70 100644 --- a/test/implementations/catalogFactory.ts +++ b/test/implementations/catalogFactory.ts @@ -32,11 +32,10 @@ describe('CatalogImpl', async () => { throw new Error('Catalog address not found'); } const catalog = await ethers.getContractAt('RMRKCatalogImpl', catalogAddress); - const metadataURI = await catalog.getMetadataURI(); - const mediaType = await catalog.getType(); - expect(metadataURI).to.equal('ipfs://catalogMetadata'); - expect(mediaType).to.equal('img/jpeg'); + expect(await catalog.getMetadataURI()).to.equal('ipfs://catalogMetadata'); + expect(await catalog.getType()).to.equal('img/jpeg'); + expect(await catalog.owner()).to.equal(deployer1.address); }); it('can get catalogs deployed by a deployer', async () => { From 1220e5225b7bfaa227e35a87b654570783b57107 Mon Sep 17 00:00:00 2001 From: steven2308 Date: Mon, 17 Jun 2024 11:26:57 +0200 Subject: [PATCH 3/4] Adds RmrkEquippable version where children are directly accepted. This saves about 40% of gas since accept operation is not needed, and pending children array is not used. It also saves 4.5% of contract size. --- .../RMRKMinifiedEquippableChildAutoAccept.sol | 2049 +++++++++++++++++ ...KMinifiedEquippableChildAutoAcceptMock.sol | 68 + 2 files changed, 2117 insertions(+) create mode 100644 contracts/RMRK/equippable/RMRKMinifiedEquippableChildAutoAccept.sol create mode 100644 contracts/mocks/RMRKMinifiedEquippableChildAutoAcceptMock.sol 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 + ); + } +} From 623fcc8d2abf01510ec190e4b6f22908ea3953db Mon Sep 17 00:00:00 2001 From: steven2308 Date: Mon, 17 Jun 2024 11:43:10 +0200 Subject: [PATCH 4/4] Bumps version to 2.5.6. --- CHANGELOG.md | 6 ++++++ contracts/RMRK/core/RMRKCore.sol | 2 +- package.json | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55ea7862..5aaa4d14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [2.5.6] - 2024-06-17 + +### Added + + - RMRKMinifiedEquippableChildAutoAcceptMock. Which bypasses accepting children. + ## [2.5.5] - 2024-04-10 ### Changed diff --git a/contracts/RMRK/core/RMRKCore.sol b/contracts/RMRK/core/RMRKCore.sol index 8bbe0293..768cc052 100644 --- a/contracts/RMRK/core/RMRKCore.sol +++ b/contracts/RMRK/core/RMRKCore.sol @@ -9,7 +9,7 @@ pragma solidity ^0.8.21; * @dev This is currently just a passthrough contract which allows for granular editing of base-level ERC721 functions. */ contract RMRKCore { - string private constant _VERSION = "2.5.5"; + string private constant _VERSION = "2.5.6"; bytes4 private constant _RMRK_INTERFACE = 0x524D524B; // "RMRK" in ASCII hex /** diff --git a/package.json b/package.json index 032ed27c..4099fc35 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rmrk-team/evm-contracts", - "version": "2.5.5", + "version": "2.5.6", "license": "Apache-2.0", "files": [ "contracts/RMRK/*",