首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >ERC-777标准规范

ERC-777标准规范

原创
作者头像
Al1ex
修改于 2021-07-16 02:44:25
修改于 2021-07-16 02:44:25
1.7K00
代码可运行
举报
文章被收录于专栏:网络安全攻防网络安全攻防
运行总次数:0
代码可运行

文章前言

在经典的ERC-20场景中,如果用户想要授权给第三方账户或者智能合约进行转账操作,那么需要通过两个事务来完成整个转账的操作,在这里需要注意的是在授权是需要指定对应的amount数量,那么当每次进行授权转账时都需要进行一次查询或者让A用户再次授权给B用户:

  • A用户授权B用户可以操作A用户amount数量的token
  • B用户获得A用户的授权之后调用转账函数进行转账

ERC-777标准引入了运营商的概念来解决授权给第三方账户或智能合约进行转账操作的问题,在ERC-777中运营商有两种类型:

  • 常规运营商:一个地址,允许代表另一个地址发送和记录代币
  • 默认运营商:一个地址,允许所有代币持有者发送和记录代币

优势对比

ERC-777与ERC-20对比具有以下优势:

  • 采用接口send(dest,value,data)发送代币,先前兼容
  • 任何合约都可以定义收到代币时触发的tokensReceived事件,避免ERC20代币中的双重调用问题
  • 合约和常规地址可以通过注册一个tokensToSend或tokensReceivedFunction函数来控制或拒绝发送或接收的代币,避免ERC20代币中存在的代币卡死问题
  • 代币持有者可以授权或回收管理其代币的操作员权限,这些操作员通常是交易所合约或自动收费系统中的支付处理器
  • 每个代币交易都包含userData数据字段,在操作员操作时也有类似的operatorData字段,从而可以自由地将数据传递给接收方
  • 向后兼容不支持tokensReceived函数的钱包

接口列表

这里我们以OpenZeppelin官方提供的ERC-777标准为例进行分析,首先我们看以下IERC-777中指明的一个ERC-777需要实现的接口:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#FUNCTIONS
name()
symbol()
granularity()
totalSupply()
balanceOf(owner)
send(recipient, amount, data)
burn(amount, data)
isOperatorFor(operator, tokenHolder)
authorizeOperator(operator)
revokeOperator(operator)
defaultOperators()
operatorSend(sender, recipient, amount, data, operatorData)
operatorBurn(account, amount, data, operatorData)

#EVENTS
Sent(operator, from, to, amount, data, operatorData)
Minted(operator, to, amount, data, operatorData)
Burned(operator, from, amount, data, operatorData)
AuthorizedOperator(operator, tokenHolder)
RevokedOperator(operator, tokenHolder)

源码分析

OpenZeppelin官方提供了ERC-777标准示例代码如下:

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC777/ERC777.sol

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./IERC777.sol";
import "./IERC777Recipient.sol";
import "./IERC777Sender.sol";
import "../ERC20/IERC20.sol";
import "../../utils/Address.sol";
import "../../utils/Context.sol";
import "../../utils/introspection/IERC1820Registry.sol";

/**
 * @dev Implementation of the {IERC777} interface.
 *
 * This implementation is agnostic to the way tokens are created. This means
 * that a supply mechanism has to be added in a derived contract using {_mint}.
 *
 * Support for ERC20 is included in this contract, as specified by the EIP: both
 * the ERC777 and ERC20 interfaces can be safely used when interacting with it.
 * Both {IERC777-Sent} and {IERC20-Transfer} events are emitted on token
 * movements.
 *
 * Additionally, the {IERC777-granularity} value is hard-coded to `1`, meaning that there
 * are no special restrictions in the amount of tokens that created, moved, or
 * destroyed. This makes integration with ERC20 applications seamless.
 */
contract ERC777 is Context, IERC777, IERC20 {
    using Address for address;

    IERC1820Registry constant internal _ERC1820_REGISTRY = IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24);

    mapping(address => uint256) private _balances;

    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");

    // This isn't ever read from - it's only used to respond to the defaultOperators query.
    address[] private _defaultOperatorsArray;

    // Immutable, but accounts may revoke them (tracked in __revokedDefaultOperators).
    mapping(address => bool) private _defaultOperators;

    // For each account, a mapping of its operators and revoked default operators.
    mapping(address => mapping(address => bool)) private _operators;
    mapping(address => mapping(address => bool)) private _revokedDefaultOperators;

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

    /**
     * @dev `defaultOperators` may be an empty array.
     */
    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));
    }

    /**
     * @dev See {IERC777-name}.
     */
    function name() public view virtual override returns (string memory) {
        return _name;
    }

    /**
     * @dev See {IERC777-symbol}.
     */
    function symbol() public view virtual override returns (string memory) {
        return _symbol;
    }

    /**
     * @dev See {ERC20-decimals}.
     *
     * Always returns 18, as per the
     * [ERC777 EIP](https://eips.ethereum.org/EIPS/eip-777#backward-compatibility).
     */
    function decimals() public pure virtual returns (uint8) {
        return 18;
    }

    /**
     * @dev See {IERC777-granularity}.
     *
     * This implementation always returns `1`.
     */
    function granularity() public view virtual override returns (uint256) {
        return 1;
    }

    /**
     * @dev See {IERC777-totalSupply}.
     */
    function totalSupply() public view virtual override(IERC20, IERC777) returns (uint256) {
        return _totalSupply;
    }

    /**
     * @dev Returns the amount of tokens owned by an account (`tokenHolder`).
     */
    function balanceOf(address tokenHolder) public view virtual override(IERC20, IERC777) returns (uint256) {
        return _balances[tokenHolder];
    }

    /**
     * @dev See {IERC777-send}.
     *
     * Also emits a {IERC20-Transfer} event for ERC20 compatibility.
     */
    function send(address recipient, uint256 amount, bytes memory data) public virtual override  {
        _send(_msgSender(), recipient, amount, data, "", true);
    }

    /**
     * @dev See {IERC20-transfer}.
     *
     * Unlike `send`, `recipient` is _not_ required to implement the {IERC777Recipient}
     * interface if it is a contract.
     *
     * Also emits a {Sent} event.
     */
    function transfer(address recipient, uint256 amount) public virtual override returns (bool) {
        require(recipient != address(0), "ERC777: transfer to the zero address");

        address from = _msgSender();

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

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

        _callTokensReceived(from, from, recipient, amount, "", "", false);

        return true;
    }

    /**
     * @dev See {IERC777-burn}.
     *
     * Also emits a {IERC20-Transfer} event for ERC20 compatibility.
     */
    function burn(uint256 amount, bytes memory data) public virtual override  {
        _burn(_msgSender(), amount, data, "");
    }

    /**
     * @dev See {IERC777-isOperatorFor}.
     */
    function isOperatorFor(address operator, address tokenHolder) public view virtual override returns (bool) {
        return operator == tokenHolder ||
            (_defaultOperators[operator] && !_revokedDefaultOperators[tokenHolder][operator]) ||
            _operators[tokenHolder][operator];
    }

    /**
     * @dev See {IERC777-authorizeOperator}.
     */
    function authorizeOperator(address operator) public virtual override  {
        require(_msgSender() != operator, "ERC777: authorizing self as operator");

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

        emit AuthorizedOperator(operator, _msgSender());
    }

    /**
     * @dev See {IERC777-revokeOperator}.
     */
    function revokeOperator(address operator) public virtual override  {
        require(operator != _msgSender(), "ERC777: revoking self as operator");

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

        emit RevokedOperator(operator, _msgSender());
    }

    /**
     * @dev See {IERC777-defaultOperators}.
     */
    function defaultOperators() public view virtual override returns (address[] memory) {
        return _defaultOperatorsArray;
    }

    /**
     * @dev See {IERC777-operatorSend}.
     *
     * Emits {Sent} and {IERC20-Transfer} events.
     */
    function operatorSend(
        address sender,
        address recipient,
        uint256 amount,
        bytes memory data,
        bytes memory operatorData
    )
        public
        virtual
        override
    {
        require(isOperatorFor(_msgSender(), sender), "ERC777: caller is not an operator for holder");
        _send(sender, recipient, amount, data, operatorData, true);
    }

    /**
     * @dev See {IERC777-operatorBurn}.
     *
     * Emits {Burned} and {IERC20-Transfer} events.
     */
    function operatorBurn(address account, uint256 amount, bytes memory data, bytes memory operatorData) public virtual override {
        require(isOperatorFor(_msgSender(), account), "ERC777: caller is not an operator for holder");
        _burn(account, amount, data, operatorData);
    }

    /**
     * @dev See {IERC20-allowance}.
     *
     * Note that operator and allowance concepts are orthogonal: operators may
     * not have allowance, and accounts with allowance may not be operators
     * themselves.
     */
    function allowance(address holder, address spender) public view virtual override returns (uint256) {
        return _allowances[holder][spender];
    }

    /**
     * @dev See {IERC20-approve}.
     *
     * Note that accounts cannot have allowance issued by their operators.
     */
    function approve(address spender, uint256 value) public virtual override returns (bool) {
        address holder = _msgSender();
        _approve(holder, spender, value);
        return true;
    }

   /**
    * @dev See {IERC20-transferFrom}.
    *
    * Note that operator and allowance concepts are orthogonal: operators cannot
    * call `transferFrom` (unless they have allowance), and accounts with
    * allowance cannot call `operatorSend` (unless they are operators).
    *
    * Emits {Sent}, {IERC20-Transfer} and {IERC20-Approval} events.
    */
    function transferFrom(address holder, address recipient, uint256 amount) public virtual override returns (bool) {
        require(recipient != address(0), "ERC777: transfer to the zero address");
        require(holder != address(0), "ERC777: transfer from the zero address");

        address spender = _msgSender();

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

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

        uint256 currentAllowance = _allowances[holder][spender];
        require(currentAllowance >= amount, "ERC777: transfer amount exceeds allowance");
        _approve(holder, spender, currentAllowance - amount);

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

        return true;
    }

    /**
     * @dev Creates `amount` tokens and assigns them to `account`, increasing
     * the total supply.
     *
     * If a send hook is registered for `account`, the corresponding function
     * will be called with `operator`, `data` and `operatorData`.
     *
     * See {IERC777Sender} and {IERC777Recipient}.
     *
     * Emits {Minted} and {IERC20-Transfer} events.
     *
     * Requirements
     *
     * - `account` cannot be the zero address.
     * - if `account` is a contract, it must implement the {IERC777Recipient}
     * interface.
     */
    function _mint(
        address account,
        uint256 amount,
        bytes memory userData,
        bytes memory operatorData
    )
        internal
        virtual
    {
        _mint(account, amount, userData, operatorData, true);
    }

    /**
     * @dev Creates `amount` tokens and assigns them to `account`, increasing
     * the total supply.
     *
     * If `requireReceptionAck` is set to true, and if a send hook is
     * registered for `account`, the corresponding function will be called with
     * `operator`, `data` and `operatorData`.
     *
     * See {IERC777Sender} and {IERC777Recipient}.
     *
     * Emits {Minted} and {IERC20-Transfer} events.
     *
     * Requirements
     *
     * - `account` cannot be the zero address.
     * - if `account` is a contract, it must implement the {IERC777Recipient}
     * interface.
     */
    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);

        // Update state variables
        _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);
    }

    /**
     * @dev Send tokens
     * @param from address token holder address
     * @param to address recipient address
     * @param amount uint256 amount of tokens to transfer
     * @param userData bytes extra information provided by the token holder (if any)
     * @param operatorData bytes extra information provided by the operator (if any)
     * @param requireReceptionAck if true, contract recipients are required to implement ERC777TokensRecipient
     */
    function _send(
        address from,
        address to,
        uint256 amount,
        bytes memory userData,
        bytes memory operatorData,
        bool requireReceptionAck
    )
        internal
        virtual
    {
        require(from != address(0), "ERC777: send from the zero address");
        require(to != address(0), "ERC777: send to the zero address");

        address operator = _msgSender();

        _callTokensToSend(operator, from, to, amount, userData, operatorData);

        _move(operator, from, to, amount, userData, operatorData);

        _callTokensReceived(operator, from, to, amount, userData, operatorData, requireReceptionAck);
    }

    /**
     * @dev Burn tokens
     * @param from address token holder address
     * @param amount uint256 amount of tokens to burn
     * @param data bytes extra information provided by the token holder
     * @param operatorData bytes extra information provided by the operator (if any)
     */
    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);

        // Update state variables
        uint256 fromBalance = _balances[from];
        require(fromBalance >= amount, "ERC777: burn amount exceeds balance");
        _balances[from] = fromBalance - amount;
        _totalSupply -= amount;

        emit Burned(operator, from, amount, data, operatorData);
        emit Transfer(from, address(0), amount);
    }

    function _move(
        address operator,
        address from,
        address to,
        uint256 amount,
        bytes memory userData,
        bytes memory operatorData
    )
        private
    {
        _beforeTokenTransfer(operator, from, to, amount);

        uint256 fromBalance = _balances[from];
        require(fromBalance >= amount, "ERC777: transfer amount exceeds balance");
        _balances[from] = fromBalance - amount;
        _balances[to] += amount;

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

    /**
     * @dev See {ERC20-_approve}.
     *
     * Note that accounts cannot have allowance issued by their operators.
     */
    function _approve(address holder, address spender, uint256 value) internal {
        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);
    }

    /**
     * @dev Call from.tokensToSend() if the interface is registered
     * @param operator address operator requesting the transfer
     * @param from address token holder address
     * @param to address recipient address
     * @param amount uint256 amount of tokens to transfer
     * @param userData bytes extra information provided by the token holder (if any)
     * @param operatorData bytes extra information provided by the operator (if any)
     */
    function _callTokensToSend(
        address operator,
        address from,
        address to,
        uint256 amount,
        bytes memory userData,
        bytes memory operatorData
    )
        private
    {
        address implementer = _ERC1820_REGISTRY.getInterfaceImplementer(from, _TOKENS_SENDER_INTERFACE_HASH);
        if (implementer != address(0)) {
            IERC777Sender(implementer).tokensToSend(operator, from, to, amount, userData, operatorData);
        }
    }

    /**
     * @dev Call to.tokensReceived() if the interface is registered. Reverts if the recipient is a contract but
     * tokensReceived() was not registered for the recipient
     * @param operator address operator requesting the transfer
     * @param from address token holder address
     * @param to address recipient address
     * @param amount uint256 amount of tokens to transfer
     * @param userData bytes extra information provided by the token holder (if any)
     * @param operatorData bytes extra information provided by the operator (if any)
     * @param requireReceptionAck if true, contract recipients are required to implement ERC777TokensRecipient
     */
    function _callTokensReceived(
        address operator,
        address from,
        address to,
        uint256 amount,
        bytes memory userData,
        bytes memory operatorData,
        bool requireReceptionAck
    )
        private
    {
        address implementer = _ERC1820_REGISTRY.getInterfaceImplementer(to, _TOKENS_RECIPIENT_INTERFACE_HASH);
        if (implementer != address(0)) {
            IERC777Recipient(implementer).tokensReceived(operator, from, to, amount, userData, operatorData);
        } else if (requireReceptionAck) {
            require(!to.isContract(), "ERC777: token recipient contract has no implementer for ERC777TokensRecipient");
        }
    }

    /**
     * @dev Hook that is called before any token transfer. This includes
     * calls to {send}, {transfer}, {operatorSend}, minting and burning.
     *
     * Calling conditions:
     *
     * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens
     * will be to transferred to `to`.
     * - when `from` is zero, `amount` tokens will be minted for `to`.
     * - when `to` is zero, `amount` of ``from``'s tokens will be burned.
     * - `from` and `to` are never both zero.
     *
     * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
     */
    function _beforeTokenTransfer(address operator, address from, address to, uint256 amount) internal virtual { }
}

合约继承

在合约的开头处可以看到ERC-777合约继承自IERC777, IERC20合约,这里的继承关系和JAVA中类的继承关系类型,子类可以继承父类的所有方法也可以重写父类的方法:

全局变量

ERC-777标准中定义了以下全局变量:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
using Address for address;

IERC1820Registry constant internal _ERC1820_REGISTRY = IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24);

mapping(address => uint256) private _balances;

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");

// This isn't ever read from - it's only used to respond to the defaultOperators query.
address[] private _defaultOperatorsArray;

// Immutable, but accounts may revoke them (tracked in __revokedDefaultOperators).
mapping(address => bool) private _defaultOperators;

// For each account, a mapping of its operators and revoked default operators.
mapping(address => mapping(address => bool)) private _operators;
mapping(address => mapping(address => bool)) private _revokedDefaultOperators;

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

构造函数

ERC-777合约中的构造函数与JAVA中的构造函数类似,都是用于初始化操作,从下面的ERC-777的构造函数中可以看到这里在初始化过程中初始化了代币的名称、标识符、默认操作员(操作员时可以代表持有者发送和销毁代币的账号地址),然后注册了ERC777Token与ERC20Token的接口:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
* @dev `defaultOperators` may be an empty array.
*/
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));
 }

基本查询

ERC-777为用户提供了基本的代币名称查询、代币标识符查询、代币精度查询、代币粒度查询、代币总量查询、账户可用余额查询:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    /**
     * @dev See {IERC777-name}.
     */
    function name() public view virtual override returns (string memory) {
        return _name;
    }

    /**
     * @dev See {IERC777-symbol}.
     */
    function symbol() public view virtual override returns (string memory) {
        return _symbol;
    }

    /**
     * @dev See {ERC20-decimals}.
     *
     * Always returns 18, as per the
     * [ERC777 EIP](https://eips.ethereum.org/EIPS/eip-777#backward-compatibility).
     */
    function decimals() public pure virtual returns (uint8) {
        return 18;
    }

    /**
     * @dev See {IERC777-granularity}.
     *
     * This implementation always returns `1`.
     */
    function granularity() public view virtual override returns (uint256) {
        return 1;
    }

    /**
     * @dev See {IERC777-totalSupply}.
     */
    function totalSupply() public view virtual override(IERC20, IERC777) returns (uint256) {
        return _totalSupply;
    }

    /**
     * @dev Returns the amount of tokens owned by an account (`tokenHolder`).
     */
    function balanceOf(address tokenHolder) public view virtual override(IERC20, IERC777) returns (uint256) {
        return _balances[tokenHolder];
    }

转账操作

send方式

ERC-777中提供了种转账方式,我们首先来看send转账,具体实现代码如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    /**
     * @dev See {IERC777-send}.
     *
     * Also emits a {IERC20-Transfer} event for ERC20 compatibility.
     */
    function send(address recipient, uint256 amount, bytes memory data) public virtual override  {
        _send(_msgSender(), recipient, amount, data, "", true);
    }

这里的send()需要函数调用者传递3个参数:

  • recipient:代币接受地址
  • amount:代币数量
  • data:Token持有者提供的额外信息

之后调用_send函数来进行后续的转账,_send函数的代码如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    /**
     * @dev Send tokens
     * @param from address token holder address
     * @param to address recipient address
     * @param amount uint256 amount of tokens to transfer
     * @param userData bytes extra information provided by the token holder (if any)
     * @param operatorData bytes extra information provided by the operator (if any)
     * @param requireReceptionAck if true, contract recipients are required to implement ERC777TokensRecipient
     */
    function _send(
        address from,
        address to,
        uint256 amount,
        bytes memory userData,
        bytes memory operatorData,
        bool requireReceptionAck
    )
        internal
        virtual
    {
        require(from != address(0), "ERC777: send from the zero address");
        require(to != address(0), "ERC777: send to the zero address");

        address operator = _msgSender();

        _callTokensToSend(operator, from, to, amount, userData, operatorData);

        _move(operator, from, to, amount, userData, operatorData);

        _callTokensReceived(operator, from, to, amount, userData, operatorData, requireReceptionAck);
    }

从上面可以看到这里会首先检查代币接受地址以及代币来源地址是否为空,之后设定当前操作者即msg.sender,之后调用发送钩子函数,在钩子函数中首先会获取发送账户的接口地址,之后检查接口地址是否为空,如果不为空则执行接口地址的tokensToSend方法:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    /**
     * @dev Call from.tokensToSend() if the interface is registered
     * @param operator address operator requesting the transfer
     * @param from address token holder address
     * @param to address recipient address
     * @param amount uint256 amount of tokens to transfer
     * @param userData bytes extra information provided by the token holder (if any)
     * @param operatorData bytes extra information provided by the operator (if any)
     */
    function _callTokensToSend(
        address operator,
        address from,
        address to,
        uint256 amount,
        bytes memory userData,
        bytes memory operatorData
    )
        private
    {
        address implementer = _ERC1820_REGISTRY.getInterfaceImplementer(from, _TOKENS_SENDER_INTERFACE_HASH);
        if (implementer != address(0)) {
            IERC777Sender(implementer).tokensToSend(operator, from, to, amount, userData, operatorData);
        }
    }
    

这里的tokenToSend接口如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

/**
 * @dev Interface of the ERC777TokensSender standard as defined in the EIP.
 *
 * {IERC777} Token holders can be notified of operations performed on their
 * tokens by having a contract implement this interface (contract holders can be
 *  their own implementer) and registering it on the
 * https://eips.ethereum.org/EIPS/eip-1820[ERC1820 global registry].
 *
 * See {IERC1820Registry} and {ERC1820Implementer}.
 */
interface IERC777Sender {
    /**
     * @dev Called by an {IERC777} token contract whenever a registered holder's
     * (`from`) tokens are about to be moved or destroyed. The type of operation
     * is conveyed by `to` being the zero address or not.
     *
     * This call occurs _before_ the token contract's state is updated, so
     * {IERC777-balanceOf}, etc., can be used to query the pre-operation state.
     *
     * This function may revert to prevent the operation from being executed.
     */
    function tokensToSend(
        address operator,
        address from,
        address to,
        uint256 amount,
        bytes calldata userData,
        bytes calldata operatorData
    ) external;
}

之后调用_move函数进行转账操作,在这里会首先检查转账操作发起账号是否有足够的token用于转账操作,之后进行更新操作,然后通过emit触发事件:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    function _move(
        address operator,
        address from,
        address to,
        uint256 amount,
        bytes memory userData,
        bytes memory operatorData
    )
        private
    {
        _beforeTokenTransfer(operator, from, to, amount);

        uint256 fromBalance = _balances[from];
        require(fromBalance >= amount, "ERC777: transfer amount exceeds balance");
        _balances[from] = fromBalance - amount;
        _balances[to] += amount;

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

最后调用_callTokensReceived接受钩子函数:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    /**
     * @dev Call to.tokensReceived() if the interface is registered. Reverts if the recipient is a contract but
     * tokensReceived() was not registered for the recipient
     * @param operator address operator requesting the transfer
     * @param from address token holder address
     * @param to address recipient address
     * @param amount uint256 amount of tokens to transfer
     * @param userData bytes extra information provided by the token holder (if any)
     * @param operatorData bytes extra information provided by the operator (if any)
     * @param requireReceptionAck if true, contract recipients are required to implement ERC777TokensRecipient
     */
    function _callTokensReceived(
        address operator,
        address from,
        address to,
        uint256 amount,
        bytes memory userData,
        bytes memory operatorData,
        bool requireReceptionAck
    )
        private
    {
        address implementer = _ERC1820_REGISTRY.getInterfaceImplementer(to, _TOKENS_RECIPIENT_INTERFACE_HASH);
        if (implementer != address(0)) {
            IERC777Recipient(implementer).tokensReceived(operator, from, to, amount, userData, operatorData);
        } else if (requireReceptionAck) {
            require(!to.isContract(), "ERC777: token recipient contract has no implementer for ERC777TokensRecipient");
        }
    }

在这里会首先获取接收账户的接口地址,之后检查接口地址是否为空,如果不为空则调用执行接口地址的tokensReceived方法:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

/**
 * @dev Interface of the ERC777TokensRecipient standard as defined in the EIP.
 *
 * Accounts can be notified of {IERC777} tokens being sent to them by having a
 * contract implement this interface (contract holders can be their own
 * implementer) and registering it on the
 * https://eips.ethereum.org/EIPS/eip-1820[ERC1820 global registry].
 *
 * See {IERC1820Registry} and {ERC1820Implementer}.
 */
interface IERC777Recipient {
    /**
     * @dev Called by an {IERC777} token contract whenever tokens are being
     * moved or created into a registered account (`to`). The type of operation
     * is conveyed by `from` being the zero address or not.
     *
     * This call occurs _after_ the token contract's state is updated, so
     * {IERC777-balanceOf}, etc., can be used to query the post-operation state.
     *
     * This function may revert to prevent the operation from being executed.
     */
    function tokensReceived(
        address operator,
        address from,
        address to,
        uint256 amount,
        bytes calldata userData,
        bytes calldata operatorData
    ) external;
}

如果接受地址为空(代币销毁),则检查"requireReceptionAck"是否为true,如果为true则要求接受合约必须实现ERC777TokensRecipient的接口!

transfer方式

当然,还有另外一种转账方式,也就是通过我们常见的transfer函数,与send方法类似,在执行发送方法时会调用两次钩子方法,一次是调用发送钩子,一次是调用接收钩子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    /**
     * @dev See {IERC20-transfer}.
     *
     * Unlike `send`, `recipient` is _not_ required to implement the {IERC777Recipient}
     * interface if it is a contract.
     *
     * Also emits a {Sent} event.
     */
    function transfer(address recipient, uint256 amount) public virtual override returns (bool) {
        require(recipient != address(0), "ERC777: transfer to the zero address");

        address from = _msgSender();

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

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

        _callTokensReceived(from, from, recipient, amount, "", "", false);

        return true;
    }

operatorSend

ERC-777提供的另外一种代币转账操作方法是operatorSend,具体实现代码如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    /**
     * @dev See {IERC777-operatorSend}.
     *
     * Emits {Sent} and {IERC20-Transfer} events.
     */
    function operatorSend(
        address sender,
        address recipient,
        uint256 amount,
        bytes memory data,
        bytes memory operatorData
    )
        public
        virtual
        override
    {
        require(isOperatorFor(_msgSender(), sender), "ERC777: caller is not an operator for holder");
        _send(sender, recipient, amount, data, operatorData, true);
    }

从上述代码中可以看到这里会首先调用isOperatorFor来校验当前操作者是否有操作sender资产的权限,之后再调用_send函数来进行转账,其余的和send函数类似,不再赘述~

代币销毁

ERC-777中关于代币销毁提供了2种方式:以下实现代码示例:

burn方式

通过burn方式来销毁代币的代码如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    /**
     * @dev See {IERC777-burn}.
     *
     * Also emits a {IERC20-Transfer} event for ERC20 compatibility.
     */
    function burn(uint256 amount, bytes memory data) public virtual override  {
        _burn(_msgSender(), amount, data, "");
    }

函数调用者需要传递2个参数,一个是销毁的代币数量,另一个是Token持有者提供的额外信息,之后调用_burn函数进行销毁:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    /**
     * @dev Burn tokens
     * @param from address token holder address
     * @param amount uint256 amount of tokens to burn
     * @param data bytes extra information provided by the token holder
     * @param operatorData bytes extra information provided by the operator (if any)
     */
    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);

        // Update state variables
        uint256 fromBalance = _balances[from];
        require(fromBalance >= amount, "ERC777: burn amount exceeds balance");
        _balances[from] = fromBalance - amount;
        _totalSupply -= amount;

        emit Burned(operator, from, amount, data, operatorData);
        emit Transfer(from, address(0), amount);
    }

在以上的burn函数中首先会检查from账户地址是否为空,而这里的from账户地址也就是从上面传入进来的函数调用者地址,之后调用一次代币发送钩子,然后检查要销毁的代币数量是否小于等于当前账户的代币总量,之后更新账户代币数量,更新代币总量,之后通过emit触发代币销毁和代币转移事件。

operatorBurn

operatorBurn是ERC-777提供的另一种代币销毁方式,该方式就是在burn的基础之上多了一个对当前账户是否有操作token的权限校验,当然代币销毁都是销毁自身的代币,所以两者在功能实现上基本没什么差别:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    /**
     * @dev See {IERC777-operatorBurn}.
     *
     * Emits {Burned} and {IERC20-Transfer} events.
     */
    function operatorBurn(address account, uint256 amount, bytes memory data, bytes memory operatorData) public virtual override {
        require(isOperatorFor(_msgSender(), account), "ERC777: caller is not an operator for holder");
        _burn(account, amount, data, operatorData);
    }

代币增发

ERC-777提供了以下代币增发逻辑实现代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    /**
     * @dev Creates `amount` tokens and assigns them to `account`, increasing
     * the total supply.
     *
     * If a send hook is registered for `account`, the corresponding function
     * will be called with `operator`, `data` and `operatorData`.
     *
     * See {IERC777Sender} and {IERC777Recipient}.
     *
     * Emits {Minted} and {IERC20-Transfer} events.
     *
     * Requirements
     *
     * - `account` cannot be the zero address.
     * - if `account` is a contract, it must implement the {IERC777Recipient}
     * interface.
     */
    function _mint(
        address account,
        uint256 amount,
        bytes memory userData,
        bytes memory operatorData
    )
        internal
        virtual
    {
        _mint(account, amount, userData, operatorData, true);
    }
        /**
     * @dev Creates `amount` tokens and assigns them to `account`, increasing
     * the total supply.
     *
     * If `requireReceptionAck` is set to true, and if a send hook is
     * registered for `account`, the corresponding function will be called with
     * `operator`, `data` and `operatorData`.
     *
     * See {IERC777Sender} and {IERC777Recipient}.
     *
     * Emits {Minted} and {IERC20-Transfer} events.
     *
     * Requirements
     *
     * - `account` cannot be the zero address.
     * - if `account` is a contract, it must implement the {IERC777Recipient}
     * interface.
     */
    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);

        // Update state variables
        _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);
    }

在以上代码中首先校验代币接受地址是否为空,之后更新代币总量与代币接受地址的代币总量,之后通过emit来触发铸币和代币转移事件。

管理操作

ERC-777中提供了defaultOperators, authorizeOperator, revokeOperator,isOperatorFor来实现对操作员的管理,下面我们逐一分析:

defaultOperators——返回默认的操作员列表信息

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    /**
     * @dev See {IERC777-defaultOperators}.
     */
    function defaultOperators() public view virtual override returns (address[] memory) {
        return _defaultOperatorsArray;
    }

authorizeOperator——将第三方操作员地址设置为msg.sender的操作员,以代表其发送和销毁Token,这里的持有者(msg.sender)永远是自己的操作者,此项权利不得撤销,因此如果该函数被调用来授权持有者(msgSender())作为其自身的操作者(即如果操作者等于msgSener),则该函数必须revert

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    /**
     * @dev See {IERC777-authorizeOperator}.
     */
    function authorizeOperator(address operator) public virtual override  {
        require(_msgSender() != operator, "ERC777: authorizing self as operator");

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

        emit AuthorizedOperator(operator, _msgSender());
    }

revokeOperator——回收操作权限

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    /**
     * @dev See {IERC777-revokeOperator}.
     */
    function revokeOperator(address operator) public virtual override  {
        require(operator != _msgSender(), "ERC777: revoking self as operator");

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

        emit RevokedOperator(operator, _msgSender());
    }

isOperatorFor——对操作者进行鉴权操作,在isOperatorFor中会检查当前操作者是否是token的持有者,或者是默认操作员(操作员时可以代表持有者发送和销毁代币的账号地址)、或者是代币持有者指定的代币授权操作者:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    /**
     * @dev See {IERC777-isOperatorFor}.
     */
    function isOperatorFor(address operator, address tokenHolder) public view virtual override returns (bool) {
        return operator == tokenHolder ||
            (_defaultOperators[operator] && !_revokedDefaultOperators[tokenHolder][operator]) ||
            _operators[tokenHolder][operator];
    }

授权转账

ERC-777提供了以下授权转账操作:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    /**
     * @dev See {IERC20-approve}.
     *
     * Note that accounts cannot have allowance issued by their operators.
     */
    function approve(address spender, uint256 value) public virtual override returns (bool) {
        address holder = _msgSender();
        _approve(holder, spender, value);
        return true;
    }
    /**
     * @dev See {ERC20-_approve}.
     *
     * Note that accounts cannot have allowance issued by their operators.
     */
    function _approve(address holder, address spender, uint256 value) internal {
        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);
    }

从上述代码中可以看到在这里首先会检查一次授权账户地址和被赋予权限的账户地址是否为空地址,之后直接进行授权,然后通过emit来触发事件~

文末小结

ERC-777总体来看可以说是ERC-20的升级版本,ERC-777中引入了运营商的概念来解决授权转账中的多步骤操作问题,同时采用接口send(dest,value,data)发送代币,先前兼容。同时在转账时为用户提供了可以携带额外信息的功能,也提供了转账通知机制。

参考链接

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

https://docs.openzeppelin.com/contracts/4.x/api/token/erc777

https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts/token/ERC777

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
ERC-777标准规范
在经典的ERC-20场景中,如果用户想要授权给第三方账户或者智能合约进行转账操作,那么需要通过两个事务来完成整个转账的操作,在这里需要注意的是在授权是需要指定对应的amount数量,那么当每次进行授权转账时都需要进行一次查询或者让A用户再次授权给B用户:
Al1ex
2021/07/21
1.2K0
DAPP智能合约质押借贷挖矿理财系统开发案例详情
智能合约已在各种区块链网络中得以实施,其中主要的依然是比特币和以太坊。虽然比特币网络以使用比特币执行交易闻名,它的协议也可以用来创建智能合约。
开发v_hkkf5566
2023/03/06
4850
ERC777 功能型代币(通证)最佳实践
想必很多同学都已经使用过ERC20 创建过代币[1],或许已经被老板要求在ERC20代币上实现一些附加功能搞的焦头烂额,如果还有选择,一定要选择 ERC777 。
Tiny熊
2019/09/30
1.3K0
ERC-1155标准规范
本篇文章将对ERC-1155标准规范进行简单介绍,在介绍之前我们先来看一下之前的ERC-20、ERC-721、ERC-777都解决了什么问题,主要应用与那些场景:
Al1ex
2021/07/16
5.1K2
ERC-1155标准规范
【ERC1400标准】支持证券增发,交易,相关法律文件存储的证券类同质化通证1,关于ERC14002, Security Token Standard
"ERC1400"是新提案的证券型代币的标准,新标准主要是把 Token 的互换性(fungible)结合证券相关的业务场景,设计了一套通用接口。
辉哥
2018/10/10
1K0
ERC-721标准规范
ERC-721的官方解释是"Non-Fungible Tokens",英文简写为"NFT",可以翻译为不可互换的Tokens,简单地说就是每个Token都是独一无二的且不能互换的。
Al1ex
2021/03/23
4.2K1
ERC-721标准规范
ERC-20标准规范
ERC-20为以太坊智能合约提供了一套编写规范,而IERC-20则规定了一个Token需要实现的基本接口,本篇文章将对此进行解读。
Al1ex
2021/03/23
2.5K0
ERC-20标准规范
智能合约中approve函数详解
我问他为什么不只用Token的标准approve 函数外部提前调用一次、然后直接执行第二步就行了、他们都有各自的理由、咱也不好细问。
终有链响
2024/08/06
2670
智能合约中approve函数详解
ERC-777以太坊新代币标准解读
ERC777是一个新的高级代币标准,可以视为ERC20的升级版本,因此它解决了ERC20以及ERC223存在的一些问题,开发者可以根据自己的具体需求进行选型。
用户1408045
2019/10/23
1.2K0
ERC-777以太坊新代币标准解读
paradigm ctf 2022 - Hint finance
这是一个主网的 fork,三个 underlying token 分别是:PNT,SAND,AMP;其中,PNT 和 AMP 都是 777 token,在 transfer 之前和之后都有 callback 回掉。而 SAND token 是一个 ERC20 Token。可以访问这个链接[2]查看具体的 777 标准。简单概括,该标准要求 ERC777 token 的发送方和接受方要到 EIP-1820 这个地址上进行注册,注册时,需要调用setInterfaceImplementer方法,传入需要注册的 key 和 address;
Tiny熊
2022/11/07
1.5K0
paradigm ctf 2022 - Hint finance
第三十一课 ERC1410标准从分析到代码实现1,摘要2, ERC1410和ERC1411(ERC1400),ERC1404的区别3,同质化通证(FT),非同质化通证(NFT),部分同质化通证(PFT
ERC1410为STO环境中使用的一个以太坊协议标准。辉哥着眼于深度理解和编码实现,从以下几个方面阐述对ERC1410的理解。 1) ERC1410和ERC1411(ERC1400),ERC1404的区别 2)同质化通证,非同质化通证,部分同质化通证的区别 3)ERC1410标准的数据结构分析 4) ERC1410的接口函数分析 5) ERC1410的场景尝试 6) 代码部署和测试
辉哥
2018/12/19
1.2K0
ERC777的特点
ERC777[2] 与 ERC20 都是一类的合约,都是fungible tokens的一种标准。并且 ERC777 是对 ERC20 兼容的,ERC20 中的相关操作在 ERC777 中都能够实现,并且 ERC777 还提供了更加复杂的操作,还在 ERC20 的不足的地方进行了改善提升。可以说ERC777 是在 ERC20 的基础上进行的升级改造,但是由于 ERC777 出现的时间较晚,现在市场上主流的货币还是使用的 ERC20,但是这并不能否定 ERC777 相比于 ERC20 更高效与更安全。详情可以查看https://docs.openzeppelin.com/contracts/4.x/erc777.
Tiny熊
2023/01/09
5600
隐秘的交易:暗藏危机的智能合约恶意调用
在这篇文章中,我们将对曾经出现过的一种叫做evilReflex的安全漏洞进行分析研究,攻击者可以通过该漏洞将存在evilReflex漏洞的合约中的任意数量的token转移到任意地址。
Al1ex
2021/07/21
1K0
隐秘的交易:暗藏危机的智能合约恶意调用
以太坊ERC20协议以及发行自己代币
ERC-20 标准是在2015年11月份推出的,使用这种规则的代币,表现出一种通用的和可预测的方式。
若与
2018/11/23
2.5K0
第七课 技术小白如何在45分钟内发行通证(TOKEN)并上线交易
通过逐步的指导和截图举证,一步步带领一个技术小白完成一个数字货币(通证,代币,TOKEN)的发布演示和上线交易。
辉哥
2018/08/10
1.3K0
第七课 技术小白如何在45分钟内发行通证(TOKEN)并上线交易
技术分析 Lendf.me 被攻击,ERC777到底该不该用?
我在去年 9 月写过一篇ERC科普文章:ERC777 功能型代币(通证)最佳实践[1] ,文章里我推荐新开发的代币使用 ERC777 标准。
Tiny熊
2020/04/21
9410
技术分析  Lendf.me 被攻击,ERC777到底该不该用?
智能合约审计之evilReflex攻击
在这篇文章中,我们对曾经出现过的一种叫做evilReflex的安全漏洞进行分析研究,攻击者可以通过该漏洞将存在evilReflex漏洞的合约中的任意数量的token转移到任意地址。
Al1ex
2021/07/16
5140
智能合约审计之evilReflex攻击
BSC智能链挖矿dapp系统开发智能合约技术指南
币安智能链(Binance Smart Chain,简称 BSC )是一条以太坊虚拟机兼容,与币安链并行的区块链,是加密资产行业顶尖项目的测试和前沿探索。
开发v_hkkf5566
2022/10/25
1.4K0
快速学习-ERC20 代币合约
ERC20 代币合约 pragma solidity ^ 0.4 .16; interface tokenRecipient { function receiveApproval(address _from, uint256 _value, address _token, bytes _extraData) external; } contract TokenERC20 { // Public variables of the token string public name; string pub
cwl_java
2020/04/16
6920
Xn00d被攻击事件分析
ERC777 是 ERC20 标准的高级代币标准,要提供了一些新的功能:运营商及钩子。
Tiny熊
2023/01/09
8620
Xn00d被攻击事件分析
相关推荐
ERC-777标准规范
更多 >
LV.2
安全研究员
目录
  • 文章前言
  • 优势对比
  • 接口列表
  • 源码分析
    • 合约继承
    • 全局变量
    • 构造函数
    • 基本查询
    • 转账操作
      • send方式
      • transfer方式
      • operatorSend
    • 代币销毁
      • burn方式
      • operatorBurn
    • 代币增发
    • 管理操作
    • 授权转账
  • 文末小结
  • 参考链接
加入讨论
的问答专区 >
职务职务职务职务职务职务擅长3个领域
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档