- Authors
What is ERC-1155?
Multi Token Standard
ERC-1155 is a multi-token standard proposed by the Enjin team in 2018. It allows managing multiple types of tokens (fungible + non-fungible) within a single contract.
As blockchain gaming grew, the limitations of existing token standards became clear. Creating a single game required deploying separate ERC-20 contracts for currency and ERC-721 contracts for items. Distributing rewards to players required multiple transactions, causing gas fees to skyrocket. ERC-1155 is a standard contract designed to solve these problems.
Problems before ERC-1155:
- ERC-20: Only supports fungible tokens (e.g., USDT, UNI)
- ERC-721: Only supports non-fungible NFTs (e.g., Pudgy Penguins, BAYC)
- Managing coins + items in games required multiple contracts
- Transferring multiple tokens required multiple transactions → increased gas fees
- Deployment costs and management overhead for each contract
ERC-1155 Solutions:
- Manage multiple token types with a single contract
- Save up to 90% on gas fees with batch transfers
- Supports both fungible and non-fungible tokens
- Efficient metadata URI management
- Minimized deployment and maintenance costs
ERC-1155 vs ERC-20 vs ERC-721
ERC-20: 1 contract = 1 token
- A USDT contract only issues USDT. To create a different token, you need to deploy a new contract.
ERC-721: 1 contract = multiple unique NFTs
- A Bored Ape contract manages thousands of NFTs like #1, #2, #3..., but each represents a completely different ape.
ERC-1155: 1 contract = fungible tokens + NFTs combined
- A single game item contract can issue Gold (fungible) and Legendary Sword (NFT). Each token is distinguished by its ID.
ERC-1155 Data Structure
ERC-1155 distinguishes each token type by id, and each id has an amount.
Double Mapping Structure
The core of ERC-1155 is a double mapping structure. The first key is the token ID, and the second key is the owner's address.
// Inside ERC-1155 contract
// id => (owner => balance)
mapping(uint256 => mapping(address => uint256)) private _balances;
// Example: Game items
_balances[0][alice] = 500 // Alice has 500 Gold
_balances[0][bob] = 300 // Bob has 300 Gold
_balances[1][alice] = 1 // Alice owns 1 Sword (id=1)
_balances[2][bob] = 1 // Bob owns 1 Shield (id=2)
_balances[3][alice] = 10 // Alice has 10 Potions
This structure allows multiple people to own the same token ID simultaneously. Alice can have 500 Gold (id=0) while Bob has 300.
Difference from ERC-721
ERC-721 has a 1:1 mapping of tokenId → owner, but ERC-1155 has a 1:N mapping of id → (owner → amount).
// ERC-721: tokenId → owner (1:1 mapping)
mapping(uint256 => address) private _owners;
_owners[5] = alice // Owner of NFT #5 is Alice (only one person possible)
// ERC-1155: id → (owner → amount) (1:N mapping)
mapping(uint256 => mapping(address => uint256)) private _balances;
_balances[0][alice] = 500 // Alice has 500 Gold
_balances[0][bob] = 300 // Bob has 300 Gold (multiple owners possible)
ERC-1155 Core Functions
1. View Functions
// Query how many tokens of a specific id an account holds
// Combines concepts from ERC-20's balanceOf + ERC-721
function balanceOf(address account, uint256 id) public view returns (uint256)
// Query multiple account balances for multiple tokens at once (batch query)
// accounts and ids arrays must have the same length
function balanceOfBatch(
address[] accounts,
uint256[] ids
) public view returns (uint256[])
// Returns the token's metadata URI
// Replace {id} with the actual token ID
function uri(uint256 id) public view returns (string)
// Check if operator has permission to manage all of owner's tokens
function isApprovedForAll(address owner, address operator) public view returns (bool)
2. Transaction Functions
// Single token transfer
// Transfer amount of id tokens from 'from' to 'to'
function safeTransferFrom(
address from,
address to,
uint256 id,
uint256 amount,
bytes data
) public
// Batch transfer (transfer multiple token types at once)
// ids and amounts arrays must have the same length
// Example: ids=[0,1,3], amounts=[100,1,10]
// → Transfer 100 Gold, 1 Sword, 10 Potions simultaneously
function safeBatchTransferFrom(
address from,
address to,
uint256[] ids,
uint256[] amounts,
bytes data
) public
// Approve/revoke operator to manage all of owner's tokens
// Same as ERC-721's setApprovalForAll
function setApprovalForAll(address operator, bool approved) public
3. Events
// Emitted when a single token is transferred
event TransferSingle(
address indexed operator, // Address that executed the transfer
address indexed from,
address indexed to,
uint256 id,
uint256 amount
)
// Emitted when a batch transfer occurs
event TransferBatch(
address indexed operator,
address indexed from,
address indexed to,
uint256[] ids,
uint256[] amounts
)
// Emitted when full approval is set/revoked
event ApprovalForAll(
address indexed owner,
address indexed operator,
bool approved
)
// Emitted when URI changes
event URI(string value, uint256 indexed id)
ERC-1155 Contract Implementation Example
Here's how a real ERC-1155 contract is implemented:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleERC1155 {
// Token ID => (Owner => Balance)
mapping(uint256 => mapping(address => uint256)) private _balances;
// Owner => (Approved address => Approval status)
mapping(address => mapping(address => bool)) private _operatorApprovals;
// Metadata URI (shared by all tokens)
string private _uri;
// Events
event TransferSingle(
address indexed operator,
address indexed from,
address indexed to,
uint256 id,
uint256 amount
);
event TransferBatch(
address indexed operator,
address indexed from,
address indexed to,
uint256[] ids,
uint256[] amounts
);
event ApprovalForAll(
address indexed owner,
address indexed operator,
bool approved
);
event URI(string value, uint256 indexed id);
constructor(string memory uri_) {
_uri = uri_;
}
// Query balance
function balanceOf(address account, uint256 id) public view returns (uint256) {
require(account != address(0), "Invalid address");
return _balances[id][account];
}
// Batch balance query
function balanceOfBatch(
address[] memory accounts,
uint256[] memory ids
) public view returns (uint256[] memory) {
require(accounts.length == ids.length, "Array lengths must match");
uint256[] memory batchBalances = new uint256[](accounts.length);
for (uint256 i = 0; i < accounts.length; i++) {
batchBalances[i] = balanceOf(accounts[i], ids[i]);
}
return batchBalances;
}
// Query URI
function uri(uint256 id) public view returns (string memory) {
return _uri;
}
// Single transfer
function safeTransferFrom(
address from,
address to,
uint256 id,
uint256 amount,
bytes memory data
) public {
require(to != address(0), "Invalid recipient address");
require(
from == msg.sender || isApprovedForAll(from, msg.sender),
"No transfer permission"
);
require(_balances[id][from] >= amount, "Insufficient balance");
_balances[id][from] -= amount;
_balances[id][to] += amount;
emit TransferSingle(msg.sender, from, to, id, amount);
}
// Batch transfer
function safeBatchTransferFrom(
address from,
address to,
uint256[] memory ids,
uint256[] memory amounts,
bytes memory data
) public {
require(to != address(0), "Invalid recipient address");
require(ids.length == amounts.length, "Array lengths must match");
require(
from == msg.sender || isApprovedForAll(from, msg.sender),
"No transfer permission"
);
for (uint256 i = 0; i < ids.length; i++) {
uint256 id = ids[i];
uint256 amount = amounts[i];
require(_balances[id][from] >= amount, "Insufficient balance");
_balances[id][from] -= amount;
_balances[id][to] += amount;
}
emit TransferBatch(msg.sender, from, to, ids, amounts);
}
// Full approval
function setApprovalForAll(address operator, bool approved) public {
require(operator != msg.sender, "Cannot approve to self");
_operatorApprovals[msg.sender][operator] = approved;
emit ApprovalForAll(msg.sender, operator, approved);
}
// Query approval
function isApprovedForAll(address owner, address operator) public view returns (bool) {
return _operatorApprovals[owner][operator];
}
// Mint tokens
function mint(address to, uint256 id, uint256 amount) public {
require(to != address(0), "Invalid address");
_balances[id][to] += amount;
emit TransferSingle(msg.sender, address(0), to, id, amount);
}
// Batch mint
function mintBatch(
address to,
uint256[] memory ids,
uint256[] memory amounts
) public {
require(to != address(0), "Invalid address");
require(ids.length == amounts.length, "Array lengths must match");
for (uint256 i = 0; i < ids.length; i++) {
_balances[ids[i]][to] += amounts[i];
}
emit TransferBatch(msg.sender, address(0), to, ids, amounts);
}
}
Core Operating Principles
1. Single Token Transfer (safeTransferFrom)
Example of Alice transferring 100 Gold to Bob:
// Alice transfers 100 Gold (id=0) to Bob
token.safeTransferFrom(alice, bob, 0, 100, "");
// What happens inside the contract:
require(_balances[0][alice] >= 100, "Insufficient balance");
_balances[0][alice] -= 100; // Alice: 1000 → 900
_balances[0][bob] += 100; // Bob: 500 → 600
emit TransferSingle(msg.sender, alice, bob, 0, 100);
2. Batch Transfer (safeBatchTransferFrom)
Transfer multiple token types in a single transaction to save on gas fees.
// Alice transfers multiple items to Bob simultaneously
// 100 Gold + 1 Sword + 10 Potions
uint256[] memory ids = [0, 1, 3];
uint256[] memory amounts = [100, 1, 10];
token.safeBatchTransferFrom(alice, bob, ids, amounts, "");
// What happens inside the contract (processed in a loop):
// Gold (id=0)
_balances[0][alice] -= 100;
_balances[0][bob] += 100;
// Sword (id=1)
_balances[1][alice] -= 1;
_balances[1][bob] += 1;
// Potion (id=3)
_balances[3][alice] -= 10;
_balances[3][bob] += 10;
emit TransferBatch(msg.sender, alice, bob, [0,1,3], [100,1,10]);
3. Approval (setApprovalForAll)
What is Approval?
Approval is a function that grants permission for another address (person or contract) to transfer your tokens on your behalf. For example, to sell items on NFT marketplaces (OpenSea, Blur, etc.), the marketplace contract needs to be able to transfer tokens to buyers. To enable this, sellers must first approve the marketplace with "permission to transfer my tokens on my behalf."
Without approval, other addresses cannot move your tokens. However, once approved, that address can transfer tokens without your explicit permission each time. Therefore, you should only approve trusted contracts.
ERC-1155's Approval Method
Unlike ERC-721, ERC-1155 does not have individual token approval. Only setApprovalForAll() is provided.
This means once approved, the operator can freely transfer all token IDs and all quantities owned by the owner. For example, approving OpenSea grants permission to transfer all 1000 Gold, 1 Legendary Sword, and 50 Potions.
Therefore, never execute setApprovalForAll() for untrusted contracts or addresses. After approval, all your assets could be stolen.
// Alice grants OpenSea permission to manage all her ERC-1155 tokens
token.setApprovalForAll(openSeaAddress, true);
// Inside the contract:
_operatorApprovals[alice][openSea] = true;
emit ApprovalForAll(alice, openSea, true);
// Now OpenSea can transfer all of Alice's tokens (id=0,1,2,3...)
Difference from ERC-721
// ERC-721: Both individual and full approval possible
nft.approve(marketplace, tokenId); // Specific NFT only
nft.setApprovalForAll(marketplace, true); // All NFTs
// ERC-1155: Only full approval available
multiToken.setApprovalForAll(marketplace, true); // All tokens
// No approve() function
Creating ERC-1155 Tokens with OpenZeppelin
When writing ERC-1155 contracts, it's best to use the verified implementation from OpenZeppelin ERC-1155.
// contracts/GameItems.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {ERC1155} from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
contract GameItems is ERC1155 {
// Token ID definitions
uint256 public constant GOLD = 0; // Fungible game currency
uint256 public constant SILVER = 1; // Fungible game currency
uint256 public constant THORS_HAMMER = 2; // Unique NFT (quantity 1)
uint256 public constant SWORD = 3; // Fungible item
uint256 public constant SHIELD = 4; // Fungible item
constructor() ERC1155("https://game.example/api/item/{id}.json") {
// Initial item minting
_mint(msg.sender, GOLD, 10 ** 18, ""); // 1,000,000,000,000,000,000 (18 decimals)
_mint(msg.sender, SILVER, 10 ** 27, ""); // 1,000,000,000,000,000,000,000,000,000
_mint(msg.sender, THORS_HAMMER, 1, ""); // 1 unique item
_mint(msg.sender, SWORD, 10 ** 9, ""); // 1,000,000,000 units
_mint(msg.sender, SHIELD, 10 ** 9, ""); // 1,000,000,000 units
}
}