ERC777

ERC777

ERC777 官方文档

ERC777 合约仓库

由于 ERC20 标准没有一个转账通知机制,并且在转账时无法携带额外的信息,因此出现了 ERC777 来解决这些问题。

ERC777 在 ERC20 的基础上定义了 send(dest, value, data) 来转移代币, send 函数额外的参数用来携带其他的信息,send 函数会检查持有者和接收者是否实现了相应的钩子函数,如果有实现(不管是普通用户地址还是合约地址都可以实现钩子函数),则调用相应的钩子函数。

EIP1820

EIP1820 文档

ERC1820 取代了 ERC820ERC1820 修复了 Solidty 0.5 更新带来的与ERC165不兼容的问题。这里是官方声明erc1820-annoucement,除了这个bug之外,ERC1820 功能上等价于 ERC820

ERC1820 标准定义了一个通用注册表合约,任何地址(合约或普通用户帐户)都可以注册它支持的接口以及哪个智能合约负责接口实现。

ERC1820标准定义智能合约和普通用户帐户可以向注册表发布其实现了哪些功能(普通用户帐户通过代理合约实现)

任何人都可以查询此注册表,询问哪个地址是否实现了给定的接口以及哪个智能合约处理实现逻辑。

ERC1820注册表合约可以部署在任何链上,并在所有链上的地址是相同的。

接口的后28个字节都为0的话,会认为是 ERC165 接口,并且注册表将转发到合约以查看是否实现了接口。

此合约还充当 ERC165 缓存,以减少 gas 消耗。

这里介绍 ERC1820 注册表合约 中的两个主要函数

setInterfaceImplementer 设置某个地址的接口由哪个合约实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/// @notice 设置某个地址的接口由哪个合约实现,需要由管理员来设置。(每个地址是他自己的管理员,直到设置了一个新的地址)。
/// @param _addr 待设置的关联接口的地址(如果'_addr'是零地址,则假定为'msg.sender')
/// @param _interfaceHash 接口,它是接口名称字符串的 keccak256 哈希值
/// 例如: 'web3.utils.keccak256("ERC777TokensRecipient")' 表示 'ERC777TokensRecipient' 接口。
/// @param _implementer 为地址'_addr'实现了 '_interfaceHash'接口的合约地址
function setInterfaceImplementer(address _addr, bytes32 _interfaceHash, address _implementer) external {
address addr = _addr == address(0) ? msg.sender : _addr;
require(getManager(addr) == msg.sender, "Not the manager");

require(!isERC165Interface(_interfaceHash), "Must not be an ERC165 hash");
if (_implementer != address(0) && _implementer != msg.sender) {
require(
ERC1820ImplementerInterface(_implementer)
.canImplementInterfaceForAddress(_interfaceHash, addr) == ERC1820_ACCEPT_MAGIC,
"Does not implement the interface"
);
}
interfaces[addr][_interfaceHash] = _implementer;
emit InterfaceImplementerSet(addr, _interfaceHash, _implementer);
}

getInterfaceImplementer 查询地址是否实现了接口以及通过哪个合约实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
/// @notice 查询地址是否实现了接口以及通过哪个合约实现的。
/// @param _addr 查询地址(如果'_addr'是零地址,则假定为'msg.sender')。
/// @param _interfaceHash 查询接口,它是接口名称字符串的 keccak256 哈希值
/// 例如: 'web3.utils.keccak256("ERC777TokensRecipient")' 表示 'ERC777TokensRecipient' 接口.
/// @return 返回实现者的地址,没有实现返回 ‘0’
function getInterfaceImplementer(address _addr, bytes32 _interfaceHash) external view returns (address) {
address addr = _addr == address(0) ? msg.sender : _addr;
if (isERC165Interface(_interfaceHash)) {
bytes4 erc165InterfaceHash = bytes4(_interfaceHash);
return implementsERC165Interface(addr, erc165InterfaceHash) ? addr : address(0);
}
return interfaces[addr][_interfaceHash];
}

ERC777 使用 send 转账时会分别在持有者和接收者地址上使用 ERC1820 的getInterfaceImplementer 函数进行查询,查看是否有对应的实现合约,ERC777 标准规范里预定了接口及函数名称,如果有实现则进行相应的调用。

基本变量

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
// ERC1820 注册表合约地址
IERC1820Registry private _erc1820 = IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24);

mapping(address => uint256) private _balances;

uint256 private _totalSupply;

string private _name;
string private _symbol;


// 发送者接口hash 硬编码 keccak256("ERC777TokensSender") 为了减少 gas, 用户查询是否实现了对应的接口。
bytes32 constant private TOKENS_SENDER_INTERFACE_HASH =
0x29ddb589b1fb5fc7cf394961c1adf5f8c6454761adf795e67fe149f658abe895;

// keccak256("ERC777TokensRecipient")
bytes32 constant private TOKENS_RECIPIENT_INTERFACE_HASH =
0xb281fc8c12954d22544db45de3159a39272895b169a852b314f9cc762e44c53b;

// 保存默认操作者列表,只能在代币创建时定义
address[] private _defaultOperatorsArray;

// 为了索引默认操作者状态 使用的mapping
mapping(address => bool) private _defaultOperators;

// 保存授权的操作者
mapping(address => mapping(address => bool)) private _operators;
// 保存取消授权的默认操作者
mapping(address => mapping(address => bool)) private _revokedDefaultOperators;

// 为了兼容 ERC20 (授权信息)
mapping (address => mapping (address => uint256)) private _allowances;

构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* @dev `defaultOperators` 可以为空.
*/
constructor(
string memory name,
string memory symbol,
address[] memory defaultOperators
) public {
_name = name;
_symbol = symbol;

// 初始化默认操作者列表
_defaultOperatorsArray = defaultOperators;
for (uint256 i = 0; i < _defaultOperatorsArray.length; i++) {
_defaultOperators[_defaultOperatorsArray[i]] = true;
}

// 注册接口
_erc1820.setInterfaceImplementer(address(this), keccak256("ERC777Token"), address(this));
_erc1820.setInterfaceImplementer(address(this), keccak256("ERC20Token"), address(this));
}

查询函数

name(),symbol(),totalSupply(),balanceOf(address) 和含义和在ERC20 中完全一样。

granularity() 用来定义代币最小的划分粒度(>=1), 要求必须在创建时设定,之后不可以更改,不管是在铸币、发送还是销毁操作的代币数量,必需是粒度的整数倍。

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
function name() public view returns (string memory) {
return _name;
}

function symbol() public view returns (string memory) {
return _symbol;
}

// 定义小数位数,为了兼容 ERC20
function decimals() public pure returns (uint8) {
return 18;
}

// 默认粒度为1,粒度表示代币最小的分隔单位
function granularity() public view returns (uint256) {
return 1;
}

function totalSupply() public view returns (uint256) {
return _totalSupply;
}

function balanceOf(address tokenHolder) public view returns (uint256) {
return _balances[tokenHolder];
}

操作员

ERC777 定义了一个新的操作员角色,操作员被作为移动代币的地址。 每个地址直观地移动自己的代币,将持有人和操作员的概念分开可以提供更大的灵活性。

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
40
// 是否是操作员
function isOperatorFor(
address operator,
address tokenHolder
) public view returns (bool) {
return operator == tokenHolder ||
(_defaultOperators[operator] && !_revokedDefaultOperators[tokenHolder][operator]) ||
_operators[tokenHolder][operator];
}

// 授权操作员
function authorizeOperator(address operator) external {
require(msg.sender != operator, "ERC777: authorizing self as operator");

if (_defaultOperators[operator]) {
delete _revokedDefaultOperators[msg.sender][operator];
} else {
_operators[msg.sender][operator] = true;
}

emit AuthorizedOperator(operator, msg.sender);
}

// 撤销操作员
function revokeOperator(address operator) external {
require(operator != msg.sender, "ERC777: revoking self as operator");

if (_defaultOperators[operator]) {
_revokedDefaultOperators[msg.sender][operator] = true;
} else {
delete _operators[msg.sender][operator];
}

emit RevokedOperator(operator, msg.sender);
}

// 默认操作者
function defaultOperators() public view returns (address[] memory) {
return _defaultOperatorsArray;
}

转账逻辑

如果持有者希望在转账时收到代币转移通知,就需要在ERC1820合约上注册及实现 ERC777TokensSender 接口

如果接收者希望在转账时收到代币转移通知,就需要在ERC1820合约上注册及实现 ERC777TokensRecipient 接口

注意: 对于所有的 ERC777 合约, 一个持有者地址只能注册一个ERC777TokensSender接口实现。因此 ERC777TokensSender 实现会被多个ERC777合约调用,在ERC777TokensSender接口的实现合约里, msg.sender 是ERC777合约地址,而不是操作者。

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
   // ERC777 定义的转账函数, 同时触发 ERC20的 `Transfer` 事件
function send(address recipient, uint256 amount, bytes calldata data) external {
_send(msg.sender, msg.sender, recipient, amount, data, "", true);
}

// 转移代币,需要有操作者权限,触发 `Sent` 和 `Transfer` 事件
function operatorSend(
address sender,
address recipient,
uint256 amount,
bytes calldata data,
bytes calldata operatorData
)
external
{
require(isOperatorFor(msg.sender, sender), "ERC777: caller is not an operator for holder");
_send(msg.sender, sender, recipient, amount, data, operatorData, true);
}

// 为兼容 ERC20,同时触发 `Sent` 事件
function transfer(address recipient, uint256 amount) external returns (bool) {
require(recipient != address(0), "ERC777: transfer to the zero address");

address from = msg.sender;

_callTokensToSend(from, from, recipient, amount, "", "");

_move(from, from, recipient, amount, "", "");

//最后一个参数表示不要求接收者实现钩子函数 `tokensReceived`
_callTokensReceived(from, from, recipient, amount, "", "", false);

return true;
}

// 转移 token
// 最后一个参数 requireReceptionAck 表示是否必须实现 ERC777TokensRecipient
function _send(
address operator,
address from,
address to,
uint256 amount,
bytes memory userData,
bytes memory operatorData,
bool requireReceptionAck
)
private
{
require(from != address(0), "ERC777: send from the zero address");
require(to != address(0), "ERC777: send to the zero address");

// 在修改余额状态之前调用tokensToSend
_callTokensToSend(operator, from, to, amount, userData, operatorData);

// 修改余额状态
_move(operator, from, to, amount, userData, operatorData);

// 在修改余额状态之后调用tokensReceived
_callTokensReceived(operator, from, to, amount, userData, operatorData, requireReceptionAck);
}

// 转移所有权
function _move(
address operator,
address from,
address to,
uint256 amount,
bytes memory userData,
bytes memory operatorData
)
private
{
_balances[from] = _balances[from].sub(amount);
_balances[to] = _balances[to].add(amount);

emit Sent(operator, from, to, amount, userData, operatorData);
emit Transfer(from, to, amount);
}

// 尝试调用持有者的 tokensToSend() 函数
// 如果持有者有通过 ERC1820 注册 ERC777TokensSender 实现接口, 代币合约必须调用其 tokensToSend 钩子函数
function _callTokensToSend(
address operator,
address from,
address to,
uint256 amount,
bytes memory userData,
bytes memory operatorData
)
private
{
address implementer = _erc1820.getInterfaceImplementer(from, TOKENS_SENDER_INTERFACE_HASH);
if (implementer != address(0)) {
IERC777Sender(implementer).tokensToSend(operator, from, to, amount, userData, operatorData);
}
}

// 尝试调用接收者的 tokensReceived()
// 如果接收者有通过 ERC1820 注册 ERC777TokensRecipient 实现接口, 代币合约必须调用其 tokensReceived 钩子函数。
function _callTokensReceived(
address operator,
address from,
address to,
uint256 amount,
bytes memory userData,
bytes memory operatorData,
bool requireReceptionAck
)
private
{
address implementer = _erc1820.getInterfaceImplementer(to, TOKENS_RECIPIENT_INTERFACE_HASH);
if (implementer != address(0)) {
IERC777Recipient(implementer).tokensReceived(operator, from, to, amount, userData, operatorData);
} else if (requireReceptionAck) {
// 如果接收者是一个合约地址, 则必须要注册及实现 ERC777TokensRecipient 接口(这样可以防止代币被锁死),如果没有实现,ERC777代币合约必须revert 回退交易状态
require(!to.isContract(), "ERC777: token recipient contract has no implementer for ERC777TokensRecipient");
}
}

以及一个 transferFrom 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 注意, 操作员没有权限调用(除非经过approve)
// 触发 `Sent` and `Transfer`事件

function transferFrom(address holder, address recipient, uint256 amount) external returns (bool) {
require(recipient != address(0), "ERC777: transfer to the zero address");
require(holder != address(0), "ERC777: transfer from the zero address");

address spender = msg.sender;

_callTokensToSend(spender, holder, recipient, amount, "", "");

_move(spender, holder, recipient, amount, "", "");
_approve(holder, spender, _allowances[holder][spender].sub(amount));

_callTokensReceived(spender, holder, recipient, amount, "", "", false);

return true;
}

铸币操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 铸币函数(即常说的挖矿)
function _mint(
address operator,
address account,
uint256 amount,
bytes memory userData,
bytes memory operatorData
)
internal
{
require(account != address(0), "ERC777: mint to the zero address");

// Update state variables
// 发行量需要加上铸币量
_totalSupply = _totalSupply.add(amount);
// 接收者余额加上铸币量
_balances[account] = _balances[account].add(amount);

_callTokensReceived(operator, address(0), account, amount, userData, operatorData, true);

// 触发 Minted 事件
emit Minted(operator, account, amount, userData, operatorData);
emit Transfer(address(0), account, amount);
}

代币销毁

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
// 为了兼容 ERC20, 触发 `Transfer` 事件
function burn(uint256 amount, bytes calldata data) external {
_burn(msg.sender, msg.sender, amount, data, "");
}

// 销毁
function operatorBurn(address account, uint256 amount, bytes calldata data, bytes calldata operatorData) external {
require(isOperatorFor(msg.sender, account), "ERC777: caller is not an operator for holder");
_burn(msg.sender, account, amount, data, operatorData);
}

// 销毁代币实现
function _burn(
address operator,
address from,
uint256 amount,
bytes memory data,
bytes memory operatorData
)
private
{
require(from != address(0), "ERC777: burn from the zero address");

_callTokensToSend(operator, from, address(0), amount, data, operatorData);

// Update state variables
// 总供应量必须减少代币销毁量
_totalSupply = _totalSupply.sub(amount);
// 持有者的余额必须减少代币销毁的数量
_balances[from] = _balances[from].sub(amount);

// 触发 Burned 事件
emit Burned(operator, from, amount, data, operatorData);
emit Transfer(from, address(0), amount);
}

监听示例

通过实现 ERC777TokensRecipient接口来监听代币收款

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
40
41
42
43
44
45
46
pragma solidity ^0.5.0;

import "@openzeppelin/contracts/token/ERC777/IERC777Recipient.sol";
import "@openzeppelin/contracts/token/ERC777/IERC777.sol";
import "@openzeppelin/contracts/introspection/IERC1820Registry.sol";

contract Merit is IERC777Recipient {

mapping(address => uint) public givers;
address _owner;
IERC777 _token;

IERC1820Registry private _erc1820 = IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24);

// keccak256("ERC777TokensRecipient")
bytes32 constant private TOKENS_RECIPIENT_INTERFACE_HASH =
0xb281fc8c12954d22544db45de3159a39272895b169a852b314f9cc762e44c53b;

constructor(IERC777 token) public {
_erc1820.setInterfaceImplementer(address(this), TOKENS_RECIPIENT_INTERFACE_HASH, address(this));
_owner = msg.sender;
_token = token;
}

// 收款时被回调
function tokensReceived(
address operator,
address from,
address to,
uint amount,
bytes calldata userData,
bytes calldata operatorData
) external {
givers[from] += amount;
}

// 方丈取回功德箱token
function withdraw () external {
require(msg.sender == _owner, "no permision");
uint balance = _token.balanceOf(address(this));
_token.send(_owner, balance, "");
}

}


调用 ERC1820 注册表合约的 setInterfaceImplementer函数 注册ERC777TokensRecipient接口实现(接口的实现是自身),这样在收到代币时,会回调 tokensReceived函数,tokensReceived函数通过givers映射来保存每个用户的代币金额。

安全事件

Uniswap 的 ERC777 重入风险


ERC777
http://sissice.github.io/2022/10/31/erc777/
作者
Sissice
发布于
2022年10月31日
许可协议