Openzeppelin之ERC777源码解析(上)

365 阅读8分钟

一、ERC220和ERC777

1.1 ERC20的问题

  • 1. 当给某个合约转币,ERC20无法在合约里面记录是谁发过来的币,导致不能进行后续的一系统操作,因为接收者并不知道自己接受了ERC20代币。

上面这一操作可以通过另外的方法:先把用户要转移的金额通过approve方法授权过存币生息合约,再让用户调用存币生息合约的计息函数,计息函数有transforfrom函数把代币从用户手中转移给合约内。

  • 2. ERC20合约没有转账通知机制,导致很多ERC20代币误转到合约后,无法提取出来,导致大量代币被锁死,例如:如锁死的QTUM,锁死的EOS.

  • ERC20转账时,无法携带额外的信息

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

1.2 ERC777

ERC777很好的解决了上述的问题,并且兼容ERC20标准,现在大部分DAPP合约正在从ERC20转向ERC777合约,强烈建议开发者们使用。

ERC777可以允许第三方帐户(或合约)使用您的代币。最常见的需求是在使用去中心化交易所智能合约交一键发币时。

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

ERC777的优点如下:

  1. 合约和普通地址都可以通过注册tokensToSend hook函数来控制和拒绝发送哪些token(拒绝发送通过在hook函数tokensToSend 里 revert 来实现)。
  2. 合约和普通地址都可以通过注册tokensReceived hook函数来控制和拒绝接受哪些token(拒绝接受通过在hook函数tokensReceived 里 revert 来实现)。
  3. tokensReceived 可以通过hook函数可以做到在一个交易里完成发送代币和通知合约接受代币,而不像 ERC20必须通过两次调用(approve/transferFrom)来完成。
  4. 持有者可以"授权"和"撤销"操作员(operators: 可以代表持有者发送代币)。 这些操作员通常是(去中心化)交易所、支票处理机或自动支付系统。
  5. 每个代币交易都包含 data 和 operatorData 字段, 可以分别传递来自持有者和操作员的数据。
  6. 可以通过部署实现 tokensReceived 的代理合约来兼容没有实现tokensReceived 函数的地址。

二、ERC777的规范标准

ERC777除了查询函数和ERC20标准一样,其他操作接口都采用独立的命名,ERC777的接口定义如下,要求ERC777的实现合约都必须实现所定义的接口。

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC777/IERC777.sol)

pragma solidity ^0.8.0;

/**
 * @dev Interface of the ERC777Token standard as defined in the EIP.
 *
 * This contract uses the
 * https://eips.ethereum.org/EIPS/eip-1820[ERC1820 registry standard] to let
 * token holders and recipients react to token movements by using setting implementers
 * for the associated interfaces in said registry. See {IERC1820Registry} and
 * {ERC1820Implementer}.
 */
interface IERC777 {
    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 tokenHolder);
    //被取消的授权操作者 => 代币持有者
    event RevokedOperator(address indexed operator, address indexed tokenHolder);

    function name() external view returns (string memory);

    function symbol() external view returns (string memory);
    //定义代币最小的划分粒度
    function granularity() external view returns (uint256);

    function balanceOf(address owner) external view returns (uint256);
    //发送代币
    function send(
        address recipient,
        uint256 amount,
        bytes calldata data
    ) external;
    //销魂代币
    function burn(uint256 amount, bytes calldata data) external;
    //判断该操作者是否是被代币持有者授权的
    function isOperatorFor(address operator, address tokenHolder) external view returns (bool);
    //添加指定操作员
    function authorizeOperator(address operator) external;
    //撤销指定操作员
    function revokeOperator(address operator) external;
    //得到默认的操作员
    function defaultOperators() external view returns (address[] memory);

   //操作者发送代币
    function operatorSend(
        address sender,
        address recipient,
        uint256 amount,
        bytes calldata data,
        bytes calldata operatorData
    ) external;

    //操作者销魂代币
    function operatorBurn(
        address account,
        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
    );
}

三、ERC777.sol

前提知识:

ERC777标准通过引入Operator的概念来解决上述问题。像往常一样,操作者是某些具有执行特定任务权限的帐户。在ERC777标准下,有两类操作者:

  1. 默认操作者
  2. 指定操作者

一个指定操作者是允许代表另一个地址发送和销毁代币的地址。 一个默认操作者是允许代表所有代币持有者发送和销毁代币的地址。

任意ERC777代币持有者地址可以通过调用authorizeOperator方法函数授权给一个指定操作者获得权限。与此类似,任意ERC777代币持有者都可以通过调用revokeOperator方法函数撤销操作者地址的授权(包括默认运算符)。

在一键发币的ERC777实现中, 默认操作者是用于部署代币的地址。

1.属性

  • using Address for address给address地址使用Address库,可以调用库里面的方法

  • _totalSupply,_name,_symbol和ERC20的标准一样

  • _defaultOperatorsArray用于存放默认的操作员,默认操作员只能在代币初始创建时被定义,并且无法被更改。

    using Address for address;

    IERC1820Registry internal constant _ERC1820_REGISTRY = IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24);

    uint256 private _totalSupply;

    string private _name;
    string private _symbol;

    bytes32 private constant _TOKENS_SENDER_INTERFACE_HASH = keccak256("ERC777TokensSender");
    bytes32 private constant _TOKENS_RECIPIENT_INTERFACE_HASH = keccak256("ERC777TokensRecipient");

    address[] private _defaultOperatorsArray;

2.映射

  • _balances表示代币持有人的地址与对应的代币数量

  • _defaultOperators表示该默认操作者地址是否存在

  • _operators 表示该代币持有者地址对应的指定操作者是否存在

  • _revokedDefaultOperators 表示代币持有者地址对应的撤销操作者是否存在

  • _allowances和ERC20标准中的一样,授权某个地址可以使用代币持有者的一定数量代币,这个被授权的地址不一定是操作者。

    mapping(address => uint256) private _balances;
    mapping(address => bool) private _defaultOperators;

    mapping(address => mapping(address => bool)) private _operators;
    mapping(address => mapping(address => bool)) private _revokedDefaultOperators;

    // ERC20-allowances
    mapping(address => mapping(address => uint256)) private _allowances;

3.构造器

在合约首次初始化的时,初始化代币名称,符号(代币的图片),默认操作者地址数组,遍历数组,并更新_defalutOperatorsmapping

    constructor(
        string memory name_,
        string memory symbol_,
        address[] memory defaultOperators_
    ) {
        _name = name_;
        _symbol = symbol_;

        _defaultOperatorsArray = defaultOperators_;
        for (uint256 i = 0; i < defaultOperators_.length; i++) {
            _defaultOperators[defaultOperators_[i]] = true;
        }

        // register interfaces
        _ERC1820_REGISTRY.setInterfaceImplementer(address(this), keccak256("ERC777Token"), address(this));
        _ERC1820_REGISTRY.setInterfaceImplementer(address(this), keccak256("ERC20Token"), address(this));
    }

4.合约函数

4.1基本函数

  • name,symbol,decimals, totalSupply(), balanceOf函数和ERC20标准一样

  • granularity() 用来定义代币最小的划分粒度(>=1), 要求必须在创建时设定,之后不可以更改,不管是在铸币、发送还是销毁操作的代币数量,必须是粒度的整数倍。例如:如果granularity()返回的是2,则最小转账单位是2

    function name() public view virtual override returns (string memory) {
        return _name;
    }

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

    function decimals() public pure virtual returns (uint8) {
        return 18;
    }

    function granularity() public view virtual override returns (uint256) {
        return 1;
    }

    function totalSupply() public view virtual override(IERC20, IERC777) returns (uint256) {
        return _totalSupply;
    }

    function balanceOf(address tokenHolder) public view virtual override(IERC20, IERC777) returns (uint256) {
        return _balances[tokenHolder];
    }
  • _mint()

1)先判断接收者地址是否为0地址

2)将调用该合约的地址命为操作者,调用 _beforeTokenTransfer方法可在转账之前做一些操作

3)将总的代币数量加加,以及接收者地址代币数量加加

4)调用_callTokensReceived()判断接收者地址是否实现了tokensReceived()方法,只有实现了该方法,才可以接收代币,否则转账交易终止。

  • _burn()

1)先判断销毁者地址是否为0地址

2)将调用该合约的地址命为操作者

3)_callTokensToSend()表明要销毁代币的持有者必须实现tokensToSend()的方法才可以进行代币发出或交易的操作

4)调用 _beforeTokenTransfer方法可在转账之前做一些操作

  • _approve()

1)判断接收者地址和被授权者地址不为0

2)更新_allowancemapping

    function _mint(address account, uint256 amount,bytes memory userData,bytes memory operatorData,bool requireReceptionAck
    ) internal virtual {
        require(account != address(0), "ERC777: mint to the zero address");
        address operator = _msgSender();
        _beforeTokenTransfer(operator, address(0), account, amount);

        _totalSupply += amount;
        _balances[account] += amount;

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

        emit Minted(operator, account, amount, userData, operatorData);
        emit Transfer(address(0), account, amount);
    }
    
    function _burn(address from,uint256 amount,bytes memory data, bytes memory operatorData
    ) internal virtual {
        require(from != address(0), "ERC777: burn from the zero address");
        address operator = _msgSender();

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

        uint256 fromBalance = _balances[from];
        require(fromBalance >= amount, "ERC777: burn amount exceeds balance");
        unchecked {
            _balances[from] = fromBalance - amount;
        }
        _totalSupply -= amount;

        emit Burned(operator, from, amount, data, operatorData);
        emit Transfer(from, address(0), amount);
    }
    
    function _approve( address holder,address spender, uint256 value) internal virtual {
        require(holder != address(0), "ERC777: approve from the zero address");
        require(spender != address(0), "ERC777: approve to the zero address");

        _allowances[holder][spender] = value;
        emit Approval(holder, spender, value);
    }

ERC777的其他重点函数将在下一讲介绍。

参考文章:

EIP 777: ERC777 代币标准

什么是ERC777