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:
- Basic knowledge of Solidity programming
- Familiarity with ERC-20 and ERC-721 standards
- A development environment set up (see our Development Environment guide)
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:
// 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:
npm install @openzeppelin/contractsNow, create a file called MyMultiToken.sol with the following code:
// 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:
// 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:
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 ettiosMainnetInteracting with ERC-1155 Tokens
Checking Balances
// 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
// 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.
Next Steps
Now that you understand ERC-1155, explore these advanced topics: