Разработка 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 функций с внешней логикой.







