深入剖析 ERC1155

  • BY_DLIFE
  • 更新于 2024-05-22 22:26
  • 阅读 1561

这是一个管理多种代币类型的合约标准,该合约可以包括同质化代币和非同质化代币,可以代表任意数量的同质化和非同质化的代币类型,抽象上可以解释为:ERC1155 囊括了 ERC20和ERC777这两种标准。

1. ERC1155 简介

这是一个管理多种代币类型的合约标准,该合约可以包括同质化代币和非同质化代币,可以代表任意数量的同质化和非同质化的代币类型,抽象上可以解释为:ERC1155 囊括了 ERC20和ERC777这两种标准。ERC1155的用处,举个游戏的例子(王者荣耀)例子:要是使用ERC20来表示游戏的金币、钻石、点券,很明显ERC20无法做到,因为ERC20 token是同质化的,不能明确区分token与token之间的不同;要是使用ERC721,根据ERC721非同质化的特点,确实是可以表示金币、钻石、点券,但是ERC721 token不能细分,从而导致只能表示“1”个金币、钻石、点券,这很显然是不可取的。正是为了解决这些不足,从而发行了ERC1155标准,这可以很完美的解决上述问题。在ERC1155 token中不同的id表示不同的属性,而且还可以给id设置数量,有了这些特性,便可以很好的解决上述痛点。

  • 同质化代币的表示方式为:如果某个id对应的代币总量为1,那么它就是非同质化代币,类似ERC721
  • 非同质化代币的表示方式为:如果某个id对应的代币总量大于1,那么他就是同质化代币,因为这些代币都分享同一个id,类似ERC20

2. ERC1155代码解读

代码来自 openzepelin:链接

协议的官方文档:链接

2.1 Core

IERC1155.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {IERC165} from "../../utils/introspection/IERC165.sol";

/**
 * @dev Required interface of an ERC-1155 compliant contract, as defined in the
 * https://eips.ethereum.org/EIPS/eip-1155[ERC].
 */
interface IERC1155 is IERC165 {

    event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value);
    event TransferBatch(
        address indexed operator,
        address indexed from,
        address indexed to,
        uint256[] ids,
        uint256[] values
    );
    event ApprovalForAll(address indexed account, address indexed operator, bool approved);
    event URI(string value, uint256 indexed id);

    function balanceOf(address account, uint256 id) external view returns (uint256);
    function balanceOfBatch(
        address[] calldata accounts,
        uint256[] calldata ids
    ) external view returns (uint256[] memory);
    function setApprovalForAll(address operator, bool approved) external;
    function isApprovedForAll(address account, address operator) external view returns (bool);
    function safeTransferFrom(address from, address to, uint256 id, uint256 value, bytes calldata data) external;

    function safeBatchTransferFrom(
        address from,
        address to,
        uint256[] calldata ids,
        uint256[] calldata values,
        bytes calldata data
    ) external;
}

这是IERC1155接口,接口中定义了六个函数

  • balanceOf():单币种余额查询,返回account拥有的id种类的代币的持仓量。
  • balanceOfBatch():多币种余额查询,查询的地址accounts数组和代币种类ids数组的长度要相等。
  • setApprovalForAll():批量授权,将调用者的代币授权给operator地址。。
  • isApprovedForAll():查询批量授权信息,如果授权地址operatoraccount授权,则返回true
  • safeTransferFrom():安全单币转账,将amount单位id种类的代币从from地址转账给to地址。如果to地址是合约,则会验证是否实现了onERC1155Received()接收函数。
  • safeBatchTransferFrom():安全多币转账,与单币转账类似,只不过转账数量amounts和代币种类ids变为数组,且长度相等。如果to地址是合约,则会验证是否实现了onERC1155BatchReceived()接收函数。

IERC1155MetadataURI.sol

interface IERC1155MetadataURI is IERC1155 {
    function uri(uint256 id) external view returns (string memory);
}

这是一个可选接口,用于查询指定 token ID的 uri。如果继承了该接口,则需要在 ERC165supportsInterface()函数中返回常量(用来检验是否实现该接口)。注意:该 uri()函数不得用于检查令牌是否存在,因为即使令牌不存在,实现也可能返回有效的字符串。

IERC1155Receiver.sol

如果ERC1155TOKEN的接收者receiver是一个合约地址,那么接收者必须要实现该接口。

该接口有两个函数:(前提是接收者是合约地址)

  • onERC1155Received:这个函数是在调用 ERC1155safeTransferFrom()_mint()时,接收者的该函数会被调用,并按要求返回指定的值 bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))
  • onERC1155BatchReceived:这个函数时在调用 ERC1155safeBatchTransferFrom()时,接收者的该函数会被调用,并按要求返回指定的值 bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))

ERC1155.sol

    mapping(uint256 id => mapping(address account => uint256)) private _balances;

    mapping(address account => mapping(address operator => bool)) private _operatorApprovals;
  • _balances:用来保存 代币种类id对应 账户 account的余额,即保存 account拥有多少种类为idtoken个数。
  • _operatorApprovals:用来保存 accountoperator的授权情况,true表示已经授权,false表示未授权。
    function unsafeMemoryAccess(uint256[] memory arr, uint256 pos) internal pure returns (uint256 res) {
        assembly {
            res := mload(add(add(arr, 0x20), mul(pos, 0x20)))
        }
    }

这是库合约中的函数,功能时读取 arr数组指定索引的值。

解释汇编

mload(add(add(arr, 0x20), mul(pos, 0x20)))
add(arr,0x20) //跳过数组长度,定位到数据段
mul(pos, 0x20) // EVM的存储机制是以32bytes为一个单位,这段操作码的结果是计算长度,比如 pos=2,则表示 2 * 32 bytes
add(add(arr, 0x20), mul(pos, 0x20)) // 计算 32bytes + pos * 32bytes
mload(add(add(arr, 0x20), mul(pos, 0x20))) // 设 x = add(add(arr, 0x20), mul(pos, 0x20)),则表示读取[x, x+32bytes)的数据

其实就是用汇编的语言实现,读取数组指定索引的值。

    function _asSingletonArrays(
        uint256 element1,
        uint256 element2
    ) private pure returns (uint256[] memory array1, uint256[] memory array2)

这个函数的功能则是将传入的两个参数分别封装成两个 uint256[]类型的数组。汇编实现的逻辑都有注释,写得很清楚。

function _update(address from, address to, uint256[] memory ids, uint256[] memory values) internal virtual {
        if (ids.length != values.length) {
            revert ERC1155InvalidArrayLength(ids.length, values.length);
        }

        address operator = _msgSender();

        for (uint256 i = 0; i < ids.length; ++i) {
            uint256 id = ids.unsafeMemoryAccess(i);
            uint256 value = values.unsafeMemoryAccess(i);

            if (from != address(0)) {
                uint256 fromBalance = _balances[id][from];
                if (fromBalance < value) {
                    revert ERC1155InsufficientBalance(from, fromBalance, value, id);
                }
                unchecked {
                    // Overflow not possible: value <= fromBalance
                    _balances[id][from] = fromBalance - value;
                }
            }

            if (to != address(0)) {
                _balances[id][to] += value;
            }
        }

        if (ids.length == 1) {
            uint256 id = ids.unsafeMemoryAccess(0);
            uint256 value = values.unsafeMemoryAccess(0);
            emit TransferSingle(operator, from, to, id, value);
        } else {
            emit TransferBatch(operator, from, to, ids, values);
        }
    }

这是资产更新的核心函数,参与完成铸币,转账,销币操作。要求参数的两个数组长度相等。

  • 铸币:参数from的值为 address(0),通过for循环为 _balances[id][to] += value添加余额,达成铸币。这对单次铸币和批量铸币都适用。
  • 转账:参数fromto都不为address(0),通过for循环完成对 fromto的余额修改,这对单次转账和批量转账都适用。
  • 销币:参数toaddress(0),通过for循环修改_balances[id][from] = fromBalance - value;,要求fromBalance >=value,这对单次销币和批量销币都适用。
    function _updateWithAcceptanceCheck(
        address from,
        address to,
        uint256[] memory ids,
        uint256[] memory values,
        bytes memory data
    ) internal virtual {
        _update(from, to, ids, values);
        if (to != address(0)) {
            address operator = _msgSender();
            if (ids.length == 1) {
                uint256 id = ids.unsafeMemoryAccess(0);
                uint256 value = values.unsafeMemoryAccess(0);
                ERC1155Utils.checkOnERC1155Received(operator, from, to, id, value, data);
            } else {
                ERC1155Utils.checkOnERC1155BatchReceived(operator, from, to, ids, values, data);
            }
        }
    }

这个函数负责更新用户资产以及检验合约接受者是否实现了 checkOnERC1155Received接口,这里采用了 checks-effect-interaction的方式,将合约的交互放在了_update函数后面,一定程度上限制了对资金的重入风险,但是这里依旧存在重入的风险。

function _safeTransferFrom(address from, address to, uint256 id, uint256 value, bytes memory data) internal

实现单笔转账,fromto都不能为address(0),先通过_asSingletonArrays(id, value)idvalue包装成两个数组,再调用_updateWithAcceptanceCheck(from, to, ids, values, data),进行资产的更新和对合约接收者的接口检验。

function safeTransferFrom(address from, address to, uint256 id, uint256 value, bytes memory data) public virtual

实现安全单笔转账,要求msg.senderfrom或者frommsg.sender执行了授权操作,否则revert()。转账逻辑调用_safeTransferFrom(from, to, id, value, data)

function _safeBatchTransferFrom(
        address from,
        address to,
        uint256[] memory ids,
        uint256[] memory values,
        bytes memory data
    ) internal

实现批量转账,fromto都不能为address(0),调用_updateWithAcceptanceCheck(from, to, ids, values, data);进行资产的更新和对合约接收者的接口检验。

 function safeBatchTransferFrom(
        address from,
        address to,
        uint256[] memory ids,
        uint256[] memory values,
        bytes memory data
    ) public virtual

实现安全批量转账,将from所拥有的 ids,向to转移values,ids和values的索引是一一对应的。要求msg.senderfrom或者frommsg.sender执行了授权操作,否则revert()。转账逻辑调用_safeBatchTransferFrom(from, to, ids, values, data);

    function _setApprovalForAll(address owner, address operator, bool approved) internal virtual {
        if (operator == address(0)) {
            revert ERC1155InvalidOperator(address(0));
        }
        _operatorApprovals[owner][operator] = approved;
        emit ApprovalForAll(owner, operator, approved);
    }

授权操作,owneroperator执行授权操作,operator被授权之后可以操作owner的资产。同时也可以取消授权,即传入的参数approvefalse

    function _mint(address to, uint256 id, uint256 value, bytes memory data) internal {
        if (to == address(0)) {
            revert ERC1155InvalidReceiver(address(0));
        }
        (uint256[] memory ids, uint256[] memory values) = _asSingletonArrays(id, value);
        _updateWithAcceptanceCheck(address(0), to, ids, values, data);
    }

实现铸造ID为id的代币,且发行量为value。这里调用了_updateWithAcceptanceCheck()函数存在重入风险。

    function _mintBatch(address to, uint256[] memory ids, uint256[] memory values, bytes memory data) internal {
        if (to == address(0)) {
            revert ERC1155InvalidReceiver(address(0));
        }
        _updateWithAcceptanceCheck(address(0), to, ids, values, data);
    }

实现铸造ID为ids的代币,且发行量为values,代币ID号和发行量一一对应。这里调用了_updateWithAcceptanceCheck()函数存在重入风险。

2.2 Extensions

ERC1155Pausable.sol

function _update(
        address from,
        address to,
        uint256[] memory ids,
        uint256[] memory values
    ) internal virtual override whenNotPaused {
        super._update(from, to, ids, values);
    }

实现了合约暂停功能,重写了ERC1155的_update函数,使得凡是调用该函数的操作都会受到控制。

ERC1155Burnable.sol

提供了代币注销功能,即间接的将两个内部的销币函数设置为external函数。当然了,执行销币的前提是msg.sender是token的owner或者是 operator。

ERC1155Supply.sol

主要提供了一个统计发行量的功能,铸币会使得_totalSupply[id]发行量增大;销币会使得_totalSupply[id]发行量减小。同时还可以通过exists(uint256 id)查询 token id 是否以及存在。

ERC1155URIStorage.sol

通过了设置 token 的 URI功能,同时还实现了为每一种 token设置 tokenURI。

2.3 Utilities

ERC1155Utils.sol

提供了两个用来验证接收者是否实现了指定接口和函数的功能。

//     function checkOnERC1155Received
try IERC1155Receiver(to).onERC1155Received(operator, from, id, value, data) returns (bytes4 response)
// function checkOnERC1155BatchReceived
try IERC1155Receiver(to).onERC1155BatchReceived(operator, from, ids, values, data) returns (
                bytes4 response
            )

3. ERC1155安全隐患

ERC1155存在重入风险,有重入风险的函数分别是:

  • _updateWithAcceptanceCheck()
  • _mint()
  • _mintBatch()
  • _safeTransferFrom()
  • safeTransferFrom()
  • _safeBatchTransferFrom()
  • safeBatchTransferFrom()

具体攻击事件稍后分析。

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
BY_DLIFE
BY_DLIFE
0x39CF...9999
立志成为一名优秀的智能合约审计师、智能合约开发工程师,文章内容为个人理解,如有错误,欢迎在评论区指出。