抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 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

1
2
3
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

1
2
3
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表示未授权。
1
2
3
4
5
function unsafeMemoryAccess(uint256[] memory arr, uint256 pos) internal pure returns (uint256 res) {
assembly {
res := mload(add(add(arr, 0x20), mul(pos, 0x20)))
}
}

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

解释汇编

1
2
3
4
5
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)的数据

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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,这对单次销币和批量销币都适用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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函数后面,一定程度上限制了对资金的重入风险,但是这里依旧存在重入的风险。

1
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),进行资产的更新和对合约接收者的接口检验。

1
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)

1
2
3
4
5
6
7
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);进行资产的更新和对合约接收者的接口检验。

1
2
3
4
5
6
7
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);

1
2
3
4
5
6
7
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

1
2
3
4
5
6
7
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()函数存在重入风险。

1
2
3
4
5
6
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

1
2
3
4
5
6
7
8
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

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

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

3. ERC1155安全隐患

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

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

评论



政策 · 统计 | 本站使用 Volantis 主题设计