ERC-1155
ERC-1155 is the multi-token standard: one contract holds any number of fungible (currency, ammo) and non-fungible (items, badges) token IDs. Batch transfers, lower gas, simpler approvals — the right choice for games, marketplaces, and editions where ERC-20 + ERC-721 would mean two contracts and double the gas.
OpenZeppelin, mint, batch, royalties
EXAMPLE
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Supply.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/common/ERC2981.sol";
contract GameItems is ERC1155, ERC1155Supply, ERC2981, Ownable {
// Token IDs — convention: distinct ranges per category
uint256 public constant GOLD = 0;
uint256 public constant SWORD = 1;
uint256 public constant SHIELD = 2;
uint256 public constant POTION = 3;
uint256 public constant CROWN = 1000; // unique 1-of-1
string public constant NAME = "My Game Items";
string public constant SYMBOL = "MGI";
constructor(string memory baseUri, address royaltyReceiver, uint96 royaltyBps)
ERC1155(baseUri) // e.g. 'ipfs://Qm.../{id}.json'
Ownable(msg.sender)
{
_setDefaultRoyalty(royaltyReceiver, royaltyBps);
_mint(msg.sender, GOLD, 100_000, "");
_mint(msg.sender, SWORD, 100, "");
}
// ─── MINT ──────────────────────────────────────────────────
function mint(address to, uint256 id, uint256 amount, bytes calldata data) external onlyOwner {
_mint(to, id, amount, data);
}
function mintBatch(address to, uint256[] calldata ids, uint256[] calldata amounts, bytes calldata data)
external onlyOwner
{
_mintBatch(to, ids, amounts, data);
}
// ─── ADMIN ─────────────────────────────────────────────────
function setURI(string calldata newUri) external onlyOwner {
_setURI(newUri);
}
function setDefaultRoyalty(address receiver, uint96 bps) external onlyOwner {
_setDefaultRoyalty(receiver, bps);
}
// ─── REQUIRED OVERRIDES ────────────────────────────────────
function _update(address from, address to, uint256[] memory ids, uint256[] memory values)
internal override(ERC1155, ERC1155Supply)
{
super._update(from, to, ids, values);
}
function supportsInterface(bytes4 iid)
public view override(ERC1155, ERC2981) returns (bool)
{
return super.supportsInterface(iid);
}
// ─── METADATA ──────────────────────────────────────────────
// URI substitutes {id} (lowercase hex, padded to 64 chars)
// baseURI = 'ipfs://Qm.../{id}.json'
// GOLD → ipfs://Qm.../0000000000000000000000000000000000000000000000000000000000000000.json
// SWORD → ipfs://Qm.../0000000000000000000000000000000000000000000000000000000000000001.json
}
// 1) Why ERC-1155 over ERC-20 + ERC-721
// • One contract = one approval, one deployment, lower per-token gas
// • Batch transfers — send 5 items in a single transaction
// • Marketplace listing of multiple IDs is much cheaper
// • Backwards-compatible-ish: ERC-1155 events are richer than ERC-721's
// 2) ERC-1155 events
// • TransferSingle(operator, from, to, id, value)
// • TransferBatch(operator, from, to, ids[], values[])
// • ApprovalForAll(account, operator, approved)
// • URI(value, id)
//
// Indexers (The Graph, Alchemy, Moralis) parse these directly.
// 3) Approvals — different from ERC-20
// No per-amount approval. Only 'set approval for all':
// setApprovalForAll(operator, approved)
// Used by marketplaces. The user approves the marketplace ONCE for everything.
// 4) Metadata patterns
//
// Centralised URI (simple):
// 'https://api.example.com/items/{id}'
// App returns JSON. Easy to evolve; depends on a server.
//
// IPFS URI (decentralised):
// 'ipfs://Qm.../{id}.json'
// JSONs pinned to IPFS. Resilient; harder to update.
//
// On-chain URI (rare for big collections):
// Override uri(id) to construct on-chain JSON or SVG.
// 5) Frontend integration (ethers v6)
import { ethers } from 'ethers';
import abi from './GameItems.abi.json';
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const items = new ethers.Contract(addr, abi, signer);
// Balance
await items.balanceOf(await signer.getAddress(), 0); // GOLD balance
// Batch balance
const me = await signer.getAddress();
await items.balanceOfBatch([me, me, me], [0, 1, 2]);
// Transfer
await items.safeTransferFrom(me, friend, 0, 100, '0x');
// Batch transfer
await items.safeBatchTransferFrom(me, friend, [0, 1, 2], [50, 1, 3], '0x');
// Marketplace approval
await items.setApprovalForAll(marketAddr, true);
// 6) Token receiver — contracts must implement ERC1155Receiver
// If you transfer to a contract that doesn't implement the receiver hooks, the call reverts.
import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol";
contract MyContract is ERC1155Holder { /* … */ }
// 7) ERC1155Supply — tracks per-id totalSupply()
await items.totalSupply(SWORD); // total minted across all holders
await items.exists(SWORD); // any have been minted?
// 8) Royalties — EIP-2981 (same as ERC-721)
await items.royaltyInfo(SWORD, ethers.parseEther('1'));
// Returns: (receiver, royaltyAmount)
// Marketplaces honor (or ignore) per their policy.
// 9) Burning
// ERC-1155 has _burn(address, id, amount) and _burnBatch.
// Add a public 'burn' wrapper if your design requires user-initiated burns:
function burn(address account, uint256 id, uint256 amount) external {
require(account == msg.sender || isApprovedForAll(account, msg.sender), 'not allowed');
_burn(account, id, amount);
}
// 10) Gas tips
// • Batch mints + batch transfers — significantly cheaper than per-id
// • Pack token IDs in ranges so iteration is cheap (consecutive ids)
// • Skip ERC1155Supply if you don't need totalSupply — saves gas per mint
// • Use immutable for one-shot values set in constructor
// 11) Real-world use cases
// • Game items (sword, shield, gold, potions) — one contract, all assets
// • Editions (1 painting, 100 prints) — same id, different supply per id
// • Limited-edition collectibles (1-of-1 NFTs alongside fungible currency)
// • Marketplace bundles (transfer 5 things in one tx)
// • Loyalty + reward systems with multiple token types
// 12) Vs ERC-721A
// • ERC-1155 is BATCH-first; ERC-721A optimises BULK MINTING of unique NFTs
// • For a 10k pfp collection: ERC-721A (cheap bulk mint, ERC-721 ecosystem support)
// • For a game economy: ERC-1155 (mixed fungible + non-fungible, batch ops)
// 13) Audits
// • OpenZeppelin's contracts are battle-tested; lean on them
// • Slither + Foundry fuzzing for custom logic
// • Real audit (Trail of Bits, OpenZeppelin, ConsenSys Diligence) before significant TVL
// 14) Common bugs
// • _mint to a contract that doesn't implement ERC1155Receiver → reverts (use _safeMint? No — _mint is safe in ERC1155; the receiver check is built in)
// • Mistaking uri(id) format — '{id}' is replaced by lowercase hex 64-char id
// • setApprovalForAll given to a malicious operator — entire wallet drained; warn users in UI
// • Storing token state in the contract that depends on transfer count → recompute on transfer, watch gas
// • Reentrancy in onERC1155Received — protect mints + transfers; use ReentrancyGuard if you do CALL within hooks
// • Mixing ERC-721 + ERC-1155 marketplace logic — different approval models; integrate carefully
// • Forgetting per-id royalty — _setTokenRoyalty(id, …) for unique items vs _setDefaultRoyalty
// • mintBatch with mismatched array lengths → reverts; validate upstream
Why it matters
ERC-1155 is the right choice when you have many token types (fungible + non-fungible) under one contract — games, marketplaces, edition runs. Lean on OpenZeppelin’s implementation, use batch operations to slash gas, store metadata on IPFS with the {id} placeholder, and warn users about setApprovalForAll’s blanket scope.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
contract Items is ERC1155 {
constructor() ERC1155("https://example.com/api/{id}.json") {}
}
Try it Yourself »
Discussion
Loading…