Skip to content
On this page

L1-14: ERC-1155 Game Items (Multi Token Standard)

1. Problem

Implement the ERC-1155 multi-token standard contract, managing multiple token types (fungible tokens FT + non-fungible tokens NFT) within a single contract, supporting batch transfers, batch balance queries, and owner creation and minting of new token types.

A typical gaming scenario: a single contract containing gold coins (FT, fungible), rare skins (NFT, limited to 1 copy), and potions (semi-FT, limited to 100 bottles).

2. Why

ERC-721 requires a separate contract for each token type -- gas costs are unacceptable for games that may have hundreds of items. ERC-1155 solves this:

  • Single contract, multiple tokens: one contract manages a theoretically unlimited number of token types
  • Batch operations: safeBatchTransferFrom transfers multiple token types in a single transaction -- roughly 50% gas savings
  • Batch queries: balanceOfBatch queries balances across multiple addresses/tokens in one call -- significantly improved frontend performance
  • FT + NFT hybrid: the same contract can contain both fungible and non-fungible tokens

ERC-1155 is a complementary standard to ERC-721, not a replacement. It is widely used in gaming (Axie Infinity, Gods Unchained), membership cards, and any scenario requiring multiple token types.

3. Solution

Contract Architecture

Implement the full IERC-1155 interface from scratch (without inheriting from OpenZeppelin, to deepen understanding):

solidity
contract GameItems is IERC1155, Ownable {
    // tokenId => owner => balance
    mapping(uint256 => mapping(address => uint256)) private _balances;
    // owner => operator => approved
    mapping(address => mapping(address => bool)) private _operatorApprovals;
    // tokenId => URI
    mapping(uint256 => string) private _tokenURIs;

    uint256 private _tokenIdCounter;
}

Core Interface

MethodDescription
balanceOf(owner, id)Query single token balance
balanceOfBatch(owners[], ids[])Batch balance query
safeTransferFrom(from, to, id, amount, data)Single token transfer
safeBatchTransferFrom(from, to, ids[], amounts[], data)Batch transfer
setApprovalForAll(operator, approved)Authorize operator to manage all tokens

Token Creation

Only the Owner can create new token types:

solidity
function createToken(
    string calldata _uri,
    address[] calldata _recipients,
    uint256[] calldata _amounts
) external onlyOwner returns (uint256) {
    uint256 tokenId = _tokenIdCounter++;
    _tokenURIs[tokenId] = _uri;

    for (uint256 i = 0; i < _recipients.length; i++) {
        _balances[tokenId][_recipients[i]] += _amounts[i];
        emit TransferSingle(msg.sender, address(0), _recipients[i], tokenId, _amounts[i]);
    }
    return tokenId;
}

4. Pitfalls Encountered

4.1 Callback Safety Check in safeTransferFrom

ERC-1155 requires that when transferring to a contract address, the recipient must implement the IERC1155Receiver interface. If this check is forgotten, tokens can be permanently locked in contracts that cannot handle ERC-1155.

The implementation must:

solidity
function _doSafeTransferAcceptanceCheck(...) private {
    if (to.code.length > 0) {
        try IERC1155Receiver(to).onERC1155Received(...) returns (bytes4 response) {
            require(response == IERC1155Receiver.onERC1155Received.selector, "ERC1155 rejected");
        } catch {
            revert("ERC1155 transfer to non-ERC1155Receiver");
        }
    }
}

4.2 Array Length Validation in Batch Operations

Both safeBatchTransferFrom and balanceOfBatch accept multiple array parameters. It is essential to ensure all array lengths are equal; otherwise, array-out-of-bounds or logic errors will occur.

4.3 Differences in the Approval Model

ERC-1155 uses setApprovalForAll (all-or-nothing), lacking ERC-721's approve (per-token authorization). This means that once an operator is authorized, it can operate on all of your token types.

4.4 Implementing supportsInterface

ERC-165 interface detection is a requirement for all ERC standards. If supportsInterface returns an incorrect value, marketplaces and wallets may fail to recognize your contract as ERC-1155.

5. Why the Pitfalls Happen

5.1

ERC-1155's safe transfer mechanism is a critical line of defense against token loss. Without this check, users might accidentally transfer game items to an exchange contract (which does not know how to handle ERC-1155), causing permanent loss of the items.

5.2

Solidity does not automatically check array lengths. A caller could pass ids.length=3 but amounts.length=2 -- if not validated at the start of the function, the loop would try to access a non-existent index, causing a revert (wasting gas) or worse.

6. How to Resolve the Pitfalls

  • Validate at the beginning of all functions accepting multiple arrays: require(ids.length == amounts.length, "length mismatch")
  • Implement complete _doSafeTransferAcceptanceCheck and _doSafeBatchTransferAcceptanceCheck
  • supportsInterface must return the correct interface ID: type(IERC1155).interfaceId
  • Use safeTransferFrom rather than transferFrom (the ERC-1155 standard only has the safe version)
  • Emit the TransferSingle event in createToken (from = address(0) indicates minting)

7. Technical Highlights

PointDescription
Single contract, multiple tokenstokenId distinguishes types, replacing multi-contract deployment
Batch transfersafeBatchTransferFrom transfers multiple tokens at once
Batch querybalanceOfBatch queries multiple balances at once
FT + NFT hybridThe total supply of a tokenId determines if it is FT (>1) or NFT (=1)
IERC1155ReceiverSafe callback interface that contract recipients must implement
URI systemIndependent metadata URI per token type
Approval modelsetApprovalForAll all-or-nothing authorization

Built with AiAda