Розробка ERC-1155 токена (мульти-токен)

Проєктуємо та розробляємо блокчейн-рішення повного циклу: від архітектури смарт-контрактів до запуску DeFi-протоколів, NFT-маркетплейсів та криптобірж. Аудит безпеки, токеноміка, інтеграція з наявною інфраструктурою.
Показано 1 з 1Усі 1306 послуг
Розробка ERC-1155 токена (мульти-токен)
Середній
~2-3 дні
Часті запитання

Напрямки блокчейн-розробки

Етапи блокчейн-розробки

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1306
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1218
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    919
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1147
  • image_logo-advance_0.webp
    Розробка логотипу компанії B2B Advance
    609
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    884

Розробка токена ERC-1155 (мультитокен)

ERC-721 хороший для унікальних активів, ERC-20 — для взаємозамінюваних. ERC-1155 з'явився тому, що реальні застосунки часто потребують обох типів одночасно. У ігровому проекті: золото та ресурси — це fungible токени, персонажі — NFT, зелля — semi-fungible (100 одиниць одного типу). До ERC-1155 для цього розгортали кілька контрактів. EIP-1155 вирішує це в одному контракті з істотно меншими газовими витратами.

Ключові відмінності від ERC-20 та ERC-721

Batch операції — головна перевага. Замість N транзакцій для передачі N різних токенів:

// ERC-721: N транзакцій
for (uint i = 0; i < tokenIds.length; i++) {
    nft.transferFrom(from, to, tokenIds[i]); // N*gas
}

// ERC-1155: одна транзакція
erc1155.safeBatchTransferFrom(from, to, ids, amounts, data); // ~gas

Економія газу при batch transfer: 40–60% порівняно з еквівалентними окремими транзакціями.

Єдиний реєстр балансів:

// Баланс конкретного токена у конкретної адреси
mapping(uint256 => mapping(address => uint256)) private _balances;

// Перевірка балансу
function balanceOf(address account, uint256 id) public view returns (uint256) {
    return _balances[id][account];
}

Semi-fungible токени — токени з унікальним ID, але кількістю > 1. Обмежене видання 1000 копій плаката (усі ідентичні, але обмежені), ігрові предмети з однаковими властивостями. Реалізація залежить від угоди про те, як інтерпретувати ID.

Проектування схеми ID

Uint256 для ID дає величезний простір. Стандартні схеми:

Simple sequential — токени з ID 1, 2, 3... Підходить для простих випадків. Немає вбудованої семантики.

Bit-packed ID — різні частини uint256 кодують різні властивості:

// Приклад: верхні 128 біт = тип, нижні 128 = 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; // ID екземпляра = 0 означає базовий тип
}

Hierarchical scheme для gaming:

bits [255:240] = категорія   (зброя, броня, ресурси, спожиткові)
bits [239:224] = рідкість     (звичайна, незвичайна, рідка, епічна, легендарна)
bits [223:128] = тип предмету  (меч, щит, зілля...)
bits [127:0]   = екземпляр   (0 для fungible, >0 для NFT)

Це дозволяє ефективно фільтрувати: tokenId & CATEGORY_MASK == WEAPONS_CATEGORY.

Реалізація: що важливо при розробці

Стандартна база — OpenZeppelin

OpenZeppelin ERC1155 добре протестований і охоплює стандарт. Не винаходьте велосипед:

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 для унікальних метаданих кожного типу
    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: onERC1155Received

ERC-1155 потребує перевірки, що контракт-отримувач реалізує IERC1155Receiver. Якщо контракт не реалізує інтерфейс — safeTransferFrom повертає помилку, токени не втрачаються. Це важлива відмінність від ERC-20, де токени можна відправити на контракт без підтримки і втратити.

Типова помилка: перевіряти supportsInterface замість виклику onERC1155Received. OpenZeppelin робить це правильно, але при кастомній реалізації легко помилитися.

Operator approvals

ERC-1155 використовує setApprovalForAll — апрув дається на ВСІ токени в контракті. Немає аналога approve(spender, tokenId, amount) з ERC-20/721. Це зручно для ігрових маркетплейсів (один апрув → маркетплейс може переміщати всі токени), але створює ризик при скомпрометованій адресі оператора.

// Для контрактів-операторів потрібен 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);
}

Метаданні: URI схема

Стандарт визначає, що uri(uint256 id) повинна повертати URI з плейсхолдером {id}:

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

{id} замінюється на hex-кодований малелітерний ID, доповнений нулями до 64 символів.

Для NFT всередині ERC-1155 потрібен per-token URI. OpenZeppelin ERC1155URIStorage це підтримує:

// Встановлення URI для конкретного токена
function setTokenURI(uint256 tokenId, string memory tokenURI)
    external onlyRole(MINTER_ROLE) {
    _setURI(tokenId, tokenURI);
}

On-chain метаданні — для простих токенів (ігрові валюти, предмети з базовими атрибутами) можна зберігати основні властивості on-chain, генерувати JSON в 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, '"}]}'
        )))
    ));
}

Інтеграція з маркетплейсами

OpenSea / Blur підтримують ERC-1155. Для коректного відображення:

  • Для NFT (quantity=1): стандартні метаданні JSON з image, attributes
  • Для fungible (quantity>1): decimals в метаданих (OpenSea використовує для відображення кількості)
  • contractURI() — метаданні колекції (назва, опис, зображення, royalties)

Royalties — EIP-2981 для інформації про royalties:

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

contract GameItems is ERC1155, ERC2981 {
    constructor() ERC1155("") {
        // 5% royalty на всі токени
        _setDefaultRoyalty(treasury, 500); // 500 = 5% (basis points)
    }

    // Per-token royalty для спеціальних токенів
    function setTokenRoyalty(uint256 tokenId, address receiver, uint96 feeNumerator)
        external onlyRole(DEFAULT_ADMIN_ROLE) {
        _setTokenRoyalty(tokenId, receiver, feeNumerator);
    }
}

Типові помилки

Supply tracking — ERC-1155 базовий не відстежує totalSupply. Якщо потрібен cap на mintable кількість — використовуйте ERC1155Supply і додавайте перевірки в mint функції.

Burn та totalSupply — після burn totalSupply повинен зменшуватися. ERC1155Supply робить це автоматично, кастомні реалізації часто забувають.

Reentrancy через onERC1155Received_mint викликає onERC1155Received на отримувачі. Якщо отримувач — злоумисний контракт, він може викликати mint знову всередині callback. ReentrancyGuard обов'язковий для mint функцій із зовнішньою логікою.