# 聊聊ERC1155

By [xyyme.eth](https://paragraph.com/@xyyme) · 2022-03-17

---

### 概述

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 [官方文档](https://docs.openzeppelin.com/contracts/4.x/erc1155)给出的一个例子：

    // 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 的[代码](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC1155/ERC1155.sol)。

数据结构：

    // 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 居多。

### 参考

[

ERC1155
-------

The official documentation for OpenZeppelin Libraries and Tools

https://docs.openzeppelin.com

![](https://storage.googleapis.com/papyrus_images/0f782755196baf7d7c54bc0aa1bff0f75fa123c45dd4c557b0b1c2c75bf40bb6.png)

](https://docs.openzeppelin.com/contracts/4.x/erc1155)

---

*Originally published on [xyyme.eth](https://paragraph.com/@xyyme/erc1155)*
