iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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…