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







