// SPDX-License-Identifier: MIT pragma solidity >=0.7.6 <0.8.0; import {AddressIsContract} from "@animoca/ethereum-contracts-core/contracts/utils/types/AddressIsContract.sol"; import {ERC1155InventoryIdentifiersLib} from "./ERC1155InventoryIdentifiersLib.sol"; import {IERC1155Inventory} from "./interfaces/IERC1155Inventory.sol"; import {ERC1155InventoryBase} from "./ERC1155InventoryBase.sol"; /** * @title ERC1155Inventory, a contract which manages up to multiple Collections of Fungible and Non-Fungible Tokens. * @dev The function `uri(uint256)` needs to be implemented by a child contract, for example with the help of `NFTBaseMetadataURI`. */ abstract contract ERC1155Inventory is ERC1155InventoryBase { using AddressIsContract for address; using ERC1155InventoryIdentifiersLib for uint256; constructor(uint256 collectionMaskLength) ERC1155InventoryBase(collectionMaskLength) {} //======================================================= ERC1155 =======================================================// /// @inheritdoc IERC1155Inventory function safeTransferFrom( address from, address to, uint256 id, uint256 value, bytes memory data ) public virtual override { address sender = _msgSender(); require(to != address(0), "Inventory: transfer to zero"); require(_isOperatable(from, sender), "Inventory: non-approved sender"); if (id.isFungibleToken()) { _transferFungible(from, to, id, value); } else if (id.isNonFungibleToken(_collectionMaskLength)) { _transferNFT(from, to, id, value, false); } else { revert("Inventory: not a token id"); } emit TransferSingle(sender, from, to, id, value); if (to.isContract()) { _callOnERC1155Received(from, to, id, value, data); } } /// @inheritdoc IERC1155Inventory function safeBatchTransferFrom( address from, address to, uint256[] memory ids, uint256[] memory values, bytes memory data ) public virtual override { // internal function to avoid stack too deep error _safeBatchTransferFrom(from, to, ids, values, data); } //============================================ High-level Internal Functions ============================================// function _safeBatchTransferFrom( address from, address to, uint256[] memory ids, uint256[] memory values, bytes memory data ) internal { require(to != address(0), "Inventory: transfer to zero"); uint256 length = ids.length; require(length == values.length, "Inventory: inconsistent arrays"); address sender = _msgSender(); require(_isOperatable(from, sender), "Inventory: non-approved sender"); uint256 nfCollectionId; uint256 nfCollectionCount; for (uint256 i; i != length; ++i) { uint256 id = ids[i]; uint256 value = values[i]; if (id.isFungibleToken()) { _transferFungible(from, to, id, value); } else if (id.isNonFungibleToken(_collectionMaskLength)) { _transferNFT(from, to, id, value, true); uint256 nextCollectionId = id.getNonFungibleCollection(_collectionMaskLength); if (nfCollectionId == 0) { nfCollectionId = nextCollectionId; nfCollectionCount = 1; } else { if (nextCollectionId != nfCollectionId) { _transferNFTUpdateCollection(from, to, nfCollectionId, nfCollectionCount); nfCollectionId = nextCollectionId; nfCollectionCount = 1; } else { ++nfCollectionCount; } } } else { revert("Inventory: not a token id"); } } if (nfCollectionId != 0) { _transferNFTUpdateCollection(from, to, nfCollectionId, nfCollectionCount); } emit TransferBatch(sender, from, to, ids, values); if (to.isContract()) { _callOnERC1155BatchReceived(from, to, ids, values, data); } } function _safeMint( address to, uint256 id, uint256 value, bytes memory data ) internal { require(to != address(0), "Inventory: mint to zero"); if (id.isFungibleToken()) { _mintFungible(to, id, value); } else if (id.isNonFungibleToken(_collectionMaskLength)) { _mintNFT(to, id, value, false); } else { revert("Inventory: not a token id"); } emit TransferSingle(_msgSender(), address(0), to, id, value); if (to.isContract()) { _callOnERC1155Received(address(0), to, id, value, data); } } function _safeBatchMint( address to, uint256[] memory ids, uint256[] memory values, bytes memory data ) internal virtual { require(to != address(0), "Inventory: mint to zero"); uint256 length = ids.length; require(length == values.length, "Inventory: inconsistent arrays"); uint256 nfCollectionId; uint256 nfCollectionCount; for (uint256 i; i != length; ++i) { uint256 id = ids[i]; uint256 value = values[i]; if (id.isFungibleToken()) { _mintFungible(to, id, value); } else if (id.isNonFungibleToken(_collectionMaskLength)) { _mintNFT(to, id, value, true); uint256 nextCollectionId = id.getNonFungibleCollection(_collectionMaskLength); if (nfCollectionId == 0) { nfCollectionId = nextCollectionId; nfCollectionCount = 1; } else { if (nextCollectionId != nfCollectionId) { _balances[nfCollectionId][to] += nfCollectionCount; _supplies[nfCollectionId] += nfCollectionCount; nfCollectionId = nextCollectionId; nfCollectionCount = 1; } else { ++nfCollectionCount; } } } else { revert("Inventory: not a token id"); } } if (nfCollectionId != 0) { _balances[nfCollectionId][to] += nfCollectionCount; _supplies[nfCollectionId] += nfCollectionCount; } emit TransferBatch(_msgSender(), address(0), to, ids, values); if (to.isContract()) { _callOnERC1155BatchReceived(address(0), to, ids, values, data); } } //============================================== Helper Internal Functions ==============================================// function _mintFungible( address to, uint256 id, uint256 value ) internal { require(value != 0, "Inventory: zero value"); uint256 supply = _supplies[id]; uint256 newSupply = supply + value; require(newSupply > supply, "Inventory: supply overflow"); _supplies[id] = newSupply; // cannot overflow as any balance is bounded up by the supply which cannot overflow _balances[id][to] += value; } function _mintNFT( address to, uint256 id, uint256 value, bool isBatch ) internal { require(value == 1, "Inventory: wrong NFT value"); require(_owners[id] == 0, "Inventory: existing/burnt NFT"); _owners[id] = uint256(uint160(to)); if (!isBatch) { uint256 collectionId = id.getNonFungibleCollection(_collectionMaskLength); // it is virtually impossible that a Non-Fungible Collection supply // overflows due to the cost of minting individual tokens ++_supplies[collectionId]; // cannot overflow as supply cannot overflow ++_balances[collectionId][to]; } } function _transferFungible( address from, address to, uint256 id, uint256 value ) internal { require(value != 0, "Inventory: zero value"); uint256 balance = _balances[id][from]; require(balance >= value, "Inventory: not enough balance"); if (from != to) { _balances[id][from] = balance - value; // cannot overflow as supply cannot overflow _balances[id][to] += value; } } function _transferNFT( address from, address to, uint256 id, uint256 value, bool isBatch ) internal { require(value == 1, "Inventory: wrong NFT value"); require(from == address(uint160(_owners[id])), "Inventory: non-owned NFT"); _owners[id] = uint256(uint160(to)); if (!isBatch) { uint256 collectionId = id.getNonFungibleCollection(_collectionMaskLength); // cannot underflow as balance is verified through ownership _balances[collectionId][from] -= 1; // cannot overflow as supply cannot overflow _balances[collectionId][to] += 1; } } function _transferNFTUpdateCollection( address from, address to, uint256 collectionId, uint256 amount ) internal virtual { if (from != to) { // cannot underflow as balance is verified through ownership _balances[collectionId][from] -= amount; // cannot overflow as supply cannot overflow _balances[collectionId][to] += amount; } } }