ERC-1155 Multi-Token Development

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1 servicesAll 1306 services
ERC-1155 Multi-Token Development
Medium
~2-3 business days
FAQ
Blockchain Development Services
Blockchain Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1214
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823

ERC-1155 Token Development (Multi-token)

ERC-721 is good for unique assets, ERC-20 for fungible. ERC-1155 appeared because real applications often need both types simultaneously. In a gaming project: gold and resources are fungible tokens, characters are NFTs, potions are semi-fungible (100 units of one type). Before ERC-1155, you deployed multiple contracts. EIP-1155 solves this in one contract with significantly lower gas overhead.

Key differences from ERC-20 and ERC-721

Batch operations — the main advantage. Instead of N transactions to transfer N different tokens:

// ERC-721: N transactions
for (uint i = 0; i < tokenIds.length; i++) {
    nft.transferFrom(from, to, tokenIds[i]); // N*gas
}

// ERC-1155: one transaction
erc1155.safeBatchTransferFrom(from, to, ids, amounts, data); // ~gas

Gas savings on batch transfer: 40–60% compared to equivalent separate transactions.

Single balance registry:

// Balance of specific token for specific address
mapping(uint256 => mapping(address => uint256)) private _balances;

// Check balance
function balanceOf(address account, uint256 id) public view returns (uint256) {
    return _balances[id][account];
}

Semi-fungible tokens — tokens with unique ID but quantity > 1. Limited edition of 1000 copies of a poster (all identical but limited), game items with identical properties. Implementation depends on agreement on how to interpret ID.

ID scheme design

Uint256 for ID provides huge space. Standard schemes:

Simple sequential — tokens with ID 1, 2, 3... Works for simple cases. No built-in semantics.

Bit-packed ID — different uint256 parts encode different properties:

// Example: upper 128 bits = type, lower 128 = instance ID
uint256 constant TYPE_MASK = uint256(type(uint128).max) << 128;
uint256 constant NF_INDEX_MASK = type(uint128).max;

function getTokenType(uint256 id) internal pure returns (uint256) {
    return id & TYPE_MASK;
}

function isNonFungible(uint256 id) internal pure returns (bool) {
    return id & TYPE_MASK == id; // instance ID = 0 means base type
}

Hierarchical scheme for gaming:

bits [255:240] = category   (weapons, armor, resources, consumables)
bits [239:224] = rarity     (common, uncommon, rare, epic, legendary)
bits [223:128] = item_type  (sword, shield, potion...)
bits [127:0]   = instance   (0 for fungible, >0 for NFT)

This allows efficient filtering: tokenId & CATEGORY_MASK == WEAPONS_CATEGORY.

Implementation: what matters during development

Standard base — OpenZeppelin

OpenZeppelin ERC1155 is well-tested and covers the standard. Don't reinvent:

import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Burnable.sol";
import "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Supply.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155URIStorage.sol";

contract GameItems is ERC1155, ERC1155Burnable, ERC1155Supply, AccessControl {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

    // Per-token URI for unique metadata for each type
    mapping(uint256 => string) private _tokenURIs;

    constructor() ERC1155("") {
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _grantRole(MINTER_ROLE, msg.sender);
    }

    function mint(
        address to,
        uint256 id,
        uint256 amount,
        bytes memory data
    ) external onlyRole(MINTER_ROLE) {
        _mint(to, id, amount, data);
    }

    function mintBatch(
        address to,
        uint256[] memory ids,
        uint256[] memory amounts,
        bytes memory data
    ) external onlyRole(MINTER_ROLE) {
        _mintBatch(to, ids, amounts, data);
    }
}

Callback security: onERC1155Received

ERC-1155 requires checking that receiving contract implements IERC1155Receiver. If contract doesn't implement interface — safeTransferFrom reverts, tokens don't get lost. This is important difference from ERC-20, where tokens can be sent to contract without support and lost.

Typical mistake: checking supportsInterface instead of calling onERC1155Received. OpenZeppelin does this correctly, but custom implementation easily errs.

Operator approvals

ERC-1155 uses setApprovalForAll — approval is given for ALL tokens in contract. No analogue of approve(spender, tokenId, amount) from ERC-20/721. This is convenient for game marketplaces (one approval → marketplace can move all tokens), but creates risk with compromised operator address.

// For contract operators need whitelist
mapping(address => bool) public trustedOperators;

function setApprovalForAll(address operator, bool approved) public override {
    if (approved) {
        require(trustedOperators[operator], "Operator not trusted");
    }
    super.setApprovalForAll(operator, approved);
}

Metadata: URI scheme

Standard defines uri(uint256 id) should return URI with {id} placeholder:

https://api.example.com/tokens/{id}.json

{id} is replaced with hex-encoded lowercase ID value padded with zeros to 64 characters.

For NFT inside ERC-1155 you need per-token URI. OpenZeppelin ERC1155URIStorage supports this:

// Set URI for specific token
function setTokenURI(uint256 tokenId, string memory tokenURI)
    external onlyRole(MINTER_ROLE) {
    _setURI(tokenId, tokenURI);
}

On-chain metadata — for simple tokens (game currencies, items with basic attributes) you can store basic properties on-chain, generate JSON in uri():

function uri(uint256 id) public view override returns (string memory) {
    ItemDefinition memory item = itemDefinitions[id];
    return string(abi.encodePacked(
        'data:application/json;base64,',
        Base64.encode(bytes(abi.encodePacked(
            '{"name":"', item.name, '","description":"', item.description,
            '","attributes":[{"trait_type":"rarity","value":"', item.rarity, '"}]}'
        )))
    ));
}

Marketplace integration

OpenSea / Blur support ERC-1155. For correct display:

  • For NFTs (quantity=1): standard JSON metadata with image, attributes
  • For fungible (quantity>1): decimals in metadata (OpenSea uses for amount display)
  • contractURI() — collection metadata (name, description, image, royalties)

Royalties — EIP-2981 for royalty information:

import "@openzeppelin/contracts/token/common/ERC2981.sol";

contract GameItems is ERC1155, ERC2981 {
    constructor() ERC1155("") {
        // 5% royalty on all tokens
        _setDefaultRoyalty(treasury, 500); // 500 = 5% (basis points)
    }

    // Per-token royalty for special tokens
    function setTokenRoyalty(uint256 tokenId, address receiver, uint96 feeNumerator)
        external onlyRole(DEFAULT_ADMIN_ROLE) {
        _setTokenRoyalty(tokenId, receiver, feeNumerator);
    }
}

Typical mistakes

Supply tracking — ERC-1155 base doesn't track totalSupply. If you need cap on mintable amount — use ERC1155Supply and add checks in mint functions.

Burn and totalSupply — after burn totalSupply should decrease. ERC1155Supply does this automatically, custom implementations often forget.

Reentrancy through onERC1155Received_mint calls onERC1155Received on recipient. If recipient is malicious contract, it can call mint again inside callback. ReentrancyGuard is mandatory for mint functions with external logic.