ERC-1155 Multi-Token

Difficulty: Intermediate

ERC-1155, defined in EIP-1155, is a multi-token standard that allows a single contract to manage multiple token types simultaneously. Unlike ERC-20 (one contract per fungible token) or ERC-721 (one contract per NFT collection with individually unique tokens), ERC-1155 can represent any combination of fungible, non-fungible, and semi-fungible tokens within a single contract. This versatility, combined with significant gas savings from batch operations, has made ERC-1155 the standard of choice for gaming, metaverse, and multi-asset applications.

The key innovation of ERC-1155 is the `id` parameter present in all functions. Each token type is identified by a `uint256 id`, and balances are tracked per `(account, id)` pair. A single ERC-1155 contract can have id=1 representing gold coins (fungible, supply of millions), id=2 representing a legendary sword (non-fungible, supply of 1), and id=3 representing event tickets (semi-fungible, supply of 1000 initially identical tickets that become unique after use). This eliminates the need to deploy separate contracts for each asset type.

Batch operations are where ERC-1155 truly shines. The `safeBatchTransferFrom(from, to, ids, amounts, data)` function transfers multiple token types in a single transaction, dramatically reducing gas costs compared to making individual ERC-20 or ERC-721 transfers. Similarly, `balanceOfBatch(accounts, ids)` queries multiple balances in one call. For gaming applications where players might trade dozens of items at once, this efficiency is transformative.

The URI mechanism in ERC-1155 uses a single `uri(id)` function that returns a metadata URI for any token id. The standard supports a `{id}` substitution pattern: if the base URI is `https://api.game.com/items/{id}.json`, clients replace `{id}` with the hex-padded token id to construct the full URL. This means one URI template can serve metadata for billions of token types without storing individual URIs on-chain.

ERC-1155 requires recipients to implement `IERC1155Receiver` (with `onERC1155Received` and `onERC1155BatchReceived` hooks), similar to ERC-721's safe transfer checks. This prevents tokens from being permanently locked in contracts that cannot handle them. The `data` parameter in transfer functions allows passing additional context to the receiver, enabling rich interactions like automatic staking or item equipment on receipt.

Code examples

Game Items ERC-1155 Contract

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

import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract GameItems is ERC1155, Ownable {
    // Token IDs
    uint256 public constant GOLD = 0;
    uint256 public constant SILVER = 1;
    uint256 public constant SWORD = 2;
    uint256 public constant SHIELD = 3;
    uint256 public constant LEGENDARY_ARMOR = 4;

    // Token names for display
    mapping(uint256 => string) public tokenNames;

    // Track which IDs are non-fungible (max supply 1)
    mapping(uint256 => bool) public isNonFungible;
    mapping(uint256 => uint256) public totalSupply;
    mapping(uint256 => uint256) public maxSupply;

    event ItemMinted(address indexed to, uint256 indexed id, uint256 amount);
    event BatchMinted(address indexed to, uint256[] ids, uint256[] amounts);

    constructor(address initialOwner)
        ERC1155("https://game.example.com/api/items/{id}.json")
        Ownable(initialOwner)
    {
        tokenNames[GOLD] = "Gold Coin";
        tokenNames[SILVER] = "Silver Coin";
        tokenNames[SWORD] = "Iron Sword";
        tokenNames[SHIELD] = "Wooden Shield";
        tokenNames[LEGENDARY_ARMOR] = "Dragon Scale Armor";

        // Legendary armor is non-fungible
        isNonFungible[LEGENDARY_ARMOR] = true;
        maxSupply[LEGENDARY_ARMOR] = 100;

        // Currencies have unlimited supply (set max to max uint)
        maxSupply[GOLD] = type(uint256).max;
        maxSupply[SILVER] = type(uint256).max;

        // Equipment has limited supply
        maxSupply[SWORD] = 10_000;
        maxSupply[SHIELD] = 10_000;
    }

    function mint(
        address to,
        uint256 id,
        uint256 amount,
        bytes memory data
    ) external onlyOwner {
        require(totalSupply[id] + amount <= maxSupply[id], "Exceeds max supply");
        if (isNonFungible[id]) {
            require(amount == 1, "NFTs: amount must be 1");
        }
        totalSupply[id] += amount;
        _mint(to, id, amount, data);
        emit ItemMinted(to, id, amount);
    }

    function mintBatch(
        address to,
        uint256[] memory ids,
        uint256[] memory amounts,
        bytes memory data
    ) external onlyOwner {
        for (uint256 i = 0; i < ids.length; i++) {
            require(
                totalSupply[ids[i]] + amounts[i] <= maxSupply[ids[i]],
                "Exceeds max supply"
            );
            if (isNonFungible[ids[i]]) {
                require(amounts[i] == 1, "NFTs: amount must be 1");
            }
            totalSupply[ids[i]] += amounts[i];
        }
        _mintBatch(to, ids, amounts, data);
        emit BatchMinted(to, ids, amounts);
    }

    function setURI(string memory newuri) external onlyOwner {
        _setURI(newuri);
    }

    // Convenience: get balances for multiple items for one player
    function getPlayerInventory(address player)
        external
        view
        returns (uint256[] memory)
    {
        uint256[] memory balances = new uint256[](5);
        for (uint256 i = 0; i < 5; i++) {
            balances[i] = balanceOf(player, i);
        }
        return balances;
    }
}

This game items contract manages both fungible currencies (gold, silver) and semi-fungible/non-fungible equipment in a single contract. Batch minting allows creating starter packs efficiently. Supply tracking ensures limited items remain scarce.

Key points

Concepts covered

ERC-1155, Batch Transfer, Semi-Fungible, URI