ERC-1155 Multi-Token Standard

This guide explains the ERC-1155 Multi-Token standard, which allows for creating both fungible and non-fungible tokens in a single contract with improved efficiency.

Prerequisites

Before you begin, make sure you have:

What is ERC-1155?

ERC-1155 is a token standard that enables the creation of both fungible and non-fungible tokens in a single contract. It was designed to address limitations in the ERC-20 and ERC-721 standards, particularly for gaming and complex applications that require multiple token types.

Key Advantages of ERC-1155

  • Batch transfers of multiple token types in a single transaction
  • Gas efficiency through shared contract logic
  • Semi-fungible tokens (e.g., limited edition items)
  • Atomic swaps between different token types
  • Ability to create both fungible and non-fungible assets in one contract

Gaming Items

Create common items (fungible), unique weapons (NFTs), and limited edition items (semi-fungible) all in one contract.

Marketplaces

Facilitate complex trades and batch operations for various asset types in a gas-efficient manner.

DeFi Applications

Bundle different token types together and process them in a single transaction, reducing gas costs.

ERC-1155 Interface

The ERC-1155 standard introduces several functions that make it distinct from earlier standards:

ERC-1155 Interface
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IERC1155 {
    // Core functions
    function balanceOf(address account, uint256 id) external view returns (uint256);
    function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids) external view returns (uint256[] memory);
    function setApprovalForAll(address operator, bool approved) external;
    function isApprovedForAll(address account, address operator) external view returns (bool);
    function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes calldata data) external;
    function safeBatchTransferFrom(address from, address to, uint256[] calldata ids, uint256[] calldata amounts, bytes calldata data) external;

    // Events
    event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value);
    event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values);
    event ApprovalForAll(address indexed account, address indexed operator, bool approved);
    event URI(string value, uint256 indexed id);
}

Key Functions

balanceOf(address account, uint256 id)

Returns the amount of tokens of a specific token ID owned by an account.

balanceOfBatch(address[] accounts, uint256[] ids)

Batch version of balanceOf - returns the balance of multiple token IDs for multiple addresses in a single call.

safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes data)

Transfers a specific amount of a token ID from one address to another.

safeBatchTransferFrom(address from, address to, uint256[] ids, uint256[] amounts, bytes data)

Batch version of safeTransferFrom - transfers multiple token IDs with different amounts in a single transaction.

Creating an ERC-1155 Token Contract

Let's create a multi-token contract using OpenZeppelin's implementation. First, install the OpenZeppelin Contracts library:

Terminal
npm install @openzeppelin/contracts

Now, create a file called MyMultiToken.sol with the following code:

MyMultiToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

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

contract MyMultiToken is ERC1155, Ownable, ERC1155Supply {
    // Token type IDs
    uint256 public constant FUNGIBLE_TOKEN = 0;
    uint256 public constant GOLD_NFT = 1;
    uint256 public constant SILVER_NFT = 2;
    uint256 public constant BRONZE_NFT = 3;
    
    // Token names
    string public name;
    string public symbol;
    
    // Optional: Token URI mapping for NFTs
    mapping(uint256 => string) private _tokenURIs;

    constructor(string memory _name, string memory _symbol) 
        ERC1155("") 
        Ownable(msg.sender)
    {
        name = _name;
        symbol = _symbol;
        
        // Mint initial supply of fungible tokens to the owner
        _mint(msg.sender, FUNGIBLE_TOKEN, 1000000 * 10**18, "");
        
        // Mint one of each NFT to the owner
        _mint(msg.sender, GOLD_NFT, 1, "");
        _mint(msg.sender, SILVER_NFT, 1, "");
        _mint(msg.sender, BRONZE_NFT, 1, "");
    }
    
    // Mint more tokens (only owner)
    function mint(address to, uint256 id, uint256 amount, bytes memory data) public onlyOwner {
        _mint(to, id, amount, data);
    }
    
    // Mint multiple token types at once (only owner)
    function mintBatch(address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data) public onlyOwner {
        _mintBatch(to, ids, amounts, data);
    }
    
    // Set URI for a specific token ID
    function setURI(uint256 id, string memory tokenURI) public onlyOwner {
        _tokenURIs[id] = tokenURI;
        emit URI(uri(id), id);
    }
    
    // Override the uri function to return the token-specific URI if available
    function uri(uint256 id) public view override returns (string memory) {
        string memory tokenURI = _tokenURIs[id];
        
        // If there is no token-specific URI, return the base URI
        if (bytes(tokenURI).length == 0) {
            return super.uri(id);
        }
        
        return tokenURI;
    }
    
    // Set the base URI for all tokens
    function setBaseURI(string memory baseURI) public onlyOwner {
        _setURI(baseURI);
    }
    
    // Override required by Solidity for ERC1155Supply
    function _beforeTokenTransfer(
        address operator,
        address from,
        address to,
        uint256[] memory ids,
        uint256[] memory amounts,
        bytes memory data
    ) internal override(ERC1155, ERC1155Supply) {
        super._beforeTokenTransfer(operator, from, to, ids, amounts, data);
    }
}

Token ID Design Patterns

With ERC-1155, how you design your token IDs matters. Here are some common patterns:

Simple Constants

In the example above, we used simple constants for different token types:

uint256 public constant FUNGIBLE_TOKEN = 0;
uint256 public constant GOLD_NFT = 1;

This works well for a small, fixed number of token types.

Bit Manipulation

Use bit ranges to encode token properties in the ID itself:

Bit Manipulation Pattern
// First 8 bits: token type (0=fungible, 1=NFT, 2=semi-fungible)
// Next 8 bits: rarity (0-255)
// Next 16 bits: item ID

function createTokenId(uint8 tokenType, uint8 rarity, uint16 itemId) public pure returns (uint256) {
    return uint256(tokenType) << 24 | uint256(rarity) << 16 | uint256(itemId);
}

function getTokenType(uint256 id) public pure returns (uint8) {
    return uint8(id >> 24);
}

function getRarity(uint256 id) public pure returns (uint8) {
    return uint8(id >> 16);
}

function getItemId(uint256 id) public pure returns (uint16) {
    return uint16(id);
}

Range-Based Categorization

Define ID ranges for different token types:

// 0-999: Fungible tokens
// 1000-1999: Common NFTs
// 2000-2999: Rare NFTs
// 3000-3999: Legendary NFTs

function isFungible(uint256 id) public pure returns (bool) {
    return id < 1000;
}

function isLegendary(uint256 id) public pure returns (bool) {
    return id >= 3000 && id < 4000;
}

This approach is simple and intuitive for categorizing token types.

Deploying Your ERC-1155 Contract

Deploy your multi-token contract using Hardhat:

scripts/deploy-multi-token.js
const hre = require("hardhat");

async function main() {
  const MyMultiToken = await hre.ethers.getContractFactory("MyMultiToken");
  
  // Deploy with name and symbol
  const multiToken = await MyMultiToken.deploy("My Multi Token", "MMT");
  
  await multiToken.deployed();
  
  console.log("MyMultiToken deployed to:", multiToken.address);
  
  // Set the base URI for token metadata
  await multiToken.setBaseURI("https://your-api.com/token/");
  
  // Set individual URIs for NFTs
  await multiToken.setURI(1, "ipfs://QmYourIPFSHash/gold.json");
  await multiToken.setURI(2, "ipfs://QmYourIPFSHash/silver.json");
  await multiToken.setURI(3, "ipfs://QmYourIPFSHash/bronze.json");
  
  console.log("URIs set successfully");
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

Deploy to the Ettios mainnet:

npx hardhat run scripts/deploy-multi-token.js --network ettiosMainnet

Interacting with ERC-1155 Tokens

Checking Balances

JavaScript
// Get the balance of a single token ID
const balance = await contract.balanceOf(userAddress, tokenId);
console.log("User has", balance.toString(), "of token ID", tokenId);

// Get balances of multiple token IDs in one call
const addresses = [userAddress, userAddress, userAddress];
const ids = [0, 1, 2]; // Different token IDs
const balances = await contract.balanceOfBatch(addresses, ids);
console.log("Token ID 0 balance:", balances[0].toString());
console.log("Token ID 1 balance:", balances[1].toString());
console.log("Token ID 2 balance:", balances[2].toString());

Transferring Tokens

JavaScript
// Transfer a single token type
await contract.safeTransferFrom(
  fromAddress,
  toAddress,
  tokenId,
  amount,
  "0x" // No data
);

// Transfer multiple token types in one transaction
await contract.safeBatchTransferFrom(
  fromAddress,
  toAddress,
  [0, 1, 2], // Array of token IDs
  [100, 1, 1], // Array of amounts
  "0x" // No data
);

Important Note

When receiving ERC-1155 tokens, contracts must implement the ERC1155TokenReceiver interface. Individual users (EOA accounts) don't need this.

Use Case: Game Items

Let's see how ERC-1155 can be used for a game with various item types:

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

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

contract GameItems is ERC1155, Ownable {
    using Strings for uint256;
    
    // Item types
    uint256 public constant GOLD_COIN = 0;       // Fungible currency
    uint256 public constant HEALTH_POTION = 1;   // Fungible consumable
    uint256 public constant LEGENDARY_SWORD = 2; // NFT (unique item)
    uint256 public constant SHIELD = 3;          // Semi-fungible (limited edition)
    
    // Counter for creating new item types
    uint256 private _nextItemId = 4;
    
    // Item name mapping
    mapping(uint256 => string) private _itemNames;
    
    // Max supply tracking
    mapping(uint256 => uint256) private _maxSupply;
    mapping(uint256 => uint256) private _totalSupply;
    
    constructor() ERC1155("https://game-api.example/items/{id}.json") Ownable(msg.sender) {
        // Set item names
        _itemNames[GOLD_COIN] = "Gold Coin";
        _itemNames[HEALTH_POTION] = "Health Potion";
        _itemNames[LEGENDARY_SWORD] = "Legendary Sword";
        _itemNames[SHIELD] = "Shield";
        
        // Set max supply
        _maxSupply[LEGENDARY_SWORD] = 1;  // Only one legendary sword
        _maxSupply[SHIELD] = 100;         // Limited edition shields
        
        // Pre-mint items to the game treasury
        _mint(msg.sender, GOLD_COIN, 1000000, "");
        _mint(msg.sender, HEALTH_POTION, 1000, "");
        _mint(msg.sender, LEGENDARY_SWORD, 1, "");
        _mint(msg.sender, SHIELD, 100, "");
        
        // Track supply for limited items
        _totalSupply[LEGENDARY_SWORD] = 1;
        _totalSupply[SHIELD] = 100;
    }
    
    // Create a new item type
    function createItemType(string memory name, uint256 initialSupply, uint256 maxSupply_) 
        public 
        onlyOwner 
        returns (uint256) 
    {
        uint256 newItemId = _nextItemId;
        _nextItemId++;
        
        _itemNames[newItemId] = name;
        
        if (maxSupply_ > 0) {
            _maxSupply[newItemId] = maxSupply_;
        }
        
        if (initialSupply > 0) {
            _mint(msg.sender, newItemId, initialSupply, "");
            if (maxSupply_ > 0) {
                _totalSupply[newItemId] = initialSupply;
            }
        }
        
        return newItemId;
    }
    
    // Mint more of an existing item
    function mintItems(address to, uint256 id, uint256 amount) public onlyOwner {
        // Check max supply for limited items
        if (_maxSupply[id] > 0) {
            require(_totalSupply[id] + amount <= _maxSupply[id], "Max supply exceeded");
            _totalSupply[id] += amount;
        }
        
        _mint(to, id, amount, "");
    }
    
    // Burn items (e.g., when used in the game)
    function burnItems(address from, uint256 id, uint256 amount) public {
        require(
            from == msg.sender || isApprovedForAll(from, msg.sender),
            "Caller is not owner nor approved"
        );
        
        if (_maxSupply[id] > 0) {
            _totalSupply[id] -= amount;
        }
        
        _burn(from, id, amount);
    }
    
    // Override URI generation to use item names in metadata
    function uri(uint256 id) public view override returns (string memory) {
        require(bytes(_itemNames[id]).length > 0, "URI query for nonexistent item");
        
        string memory baseURI = super.uri(id);
        return bytes(baseURI).length > 0 ? 
            string(abi.encodePacked(baseURI, id.toString(), ".json")) : 
            "";
    }
    
    // Get item name
    function itemName(uint256 id) public view returns (string memory) {
        return _itemNames[id];
    }
    
    // Check if an item is limited edition
    function isLimited(uint256 id) public view returns (bool) {
        return _maxSupply[id] > 0;
    }
    
    // Get max supply of an item
    function maxSupply(uint256 id) public view returns (uint256) {
        return _maxSupply[id];
    }
    
    // Get current supply of a limited item
    function totalSupply(uint256 id) public view returns (uint256) {
        return _totalSupply[id];
    }
}

This example demonstrates how ERC-1155 can elegantly handle various token types in a single contract, making it perfect for games with complex item economies.