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

Проектируем и разрабатываем блокчейн-решения полного цикла: от архитектуры смарт-контрактов до запуска DeFi-протоколов, NFT-маркетплейсов и криптобирж. Аудит безопасности, токеномика, интеграция с существующей инфраструктурой.
Показано 1 из 1 услугВсе 1306 услуг
Разработка ERC-1155 токена (мульти-токен)
Средняя
~2-3 рабочих дня
Часто задаваемые вопросы
Направления блокчейн-разработки
Этапы блокчейн-разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1221
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1163
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    855
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1056
  • image_logo-advance_0.png
    Разработка логотипа компании B2B Advance
    561
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    828

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

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

Ключевые отличия от 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 = 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 значит базовый тип
}

Hierarchical scheme для 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 для 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 revertsит, токены не потеряются. Это важное отличие от 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-encoded lowercase значение 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 metadata — для простых токенов (game currencies, предметы с базовыми атрибутами) можно хранить базовые свойства 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 в metadata (OpenSea использует для отображения количества)
  • contractURI() — метаданные коллекции (name, description, image, royalties)

Royalties — EIP-2981 для royalty информации:

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 base не отслеживает totalSupply. Если нужен cap на mintable количество — используйте ERC1155Supply и добавляйте проверки в mint функции.

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

Reentrancy через onERC1155Received_mint вызывает onERC1155Received на получателе. Если получатель — злоумышленный контракт, он может вызвать mint снова внутри callback. ReentrancyGuard обязателен для mint функций с внешней логикой.