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.
// 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.
ERC-1155, Batch Transfer, Semi-Fungible, URI