聊聊ERC1155

概述

ERC1155 是 ERC721 之外的另一种 NFT 标准,可以理解为 ERC20 与 ERC721 的融合体。简单举几个例子理解一下:

  • ERC20 就好像银行发行的一百元纸币,每张纸币的价值相同
  • ERC721 就好像艺术馆里面的艺术品,每件艺术品都是独一无二的
  • ERC1155 类似于游戏中的道具,比如一些普通道具可能有成千上万件,而一些稀有道具只有几十件。也类似于银行发行的不同面额纸币,相同面额价值相同,可以互换。不同面额价值不同,不可互换

对应到代码实现中:

  • ERC20 合约中每个 token 是同一种类型,它们的价值都是一样的
  • ERC721 合约中每个 tokenId 都是不同类型,各自拥有不同的属性。每个 tokenId 有且只有一个
  • ERC1155 合约中根据 id 有不同的分类,每一个 id 是一种类型。当这个 id 下有多个 token 时,就是 fungible token(类似 ERC20);当这个 id 下只有一个 token 时,就是 non-fungible token(类似ERC721)

我们直接来看一下 OpenZeppelin 官方文档给出的一个例子:

// contracts/GameItems.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";

// 继承ERC1155标准
contract GameItems is ERC1155 {
    // 发行5种道具,id分别为0~5
    uint256 public constant GOLD = 0;
    uint256 public constant SILVER = 1;
    uint256 public constant THORS_HAMMER = 2;
    uint256 public constant SWORD = 3;
    uint256 public constant SHIELD = 4;

    constructor() ERC1155("https://game.example/api/item/{id}.json") {
        // id不同,mint的数量不同
        _mint(msg.sender, GOLD, 10**18, "");
        _mint(msg.sender, SILVER, 10**27, "");
        // THORS_HAMMER只mint一个,即non-fungible
        _mint(msg.sender, THORS_HAMMER, 1, "");
        _mint(msg.sender, SWORD, 10**9, "");
        _mint(msg.sender, SHIELD, 10**9, "");
    }
}

代码

接下来看看 OpenZeppelin ERC1155 的代码

数据结构:

// id -> 用户地址 -> 数量,即用户拥有的某id token的数量
mapping(uint256 => mapping(address => uint256)) private _balances;

// 授权地址信息
// owner -> 授权人 -> 是否授权
mapping(address => mapping(address => bool)) private _operatorApprovals;

// uri,根据id区分,格式应该为 https://token-cdn-domain/{id}.json
string private _uri;

这里需要注意一下 uri 的格式,根据 OpenZeppelin 文档:

The uri can include the string {id} which clients must replace with the actual token ID, in lowercase hexadecimal (with no 0x prefix) and leading zero padded to 64 hex characters.

For token ID 2 and uri https://game.example/api/item/{id}.json clients would replace {id} with 0000000000000000000000000000000000000000000000000000000000000002 to retrieve JSON at https://game.example/api/item/0000000000000000000000000000000000000000000000000000000000000002.json

所有的 id,前后缀都相同,只是在 id 部分区分,需要将其补足为 64 位。同时,这个 uri 打开后的格式应该为 Json 文本,例如:

{
    "name": "Thor's hammer",
    "description": "Mjölnir, the legendary hammer of the Norse god of thunder.",
    "image": "https://game.example/item-id-8u5h2m.png",
    "strength": 20
}

在代码中看到,ERC1155 只能对所有的 token 进行授权,而不能像 ERC721 那样只对某一个 tokenId 进行授权(ERC721 也包括全部授权的方法)。

查询用户某一个 id 类型下的数量:

function balanceOf(address account, uint256 id) public view virtual override returns (uint256) {
    require(account != address(0), "ERC1155: balance query for the zero address");
    return _balances[id][account];
}

转账:

function _safeTransferFrom(
    address from,
    address to,
    uint256 id,
    uint256 amount,
    bytes memory data
) internal virtual {
    require(to != address(0), "ERC1155: transfer to the zero address");

    address operator = _msgSender();

    _beforeTokenTransfer(operator, from, to, _asSingletonArray(id), _asSingletonArray(amount), data);

    // 更新转出与转入者的数量
    uint256 fromBalance = _balances[id][from];
    require(fromBalance >= amount, "ERC1155: insufficient balance for transfer");
    unchecked {
        _balances[id][from] = fromBalance - amount;
    }
    _balances[id][to] += amount;

    emit TransferSingle(operator, from, to, id, amount);

    _doSafeTransferAcceptanceCheck(operator, from, to, id, amount, data);
}

其他的方法例如 mint, burn 等其实都大同小异,没有什么难度。

在转账时,如果接收地址是合约,需要验证其实现了钩子函数接口,防止 token 永久锁死在合约中:

function _doSafeTransferAcceptanceCheck(
    address operator,
    address from,
    address to,
    uint256 id,
    uint256 amount,
    bytes memory data
) private {
    if (to.isContract()) {
        // 校验合约是否实现了 onERC1155Received 方法
        try IERC1155Receiver(to).onERC1155Received(operator, from, id, amount, data) returns (bytes4 response) {
            if (response != IERC1155Receiver.onERC1155Received.selector) {
                revert("ERC1155: ERC1155Receiver rejected tokens");
            }
        } catch Error(string memory reason) {
            revert(reason);
        } catch {
            revert("ERC1155: transfer to non ERC1155Receiver implementer");
        }
    }
}

总结

ERC1155 概念与代码实现都很简单。我个人觉得 ERC1155 更适合的场景应该是在游戏中,比如对应不同数量的道具这类场景。而 ERC721 则更适合艺术品这个场景。目前在 OpenSea 上的艺术品还是以 ERC721 居多。

参考

Subscribe to xyyme.eth
Receive the latest updates directly to your inbox.
Verification
This entry has been permanently stored onchain and signed by its creator.