Appearance
L1-14: ERC-1155 Game Items(多代币标准)
1. 问题
实现 ERC-1155 多代币标准合约,在一个合约中同时管理多种代币类型(同质化代币 FT + 非同质化代币 NFT),支持批量转账、批量余额查询,以及 Owner 创建和 mint 新代币类型。
典型的游戏场景:一个合约中同时包含金币(FT,同质化)、稀有皮肤(NFT,限量 1 份)、药水(semi-FT,限量 100 瓶)。
2. 原因
ERC-721 每种代币需要一个独立合约——对于游戏(可能有数百种道具)来说 Gas 成本不可接受。ERC-1155 解决了这个问题:
- 单合约多代币:一个合约管理理论上无限种代币
- 批量操作:
safeBatchTransferFrom一次交易转移多种代币——Gas 节省约 50% - 批量查询:
balanceOfBatch一次查询多个地址/代币的余额——前端性能大幅提升 - FT + NFT 混合:同一个合约可以同时包含同质化和非同质化代币
ERC-1155 是 ERC-721 的互补标准而非替代。它被广泛应用于游戏(Axie Infinity、Gods Unchained)、会员卡、和任何需要多种代币类型的场景。
3. 方案
合约架构
从零实现完整的 IERC-1155 接口(不继承 OpenZeppelin 实现,以加深理解):
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;
}
核心接口
| 方法 | 描述 |
|---|---|
balanceOf(owner, id) | 查询单个代币余额 |
balanceOfBatch(owners[], ids[]) | 批量查询 |
safeTransferFrom(from, to, id, amount, data) | 单代币转账 |
safeBatchTransferFrom(from, to, ids[], amounts[], data) | 批量转账 |
setApprovalForAll(operator, approved) | 授权操作者管理所有代币 |
Token 创建
只有 Owner 可以创建新代币类型:
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. 遭遇的陷阱
4.1 safeTransferFrom 的回调安全检查
ERC-1155 要求转账到合约地址时,接收方必须实现 IERC1155Receiver 接口。如果忘记这个检查,代币会被永久锁在无法处理 ERC-1155 的合约中。
实现必须:
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 批量操作的数组长度验证
safeBatchTransferFrom 和 balanceOfBatch 都接受多个数组参数。必须确保所有数组长度相等,否则会导致数组越界或逻辑错误。
4.3 Approval 模型的差异
ERC-1155 使用 setApprovalForAll(全有或全无),没有 ERC-721 的 approve(单独授权)。这意味着一旦授权某个 operator,它可以操作你的所有代币类型。
4.4 supportsInterface 的实现
ERC-165 接口检测是所有 ERC 标准的要求。如果 supportsInterface 返回值不正确,市场和钱包可能无法识别你的合约是 ERC-1155。
5. 陷阱的原因
5.1
ERC-1155 的安全转账机制是防止代币丢失的关键防线。没有这个检查,用户可能意外将游戏道具转到交易所合约(它不知道如何处理 ERC-1155),导致道具永久丢失。
5.2
Solidity 不会自动检查数组长度。调用者可能传入 ids.length=3 但 amounts.length=2——如果不在函数开头验证,会导致循环访问不存在的索引而 revert(浪费 gas)或更糟。
6. 如何解决陷阱
- 在所有接受多数组的函数开头验证
require(ids.length == amounts.length, "length mismatch") - 实现完整的
_doSafeTransferAcceptanceCheck和_doSafeBatchTransferAcceptanceCheck supportsInterface返回正确的接口 ID:type(IERC1155).interfaceId- 使用
safeTransferFrom而非transferFrom(ERC-1155 标准中只有 safe 版本) - 在
createToken时发出TransferSingle事件(from = address(0) 表示铸造)
7. 技术要点
| 要点 | 说明 |
|---|---|
| 单合约多代币 | tokenId 区分不同类型,替代多合约部署 |
| 批量转账 | safeBatchTransferFrom 一次转移多种代币 |
| 批量查询 | balanceOfBatch 一次查询多个余额 |
| FT + NFT 混合 | tokenId 的总供应量决定是 FT(>1) 还是 NFT(=1) |
| IERC1155Receiver | 合约接收方必须实现的安全回调接口 |
| URI 系统 | 每种代币独立的 metadata URI |
| Approval 模型 | setApprovalForAll 全有或全无授权 |