2403 lines
91 KiB
Solidity
2403 lines
91 KiB
Solidity
// SPDX-License-Identifier: CC0-1.0
|
|
|
|
//Generally all interactions should propagate downstream
|
|
|
|
pragma solidity ^0.8.16;
|
|
|
|
import "./ICatalog.sol";
|
|
import "./IERC6220.sol";
|
|
import "./IERC6059.sol";
|
|
import "./library/EquippableLib.sol";
|
|
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
|
|
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
|
|
import "@openzeppelin/contracts/utils/Address.sol";
|
|
import "@openzeppelin/contracts/utils/Context.sol";
|
|
import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
|
|
|
|
error ApprovalForAssetsToCurrentOwner();
|
|
error ApproveForAssetsCallerIsNotOwnerNorApprovedForAll();
|
|
error AssetAlreadyExists();
|
|
error BadPriorityListLength();
|
|
error CatalogRequiredForParts();
|
|
error ChildAlreadyExists();
|
|
error ChildIndexOutOfRange();
|
|
error EquippableEquipNotAllowedByCatalog();
|
|
error ERC721AddressZeroIsNotaValidOwner();
|
|
error ERC721ApprovalToCurrentOwner();
|
|
error ERC721ApproveCallerIsNotOwnerNorApprovedForAll();
|
|
error ERC721ApproveToCaller();
|
|
error ERC721InvalidTokenId();
|
|
error ERC721MintToTheZeroAddress();
|
|
error ERC721NotApprovedOrOwner();
|
|
error ERC721TokenAlreadyMinted();
|
|
error ERC721TransferFromIncorrectOwner();
|
|
error ERC721TransferToNonReceiverImplementer();
|
|
error ERC721TransferToTheZeroAddress();
|
|
error IdZeroForbidden();
|
|
error IndexOutOfRange();
|
|
error IsNotContract();
|
|
error MaxPendingAssetsReached();
|
|
error MaxPendingChildrenReached();
|
|
error MaxRecursiveBurnsReached(address childContract, uint256 childId);
|
|
error MintToNonNestableImplementer();
|
|
error MustUnequipFirst();
|
|
error NestableTooDeep();
|
|
error NestableTransferToDescendant();
|
|
error NestableTransferToNonNestableImplementer();
|
|
error NestableTransferToSelf();
|
|
error NoAssetMatchingId();
|
|
error NotApprovedForAssetsOrOwner();
|
|
error NotApprovedOrDirectOwner();
|
|
error NotEquipped();
|
|
error PendingChildIndexOutOfRange();
|
|
error SlotAlreadyUsed();
|
|
error TargetAssetCannotReceiveSlot();
|
|
error TokenCannotBeEquippedWithAssetIntoSlot();
|
|
error TokenDoesNotHaveAsset();
|
|
error UnexpectedAssetId();
|
|
error UnexpectedChildId();
|
|
error UnexpectedNumberOfAssets();
|
|
error UnexpectedNumberOfChildren();
|
|
|
|
/**
|
|
* @title EquippableToken
|
|
* @author RMRK team
|
|
* @notice Smart contract of the Equippable module.
|
|
*/
|
|
contract EquippableToken is
|
|
Context,
|
|
IERC165,
|
|
IERC721,
|
|
IERC6059,
|
|
IERC6220
|
|
{
|
|
using Address for address;
|
|
using EquippableLib for uint64[];
|
|
|
|
// ----------------- ERC721 -------------
|
|
|
|
// 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;
|
|
|
|
// ----------------- MULTIASSETS -------------
|
|
|
|
/// Mapping of uint64 Ids to asset metadata
|
|
mapping(uint64 => string) private _assets;
|
|
|
|
/// Mapping of tokenId to new asset, to asset to be replaced
|
|
mapping(uint256 => mapping(uint64 => uint64)) private _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 => uint16[]) internal _activeAssetPriorities;
|
|
|
|
/// Mapping of tokenId to assetId to whether the token has this asset assigned
|
|
mapping(uint256 => mapping(uint64 => bool)) private _tokenAssets;
|
|
|
|
/// Mapping from owner to operator approvals for assets
|
|
mapping(address => mapping(address => bool))
|
|
private _operatorApprovalsForAssets;
|
|
|
|
/**
|
|
* @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;
|
|
|
|
// ------------------- NESTABLE --------------
|
|
|
|
uint256 private constant _MAX_LEVELS_TO_CHECK_FOR_INHERITANCE_LOOP = 100;
|
|
|
|
// Mapping from token ID to DirectOwner struct
|
|
mapping(uint256 => DirectOwner) private _directOwners;
|
|
|
|
// Mapping of tokenId to array of active children structs
|
|
mapping(uint256 => Child[]) private _activeChildren;
|
|
|
|
// Mapping of tokenId to array of pending children structs
|
|
mapping(uint256 => Child[]) private _pendingChildren;
|
|
|
|
// Mapping of child token address to child token ID to whether they are pending or active on any token
|
|
// We might have a first extra mapping from token ID, but since the same child cannot be nested into multiple tokens
|
|
// we can strip it for size/gas savings.
|
|
mapping(address => mapping(uint256 => uint256)) private _childIsInActive;
|
|
|
|
// ------------------- EQUIPPABLE --------------
|
|
|
|
/// Mapping of uint64 asset ID to corresponding catalog address.
|
|
mapping(uint64 => address) private _catalogAddresses;
|
|
/// Mapping of uint64 ID to asset object.
|
|
mapping(uint64 => uint64) private _equippableGroupIds;
|
|
/// Mapping of assetId to catalog parts applicable to this asset, both fixed and slot
|
|
mapping(uint64 => uint64[]) private _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)))
|
|
private _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 => uint8)))
|
|
private _equipCountPerChild;
|
|
|
|
/// Mapping of `equippableGroupId` to parent contract address and valid `slotId`.
|
|
mapping(uint64 => mapping(address => uint64)) private _validParentSlots;
|
|
|
|
// -------------------------- MODIFIERS ----------------------------
|
|
|
|
/**
|
|
* @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 is its direct owner.
|
|
* @param tokenId ID of the token to check
|
|
*/
|
|
modifier onlyApprovedOrDirectOwner(uint256 tokenId) {
|
|
_onlyApprovedOrDirectOwner(tokenId);
|
|
_;
|
|
}
|
|
|
|
/**
|
|
* @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);
|
|
_;
|
|
}
|
|
|
|
// --------------------- ERC721 GETTERS ---------------------
|
|
|
|
/**
|
|
* @notice Used to retrieve the root owner of the given token.
|
|
* @dev Root owner is always the externally owned account.
|
|
* @dev If the given token is owned by another token, it will recursively query the parent tokens until reaching the
|
|
* root owner.
|
|
* @param tokenId ID of the token for which the root owner is being retrieved
|
|
* @return address Address of the root owner of the given token
|
|
*/
|
|
function ownerOf(
|
|
uint256 tokenId
|
|
) public view virtual override(IERC6059, IERC721) returns (address) {
|
|
(address owner, uint256 ownerTokenId, bool isNft) = directOwnerOf(
|
|
tokenId
|
|
);
|
|
if (isNft) {
|
|
owner = IERC6059(owner).ownerOf(ownerTokenId);
|
|
}
|
|
return owner;
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc IERC165
|
|
*/
|
|
function supportsInterface(
|
|
bytes4 interfaceId
|
|
) public view virtual returns (bool) {
|
|
return
|
|
interfaceId == type(IERC165).interfaceId ||
|
|
interfaceId == type(IERC721).interfaceId ||
|
|
interfaceId == type(IERC5773).interfaceId ||
|
|
interfaceId == type(IERC6059).interfaceId ||
|
|
interfaceId == type(IERC6220).interfaceId;
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc IERC721
|
|
*/
|
|
function balanceOf(address owner) public view virtual returns (uint256) {
|
|
if (owner == address(0)) revert ERC721AddressZeroIsNotaValidOwner();
|
|
return _balances[owner];
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc IERC721
|
|
*/
|
|
function getApproved(
|
|
uint256 tokenId
|
|
) public view virtual returns (address) {
|
|
_requireMinted(tokenId);
|
|
|
|
return _tokenApprovals[tokenId][ownerOf(tokenId)];
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc IERC721
|
|
*/
|
|
function isApprovedForAll(
|
|
address owner,
|
|
address operator
|
|
) public view virtual returns (bool) {
|
|
return _operatorApprovals[owner][operator];
|
|
}
|
|
|
|
// --------------------- ERC721 SETTERS ---------------------
|
|
|
|
/**
|
|
* @inheritdoc IERC721
|
|
*/
|
|
function approve(address to, uint256 tokenId) public virtual {
|
|
address owner = ownerOf(tokenId);
|
|
if (to == owner) revert ERC721ApprovalToCurrentOwner();
|
|
|
|
if (_msgSender() != owner && !isApprovedForAll(owner, _msgSender()))
|
|
revert ERC721ApproveCallerIsNotOwnerNorApprovedForAll();
|
|
|
|
_approve(to, tokenId);
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc IERC721
|
|
*/
|
|
function setApprovalForAll(address operator, bool approved) public virtual {
|
|
if (_msgSender() == operator) revert ERC721ApproveToCaller();
|
|
_operatorApprovals[_msgSender()][operator] = approved;
|
|
emit ApprovalForAll(_msgSender(), operator, approved);
|
|
}
|
|
|
|
/**
|
|
* @notice Used to burn a given token.
|
|
* @param tokenId ID of the token to burn
|
|
*/
|
|
function burn(uint256 tokenId) public virtual {
|
|
burn(tokenId, 0);
|
|
}
|
|
|
|
/**
|
|
* @notice Used to burn a token.
|
|
* @dev When a token is burned, its children are recursively burned as well.
|
|
* @dev The approvals are cleared when the token is burned.
|
|
* @dev Requirements:
|
|
*
|
|
* - `tokenId` must exist.
|
|
* @dev Emits a {Transfer} event.
|
|
* @param tokenId ID of the token to burn
|
|
* @param maxChildrenBurns Maximum children to recursively burn
|
|
* @return uint256 The number of recursive burns it took to burn all of the children
|
|
*/
|
|
function burn(
|
|
uint256 tokenId,
|
|
uint256 maxChildrenBurns
|
|
) public virtual onlyApprovedOrDirectOwner(tokenId) returns (uint256) {
|
|
return _burn(tokenId, maxChildrenBurns);
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc IERC721
|
|
*/
|
|
function transferFrom(
|
|
address from,
|
|
address to,
|
|
uint256 tokenId
|
|
) public virtual onlyApprovedOrDirectOwner(tokenId) {
|
|
_transfer(from, to, tokenId);
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc IERC721
|
|
*/
|
|
function safeTransferFrom(
|
|
address from,
|
|
address to,
|
|
uint256 tokenId
|
|
) public virtual {
|
|
safeTransferFrom(from, to, tokenId, "");
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc IERC721
|
|
*/
|
|
function safeTransferFrom(
|
|
address from,
|
|
address to,
|
|
uint256 tokenId,
|
|
bytes memory data
|
|
) public virtual onlyApprovedOrDirectOwner(tokenId) {
|
|
_safeTransfer(from, to, tokenId, data);
|
|
}
|
|
|
|
// --------------------- ERC721 INTERNAL ---------------------
|
|
|
|
/**
|
|
* @notice Used to grant an approval to manage a given token.
|
|
* @dev Emits an {Approval} event.
|
|
* @param to Address to which the approval is being granted
|
|
* @param tokenId ID of the token for which the approval is being granted
|
|
*/
|
|
function _approve(address to, uint256 tokenId) internal virtual {
|
|
address owner = ownerOf(tokenId);
|
|
_tokenApprovals[tokenId][owner] = to;
|
|
emit Approval(owner, to, tokenId);
|
|
}
|
|
|
|
/**
|
|
* @notice Used to update the owner of the token and clear the approvals associated with the previous owner.
|
|
* @dev The `destinationId` should equal `0` if the new owner is an externally owned account.
|
|
* @param tokenId ID of the token being updated
|
|
* @param destinationId ID of the token to receive the given token
|
|
* @param to Address of account to receive the token
|
|
* @param isNft A boolean value signifying whether the new owner is a token (`true`) or externally owned account
|
|
* (`false`)
|
|
*/
|
|
function _updateOwnerAndClearApprovals(
|
|
uint256 tokenId,
|
|
uint256 destinationId,
|
|
address to,
|
|
bool isNft
|
|
) internal {
|
|
_directOwners[tokenId] = DirectOwner({
|
|
ownerAddress: to,
|
|
tokenId: destinationId,
|
|
isNft: isNft
|
|
});
|
|
|
|
// Clear approvals from the previous owner
|
|
_approve(address(0), tokenId);
|
|
_approveForAssets(address(0), tokenId);
|
|
}
|
|
|
|
/**
|
|
* @notice Used to enforce that the given token has been minted.
|
|
* @dev Reverts if the `tokenId` has not been minted yet.
|
|
* @dev The validation checks whether the owner of a given token is a `0x0` address and considers it not minted if
|
|
* it is. This means that both tokens that haven't been minted yet as well as the ones that have already been
|
|
* burned will cause the transaction to be reverted.
|
|
* @param tokenId ID of the token to check
|
|
*/
|
|
function _requireMinted(uint256 tokenId) internal view virtual {
|
|
if (!_exists(tokenId)) revert ERC721InvalidTokenId();
|
|
}
|
|
|
|
/**
|
|
* @notice Used to check whether the given token exists.
|
|
* @dev Tokens start existing when they are minted (`_mint`) and stop existing when they are burned (`_burn`).
|
|
* @param tokenId ID of the token being checked
|
|
* @return bool The boolean value signifying whether the token exists
|
|
*/
|
|
function _exists(uint256 tokenId) internal view virtual returns (bool) {
|
|
return _directOwners[tokenId].ownerAddress != address(0);
|
|
}
|
|
|
|
/**
|
|
* @notice Used to invoke {IERC721Receiver-onERC721Received} on a target address.
|
|
* @dev The call is not executed if the target address is not a contract.
|
|
* @param from Address representing the previous owner of the given token
|
|
* @param to Yarget address that will receive the tokens
|
|
* @param tokenId ID of the token to be transferred
|
|
* @param data Optional data to send along with the call
|
|
* @return bool Boolean value signifying whether the call correctly returned the expected magic value
|
|
*/
|
|
function _checkOnERC721Received(
|
|
address from,
|
|
address to,
|
|
uint256 tokenId,
|
|
bytes memory data
|
|
) private returns (bool) {
|
|
if (to.isContract()) {
|
|
try
|
|
IERC721Receiver(to).onERC721Received(
|
|
_msgSender(),
|
|
from,
|
|
tokenId,
|
|
data
|
|
)
|
|
returns (bytes4 retval) {
|
|
return retval == IERC721Receiver.onERC721Received.selector;
|
|
} catch (bytes memory reason) {
|
|
if (reason.length == 0) {
|
|
revert ERC721TransferToNonReceiverImplementer();
|
|
} else {
|
|
/// @solidity memory-safe-assembly
|
|
assembly {
|
|
revert(add(32, reason), mload(reason))
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @notice Used to safely mint a token to a specified address.
|
|
* @dev Requirements:
|
|
*
|
|
* - `tokenId` must not exist.
|
|
* - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
|
|
* @dev Emits a {Transfer} event.
|
|
* @param to Address to which to safely mint the gven token
|
|
* @param tokenId ID of the token to mint to the specified address
|
|
*/
|
|
function _safeMint(address to, uint256 tokenId) internal virtual {
|
|
_safeMint(to, tokenId, "");
|
|
}
|
|
|
|
/**
|
|
* @notice Used to safely mint the token to the specified address while passing the additional data to contract
|
|
* recipients.
|
|
* @param to Address to which to mint the token
|
|
* @param tokenId ID of the token to mint
|
|
* @param data Additional data to send with the tokens
|
|
*/
|
|
function _safeMint(
|
|
address to,
|
|
uint256 tokenId,
|
|
bytes memory data
|
|
) internal virtual {
|
|
_mint(to, tokenId);
|
|
if (!_checkOnERC721Received(address(0), to, tokenId, data))
|
|
revert ERC721TransferToNonReceiverImplementer();
|
|
}
|
|
|
|
/**
|
|
* @notice Used to mint a specified token to a given address.
|
|
* @dev WARNING: Usage of this method is discouraged, use {_safeMint} whenever possible.
|
|
* @dev Requirements:
|
|
*
|
|
* - `tokenId` must not exist.
|
|
* - `to` cannot be the zero address.
|
|
* @dev Emits a {Transfer} event.
|
|
* @param to Address to mint the token to
|
|
* @param tokenId ID of the token to mint
|
|
*/
|
|
function _mint(address to, uint256 tokenId) internal virtual {
|
|
_innerMint(to, tokenId, 0);
|
|
|
|
emit Transfer(address(0), to, tokenId);
|
|
emit NestTransfer(address(0), to, 0, 0, tokenId);
|
|
|
|
_afterTokenTransfer(address(0), to, tokenId);
|
|
_afterNestedTokenTransfer(address(0), to, 0, 0, tokenId);
|
|
}
|
|
|
|
/**
|
|
* @notice Used to mint a child token to a given parent token.
|
|
* @param to Address of the collection smart contract of the token into which to mint the child token
|
|
* @param tokenId ID of the token to mint
|
|
* @param destinationId ID of the token into which to mint the new child token
|
|
* @param data Additional data with no specified format, sent in the addChild call
|
|
*/
|
|
function _nestMint(
|
|
address to,
|
|
uint256 tokenId,
|
|
uint256 destinationId,
|
|
bytes memory data
|
|
) internal virtual {
|
|
// It seems redundant, but otherwise it would revert with no error
|
|
if (!to.isContract()) revert IsNotContract();
|
|
if (!IERC165(to).supportsInterface(type(IERC6059).interfaceId))
|
|
revert MintToNonNestableImplementer();
|
|
|
|
_innerMint(to, tokenId, destinationId);
|
|
_sendToNFT(address(0), to, 0, destinationId, tokenId, data);
|
|
}
|
|
|
|
/**
|
|
* @notice Used to mint a child token into a given parent token.
|
|
* @dev Requirements:
|
|
*
|
|
* - `to` cannot be the zero address.
|
|
* - `tokenId` must not exist.
|
|
* - `tokenId` must not be `0`.
|
|
* @param to Address of the collection smart contract of the token into which to mint the child token
|
|
* @param tokenId ID of the token to mint
|
|
* @param destinationId ID of the token into which to mint the new token
|
|
*/
|
|
function _innerMint(
|
|
address to,
|
|
uint256 tokenId,
|
|
uint256 destinationId
|
|
) private {
|
|
if (to == address(0)) revert ERC721MintToTheZeroAddress();
|
|
if (_exists(tokenId)) revert ERC721TokenAlreadyMinted();
|
|
if (tokenId == 0) revert IdZeroForbidden();
|
|
|
|
_beforeTokenTransfer(address(0), to, tokenId);
|
|
_beforeNestedTokenTransfer(address(0), to, 0, destinationId, tokenId);
|
|
|
|
_balances[to] += 1;
|
|
_directOwners[tokenId] = DirectOwner({
|
|
ownerAddress: to,
|
|
tokenId: destinationId,
|
|
isNft: destinationId != 0
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @notice Used to burn a token.
|
|
* @dev When a token is burned, its children are recursively burned as well.
|
|
* @dev The approvals are cleared when the token is burned.
|
|
* @dev Requirements:
|
|
*
|
|
* - `tokenId` must exist.
|
|
* @dev Emits a {Transfer} event.
|
|
* @dev Emits a {NestTransfer} event.
|
|
* @param tokenId ID of the token to burn
|
|
* @param maxChildrenBurns Maximum children to recursively burn
|
|
* @return uint256 The number of recursive burns it took to burn all of the children
|
|
*/
|
|
function _burn(
|
|
uint256 tokenId,
|
|
uint256 maxChildrenBurns
|
|
) internal virtual returns (uint256) {
|
|
(address immediateOwner, uint256 parentId, ) = directOwnerOf(tokenId);
|
|
address owner = ownerOf(tokenId);
|
|
_balances[immediateOwner] -= 1;
|
|
|
|
_beforeTokenTransfer(owner, address(0), tokenId);
|
|
_beforeNestedTokenTransfer(
|
|
immediateOwner,
|
|
address(0),
|
|
parentId,
|
|
0,
|
|
tokenId
|
|
);
|
|
|
|
_approve(address(0), tokenId);
|
|
_approveForAssets(address(0), tokenId);
|
|
|
|
Child[] memory children = childrenOf(tokenId);
|
|
|
|
delete _activeChildren[tokenId];
|
|
delete _pendingChildren[tokenId];
|
|
delete _tokenApprovals[tokenId][owner];
|
|
|
|
uint256 pendingRecursiveBurns;
|
|
uint256 totalChildBurns;
|
|
|
|
uint256 length = children.length; //gas savings
|
|
for (uint256 i; i < length; ) {
|
|
if (totalChildBurns >= maxChildrenBurns)
|
|
revert MaxRecursiveBurnsReached(
|
|
children[i].contractAddress,
|
|
children[i].tokenId
|
|
);
|
|
delete _childIsInActive[children[i].contractAddress][
|
|
children[i].tokenId
|
|
];
|
|
unchecked {
|
|
// At this point we know pendingRecursiveBurns must be at least 1
|
|
pendingRecursiveBurns = maxChildrenBurns - totalChildBurns;
|
|
}
|
|
// We substract one to the next level to count for the token being burned, then add it again on returns
|
|
// This is to allow the behavior of 0 recursive burns meaning only the current token is deleted.
|
|
totalChildBurns +=
|
|
IERC6059(children[i].contractAddress).burn(
|
|
children[i].tokenId,
|
|
pendingRecursiveBurns - 1
|
|
) +
|
|
1;
|
|
unchecked {
|
|
++i;
|
|
}
|
|
}
|
|
// Can't remove before burning child since child will call back to get root owner
|
|
delete _directOwners[tokenId];
|
|
|
|
_afterTokenTransfer(owner, address(0), tokenId);
|
|
_afterNestedTokenTransfer(
|
|
immediateOwner,
|
|
address(0),
|
|
parentId,
|
|
0,
|
|
tokenId
|
|
);
|
|
emit Transfer(owner, address(0), tokenId);
|
|
emit NestTransfer(immediateOwner, address(0), parentId, 0, tokenId);
|
|
|
|
return totalChildBurns;
|
|
}
|
|
|
|
/**
|
|
* @notice Used to safely transfer the token form `from` to `to`.
|
|
* @dev The function checks that contract recipients are aware of the ERC721 protocol to prevent tokens from being
|
|
* forever locked.
|
|
* @dev This internal function is equivalent to {safeTransferFrom}, and can be used to e.g. implement alternative
|
|
* mechanisms to perform token transfer, such as signature-based.
|
|
* @dev Requirements:
|
|
*
|
|
* - `from` cannot be the zero address.
|
|
* - `to` cannot be the zero address.
|
|
* - `tokenId` token must exist and be owned by `from`.
|
|
* - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
|
|
* @dev Emits a {Transfer} event.
|
|
* @param from Address of the account currently owning the given token
|
|
* @param to Address to transfer the token to
|
|
* @param tokenId ID of the token to transfer
|
|
* @param data Additional data with no specified format, sent in call to `to`
|
|
*/
|
|
function _safeTransfer(
|
|
address from,
|
|
address to,
|
|
uint256 tokenId,
|
|
bytes memory data
|
|
) internal virtual {
|
|
_transfer(from, to, tokenId);
|
|
if (!_checkOnERC721Received(from, to, tokenId, data))
|
|
revert ERC721TransferToNonReceiverImplementer();
|
|
}
|
|
|
|
/**
|
|
* @notice Used to transfer the token from `from` to `to`.
|
|
* @dev As opposed to {transferFrom}, this imposes no restrictions on msg.sender.
|
|
* @dev Requirements:
|
|
*
|
|
* - `to` cannot be the zero address.
|
|
* - `tokenId` token must be owned by `from`.
|
|
* @dev Emits a {Transfer} event.
|
|
* @param from Address of the account currently owning the given token
|
|
* @param to Address to transfer the token to
|
|
* @param tokenId ID of the token to transfer
|
|
*/
|
|
function _transfer(
|
|
address from,
|
|
address to,
|
|
uint256 tokenId
|
|
) internal virtual {
|
|
(address immediateOwner, uint256 parentId, ) = directOwnerOf(tokenId);
|
|
if (immediateOwner != from) revert ERC721TransferFromIncorrectOwner();
|
|
if (to == address(0)) revert ERC721TransferToTheZeroAddress();
|
|
|
|
_beforeTokenTransfer(from, to, tokenId);
|
|
_beforeNestedTokenTransfer(immediateOwner, to, parentId, 0, tokenId);
|
|
|
|
_balances[from] -= 1;
|
|
_updateOwnerAndClearApprovals(tokenId, 0, to, false);
|
|
_balances[to] += 1;
|
|
|
|
emit Transfer(from, to, tokenId);
|
|
emit NestTransfer(immediateOwner, to, parentId, 0, tokenId);
|
|
|
|
_afterTokenTransfer(from, to, tokenId);
|
|
_afterNestedTokenTransfer(immediateOwner, to, parentId, 0, tokenId);
|
|
}
|
|
|
|
// --------------------- NESTABLE GETTERS ---------------------
|
|
|
|
/**
|
|
* @notice Used to retrieve the immediate owner of the given token.
|
|
* @dev In the event the NFT is owned by an externally owned account, `tokenId` will be `0` and `isNft` will be
|
|
* `false`.
|
|
* @param tokenId ID of the token for which the immediate owner is being retrieved
|
|
* @return address Address of the immediate owner. If the token is owned by an externally owned account, its address
|
|
* will be returned. If the token is owned by another token, the parent token's collection smart contract address
|
|
* is returned
|
|
* @return uint256 Token ID of the immediate owner. If the immediate owner is an externally owned account, the value
|
|
* should be `0`
|
|
* @return bool A boolean value signifying whether the immediate owner is a token (`true`) or not (`false`)
|
|
*/
|
|
function directOwnerOf(
|
|
uint256 tokenId
|
|
) public view virtual returns (address, uint256, bool) {
|
|
DirectOwner memory owner = _directOwners[tokenId];
|
|
if (owner.ownerAddress == address(0)) revert ERC721InvalidTokenId();
|
|
|
|
return (owner.ownerAddress, owner.tokenId, owner.isNft);
|
|
}
|
|
|
|
/**
|
|
* @notice Used to retrieve the active child tokens of a given parent token.
|
|
* @dev Returns array of Child structs existing for parent token.
|
|
* @dev The Child struct consists of the following values:
|
|
* [
|
|
* tokenId,
|
|
* contractAddress
|
|
* ]
|
|
* @param parentId ID of the parent token for which to retrieve the active child tokens
|
|
* @return struct[] An array of Child structs containing the parent token's active child tokens
|
|
*/
|
|
|
|
function childrenOf(
|
|
uint256 parentId
|
|
) public view virtual returns (Child[] memory) {
|
|
Child[] memory children = _activeChildren[parentId];
|
|
return children;
|
|
}
|
|
|
|
/**
|
|
* @notice Used to retrieve the pending child tokens of a given parent token.
|
|
* @dev Returns array of pending Child structs existing for given parent.
|
|
* @dev The Child struct consists of the following values:
|
|
* [
|
|
* tokenId,
|
|
* contractAddress
|
|
* ]
|
|
* @param parentId ID of the parent token for which to retrieve the pending child tokens
|
|
* @return struct[] An array of Child structs containing the parent token's pending child tokens
|
|
*/
|
|
|
|
function pendingChildrenOf(
|
|
uint256 parentId
|
|
) public view virtual returns (Child[] memory) {
|
|
Child[] memory pendingChildren = _pendingChildren[parentId];
|
|
return pendingChildren;
|
|
}
|
|
|
|
/**
|
|
* @notice Used to retrieve a specific active child token for a given parent token.
|
|
* @dev Returns a single Child struct locating at `index` of parent token's active child tokens array.
|
|
* @dev The Child struct consists of the following values:
|
|
* [
|
|
* tokenId,
|
|
* contractAddress
|
|
* ]
|
|
* @param parentId ID of the parent token for which the child is being retrieved
|
|
* @param index Index of the child token in the parent token's active child tokens array
|
|
* @return struct A Child struct containing data about the specified child
|
|
*/
|
|
function childOf(
|
|
uint256 parentId,
|
|
uint256 index
|
|
) public view virtual returns (Child memory) {
|
|
if (childrenOf(parentId).length <= index) revert ChildIndexOutOfRange();
|
|
Child memory child = _activeChildren[parentId][index];
|
|
return child;
|
|
}
|
|
|
|
/**
|
|
* @notice Used to retrieve a specific pending child token from a given parent token.
|
|
* @dev Returns a single Child struct locating at `index` of parent token's active child tokens array.
|
|
* @dev The Child struct consists of the following values:
|
|
* [
|
|
* tokenId,
|
|
* contractAddress
|
|
* ]
|
|
* @param parentId ID of the parent token for which the pending child token is being retrieved
|
|
* @param index Index of the child token in the parent token's pending child tokens array
|
|
* @return struct A Child struct containting data about the specified child
|
|
*/
|
|
function pendingChildOf(
|
|
uint256 parentId,
|
|
uint256 index
|
|
) public view virtual returns (Child memory) {
|
|
if (pendingChildrenOf(parentId).length <= index)
|
|
revert PendingChildIndexOutOfRange();
|
|
Child memory child = _pendingChildren[parentId][index];
|
|
return child;
|
|
}
|
|
|
|
/**
|
|
* @notice Used to verify that the given child tokwn is included in an active array of a token.
|
|
* @param childAddress Address of the given token's collection smart contract
|
|
* @param childId ID of the child token being checked
|
|
* @return bool A boolean value signifying whether the given child token is included in an active child tokens array
|
|
* of a token (`true`) or not (`false`)
|
|
*/
|
|
function childIsInActive(
|
|
address childAddress,
|
|
uint256 childId
|
|
) public view virtual returns (bool) {
|
|
return _childIsInActive[childAddress][childId] != 0;
|
|
}
|
|
|
|
// --------------------- NESTABLE SETTERS ---------------------
|
|
/**
|
|
* @notice Used to add a child token to a given parent token.
|
|
* @dev This adds the iichild token into the given parent token's pending child tokens array.
|
|
* @dev You MUST NOT call this method directly. To add a a child to an NFT you must use either
|
|
* `nestTransfer`, `nestMint` or `transferChild` to the NFT.
|
|
* @dev Requirements:
|
|
*
|
|
* - `ownerOf` on the child contract must resolve to the called contract.
|
|
* - The pending array of the parent contract must not be full.
|
|
* @param parentId ID of the parent token to receive the new child token
|
|
* @param childId ID of the new proposed child token
|
|
* @param data Additional data with no specified format
|
|
*/
|
|
function addChild(
|
|
uint256 parentId,
|
|
uint256 childId,
|
|
bytes memory data
|
|
) public virtual {
|
|
_requireMinted(parentId);
|
|
|
|
address childAddress = _msgSender();
|
|
if (!childAddress.isContract()) revert IsNotContract();
|
|
|
|
Child memory child = Child({
|
|
contractAddress: childAddress,
|
|
tokenId: childId
|
|
});
|
|
|
|
_beforeAddChild(parentId, childAddress, childId);
|
|
|
|
uint256 length = pendingChildrenOf(parentId).length;
|
|
|
|
if (length < 128) {
|
|
_pendingChildren[parentId].push(child);
|
|
} else {
|
|
revert MaxPendingChildrenReached();
|
|
}
|
|
|
|
// Previous length matches the index for the new child
|
|
emit ChildProposed(parentId, length, childAddress, childId);
|
|
|
|
_afterAddChild(parentId, childAddress, childId);
|
|
}
|
|
|
|
/**
|
|
* @notice @notice Used to accept a pending child token for a given parent token.
|
|
* @dev This moves the child token from parent token's pending child tokens array into the active child tokens
|
|
* array.
|
|
* @param parentId ID of the parent token for which the child token is being accepted
|
|
* @param childIndex Index of a child tokem in the given parent's pending children array
|
|
* @param childAddress Address of the collection smart contract of the child token expected to be located at the
|
|
* specified index of the given parent token's pending children array
|
|
* @param childId ID of the child token expected to be located at the specified index of the given parent token's
|
|
* pending children array
|
|
*/
|
|
function acceptChild(
|
|
uint256 parentId,
|
|
uint256 childIndex,
|
|
address childAddress,
|
|
uint256 childId
|
|
) public virtual onlyApprovedOrOwner(parentId) {
|
|
_acceptChild(parentId, childIndex, childAddress, childId);
|
|
}
|
|
|
|
/**
|
|
* @notice Used to reject all pending children of a given parent token.
|
|
* @dev Removes the children from the pending array mapping.
|
|
* @dev This does not update the ownership storage data on children. If necessary, ownership can be reclaimed by the
|
|
* rootOwner of the previous parent.
|
|
* @param tokenId ID of the parent token for which to reject all of the pending tokens
|
|
*/
|
|
function rejectAllChildren(
|
|
uint256 tokenId,
|
|
uint256 maxRejections
|
|
) public virtual onlyApprovedOrOwner(tokenId) {
|
|
_rejectAllChildren(tokenId, maxRejections);
|
|
}
|
|
|
|
/**
|
|
* @notice Used to transfer a child token from a given parent token.
|
|
* @param tokenId ID of the parent token from which the child token is being transferred
|
|
* @param to Address to which to transfer the token to
|
|
* @param destinationId ID of the token to receive this child token (MUST be 0 if the destination is not a token)
|
|
* @param childIndex Index of a token we are transferring, in the array it belongs to (can be either active array or
|
|
* pending array)
|
|
* @param childAddress Address of the child token's collection smart contract.
|
|
* @param childId ID of the child token in its own collection smart contract.
|
|
* @param isPending A boolean value indicating whether the child token being transferred is in the pending array of the
|
|
* parent token (`true`) or in the active array (`false`)
|
|
* @param data Additional data with no specified format, sent in call to `_to`
|
|
*/
|
|
function transferChild(
|
|
uint256 tokenId,
|
|
address to,
|
|
uint256 destinationId,
|
|
uint256 childIndex,
|
|
address childAddress,
|
|
uint256 childId,
|
|
bool isPending,
|
|
bytes memory data
|
|
) public virtual onlyApprovedOrOwner(tokenId) {
|
|
_transferChild(
|
|
tokenId,
|
|
to,
|
|
destinationId,
|
|
childIndex,
|
|
childAddress,
|
|
childId,
|
|
isPending,
|
|
data
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @notice Used to transfer the token into another token.
|
|
* @dev The destination token MUST NOT be a child token of the token being transferred or one of its downstream
|
|
* child tokens.
|
|
* @param from Address of the direct owner of the token to be transferred
|
|
* @param to Address of the receiving token's collection smart contract
|
|
* @param tokenId ID of the token being transferred
|
|
* @param destinationId ID of the token to receive the token being transferred
|
|
*/
|
|
function nestTransferFrom(
|
|
address from,
|
|
address to,
|
|
uint256 tokenId,
|
|
uint256 destinationId,
|
|
bytes memory data
|
|
) public virtual onlyApprovedOrDirectOwner(tokenId) {
|
|
_nestTransfer(from, to, tokenId, destinationId, data);
|
|
}
|
|
|
|
// --------------------- NESTABLE INTERNAL ---------------------
|
|
|
|
/**
|
|
* @notice Used to transfer a child token from a given parent token.
|
|
* @dev When transferring a child token, the owner of the token is set to `to`, or is not updated in the event of `to`
|
|
* being the `0x0` address.
|
|
* @dev Requirements:
|
|
*
|
|
* - `tokenId` must exist.
|
|
* @dev Emits {ChildTransferred} event.
|
|
* @param tokenId ID of the parent token from which the child token is being transferred
|
|
* @param to Address to which to transfer the token to
|
|
* @param destinationId ID of the token to receive this child token (MUST be 0 if the destination is not a token)
|
|
* @param childIndex Index of a token we are transferring, in the array it belongs to (can be either active array or
|
|
* pending array)
|
|
* @param childAddress Address of the child token's collection smart contract.
|
|
* @param childId ID of the child token in its own collection smart contract.
|
|
* @param isPending A boolean value indicating whether the child token being transferred is in the pending array of the
|
|
* parent token (`true`) or in the active array (`false`)
|
|
* @param data Additional data with no specified format, sent in call to `_to`
|
|
*/
|
|
function _transferChild(
|
|
uint256 tokenId,
|
|
address to,
|
|
uint256 destinationId, // newParentId
|
|
uint256 childIndex,
|
|
address childAddress,
|
|
uint256 childId,
|
|
bool isPending,
|
|
bytes memory data
|
|
) internal virtual {
|
|
Child memory child;
|
|
if (!isPending) {
|
|
if (isChildEquipped(tokenId, childAddress, childId))
|
|
revert MustUnequipFirst();
|
|
}
|
|
if (isPending) {
|
|
child = pendingChildOf(tokenId, childIndex);
|
|
} else {
|
|
child = childOf(tokenId, childIndex);
|
|
}
|
|
_checkExpectedChild(child, childAddress, childId);
|
|
|
|
_beforeTransferChild(
|
|
tokenId,
|
|
childIndex,
|
|
childAddress,
|
|
childId,
|
|
isPending
|
|
);
|
|
|
|
if (isPending) {
|
|
_removeChildByIndex(_pendingChildren[tokenId], childIndex);
|
|
} else {
|
|
delete _childIsInActive[childAddress][childId];
|
|
_removeChildByIndex(_activeChildren[tokenId], childIndex);
|
|
}
|
|
|
|
if (to != address(0)) {
|
|
if (destinationId == 0) {
|
|
IERC721(childAddress).safeTransferFrom(
|
|
address(this),
|
|
to,
|
|
childId,
|
|
data
|
|
);
|
|
} else {
|
|
// Destination is an NFT
|
|
IERC6059(child.contractAddress).nestTransferFrom(
|
|
address(this),
|
|
to,
|
|
child.tokenId,
|
|
destinationId,
|
|
data
|
|
);
|
|
}
|
|
}
|
|
|
|
emit ChildTransferred(
|
|
tokenId,
|
|
childIndex,
|
|
childAddress,
|
|
childId,
|
|
isPending
|
|
);
|
|
_afterTransferChild(
|
|
tokenId,
|
|
childIndex,
|
|
childAddress,
|
|
childId,
|
|
isPending
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @notice Used to accept a pending child token for a given parent token.
|
|
* @dev This moves the child token from parent token's pending child tokens array into the active child tokens
|
|
* array.
|
|
* @dev Requirements:
|
|
*
|
|
* - `tokenId` must exist
|
|
* - `index` must be in range of the pending children array
|
|
* @param parentId ID of the parent token for which the child token is being accepted
|
|
* @param childIndex Index of a child tokem in the given parent's pending children array
|
|
* @param childAddress Address of the collection smart contract of the child token expected to be located at the
|
|
* specified index of the given parent token's pending children array
|
|
* @param childId ID of the child token expected to be located at the specified index of the given parent token's
|
|
* pending children array
|
|
*/
|
|
function _acceptChild(
|
|
uint256 parentId,
|
|
uint256 childIndex,
|
|
address childAddress,
|
|
uint256 childId
|
|
) internal virtual {
|
|
if (pendingChildrenOf(parentId).length <= childIndex)
|
|
revert PendingChildIndexOutOfRange();
|
|
|
|
Child memory child = pendingChildOf(parentId, childIndex);
|
|
_checkExpectedChild(child, childAddress, childId);
|
|
if (_childIsInActive[childAddress][childId] != 0)
|
|
revert ChildAlreadyExists();
|
|
|
|
_beforeAcceptChild(parentId, childIndex, childAddress, childId);
|
|
|
|
// Remove from pending:
|
|
_removeChildByIndex(_pendingChildren[parentId], childIndex);
|
|
|
|
// Add to active:
|
|
_activeChildren[parentId].push(child);
|
|
_childIsInActive[childAddress][childId] = 1; // We use 1 as true
|
|
|
|
emit ChildAccepted(parentId, childIndex, childAddress, childId);
|
|
|
|
_afterAcceptChild(parentId, childIndex, childAddress, childId);
|
|
}
|
|
|
|
/**
|
|
* @notice Used to reject all pending children of a given parent token.
|
|
* @dev Removes the children from the pending array mapping.
|
|
* @dev This does not update the ownership storage data on children. If necessary, ownership can be reclaimed by the
|
|
* rootOwner of the previous parent.
|
|
* @dev Requirements:
|
|
*
|
|
* - `tokenId` must exist
|
|
* @param tokenId ID of the parent token for which to reject all of the pending tokens.
|
|
* @param maxRejections Maximum number of expected children to reject, used to prevent from
|
|
* rejecting children which arrive just before this operation.
|
|
*/
|
|
function _rejectAllChildren(
|
|
uint256 tokenId,
|
|
uint256 maxRejections
|
|
) internal virtual {
|
|
if (_pendingChildren[tokenId].length > maxRejections)
|
|
revert UnexpectedNumberOfChildren();
|
|
|
|
_beforeRejectAllChildren(tokenId);
|
|
delete _pendingChildren[tokenId];
|
|
emit AllChildrenRejected(tokenId);
|
|
_afterRejectAllChildren(tokenId);
|
|
}
|
|
|
|
function _checkExpectedChild(
|
|
Child memory child,
|
|
address expectedAddress,
|
|
uint256 expectedId
|
|
) private pure {
|
|
if (
|
|
expectedAddress != child.contractAddress ||
|
|
expectedId != child.tokenId
|
|
) revert UnexpectedChildId();
|
|
}
|
|
|
|
/**
|
|
* @notice Used to remove a specified child token form an array using its index within said array.
|
|
* @dev The caller must ensure that the length of the array is valid compared to the index passed.
|
|
* @dev The Child struct consists of the following values:
|
|
* [
|
|
* tokenId,
|
|
* contractAddress
|
|
* ]
|
|
* @param array An array od Child struct containing info about the child tokens in a given child tokens array
|
|
* @param index An index of the child token to remove in the accompanying array
|
|
*/
|
|
function _removeChildByIndex(Child[] storage array, uint256 index) private {
|
|
array[index] = array[array.length - 1];
|
|
array.pop();
|
|
}
|
|
|
|
/**
|
|
* @notice Used to transfer a token into another token.
|
|
* @dev Attempting to nest a token into `0x0` address will result in reverted transaction.
|
|
* @dev Attempting to nest a token into itself will result in reverted transaction.
|
|
* @param from Address of the account currently owning the given token
|
|
* @param to Address of the receiving token's collection smart contract
|
|
* @param tokenId ID of the token to transfer
|
|
* @param destinationId ID of the token receiving the given token
|
|
* @param data Additional data with no specified format, sent in the addChild call
|
|
*/
|
|
function _nestTransfer(
|
|
address from,
|
|
address to,
|
|
uint256 tokenId,
|
|
uint256 destinationId,
|
|
bytes memory data
|
|
) internal virtual {
|
|
(address immediateOwner, uint256 parentId, ) = directOwnerOf(tokenId);
|
|
if (immediateOwner != from) revert ERC721TransferFromIncorrectOwner();
|
|
if (to == address(0)) revert ERC721TransferToTheZeroAddress();
|
|
if (to == address(this) && tokenId == destinationId)
|
|
revert NestableTransferToSelf();
|
|
|
|
// Destination contract checks:
|
|
// It seems redundant, but otherwise it would revert with no error
|
|
if (!to.isContract()) revert IsNotContract();
|
|
if (!IERC165(to).supportsInterface(type(IERC6059).interfaceId))
|
|
revert NestableTransferToNonNestableImplementer();
|
|
_checkForInheritanceLoop(tokenId, to, destinationId);
|
|
|
|
_beforeTokenTransfer(from, to, tokenId);
|
|
_beforeNestedTokenTransfer(
|
|
immediateOwner,
|
|
to,
|
|
parentId,
|
|
destinationId,
|
|
tokenId
|
|
);
|
|
_balances[from] -= 1;
|
|
_updateOwnerAndClearApprovals(tokenId, destinationId, to, true);
|
|
_balances[to] += 1;
|
|
|
|
// Sending to NFT:
|
|
_sendToNFT(immediateOwner, to, parentId, destinationId, tokenId, data);
|
|
}
|
|
|
|
/**
|
|
* @notice Used to send a token to another token.
|
|
* @dev If the token being sent is currently owned by an externally owned account, the `parentId` should equal `0`.
|
|
* @dev Emits {Transfer} event.
|
|
* @dev Emits {NestTransfer} event.
|
|
* @param from Address from which the token is being sent
|
|
* @param to Address of the collection smart contract of the token to receive the given token
|
|
* @param parentId ID of the current parent token of the token being sent
|
|
* @param destinationId ID of the tokento receive the token being sent
|
|
* @param tokenId ID of the token being sent
|
|
* @param data Additional data with no specified format, sent in the addChild call
|
|
*/
|
|
function _sendToNFT(
|
|
address from,
|
|
address to,
|
|
uint256 parentId,
|
|
uint256 destinationId,
|
|
uint256 tokenId,
|
|
bytes memory data
|
|
) private {
|
|
IERC6059 destContract = IERC6059(to);
|
|
destContract.addChild(destinationId, tokenId, data);
|
|
_afterTokenTransfer(from, to, tokenId);
|
|
_afterNestedTokenTransfer(from, to, parentId, destinationId, tokenId);
|
|
|
|
emit Transfer(from, to, tokenId);
|
|
emit NestTransfer(from, to, parentId, destinationId, tokenId);
|
|
}
|
|
|
|
/**
|
|
* @notice Used to check if nesting a given token into a specified token would create an inheritance loop.
|
|
* @dev If a loop would occur, the tokens would be unmanageable, so the execution is reverted if one is detected.
|
|
* @dev The check for inheritance loop is bounded to guard against too much gas being consumed.
|
|
* @param currentId ID of the token that would be nested
|
|
* @param targetContract Address of the collection smart contract of the token into which the given token would be
|
|
* nested
|
|
* @param targetId ID of the token into which the given token would be nested
|
|
*/
|
|
function _checkForInheritanceLoop(
|
|
uint256 currentId,
|
|
address targetContract,
|
|
uint256 targetId
|
|
) private view {
|
|
for (uint256 i; i < _MAX_LEVELS_TO_CHECK_FOR_INHERITANCE_LOOP; ) {
|
|
(
|
|
address nextOwner,
|
|
uint256 nextOwnerTokenId,
|
|
bool isNft
|
|
) = IERC6059(targetContract).directOwnerOf(targetId);
|
|
// If there's a final address, we're good. There's no loop.
|
|
if (!isNft) {
|
|
return;
|
|
}
|
|
// Ff the current nft is an ancestor at some point, there is an inheritance loop
|
|
if (nextOwner == address(this) && nextOwnerTokenId == currentId) {
|
|
revert NestableTransferToDescendant();
|
|
}
|
|
// We reuse the parameters to save some contract size
|
|
targetContract = nextOwner;
|
|
targetId = nextOwnerTokenId;
|
|
unchecked {
|
|
++i;
|
|
}
|
|
}
|
|
revert NestableTooDeep();
|
|
}
|
|
|
|
// --------------------- MULTIASSET GETTERS ---------------------
|
|
|
|
/**
|
|
* @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 address Address of the account that is approved to manage the token
|
|
*/
|
|
function getApprovedForAssets(
|
|
uint256 tokenId
|
|
) public view virtual returns (address) {
|
|
_requireMinted(tokenId);
|
|
return _tokenApprovalsForAssets[tokenId][ownerOf(tokenId)];
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc IERC5773
|
|
*/
|
|
function getAssetMetadata(
|
|
uint256 tokenId,
|
|
uint64 assetId
|
|
) public view virtual returns (string memory) {
|
|
if (!_tokenAssets[tokenId][assetId]) revert TokenDoesNotHaveAsset();
|
|
return _assets[assetId];
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc IERC5773
|
|
*/
|
|
function getActiveAssets(
|
|
uint256 tokenId
|
|
) public view virtual returns (uint64[] memory) {
|
|
return _activeAssets[tokenId];
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc IERC5773
|
|
*/
|
|
function getPendingAssets(
|
|
uint256 tokenId
|
|
) public view virtual returns (uint64[] memory) {
|
|
return _pendingAssets[tokenId];
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc IERC5773
|
|
*/
|
|
function getActiveAssetPriorities(
|
|
uint256 tokenId
|
|
) public view virtual returns (uint16[] memory) {
|
|
return _activeAssetPriorities[tokenId];
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc IERC5773
|
|
*/
|
|
function getAssetReplacements(
|
|
uint256 tokenId,
|
|
uint64 newAssetId
|
|
) public view virtual returns (uint64) {
|
|
return _assetReplacements[tokenId][newAssetId];
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc IERC5773
|
|
*/
|
|
function isApprovedForAllForAssets(
|
|
address owner,
|
|
address operator
|
|
) public view virtual returns (bool) {
|
|
return _operatorApprovalsForAssets[owner][operator];
|
|
}
|
|
|
|
// --------------------- MULTIASSET SETTERS ---------------------
|
|
/**
|
|
* @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 ApprovalForAssetsToCurrentOwner();
|
|
|
|
if (
|
|
_msgSender() != owner &&
|
|
!isApprovedForAllForAssets(owner, _msgSender())
|
|
) revert ApproveForAssetsCallerIsNotOwnerNorApprovedForAll();
|
|
_approveForAssets(to, tokenId);
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc IERC5773
|
|
*/
|
|
function setApprovalForAllForAssets(
|
|
address operator,
|
|
bool approved
|
|
) public virtual {
|
|
address owner = _msgSender();
|
|
if (owner == operator) revert ApprovalForAssetsToCurrentOwner();
|
|
|
|
_operatorApprovalsForAssets[owner][operator] = approved;
|
|
emit ApprovalForAllForAssets(owner, operator, approved);
|
|
}
|
|
|
|
/**
|
|
* @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
|
|
*/
|
|
function acceptAsset(
|
|
uint256 tokenId,
|
|
uint256 index,
|
|
uint64 assetId
|
|
) public virtual onlyApprovedForAssetsOrOwner(tokenId) {
|
|
_acceptAsset(tokenId, index, assetId);
|
|
}
|
|
|
|
/**
|
|
* @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
|
|
*/
|
|
function rejectAsset(
|
|
uint256 tokenId,
|
|
uint256 index,
|
|
uint64 assetId
|
|
) public virtual onlyApprovedForAssetsOrOwner(tokenId) {
|
|
_rejectAsset(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) {
|
|
_rejectAllAssets(tokenId, maxRejections);
|
|
}
|
|
|
|
/**
|
|
* @notice Sets a new priority array for a given token.
|
|
* @dev The priority array is a non-sequential list of `uint16`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,
|
|
uint16[] calldata priorities
|
|
) public virtual onlyApprovedForAssetsOrOwner(tokenId) {
|
|
_setPriority(tokenId, priorities);
|
|
}
|
|
|
|
// --------------------- MULTIASSET INTERNAL ---------------------
|
|
|
|
/**
|
|
* @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);
|
|
}
|
|
|
|
/**
|
|
* @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[] calldata partIds
|
|
) internal virtual {
|
|
_addAssetEntry(id, metadataURI);
|
|
|
|
if (catalogAddress == address(0) && partIds.length != 0)
|
|
revert CatalogRequiredForParts();
|
|
|
|
_catalogAddresses[id] = catalogAddress;
|
|
_equippableGroupIds[id] = equippableGroupId;
|
|
_partIds[id] = partIds;
|
|
}
|
|
|
|
/**
|
|
* @notice Used to accept a pending asset.
|
|
* @dev The call is reverted if there is no pending asset at a given index.
|
|
* @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(
|
|
uint16(_activeAssets[tokenId].length)
|
|
);
|
|
_activeAssets[tokenId].push(assetId);
|
|
replacesId = uint64(0);
|
|
}
|
|
_removePendingAsset(tokenId, index, assetId);
|
|
|
|
emit AssetAccepted(tokenId, assetId, replacesId);
|
|
_afterAcceptAsset(tokenId, index, assetId);
|
|
}
|
|
|
|
/**
|
|
* @notice Used to reject the specified asset from the pending array.
|
|
* @dev The call is reverted if there is no pending asset at a given index.
|
|
* @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 expected to be in the index
|
|
*/
|
|
function _rejectAsset(
|
|
uint256 tokenId,
|
|
uint256 index,
|
|
uint64 assetId
|
|
) internal virtual {
|
|
_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 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 IndexOutOfRange();
|
|
if (assetId != _pendingAssets[tokenId][index])
|
|
revert UnexpectedAssetId();
|
|
}
|
|
|
|
/**
|
|
* @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 reject all of the pending assets for the given token.
|
|
* @dev When rejecting all assets, the pending array is indiscriminately cleared.
|
|
* @dev If the number of pending assets is greater than the value of `maxRejections`, the exectuion will be
|
|
* reverted.
|
|
* @param tokenId ID of the token to reject all of the pending assets.
|
|
* @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
|
|
) internal virtual {
|
|
uint256 len = _pendingAssets[tokenId].length;
|
|
if (len > maxRejections) revert UnexpectedNumberOfAssets();
|
|
|
|
_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 Used to specify the priorities for a given token's active assets.
|
|
* @dev If the length of the priorities array doesn't match the length of the active assets array, the execution
|
|
* will be reverted.
|
|
* @dev The position of the priority value in the array corresponds the position of the asset in the active
|
|
* assets array it will be applied to.
|
|
* @param tokenId ID of the token for which the priorities are being set
|
|
* @param priorities Array of priorities for the assets
|
|
*/
|
|
function _setPriority(
|
|
uint256 tokenId,
|
|
uint16[] calldata priorities
|
|
) internal virtual {
|
|
uint256 length = priorities.length;
|
|
if (length != _activeAssets[tokenId].length)
|
|
revert BadPriorityListLength();
|
|
|
|
_beforeSetPriority(tokenId, priorities);
|
|
_activeAssetPriorities[tokenId] = priorities;
|
|
|
|
emit AssetPrioritySet(tokenId);
|
|
_afterSetPriority(tokenId, priorities);
|
|
}
|
|
|
|
/**
|
|
* @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.
|
|
* @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 IdZeroForbidden();
|
|
if (bytes(_assets[id]).length > 0) revert AssetAlreadyExists();
|
|
|
|
_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.
|
|
* @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 AssetAlreadyExists();
|
|
|
|
if (bytes(_assets[assetId]).length == 0) revert NoAssetMatchingId();
|
|
|
|
if (_pendingAssets[tokenId].length >= 128)
|
|
revert MaxPendingAssetsReached();
|
|
|
|
_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);
|
|
}
|
|
|
|
// --------------------- EQUIPPABLE GETTERS ---------------------
|
|
|
|
/**
|
|
* @inheritdoc IERC6220
|
|
*/
|
|
function canTokenBeEquippedWithAssetIntoSlot(
|
|
address parent,
|
|
uint256 tokenId,
|
|
uint64 assetId,
|
|
uint64 slotId
|
|
) public view virtual returns (bool) {
|
|
uint64 equippableGroupId = _equippableGroupIds[assetId];
|
|
uint64 equippableSlot = _validParentSlots[equippableGroupId][parent];
|
|
if (equippableSlot == slotId) {
|
|
(, bool found) = getActiveAssets(tokenId).indexOf(assetId);
|
|
return found;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc IERC6220
|
|
*/
|
|
function isChildEquipped(
|
|
uint256 tokenId,
|
|
address childAddress,
|
|
uint256 childId
|
|
) public view virtual returns (bool) {
|
|
return _equipCountPerChild[tokenId][childAddress][childId] != uint8(0);
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc IERC6220
|
|
*/
|
|
function getAssetAndEquippableData(
|
|
uint256 tokenId,
|
|
uint64 assetId
|
|
)
|
|
public
|
|
view
|
|
virtual
|
|
returns (string memory, uint64, address, uint64[] memory)
|
|
{
|
|
return (
|
|
getAssetMetadata(tokenId, assetId),
|
|
_equippableGroupIds[assetId],
|
|
_catalogAddresses[assetId],
|
|
_partIds[assetId]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc IERC6220
|
|
*/
|
|
function getEquipment(
|
|
uint256 tokenId,
|
|
address targetCatalogAddress,
|
|
uint64 slotPartId
|
|
) public view virtual returns (Equipment memory) {
|
|
return _equipments[tokenId][targetCatalogAddress][slotPartId];
|
|
}
|
|
|
|
// --------------------- EQUIPPABLE SETTERS ---------------------
|
|
|
|
/**
|
|
* @inheritdoc IERC6220
|
|
*/
|
|
function equip(
|
|
IntakeEquip memory data
|
|
) public virtual onlyApprovedOrOwner(data.tokenId) {
|
|
_equip(data);
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc IERC6220
|
|
*/
|
|
function unequip(
|
|
uint256 tokenId,
|
|
uint64 assetId,
|
|
uint64 slotPartId
|
|
) public virtual onlyApprovedOrOwner(tokenId) {
|
|
_unequip(tokenId, assetId, slotPartId);
|
|
}
|
|
|
|
// --------------------- EQUIPPABLE INTERNAL ---------------------
|
|
|
|
/**
|
|
* @notice Private function used to equip a child into a token.
|
|
* @dev If the `Slot` already has an item equipped, the execution will be reverted.
|
|
* @dev If the child can't be used in the given `Slot`, the execution will be reverted.
|
|
* @dev If the catalog doesn't allow this equip to happen, the execution will be reverted.
|
|
* @dev The `IntakeEquip` stuct contains the following data:
|
|
* [
|
|
* tokenId,
|
|
* childIndex,
|
|
* assetId,
|
|
* slotPartId,
|
|
* childAssetId
|
|
* ]
|
|
* @param data An `IntakeEquip` struct specifying the equip data
|
|
*/
|
|
function _equip(IntakeEquip memory data) internal virtual {
|
|
address catalogAddress = _catalogAddresses[data.assetId];
|
|
uint64 slotPartId = data.slotPartId;
|
|
if (
|
|
_equipments[data.tokenId][catalogAddress][slotPartId]
|
|
.childEquippableAddress != address(0)
|
|
) revert SlotAlreadyUsed();
|
|
|
|
// Check from parent's asset perspective:
|
|
(, bool found) = _partIds[data.assetId].indexOf(slotPartId);
|
|
if (!found) revert TargetAssetCannotReceiveSlot();
|
|
|
|
IERC6059.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 TokenCannotBeEquippedWithAssetIntoSlot();
|
|
|
|
// Check from catalog perspective
|
|
if (
|
|
!ICatalog(catalogAddress).checkIsEquippable(
|
|
slotPartId,
|
|
child.contractAddress
|
|
)
|
|
) revert EquippableEquipNotAllowedByCatalog();
|
|
|
|
_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 used to unequip child from parent token.
|
|
* @param tokenId ID of the parent from which the child is being unequipped
|
|
* @param assetId ID of the parent's asset that contains the `Slot` into which the child is equipped
|
|
* @param slotPartId ID of the `Slot` from which to unequip the child
|
|
*/
|
|
function _unequip(
|
|
uint256 tokenId,
|
|
uint64 assetId,
|
|
uint64 slotPartId
|
|
) internal virtual {
|
|
address targetCatalogAddress = _catalogAddresses[assetId];
|
|
Equipment memory equipment = _equipments[tokenId][targetCatalogAddress][
|
|
slotPartId
|
|
];
|
|
if (equipment.childEquippableAddress == address(0))
|
|
revert NotEquipped();
|
|
_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);
|
|
}
|
|
|
|
/**
|
|
* @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`
|
|
* @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 IdZeroForbidden();
|
|
_validParentSlots[equippableGroupId][parentAddress] = slotPartId;
|
|
emit ValidParentEquippableGroupIdSet(
|
|
equippableGroupId,
|
|
slotPartId,
|
|
parentAddress
|
|
);
|
|
}
|
|
|
|
// --------------------- MODIFIERS IMPLEMENTATIONS ---------------------
|
|
|
|
/**
|
|
* @notice Used to verify that the caller is either the owner of the token or approved to manage it by its owner.
|
|
* @dev If the caller is not the owner of the token or approved to manage it by its owner, the execution will be
|
|
* reverted.
|
|
* @param tokenId ID of the token to check
|
|
*/
|
|
function _onlyApprovedOrOwner(uint256 tokenId) private view {
|
|
if (!_isApprovedOrOwner(_msgSender(), tokenId))
|
|
revert ERC721NotApprovedOrOwner();
|
|
}
|
|
|
|
/**
|
|
* @notice Used to verify that the caller is approved to manage the given token or it its direct owner.
|
|
* @dev This does not delegate to ownerOf, which returns the root owner, but rater uses an owner from DirectOwner
|
|
* struct.
|
|
* @dev The execution is reverted if the caller is not immediate owner or approved to manage the given token.
|
|
* @dev Used for parent-scoped transfers.
|
|
* @param tokenId ID of the token to check.
|
|
*/
|
|
function _onlyApprovedOrDirectOwner(uint256 tokenId) private view {
|
|
if (!_isApprovedOrDirectOwner(_msgSender(), tokenId))
|
|
revert NotApprovedOrDirectOwner();
|
|
}
|
|
|
|
/**
|
|
* @notice Used to verify that the caller is 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) private view {
|
|
if (!_isApprovedForAssetsOrOwner(_msgSender(), tokenId))
|
|
revert NotApprovedForAssetsOrOwner();
|
|
}
|
|
|
|
/**
|
|
* @notice Used to check whether the given account is allowed to manage the given token.
|
|
* @dev Requirements:
|
|
*
|
|
* - `tokenId` must exist.
|
|
* @param spender Address that is being checked for approval
|
|
* @param tokenId ID of the token being checked
|
|
* @return bool The boolean value indicating whether the `spender` is approved to manage the given token
|
|
*/
|
|
function _isApprovedOrOwner(
|
|
address spender,
|
|
uint256 tokenId
|
|
) internal view virtual returns (bool) {
|
|
address owner = ownerOf(tokenId);
|
|
return (spender == owner ||
|
|
isApprovedForAll(owner, spender) ||
|
|
getApproved(tokenId) == spender);
|
|
}
|
|
|
|
/**
|
|
* @notice Used to check whether the account is approved to manage the token or its direct owner.
|
|
* @param spender Address that is being checked for approval or direct ownership
|
|
* @param tokenId ID of the token being checked
|
|
* @return bool The boolean value indicating whether the `spender` is approved to manage the given token or its
|
|
* direct owner
|
|
*/
|
|
function _isApprovedOrDirectOwner(
|
|
address spender,
|
|
uint256 tokenId
|
|
) internal view virtual returns (bool) {
|
|
(address owner, uint256 parentId, ) = directOwnerOf(tokenId);
|
|
// When the parent is an NFT, only it can do operations
|
|
if (parentId != 0) {
|
|
return (spender == owner);
|
|
}
|
|
// Otherwise, the owner or approved address can
|
|
return (spender == owner ||
|
|
isApprovedForAll(owner, spender) ||
|
|
getApproved(tokenId) == spender);
|
|
}
|
|
|
|
/**
|
|
* @notice Internal function to check whether the queried user is either:
|
|
* 1. The root owner of the token associated with `tokenId`.
|
|
* 2. Is approved for all assets of the current owner via the `setApprovalForAllForAssets` function.
|
|
* 3. Is granted approval for the specific tokenId for asset management via the `approveForAssets` function.
|
|
* @param user Address of the user we are checking for permission
|
|
* @param tokenId ID of the token to query for permission for a given `user`
|
|
* @return bool A boolean value indicating whether the user is approved to manage the token or not
|
|
*/
|
|
function _isApprovedForAssetsOrOwner(
|
|
address user,
|
|
uint256 tokenId
|
|
) internal view virtual returns (bool) {
|
|
address owner = ownerOf(tokenId);
|
|
return (user == owner ||
|
|
isApprovedForAllForAssets(owner, user) ||
|
|
getApprovedForAssets(tokenId) == user);
|
|
}
|
|
|
|
// --------------------- MULTIASSET HOOKS ---------------------
|
|
|
|
/**
|
|
* @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,
|
|
uint256 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`
|
|
*/
|
|
function _afterAcceptAsset(
|
|
uint256 tokenId,
|
|
uint256 index,
|
|
uint256 assetId
|
|
) 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,
|
|
uint256 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,
|
|
uint256 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,
|
|
uint16[] calldata 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,
|
|
uint16[] calldata priorities
|
|
) internal virtual {}
|
|
|
|
// --------------------- NESTABLE HOOKS ---------------------
|
|
|
|
/**
|
|
* @notice Hook that is called before any token transfer. This includes minting and burning.
|
|
* @dev Calling conditions:
|
|
*
|
|
* - When `from` and `to` are both non-zero, ``from``'s `tokenId` will be transferred to `to`.
|
|
* - When `from` is zero, `tokenId` will be minted to `to`.
|
|
* - When `to` is zero, ``from``'s `tokenId` will be burned.
|
|
* - `from` and `to` are never zero at the same time.
|
|
*
|
|
* To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
|
|
* @param from Address from which the token is being transferred
|
|
* @param to Address to which the token is being transferred
|
|
* @param tokenId ID of the token being transferred
|
|
*/
|
|
function _beforeTokenTransfer(
|
|
address from,
|
|
address to,
|
|
uint256 tokenId
|
|
) internal virtual {}
|
|
|
|
/**
|
|
* @notice Hook that is called after any transfer of tokens. This includes minting and burning.
|
|
* @dev Calling conditions:
|
|
*
|
|
* - When `from` and `to` are both non-zero.
|
|
* - `from` and `to` are never zero at the same time.
|
|
*
|
|
* To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
|
|
* @param from Address from which the token has been transferred
|
|
* @param to Address to which the token has been transferred
|
|
* @param tokenId ID of the token that has been transferred
|
|
*/
|
|
function _afterTokenTransfer(
|
|
address from,
|
|
address to,
|
|
uint256 tokenId
|
|
) internal virtual {}
|
|
|
|
/**
|
|
* @notice Hook that is called before nested token transfer.
|
|
* @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
|
|
* @param from Address from which the token is being transferred
|
|
* @param to Address to which the token is being transferred
|
|
* @param fromTokenId ID of the token from which the given token is being transferred
|
|
* @param toTokenId ID of the token to which the given token is being transferred
|
|
* @param tokenId ID of the token being transferred
|
|
*/
|
|
function _beforeNestedTokenTransfer(
|
|
address from,
|
|
address to,
|
|
uint256 fromTokenId,
|
|
uint256 toTokenId,
|
|
uint256 tokenId
|
|
) internal virtual {}
|
|
|
|
/**
|
|
* @notice Hook that is called after nested token transfer.
|
|
* @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
|
|
* @param from Address from which the token was transferred
|
|
* @param to Address to which the token was transferred
|
|
* @param fromTokenId ID of the token from which the given token was transferred
|
|
* @param toTokenId ID of the token to which the given token was transferred
|
|
* @param tokenId ID of the token that was transferred
|
|
*/
|
|
function _afterNestedTokenTransfer(
|
|
address from,
|
|
address to,
|
|
uint256 fromTokenId,
|
|
uint256 toTokenId,
|
|
uint256 tokenId
|
|
) internal virtual {}
|
|
|
|
/**
|
|
* @notice Hook that is called before a child is added to the pending tokens array of a given token.
|
|
* @dev The Child struct consists of the following values:
|
|
* [
|
|
* tokenId,
|
|
* contractAddress
|
|
* ]
|
|
* @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
|
|
* @param tokenId ID of the token that will receive a new pending child token
|
|
* @param childAddress Address of the collection smart contract of the child token expected to be located at the
|
|
* specified index of the given parent token's pending children array
|
|
* @param childId ID of the child token expected to be located at the specified index of the given parent token's
|
|
* pending children array
|
|
*/
|
|
function _beforeAddChild(
|
|
uint256 tokenId,
|
|
address childAddress,
|
|
uint256 childId
|
|
) internal virtual {}
|
|
|
|
/**
|
|
* @notice Hook that is called after a child is added to the pending tokens array of a given token.
|
|
* @dev The Child struct consists of the following values:
|
|
* [
|
|
* tokenId,
|
|
* contractAddress
|
|
* ]
|
|
* @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
|
|
* @param tokenId ID of the token that has received a new pending child token
|
|
* @param childAddress Address of the collection smart contract of the child token expected to be located at the
|
|
* specified index of the given parent token's pending children array
|
|
* @param childId ID of the child token expected to be located at the specified index of the given parent token's
|
|
* pending children array
|
|
*/
|
|
function _afterAddChild(
|
|
uint256 tokenId,
|
|
address childAddress,
|
|
uint256 childId
|
|
) internal virtual {}
|
|
|
|
/**
|
|
* @notice Hook that is called before a child is accepted to the active tokens array of a given token.
|
|
* @dev The Child struct consists of the following values:
|
|
* [
|
|
* tokenId,
|
|
* contractAddress
|
|
* ]
|
|
* @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
|
|
* @param parentId ID of the token that will accept a pending child token
|
|
* @param childIndex Index of the child token to accept in the given parent token's pending children array
|
|
* @param childAddress Address of the collection smart contract of the child token expected to be located at the
|
|
* specified index of the given parent token's pending children array
|
|
* @param childId ID of the child token expected to be located at the specified index of the given parent token's
|
|
* pending children array
|
|
*/
|
|
function _beforeAcceptChild(
|
|
uint256 parentId,
|
|
uint256 childIndex,
|
|
address childAddress,
|
|
uint256 childId
|
|
) internal virtual {}
|
|
|
|
/**
|
|
* @notice Hook that is called after a child is accepted to the active tokens array of a given token.
|
|
* @dev The Child struct consists of the following values:
|
|
* [
|
|
* tokenId,
|
|
* contractAddress
|
|
* ]
|
|
* @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
|
|
* @param parentId ID of the token that has accepted a pending child token
|
|
* @param childIndex Index of the child token that was accpeted in the given parent token's pending children array
|
|
* @param childAddress Address of the collection smart contract of the child token that was expected to be located
|
|
* at the specified index of the given parent token's pending children array
|
|
* @param childId ID of the child token that was expected to be located at the specified index of the given parent
|
|
* token's pending children array
|
|
*/
|
|
function _afterAcceptChild(
|
|
uint256 parentId,
|
|
uint256 childIndex,
|
|
address childAddress,
|
|
uint256 childId
|
|
) internal virtual {}
|
|
|
|
/**
|
|
* @notice Hook that is called before a child is transferred from a given child token array of a given token.
|
|
* @dev The Child struct consists of the following values:
|
|
* [
|
|
* tokenId,
|
|
* contractAddress
|
|
* ]
|
|
* @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
|
|
* @param tokenId ID of the token that will transfer a child token
|
|
* @param childIndex Index of the child token that will be transferred from the given parent token's children array
|
|
* @param childAddress Address of the collection smart contract of the child token that is expected to be located
|
|
* at the specified index of the given parent token's children array
|
|
* @param childId ID of the child token that is expected to be located at the specified index of the given parent
|
|
* token's children array
|
|
* @param isPending A boolean value signifying whether the child token is being transferred from the pending child
|
|
* tokens array (`true`) or from the active child tokens array (`false`)
|
|
*/
|
|
function _beforeTransferChild(
|
|
uint256 tokenId,
|
|
uint256 childIndex,
|
|
address childAddress,
|
|
uint256 childId,
|
|
bool isPending
|
|
) internal virtual {}
|
|
|
|
/**
|
|
* @notice Hook that is called after a child is transferred from a given child token array of a given token.
|
|
* @dev The Child struct consists of the following values:
|
|
* [
|
|
* tokenId,
|
|
* contractAddress
|
|
* ]
|
|
* @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
|
|
* @param tokenId ID of the token that has transferred a child token
|
|
* @param childIndex Index of the child token that was transferred from the given parent token's children array
|
|
* @param childAddress Address of the collection smart contract of the child token that was expected to be located
|
|
* at the specified index of the given parent token's children array
|
|
* @param childId ID of the child token that was expected to be located at the specified index of the given parent
|
|
* token's children array
|
|
* @param isPending A boolean value signifying whether the child token was transferred from the pending child tokens
|
|
* array (`true`) or from the active child tokens array (`false`)
|
|
*/
|
|
function _afterTransferChild(
|
|
uint256 tokenId,
|
|
uint256 childIndex,
|
|
address childAddress,
|
|
uint256 childId,
|
|
bool isPending
|
|
) internal virtual {}
|
|
|
|
/**
|
|
* @notice Hook that is called before a pending child tokens array of a given token is cleared.
|
|
* @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
|
|
* @param tokenId ID of the token that will reject all of the pending child tokens
|
|
*/
|
|
function _beforeRejectAllChildren(uint256 tokenId) internal virtual {}
|
|
|
|
/**
|
|
* @notice Hook that is called after a pending child tokens array of a given token is cleared.
|
|
* @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
|
|
* @param tokenId ID of the token that has rejected all of the pending child tokens
|
|
*/
|
|
function _afterRejectAllChildren(uint256 tokenId) internal virtual {}
|
|
|
|
// --------------------- EQUIPPABLE 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 {}
|
|
}
|