Appearance
L1-14: ERC-1155 Game Items (Multi Token Standard)
1. Problem
Implement the ERC-1155 multi-token standard contract, managing multiple token types (fungible tokens FT + non-fungible tokens NFT) within a single contract, supporting batch transfers, batch balance queries, and owner creation and minting of new token types.
A typical gaming scenario: a single contract containing gold coins (FT, fungible), rare skins (NFT, limited to 1 copy), and potions (semi-FT, limited to 100 bottles).
2. Why
ERC-721 requires a separate contract for each token type -- gas costs are unacceptable for games that may have hundreds of items. ERC-1155 solves this:
- Single contract, multiple tokens: one contract manages a theoretically unlimited number of token types
- Batch operations:
safeBatchTransferFromtransfers multiple token types in a single transaction -- roughly 50% gas savings - Batch queries:
balanceOfBatchqueries balances across multiple addresses/tokens in one call -- significantly improved frontend performance - FT + NFT hybrid: the same contract can contain both fungible and non-fungible tokens
ERC-1155 is a complementary standard to ERC-721, not a replacement. It is widely used in gaming (Axie Infinity, Gods Unchained), membership cards, and any scenario requiring multiple token types.
3. Solution
Contract Architecture
Implement the full IERC-1155 interface from scratch (without inheriting from OpenZeppelin, to deepen understanding):
solidity
contract GameItems is IERC1155, Ownable {
// tokenId => owner => balance
mapping(uint256 => mapping(address => uint256)) private _balances;
// owner => operator => approved
mapping(address => mapping(address => bool)) private _operatorApprovals;
// tokenId => URI
mapping(uint256 => string) private _tokenURIs;
uint256 private _tokenIdCounter;
}
Core Interface
| Method | Description |
|---|---|
balanceOf(owner, id) | Query single token balance |
balanceOfBatch(owners[], ids[]) | Batch balance query |
safeTransferFrom(from, to, id, amount, data) | Single token transfer |
safeBatchTransferFrom(from, to, ids[], amounts[], data) | Batch transfer |
setApprovalForAll(operator, approved) | Authorize operator to manage all tokens |
Token Creation
Only the Owner can create new token types:
solidity
function createToken(
string calldata _uri,
address[] calldata _recipients,
uint256[] calldata _amounts
) external onlyOwner returns (uint256) {
uint256 tokenId = _tokenIdCounter++;
_tokenURIs[tokenId] = _uri;
for (uint256 i = 0; i < _recipients.length; i++) {
_balances[tokenId][_recipients[i]] += _amounts[i];
emit TransferSingle(msg.sender, address(0), _recipients[i], tokenId, _amounts[i]);
}
return tokenId;
}
4. Pitfalls Encountered
4.1 Callback Safety Check in safeTransferFrom
ERC-1155 requires that when transferring to a contract address, the recipient must implement the IERC1155Receiver interface. If this check is forgotten, tokens can be permanently locked in contracts that cannot handle ERC-1155.
The implementation must:
solidity
function _doSafeTransferAcceptanceCheck(...) private {
if (to.code.length > 0) {
try IERC1155Receiver(to).onERC1155Received(...) returns (bytes4 response) {
require(response == IERC1155Receiver.onERC1155Received.selector, "ERC1155 rejected");
} catch {
revert("ERC1155 transfer to non-ERC1155Receiver");
}
}
}
4.2 Array Length Validation in Batch Operations
Both safeBatchTransferFrom and balanceOfBatch accept multiple array parameters. It is essential to ensure all array lengths are equal; otherwise, array-out-of-bounds or logic errors will occur.
4.3 Differences in the Approval Model
ERC-1155 uses setApprovalForAll (all-or-nothing), lacking ERC-721's approve (per-token authorization). This means that once an operator is authorized, it can operate on all of your token types.
4.4 Implementing supportsInterface
ERC-165 interface detection is a requirement for all ERC standards. If supportsInterface returns an incorrect value, marketplaces and wallets may fail to recognize your contract as ERC-1155.
5. Why the Pitfalls Happen
5.1
ERC-1155's safe transfer mechanism is a critical line of defense against token loss. Without this check, users might accidentally transfer game items to an exchange contract (which does not know how to handle ERC-1155), causing permanent loss of the items.
5.2
Solidity does not automatically check array lengths. A caller could pass ids.length=3 but amounts.length=2 -- if not validated at the start of the function, the loop would try to access a non-existent index, causing a revert (wasting gas) or worse.
6. How to Resolve the Pitfalls
- Validate at the beginning of all functions accepting multiple arrays:
require(ids.length == amounts.length, "length mismatch") - Implement complete
_doSafeTransferAcceptanceCheckand_doSafeBatchTransferAcceptanceCheck supportsInterfacemust return the correct interface ID:type(IERC1155).interfaceId- Use
safeTransferFromrather thantransferFrom(the ERC-1155 standard only has the safe version) - Emit the
TransferSingleevent increateToken(from = address(0) indicates minting)
7. Technical Highlights
| Point | Description |
|---|---|
| Single contract, multiple tokens | tokenId distinguishes types, replacing multi-contract deployment |
| Batch transfer | safeBatchTransferFrom transfers multiple tokens at once |
| Batch query | balanceOfBatch queries multiple balances at once |
| FT + NFT hybrid | The total supply of a tokenId determines if it is FT (>1) or NFT (=1) |
| IERC1155Receiver | Safe callback interface that contract recipients must implement |
| URI system | Independent metadata URI per token type |
| Approval model | setApprovalForAll all-or-nothing authorization |