NEXUS

Sample ERC1155 Contract

This is an example of an ERC-1155 smart contract, structured to support minting and transferring either single or multiple types of tokens, each identified by a unique ID.

This example is adapted from OpenZeppelin ERC1155.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import "@openzeppelin/contracts/access/Ownable.sol"; // Ownership control
import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Utils.sol"; // Safe transfer verification utils
import "@openzeppelin/contracts/utils/Strings.sol"; // String conversion utils
import "@openzeppelin/contracts/utils/Arrays.sol"; // Array utils (includes unsafeMemoryAccess)

/**
 * @title Sample1155Token
 * @dev A custom ERC1155 implementation with minimal features and custom error handling
 */
contract Sample1155Token is Ownable {
    using Strings for uint256;
    using Arrays for uint256[];
    using Arrays for address[];

    // ----------------------
    // ░░░ Error Definitions ░░░
    // ----------------------

    error ERC1155InsufficientBalance(address sender, uint256 balance, uint256 needed, uint256 tokenId); // Insufficient balance for transfer or burn
    error ERC1155InvalidSender(address sender); // Invalid sender (e.g., zero address)
    error ERC1155InvalidReceiver(address receiver); // Invalid receiver (e.g., zero address)
    error ERC1155MissingApprovalForAll(address operator, address owner); // Unauthorized attempt to operate on another's tokens
    error ERC1155InvalidApprover(address approver); // Address cannot be set as approver
    error ERC1155InvalidOperator(address operator); // Address cannot be set as operator
    error ERC1155InvalidArrayLength(uint256 idsLength, uint256 valuesLength); // Length of ID array and amount array do not match

    // ----------------------
    // ░░░ Events ░░░
    // ----------------------

    event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value); // Single token transfer event
    event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values); // Batch token transfer event
    event ApprovalForAll(address indexed account, address indexed operator, bool approved); // Operator approval or revocation event

    // ----------------------
    // ░░░ Metadata ░░░
    // ----------------------

    string public name; // Collection name (e.g., "MyGameItems")
    string public symbol; // Collection symbol (e.g., "MGI")
    string private _baseURI = ""; // Base prefix path for metadata URI

    // ----------------------
    // ░░░ Token Storage ░░░
    // ----------------------

    mapping(uint256 id => uint256) private _totalSupply; // Per-token supply
    uint256 private _totalSupplyAll; // Total supply of all tokens
    mapping(uint256 id => mapping(address => uint256)) private _balances; // User balances
    mapping(address => mapping(address => bool)) private _operatorApprovals; // Operator approvals

    // ----------------------
    // ░░░ Constructor ░░░
    // ----------------------

    /// @notice Initializes the contract with a base metadata URI
    /// @param uri_ The base URI string used for all token metadata
    constructor(string memory uri_) Ownable(_msgSender()) {
        _setURI(uri_);
    }

    // ----------------------
    // ░░░ Read Functions ░░░
    // ----------------------

    /// @notice Returns the balance of a specific token ID for a given account
    /// @param account The address to query
    /// @param id The token ID to query
    /// @return The amount of tokens owned by the account
    function balanceOf(address account, uint256 id) public view returns (uint256) {
        return _balances[id][account];
    }

    /// @notice Returns the balances of multiple token IDs for multiple accounts
    /// @param accounts Array of addresses to query
    /// @param ids Array of token IDs to query (must match accounts array length)
    /// @return An array containing the balances of each (account, token ID) pair
    function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids) external view returns (uint256[] memory) {
        if (accounts.length != ids.length) revert ERC1155InvalidArrayLength(ids.length, accounts.length);
        uint256[] memory batchBalances = new uint256[](accounts.length);
        for (uint256 i = 0; i < accounts.length; ++i) {
            batchBalances[i] = balanceOf(accounts.unsafeMemoryAccess(i), ids.unsafeMemoryAccess(i));
        }
        return batchBalances;
    }

    /// @notice Returns the total minted supply of a specific token ID
    /// @param id The token ID to query
    /// @return The total number of tokens in existence for the given ID
    function totalSupply(uint256 id) external view returns (uint256) {
        return _totalSupply[id];
    }

    /// @notice Returns the total supply across all token IDs
    /// @return The aggregate number of tokens minted (excluding burns)
    function totalSupply() external view returns (uint256) {
        return _totalSupplyAll;
    }

    /// @notice Returns whether a specific token ID exists (i.e., has non-zero supply)
    /// @param id The token ID to check
    /// @return True if the token exists, false otherwise
    function exists(uint256 id) external view returns (bool) {
        return _totalSupply[id] > 0;
    }

    /// @notice Returns the metadata URI for a specific token ID
    /// @param id The token ID to query
    /// @return The metadata URI derived from baseURI + hex-encoded token ID
    function uri(uint256 id) public view returns (string memory) {
        return string(abi.encodePacked(_baseURI, Strings.toHexString(id, 32)));
    }

    /// @notice Indicates which interfaces the contract supports (ERC165, ERC1155, ERC1155MetadataURI)
    /// @param interfaceId The interface identifier to check
    /// @return True if supported, false otherwise
    function supportsInterface(bytes4 interfaceId) external pure returns (bool) {
        return interfaceId == 0x01ffc9a7 || interfaceId == 0xd9b67a26 || interfaceId == 0x0e89341c;
    }

    // ----------------------
    // ░░░ Approval Logic ░░░
    // ----------------------

    /// @notice Grants or revokes operator approval for all of the caller's tokens
    /// @param operator The address to grant or revoke approval for
    /// @param approved True to approve, false to revoke
    function setApprovalForAll(address operator, bool approved) external {
        _setApprovalForAll(_msgSender(), operator, approved);
    }

    /// @notice Checks whether an operator is approved to manage all tokens of a given account
    /// @param account The owner of the tokens
    /// @param operator The address to check for approval
    /// @return True if approved, false otherwise
    function isApprovedForAll(address account, address operator) public view returns (bool) {
        return _operatorApprovals[account][operator];
    }

    /// @dev Internal function to update approval status of an operator for a given owner
    /// @param owner The owner granting the approval
    /// @param operator The operator address
    /// @param approved True to approve, false to revoke
    function _setApprovalForAll(address owner, address operator, bool approved) internal {
        if (operator == address(0)) revert ERC1155InvalidOperator(address(0));
        _operatorApprovals[owner][operator] = approved;
        emit ApprovalForAll(owner, operator, approved);
    }

    // ----------------------
    // ░░░ Transfer Logic ░░░
    // ----------------------

    /// @notice Transfers a single token ID from one address to another
    /// @dev Caller must be the token owner or an approved operator
    /// @param from Source address
    /// @param to Target address
    /// @param id Token ID to transfer
    /// @param value Amount of tokens to transfer
    /// @param data Additional data with no specified format
    function safeTransferFrom(address from, address to, uint256 id, uint256 value, bytes memory data) external {
        address sender = _msgSender();
        if (from != sender && !isApprovedForAll(from, sender)) revert ERC1155MissingApprovalForAll(sender, from);
        _safeTransferFrom(from, to, id, value, data);
    }

    /// @notice Transfers multiple token IDs from one address to another
    /// @dev Caller must be the token owner or an approved operator
    /// @param from Source address
    /// @param to Target address
    /// @param ids List of token IDs to transfer
    /// @param values List of token amounts to transfer (must match ids array length)
    /// @param data Additional data with no specified format
    function safeBatchTransferFrom(address from, address to, uint256[] memory ids, uint256[] memory values, bytes memory data) external {
        address sender = _msgSender();
        if (from != sender && !isApprovedForAll(from, sender)) revert ERC1155MissingApprovalForAll(sender, from);
        _safeBatchTransferFrom(from, to, ids, values, data);
    }

    /// @dev Internal function to handle single token transfer with receiver check
    /// @param from Source address
    /// @param to Target address
    /// @param id Token ID to transfer
    /// @param value Amount of tokens to transfer
    /// @param data Additional data to pass to the receiver
    function _safeTransferFrom(address from, address to, uint256 id, uint256 value, bytes memory data) internal {
        if (to == address(0)) revert ERC1155InvalidReceiver(address(0));
        if (from == address(0)) revert ERC1155InvalidSender(address(0));
        (uint256[] memory ids, uint256[] memory values) = _asSingletonArrays(id, value);
        _updateWithAcceptanceCheck(from, to, ids, values, data);
    }

    /// @dev Internal function to handle batch token transfer with receiver check
    /// @param from Source address
    /// @param to Target address
    /// @param ids List of token IDs to transfer
    /// @param values List of token amounts to transfer
    /// @param data Additional data to pass to the receiver
    function _safeBatchTransferFrom(address from, address to, uint256[] memory ids, uint256[] memory values, bytes memory data) internal {
        if (to == address(0)) revert ERC1155InvalidReceiver(address(0));
        if (from == address(0)) revert ERC1155InvalidSender(address(0));
        _updateWithAcceptanceCheck(from, to, ids, values, data);
    }


    // ----------------------
    // ░░░ Mint/Burn (Owner-only) ░░░
    // ----------------------

    /// @notice Mints a specified amount of a token to a given address
    /// @dev Only the contract owner can call this function
    /// @param account The recipient of the minted tokens
    /// @param id The token ID to mint
    /// @param amount The number of tokens to mint
    /// @param data Additional data to pass to receiver (if contract)
    function mint(address account, uint256 id, uint256 amount, bytes memory data) external onlyOwner {
        _mint(account, id, amount, data);
    }

    /// @notice Mints multiple token IDs and amounts to a given address
    /// @dev Only the contract owner can call this function
    /// @param to The recipient of the minted tokens
    /// @param ids Array of token IDs to mint
    /// @param amounts Array of amounts to mint (must match ids length)
    /// @param data Additional data to pass to receiver (if contract)
    function mintBatch(address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data) external onlyOwner {
        _mintBatch(to, ids, amounts, data);
    }

    /// @notice Burns a specific amount of a token from an account
    /// @dev Caller must be the token owner or approved operator
    /// @param account The address from which the token will be burned
    /// @param id The token ID to burn
    /// @param value The amount of tokens to burn
    function burn(address account, uint256 id, uint256 value) external {
        if (account != _msgSender() && !isApprovedForAll(account, _msgSender())) {
            revert ERC1155MissingApprovalForAll(_msgSender(), account);
        }
        _burn(account, id, value);
    }

    /// @notice Burns multiple token IDs and amounts from an account
    /// @dev Caller must be the token owner or approved operator
    /// @param account The address from which the tokens will be burned
    /// @param ids Array of token IDs to burn
    /// @param values Array of amounts to burn (must match ids length)
    function burnBatch(address account, uint256[] memory ids, uint256[] memory values) external {
        if (account != _msgSender() && !isApprovedForAll(account, _msgSender())) {
            revert ERC1155MissingApprovalForAll(_msgSender(), account);
        }
        _burnBatch(account, ids, values);
    }

    /// @dev Internal minting logic for a single token ID
    /// @param to The recipient address
    /// @param id The token ID to mint
    /// @param value The amount of tokens to mint
    /// @param data Additional data for receiver checks
    function _mint(address to, uint256 id, uint256 value, bytes memory data) internal {
        if (to == address(0)) revert ERC1155InvalidReceiver(address(0));
        (uint256[] memory ids, uint256[] memory values) = _asSingletonArrays(id, value);
        _updateWithAcceptanceCheck(address(0), to, ids, values, data);
    }

    /// @dev Internal minting logic for multiple token IDs
    /// @param to The recipient address
    /// @param ids Array of token IDs to mint
    /// @param values Array of amounts to mint
    /// @param data Additional data for receiver checks
    function _mintBatch(address to, uint256[] memory ids, uint256[] memory values, bytes memory data) internal {
        if (to == address(0)) revert ERC1155InvalidReceiver(address(0));
        _updateWithAcceptanceCheck(address(0), to, ids, values, data);
    }

    /// @dev Internal burning logic for a single token ID
    /// @param from The address from which to burn tokens
    /// @param id The token ID to burn
    /// @param value The amount of tokens to burn
    function _burn(address from, uint256 id, uint256 value) internal {
        if (from == address(0)) revert ERC1155InvalidSender(address(0));
        (uint256[] memory ids, uint256[] memory values) = _asSingletonArrays(id, value);
        _updateWithAcceptanceCheck(from, address(0), ids, values, "");
    }

    /// @dev Internal burning logic for multiple token IDs
    /// @param from The address from which to burn tokens
    /// @param ids Array of token IDs to burn
    /// @param values Array of amounts to burn
    function _burnBatch(address from, uint256[] memory ids, uint256[] memory values) internal {
        if (from == address(0)) revert ERC1155InvalidSender(address(0));
        _updateWithAcceptanceCheck(from, address(0), ids, values, "");
    }


    // ----------------------
    // ░░░ Internal Helpers ░░░
    // ----------------------

    /// @dev Handles core transfer/mint/burn logic and verifies receiver acceptance if applicable
    /// @param from Source address (zero address for mint)
    /// @param to Target address (zero address for burn)
    /// @param ids Array of token IDs
    /// @param values Array of token amounts
    /// @param data Additional data forwarded to receiving contract (if any)
    function _updateWithAcceptanceCheck(
        address from,
        address to,
        uint256[] memory ids,
        uint256[] memory values,
        bytes memory data
    ) internal {
        _update(from, to, ids, values);
        if (to != address(0)) {
            address operator = _msgSender();
            if (ids.length == 1) {
                // Check acceptance of single token transfer
                ERC1155Utils.checkOnERC1155Received(operator, from, to, ids[0], values[0], data);
            } else {
                // Check acceptance of batch transfer
                ERC1155Utils.checkOnERC1155BatchReceived(operator, from, to, ids, values, data);
            }
        }
    }

    /// @dev Updates token balances, emits events, and tracks total supply changes
    /// @param from Address to subtract balance from (zero for mint)
    /// @param to Address to add balance to (zero for burn)
    /// @param ids Array of token IDs
    /// @param values Array of token amounts
    function _update(
        address from,
        address to,
        uint256[] memory ids,
        uint256[] memory values
    ) internal {
        if (ids.length != values.length) revert ERC1155InvalidArrayLength(ids.length, values.length);
        address operator = _msgSender();

        // Update balances
        for (uint256 i = 0; i < ids.length; ++i) {
            uint256 id = ids.unsafeMemoryAccess(i);
            uint256 value = values.unsafeMemoryAccess(i);

            if (from != address(0)) {
                uint256 fromBalance = _balances[id][from];
                if (fromBalance < value) revert ERC1155InsufficientBalance(from, fromBalance, value, id);
                unchecked {
                    _balances[id][from] = fromBalance - value;
                }
            }

            if (to != address(0)) {
                _balances[id][to] += value;
            }
        }

        // Emit appropriate transfer event
        if (ids.length == 1) {
            emit TransferSingle(operator, from, to, ids[0], values[0]);
        } else {
            emit TransferBatch(operator, from, to, ids, values);
        }

        // Mint: update totalSupply and totalSupplyAll
        if (from == address(0)) {
            uint256 totalMintValue = 0;
            for (uint256 i = 0; i < ids.length; ++i) {
                uint256 id = ids.unsafeMemoryAccess(i);
                uint256 value = values.unsafeMemoryAccess(i);
                _totalSupply[id] += value;
                totalMintValue += value;
            }
            _totalSupplyAll += totalMintValue;
        }

        // Burn: decrease totalSupply and totalSupplyAll
        if (to == address(0)) {
            uint256 totalBurnValue = 0;
            for (uint256 i = 0; i < ids.length; ++i) {
                uint256 id = ids.unsafeMemoryAccess(i);
                uint256 value = values.unsafeMemoryAccess(i);
                unchecked {
                    _totalSupply[id] -= value;
                    totalBurnValue += value;
                }
            }
            unchecked {
                _totalSupplyAll -= totalBurnValue;
            }
        }
    }

    /// @dev Helper to wrap two uint256 values into single-element arrays
    /// @param element1 The first value (used as token ID)
    /// @param element2 The second value (used as amount)
    /// @return array1 Single-element array containing element1
    /// @return array2 Single-element array containing element2
    function _asSingletonArrays(
        uint256 element1,
        uint256 element2
    ) private pure returns (uint256[] memory array1, uint256[] memory array2) {
        assembly ("memory-safe") {
            // Create array1 at free memory pointer
            array1 := mload(0x40)
            mstore(array1, 1) // length = 1
            mstore(add(array1, 0x20), element1)

            // Create array2 right after array1
            array2 := add(array1, 0x40)
            mstore(array2, 1)
            mstore(add(array2, 0x20), element2)

            // Advance free memory pointer
            mstore(0x40, add(array2, 0x40))
        }
    }

}

© 2025 NEXUS Co., Ltd. All Rights Reserved.