4、ERC721与ERC1155标准:非同质化代币(NFT)基础、批量代币标准、应用场景分析
4.1 从ERC20到ERC721:为什么需要非同质化?
聊到NFT,很多人第一反应是头像、图片、天价拍卖。但作为开发者,我们得先搞清楚一个根本问题:为什么有了ERC20,还要搞个ERC721?
ERC20是标准的同质化代币。你手里的一个USDT和我手里的一个USDT,完全等价,可以互换。这在货币、积分场景下没问题。但现实世界中,很多东西是独一无二的——比如房产证、艺术品、游戏里的稀有装备。
我早期参与过一个游戏项目,想把游戏里的“屠龙刀”做成代币。如果用ERC20,那所有屠龙刀都一样,玩家根本分不清哪把是+10的、哪把是+15的。这显然不合理。所以,ERC721的核心思想就是:每个代币都有一个唯一的ID。
核心区别一句话总结:
- ERC20:所有代币都一样,像人民币。
- ERC721:每个代币都不同,像身份证号。
4.2 ERC721标准核心接口
ERC721的接口定义在OpenZeppelin的库中,我建议你直接用它,别自己手写。手写容易漏掉关键函数,我在审计中见过不少因为没实现safeTransferFrom导致资产卡死的案例。
核心函数就这几个:
// 查询某个地址拥有多少个NFT
function balanceOf(address owner) external view returns (uint256 balance);
// 查询某个tokenId的拥有者
function ownerOf(uint256 tokenId) external view returns (address owner);
// 安全转账(推荐使用这个)
function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;
// 普通转账(不推荐,容易丢资产)
function transferFrom(address from, address to, uint256 tokenId) external;
// 授权
function approve(address to, uint256 tokenId) external;
// 批量授权
function setApprovalForAll(address operator, bool _approved) external;
嗯,这里有个坑。很多新手会问:safeTransferFrom和transferFrom有什么区别?
说白了,safeTransferFrom会检查接收方是不是合约。如果是合约,它会调用接收方的onERC721Received函数,确保对方能处理NFT。如果对方没实现这个接口,交易就会回滚。这能防止NFT被转到一个无法取出的合约里——我曾经见过一个项目,因为用了transferFrom,导致价值几百万的NFT卡死在合约里,最后只能硬分叉解决。
避坑指南:
我建议你永远优先使用safeTransferFrom。除非你明确知道接收方是EOA(外部账户),否则别用transferFrom。
4.3 ERC721元数据扩展
光有ID还不够,你得告诉用户这个NFT长什么样。ERC721的元数据扩展(Metadata Extension)定义了tokenURI函数:
function tokenURI(uint256 tokenId) external view returns (string memory);
这个函数返回一个JSON格式的URI,通常指向IPFS或中心化服务器。JSON结构大概长这样:
{
"name": "屠龙刀 #1",
"description": "一把传说中的武器",
"image": "ipfs://QmX...",
"attributes": [
{"trait_type": "攻击力", "value": 100},
{"trait_type": "稀有度", "value": "传说"}
]
}
我个人习惯把元数据存在IPFS上,因为去中心化。但要注意,IPFS的地址是固定的,一旦上传就不能修改。如果你需要动态更新元数据(比如游戏装备升级后攻击力变化),那就得用中心化服务器或者自己写个可升级的合约。
4.4 ERC1155:批量代币标准
好,现在问题来了。假设你做一个游戏,里面有100种装备,每种装备有1000个。如果用ERC721,你得部署100个合约,或者在一个合约里管理10万个tokenId。这太浪费gas了。
ERC1155就是来解决这个问题的。它把同质化和非同质化代币统一到一个合约里。说白了,ERC1155可以同时管理多种代币,每种代币有自己的ID,而且支持批量转账。
核心接口:
// 批量查询余额
function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids) external view returns (uint256[] memory);
// 批量转账
function safeBatchTransferFrom(address from, address to, uint256[] calldata ids, uint256[] calldata amounts, bytes calldata data) external;
你看,balanceOfBatch一次能查多个地址、多种代币的余额。这在游戏里特别有用——比如一次查询玩家所有装备的数量。
我的经验:
如果你要发一个系列(比如10000个PFP头像),用ERC721就够了。但如果你要做游戏、道具系统,ERC1155是更好的选择。它节省gas,而且代码更简洁。
4.5 应用场景分析
我整理了一个表格,方便你对比:
| 场景 | 推荐标准 | 原因 |
|---|---|---|
| 数字艺术品(单个) | ERC721 | 每个作品独一无二,不需要批量管理 |
| 游戏道具(多种类、多数量) | ERC1155 | 批量操作省gas,管理方便 |
| 房地产产权 | ERC721 | 每个房产证唯一,需要单独追踪 |
| 会员卡/门票 | ERC1155 | 同一种门票可以发多张,支持批量发放 |
| 合成资产(如多个NFT合成一个) | ERC1155 | 支持同质化与非同质化混合管理 |
你想想看,为什么现在很多GameFi项目都用ERC1155?因为游戏里经常要批量发装备、批量销毁、批量转移。如果用ERC721,一次只能操作一个,gas费高得吓人。ERC1155一次能处理256种代币,效率提升巨大。
4.6 实战中的注意事项
最后,分享几个我在实际开发中踩过的坑:
- 小心重入攻击:
safeTransferFrom和safeBatchTransferFrom会调用外部合约。如果接收方是恶意合约,它可能在onERC721Received里再次调用你的转账函数。记得加上重入锁。 - URI存储位置:别把元数据直接存在链上,太贵了。用IPFS或者Arweave。如果必须动态更新,考虑用中心化服务器+签名验证。
- 批量转账的数组长度:ERC1155的
safeBatchTransferFrom要求ids和amounts数组长度一致。我见过有人传错长度,导致部分代币没转出去。 - 授权管理:
setApprovalForAll是全局授权,要提醒用户谨慎操作。我建议在UI上明确提示“授权后该合约可以操作你所有NFT”。
总结一下:
ERC721适合做独一无二的数字藏品,ERC1155适合做批量管理的游戏道具。选哪个,取决于你的业务场景。别盲目跟风,想清楚再动手。
下一章,我们会深入讲解如何用OpenZeppelin快速搭建一个安全的NFT合约。到时候我会手把手带你写一个完整的例子。