ERC777 功能型代币(通证)最佳实践

想必很多同学都已经使用过ERC20 创建过代币,或许已经被老板要求在ERC20代币上实现一些附加功能搞的焦头烂额,如果还有选择,一定要选择 ERC777 。

ERC20 的问题

以下是一个遇到很多次的场景:有一天老板过来找你(开发者),最近存币生息很火,我们也做一个合约吧, 用户打币过来给他计算利息, 看起来是一个很简单的需求,你满口答应说好,结果自己一研究发现,使用 ERC20 标准没办法在合约里记录是谁发过来多少币,从而没法计算利息(因为接收者合约并不知道自己接收到ERC20代币)。

ERC20 标准下,可以通过一个变通的办法,采用两个交易组合完成,方法是:第1步:先让用户把要转移的金额用 ERC20 的approve 授权的存币生息合约(这步通常称为解锁),第2步:再次让用户调用存币生息合约的计息函数,计息函数中通过 transferFrom 把代币从用户手里转移的合约内,并开始计息。

同样由于ERC20 标准没有一个转账通知机制,很多ERC20代币误转到合约之后,再也没有办法把币转移出来,已经有大量的ERC20 因为这个原因被锁死,如锁死的QTUM锁死的EOS

另外一个问题是ERC20 转账时,无法携带额外的信息,例如:我们有一些客户希望让用户使用 ERC20 代币购买商品,因为转账没法携带额外的信息, 用户的代币转移过来,不知道用户具体要购买哪件商品,从而展加了线下额外的沟通成本。

ERC777很好的解决了这些问题,同时ERC777 也兼容 ERC20 标准。因此强烈建议新开发的代币使用ERC777标准。

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

ERC1820 接口注册表合约

即便是一个普通用户地址,同样可以实现对 ERC777 转账的监听, 听起来有点神奇,其实这是通过 ERC1820 接口注册表合约来是实现的。

ERC1820 如此的重要,以至于ERC777单独把它拆出来作为一个EIP。

ERC1820 是一个全局的合约,有一个唯一在以太坊链上都相同的合约地址,它总是 0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24 ,这个合约是通过非常巧妙的方式进行部署的,有兴趣的同学可以阅读EIP1820文档

ERC 1820 合约的官方实现代码在ERC1820文档可以查阅,这里说明合约实现的主要内容。

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

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

setInterfaceImplementer函数会参数信息记录到下面这个interfaces映射里:

// 记录 地址(第一个键) 的接口(第二个键)的实现地址(第二个值)
mapping(address => mapping(bytes32 => address)) interfaces;

相对应的 getInterfaceImplementer() 通过 interfaces 这个mapping 来获得接口的实现。

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

ERC777 标准规范

ERC777 接口

ERC777 为了在实现上可以兼容ERC20,除了查询函数和ERC20一致外,操作接口均采用的独立的命名(避免相同的命令无法分辨是哪个标准),ERC777的接口定义如下,要求所有的ERC777代币合约都必须实现这些接口:

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);
    function authorizeOperator(address operator) external;
    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);
}

接口定义在 openzeppelin代码库 里找到,路径为:contracts/token/ERC777/IERC777.sol

接口说明与实现约定

所有的ERC777 合约除了必须实现上述接口,还有一些其他的必须遵守的约定(直接导致了ERC777官方文档又长又臭...哭~)。

ERC777 合约必须要通过 ERC1820 注册 ERC777Token 接口,这样任何人都可以查询合约是否是ERC777标准的合约,注册方法是: 调用ERC1820 注册合约的 setInterfaceImplementer 方法,参数 _addr 及 _implementer 均是合约的地址,_interfaceHash 是 ERC777Token 的 keccak256 哈希值(0xac7fbab5f54a3ca8194167523c6753bfeb96a445279294b6125b68cce2177054)

如果 ERC777 要实现ERC20标准,还必须通过ERC1820 注册ERC20Token接口。

ERC777 信息说明函数

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

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

granularity 和 ERC20 的 decimals 不一样,decimals用来定义小数位数,decimals 是ERC20 可选函数,为了兼容 ERC20 代币, decimals 函数要求必须返回18。而 granularity 表示的是基于最小位数(内部存储)的划分粒度。例如:0.5个代币存储为 500,000,000,000,000,000 (0.5 X 10^18),如果粒度为2,则最小可切分为250,000,000,000,000,000 份。

操作员

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

与ERC20中的 approve 、 transferFrom 不同,其未明确定义批准地址的角色。

此外,ERC777还可以定义默认操作员(默认操作员列表只能在代币创建时定义的,并且不能更改),默认操作员是被所有持有人授权的操作员,这可以为项目方管理代币带来方便,当然认何持有人仍然有权撤销默认操作员。

操作员相关的函数

  • defaultOperators(): 获取代币合约默认的操作员列表.
  • authorizeOperator(address operator): 设置一个地址作为msg.sender 的操作员,需要触发AuthorizedOperator事件。
  • revokeOperator(address operator): 移除 msg.sender 上 operator 操作员的权限, 需要触发RevokedOperator事件。
  • isOperatorFor(address operator, address holder): 是否是某个持有者的操作员。

发送代币

ERC777 发送代币 使用以下两个方法:

send(address to, uint256 amount, bytes calldata data) external

function operatorSend(
    address from,
    address to,
    uint256 amount,
    bytes calldata data,
    bytes calldata operatorData
) external

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

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

ERC777TokensSender 接口定义如下:

interface ERC777TokensSender {
    function tokensToSend(
        address operator,
        address from,
        address to,
        uint256 amount,
        bytes calldata userData,
        bytes calldata operatorData
    ) external;
}

如果持有者希望在转账时收到代币转移通知,就需要在ERC1820合约上注册及实现 ERC777TokensSender 接口(稍后有案例介绍)。

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

ERC777TokensRecipient 接口定义如下:

```
interface ERC777TokensRecipient {
function tokensReceived(
address operator,
address from,
address to,
uint256 amount,
bytes calldata data,
bytes calldata operatorData
) external;

top Created with Sketch.