ERC777介绍

简介

ERC777保持与 ERC20 兼容。

操作员通常利用合约发送token,并具备发送 / 接收钩子,以使得持有者对其token拥有更多的控制权。

它利用 ERC1820 来了解在合同和用户地址收到token时是否以及在何处通知它们。

解决的问题(相对于ERC20的优点)

  1. 同样使用 send(dest, value, data) 发送Ether
  2. 使用 tokensToSend 钩子,合约和用户地址都可以控制和拒绝发送出去的token
  3. 使用 tokensReceived 钩子,合约和用户地址都可以控制和拒绝接收到的token
  4. tokensReceived 钩子允许发送token到合约地址,并在交易中心发出通知。而ERC20需要approve/transferFrom 两次调用才能达到这样的效果
  5. token持有人可以指定/去除操作员(合约)地址,这些操作员通常是已经验证过的合约contract,例如交易所,支票处理器或自动收费系统
  6. 每一笔交易含有 dataoperatorData ,它们是bytes字段,在token持有人和操作员之间传递信息
  7. tokensReceived 钩子函数可以通过代理合约来部署,所以ERC777是与ERC20钱包兼容的

构建ERC777合约

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pragma solidity ^0.5.0;

import "@openzeppelin/contracts/token/ERC777/ERC777.sol";

contract GLDToken is ERC777 {
constructor(
uint256 initialSupply,
address[] memory defaultOperators
)
ERC777("Gold", "GLD", defaultOperators)
public
{
_mint(msg.sender, msg.sender, initialSupply, "", "");
}
}

ERC777扩展了ERC20,对ERC20提供兼容性支持。使用 _mintinitialSupply 分配给部署者帐户。与ERC20的_mint不同,此参数包含一些额外的参数,但是可以放心地忽略它们。

initialSupply 没有设定小数。ERC777规定小数始终返回固定值18,因此无需自行设置。

最后,我们需要设置defaultOperators:操作员帐户(通常是智能合约地址),该帐户将能够代表其持有者转让token。如果不打算在token中使用操作员(合约),则只需传递一个空数组即可。

基本的ERC777合约就是这样! 现在,对其进行部署,并使用相同的balanceOf方法来查询部署者的余额:

1
2
> GLDToken.balanceOf(deployerAddress)
1000

要将token从一个帐户转移到另一个帐户,既可以使用ERC20的“transfer”方法,也可以使用新的ERC777的“send”方法,其作用非常相似,但是增加了一个可选的data字段:

1
2
3
4
5
6
> GLDToken.transfer(otherAddress, 300)
> GLDToken.send(otherAddress, 300, "")
> GLDToken.balanceOf(otherAddress)
600
> GLDToken.balanceOf(deployerAddress)
400

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
interface ERC777Token {
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function totalSupply() external view returns (uint256);
function balanceOf(address holder) external view returns (uint256);

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

// 操作员 相关的操作(操作员是可以代表持有者发送和销毁代币的账号地址)
function defaultOperators() external view returns (address[] memory);
function isOperatorFor(
address operator,
address holder
) external view returns (bool);
// 设置一个地址作为 msg.sender 的操作员,需要触发 AuthorizedOperator 事件
function authorizeOperator(address operator) external;
// 移除 msg.sender 上 operator 操作员的权限, 需要触发 RevokedOperator 事件
function revokeOperator(address operator) external;

// 发送代币
function send(address to, uint256 amount, bytes calldata data) external;
function operatorSend(
address from,
address to,
uint256 amount,
bytes calldata data,
bytes calldata operatorData
) external;

// 销毁代币
function burn(uint256 amount, bytes calldata data) external;
function operatorBurn(
address from,
uint256 amount,
bytes calldata data,
bytes calldata operatorData
) external;

// 发送代币事件
event Sent(
address indexed operator,
address indexed from,
address indexed to,
uint256 amount,
bytes data,
bytes operatorData
);

// 铸币事件
event Minted(
address indexed operator,
address indexed to,
uint256 amount,
bytes data,
bytes operatorData
);

// 销毁代币事件
event Burned(
address indexed operator,
address indexed from,
uint256 amount,
bytes data,
bytes operatorData
);

// 授权操作员事件
event AuthorizedOperator(
address indexed operator,
address indexed holder
);

// 撤销操作员事件
event RevokedOperator(address indexed operator, address indexed holder);
}

钩子函数如何工作

首先来了解一下 ERC1820, ERC1820是一个全局的合约,在以太坊链上有一个唯一的合约地址,它总是 0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24

ERC1820 合约提过了两个主要接口:

  • setInterfaceImplementer(address _addr, bytes32 _interfaceHash, address _implementer)
    用来设置地址(_addr)的接口(_interfaceHash 接口名称的 keccak256 )由哪个合约实现(_implementer)。
  • getInterfaceImplementer(address _addr, bytes32 _interfaceHash) external view returns (address)
    这个函数用来查询地址(_addr)的接口由哪个合约实现。

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

ERC777 使用 operatorSend 转账时,可以通过参数operatorData携带操作者的信息,发送代币除了执行对应账户的余额加减和触发事件之外,还有额外的规定

  1. 如果持有者有通过 ERC1820 注册 ERC777TokensSender 实现接口,代币合约必须调用其 tokensToSend 钩子函数。
  2. 如果接收者有通过 ERC1820 注册 ERC777TokensRecipient 实现接口, 代币合约必须调用其 tokensReceived 钩子函数。
  3. 如果有 tokensToSend 钩子函数,必须在修改余额状态之前调用。
  4. 如果有 tokensReceived 钩子函数,必须在修改余额状态之后调用。
  5. 调用钩子函数及触发事件时,dataoperatorData必须原样传递,因为 tokensToSend 和 tokensReceived 函数可能根据这个数据取消转账(触发 revert)。

从而保证了ERC777交易以及钩子函数的原子性。

参考链接 ==>

https://eips.ethereum.org/EIPS/eip-777

https://github.com/0xjac/ERC777

https://docs.openzeppelin.com/contracts/2.x/erc777

https://github.com/OpenZeppelin/openzeppelin-contracts