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
1 | // SPDX-License-Identifier: MIT |
这是IERC1155
接口,接口中定义了六个函数
balanceOf()
:单币种余额查询,返回account
拥有的id
种类的代币的持仓量。balanceOfBatch()
:多币种余额查询,查询的地址accounts
数组和代币种类ids
数组的长度要相等。setApprovalForAll()
:批量授权,将调用者的代币授权给operator
地址。。isApprovedForAll()
:查询批量授权信息,如果授权地址operator
被account
授权,则返回true
。safeTransferFrom()
:安全单币转账,将amount
单位id
种类的代币从from
地址转账给to
地址。如果to
地址是合约,则会验证是否实现了onERC1155Received()
接收函数。safeBatchTransferFrom()
:安全多币转账,与单币转账类似,只不过转账数量amounts
和代币种类ids
变为数组,且长度相等。如果to
地址是合约,则会验证是否实现了onERC1155BatchReceived()
接收函数。
IERC1155MetadataURI.sol
1 | interface IERC1155MetadataURI is IERC1155 { |
这是一个可选接口,用于查询指定 token ID的 uri
。如果继承了该接口,则需要在 ERC165
的supportsInterface()
函数中返回常量(用来检验是否实现该接口)。注意:该 uri()
函数不得用于检查令牌是否存在,因为即使令牌不存在,实现也可能返回有效的字符串。
IERC1155Receiver.sol
如果ERC1155
TOKEN的接收者receiver
是一个合约地址,那么接收者必须要实现该接口。
该接口有两个函数:(前提是接收者是合约地址)
- onERC1155Received:这个函数是在调用
ERC1155
的safeTransferFrom()
和_mint()
时,接收者的该函数会被调用,并按要求返回指定的值bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))
。 - onERC1155BatchReceived:这个函数时在调用
ERC1155
的safeBatchTransferFrom()
时,接收者的该函数会被调用,并按要求返回指定的值bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))
。
ERC1155.sol
1 | mapping(uint256 id => mapping(address account => uint256)) private _balances; |
- _balances:用来保存 代币种类
id
对应 账户account
的余额,即保存account
拥有多少种类为id
的token
个数。 - _operatorApprovals:用来保存
account
对operator
的授权情况,true
表示已经授权,false
表示未授权。
1 | function unsafeMemoryAccess(uint256[] memory arr, uint256 pos) internal pure returns (uint256 res) { |
这是库合约中的函数,功能时读取 arr
数组指定索引的值。
解释汇编
1 | mload(add(add(arr, 0x20), mul(pos, 0x20))) |
其实就是用汇编的语言实现,读取数组指定索引的值。
1 | function _asSingletonArrays( |
这个函数的功能则是将传入的两个参数分别封装成两个 uint256[]
类型的数组。汇编实现的逻辑都有注释,写得很清楚。
1 | function _update(address from, address to, uint256[] memory ids, uint256[] memory values) internal virtual { |
这是资产更新的核心函数,参与完成铸币,转账,销币操作。要求参数的两个数组长度相等。
- 铸币:参数from的值为
address(0)
,通过for循环为_balances[id][to] += value
添加余额,达成铸币。这对单次铸币和批量铸币都适用。 - 转账:参数
from
和to
都不为address(0)
,通过for循环完成对from
和to
的余额修改,这对单次转账和批量转账都适用。 - 销币:参数
to
为address(0)
,通过for循环修改_balances[id][from] = fromBalance - value;
,要求fromBalance >=value
,这对单次销币和批量销币都适用。
1 | function _updateWithAcceptanceCheck( |
这个函数负责更新用户资产以及检验合约接受者是否实现了 checkOnERC1155Received
接口,这里采用了 checks-effect-interaction
的方式,将合约的交互放在了_update
函数后面,一定程度上限制了对资金的重入风险,但是这里依旧存在重入的风险。
1 | function _safeTransferFrom(address from, address to, uint256 id, uint256 value, bytes memory data) internal |
实现单笔转账,from
和to
都不能为address(0)
,先通过_asSingletonArrays(id, value)
将id
和value
包装成两个数组,再调用_updateWithAcceptanceCheck(from, to, ids, values, data)
,进行资产的更新和对合约接收者的接口检验。
1 | function safeTransferFrom(address from, address to, uint256 id, uint256 value, bytes memory data) public virtual |
实现安全单笔转账,要求msg.sender
是 from
或者from
对msg.sender
执行了授权操作,否则revert()
。转账逻辑调用_safeTransferFrom(from, to, id, value, data)
。
1 | function _safeBatchTransferFrom( |
实现批量转账,from
和to
都不能为address(0)
,调用_updateWithAcceptanceCheck(from, to, ids, values, data);
进行资产的更新和对合约接收者的接口检验。
1 | function safeBatchTransferFrom( |
实现安全批量转账,将from
所拥有的 ids
,向to
转移values
,ids和values的索引是一一对应的。要求msg.sender
是 from
或者from
对msg.sender
执行了授权操作,否则revert()
。转账逻辑调用_safeBatchTransferFrom(from, to, ids, values, data);
。
1 | function _setApprovalForAll(address owner, address operator, bool approved) internal virtual { |
授权操作,owner
对operator
执行授权操作,operator
被授权之后可以操作owner
的资产。同时也可以取消授权,即传入的参数approve
为 false
。
1 | function _mint(address to, uint256 id, uint256 value, bytes memory data) internal { |
实现铸造ID为id
的代币,且发行量为value
。这里调用了_updateWithAcceptanceCheck()
函数存在重入风险。
1 | function _mintBatch(address to, uint256[] memory ids, uint256[] memory values, bytes memory data) internal { |
实现铸造ID为ids
的代币,且发行量为values
,代币ID号和发行量一一对应。这里调用了_updateWithAcceptanceCheck()
函数存在重入风险。
2.2 Extensions
ERC1155Pausable.sol
1 | function _update( |
实现了合约暂停功能,重写了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
提供了两个用来验证接收者是否实现了指定接口和函数的功能。
1 | // function checkOnERC1155Received |
1 | // function checkOnERC1155BatchReceived |
3. ERC1155安全隐患
ERC1155存在重入风险,有重入风险的函数分别是:
- _updateWithAcceptanceCheck()
- _mint()
- _mintBatch()
- _safeTransferFrom()
- safeTransferFrom()
- _safeBatchTransferFrom()
- safeBatchTransferFrom()