Alert Source Discuss
Standards Track: ERC

ERC-223: 具有事务处理模型的 Token

设计为与原生货币(以太币)行为相同的具有事务处理模型的 Token

Authors Dexaran (@Dexaran) <dexaran@ethereumclassic.org>
Created 2017-05-03

摘要

以下描述了可替代 Token 的接口和逻辑,该接口和逻辑支持 tokenReceived 回调,以在收到 Token 时通知合约接收者。这使得 Token 的行为与以太币相同。

动机

此 Token 引入了一种可用于理顺与此类 Token 交互的合约行为的合约通信模型。具体来说,此提案:

  1. 通知接收合约传入的 Token 转移,而不是 ERC-20,在 ERC-20 中,Token 转移的接收者不会收到通知。
  2. 在将 Token 存入合约时,更节省 gas。
  3. 允许为金融转移记录 _data

规范

打算接收这些 Token 的合约必须实现 tokenReceived

将 Token 转移到未按如下所述实现 tokenReceived 的合约必须回滚。

Token 合约

Token 方法

totalSupply
function totalSupply() view returns (uint256)

返回 Token 的总供应量。此方法的功能与 ERC-20 的功能相同。

name
function name() view returns (string memory)

返回 Token 的名称。此方法的功能与 ERC-20 的功能相同。

可选 - 此方法可用于提高可用性,但接口和其他合约不得期望存在这些值。

symbol
function symbol() view returns (string memory)

返回 Token 的符号。此方法的功能与 ERC-20 的功能相同。

可选 - 此方法可用于提高可用性,但接口和其他合约不得期望存在这些值。

decimals
function decimals() view returns (uint8)

返回 Token 的小数位数。此方法的功能与 ERC-20 的功能相同。

可选 - 此方法可用于提高可用性,但接口和其他合约不得期望存在这些值。

balanceOf
function balanceOf(address _owner) view returns (uint256)

返回具有地址 _owner 的另一个帐户的帐户余额。此方法的功能与 ERC-20 的功能相同。

transfer(address, uint)
function transfer(address _to, uint _value) returns (bool)

此函数必须转移 Token,如果 _to 是合约,则必须调用 _totokenReceived(address, uint256, bytes calldata) 函数。如果 tokenReceived 函数未在 _to(接收合约)中实现,则事务必须失败,并且必须回滚 Token 的转移。 如果 _to 是外部拥有的地址,则必须发送事务而不执行 _to 中的 tokenReceived_data 可以附加到此 Token 事务,但需要花费更多的 gas。_data 可以为空。

_totokenReceived 函数必须在所有其他操作之后调用,以避免重入攻击。

注意:如果 transfer 函数是 payable 的,并且存入了以太币,则存入的以太币金额必须与 Token 一起交付到 _to 地址。如果以这种方式与 Token 一起发送了以太币,则必须首先交付以太币,然后必须更新 Token 余额,然后如果 _to 是合约,则必须在 _to 中调用 tokenReceived 函数。

transfer(address, uint, bytes)
function transfer(address _to, uint _value, bytes calldata _data) returns (bool)

此函数必须转移 Token 并在 _to 中调用函数 tokenReceived (address, uint256, bytes),如果 _to 是合约。如果 tokenReceived 函数未在 _to(接收合约)中实现,则事务必须失败,并且不得进行 Token 的转移。 如果 _to 是外部拥有的地址(由代码大小为零确定),则必须发送事务而不执行 _to 中的 tokenReceived_data 可以附加到此 Token 事务,但需要花费更多的 gas。_data 可以为空。

注意:检查 _to 是合约还是地址的一种可能方法是组装 _to 的代码。如果 _to 中没有代码,则这是一个外部拥有的地址,否则它是一个合约。如果 transfer 函数是 payable 的,并且存入了以太币,则存入的以太币金额必须与 Token 一起交付到 _to 地址。

_totokenReceived 函数必须在所有其他操作之后调用,以避免重入攻击。

事件

Transfer
event Transfer(address indexed _from, address indexed _to, uint256 _value, bytes _data)

在转移 Token 时触发。与 ERC-20 Transfer 事件兼容且相似。

ERC-223 Token 接收器

接收器方法

function tokenReceived(address _from, uint _value, bytes calldata _data) returns (bytes4)

用于处理 Token 转移的函数,当 Token 持有者发送 Token 时,会从 Token 合约中调用该函数。_from 是 Token 发送者的地址,_value 是传入 Token 的数量,_data 是附加数据,类似于以太币事务的 msg.data。它的工作原理类似于以太币事务的回退函数,并且不返回任何内容。

注意:msg.sender 将是 tokenReceived 函数内的 Token 合约。过滤发送了哪些 Token(按 Token 合约地址)可能很重要。Token 发送者(发起 Token 事务的人)将是 tokenReceived 函数内的 _fromtokenReceived 函数在处理传入的 Token 转移后必须返回 0x8943ec02tokenReceived 函数调用可以由接收联系人的回退函数处理(在这种情况下,它可能不会返回魔术值 0x8943ec02)。

重要提示:此函数必须命名为 tokenReceived 并且采用参数 addressuint256bytes 以匹配函数签名 0x8943ec02。此函数可以由 EOA 手动调用。

原理

该标准通过强制 transfer 在目标地址中执行处理函数来引入通信模型。这是一个重要的安全考虑因素,因为需要接收者显式地实现 Token 处理函数。如果接收者未实现此类函数,则必须回滚转移。

该标准坚持推送事务模型,其中资产的转移在发送者端发起并在接收者端处理。因此,在处理合约存款时,ERC-223 转移更节省 gas,因为 ERC-223 Token 只需一个事务即可存入,而 ERC-20 Token 至少需要两次调用(一次用于 approve,第二次将调用 transferFrom)。

  • ERC-20 存款:approve ~46 gas,transferFrom ~75K gas

  • ERC-223 存款:transfer 和接收者端处理 ~54K gas

该标准引入了通过允许在接收者端处理 ANY 事务并拒绝不正确或不适当的转移来纠正用户错误的能力。此 Token 对与合约和外部拥有的地址的两种类型的交互使用一种转移方法,这可以简化用户体验并避免可能的用户错误。

常用的 ERC-20 标准的一个缺点是 ERC-223 旨在解决的问题是 ERC-20 实现了两种 Token 转移方法:(1)transfer 函数和(2)approve + transferFrom 模式。ERC-20 标准的 Transfer 函数不通知接收者,因此,如果任何 Token 使用 transfer 函数发送到合约,则接收者将不会识别此转移,并且 Token 可能会卡在接收者的地址中而没有任何恢复的可能性。ERC-20 标准将确定转移方法的责任放在用户身上,如果选择了不正确的方法,用户可能会丢失转移的 Token。ERC-223 自动确定转移方法,防止用户因选择错误的方法而丢失 Token。

ERC-223 旨在简化与旨在与 Token 一起使用的合约的交互。ERC-223 采用类似于普通以太币的“存款”模式。向合约存入 ERC-223 只是简单地调用 transfer 函数。这是一个事务,而不是 approve + transferFrom 存款的两步过程。

该标准允许使用 bytes calldata _data 参数将有效负载附加到事务,该参数可以编码目标地址中的第二个函数调用,类似于 msg.data 在以太币事务中的作用,或者允许在链上进行公共日志记录,如果金融事务需要这样做。

向后兼容性

此 Token 的接口与 ERC-20 的接口类似,并且大多数函数的用途与其在 ERC-20 中的对应函数相同。 transfer(address, uint256, bytes calldata) 函数与 ERC-20 接口不向后兼容。

ERC-20 Token 可以使用 transfer 函数交付到非合约地址。ERC-20 Token 可以使用 approve + transferFrom 模式存入合约地址。使用 transfer 函数将 ERC-20 Token 存入合约地址将始终导致接收合约无法识别 Token 存款。

以下是处理 ERC-20 Token 存款的合约代码示例。以下合约可以接受 tokenA 存款。无法阻止非 tokenA 存款到此合约。如果使用 transfer 函数存入 tokenA,则会导致存款人丢失 Token,因为用户的余额将在 tokenA 的合约中减少,但 ERC20Receiver 中的 deposits 变量的值不会增加,即存款不会被记入。截至 2023 年 5 月 9 日,在 Ethereum 主网中,价值 2.01 亿美元的 50 种经过检查的 ERC-20 Token 已经以这种方式丢失

contract ERC20Receiver
{
    address tokenA;
    mapping (address => uint256) deposits;
    function deposit(uint _value, address _token) public
    {
        require(_token == tokenA);
        IERC20(_token).transferFrom(msg.sender, address(this), _value);
        deposits[msg.sender] += _value;
    }
}

必须以相同的方式使用 transfer 函数将 ERC-223 Token 交付到非合约地址或合约地址。

以下是处理 ERC-223 Token 存款的合约代码示例。以下合约可以过滤 Token 并且仅接受 tokenA。其他 ERC-223 Token 将被拒绝。

contract ERC223Receiver
{
    address tokenA;
    mapping (address => uint256) deposits;
    function tokenReceived(address _from, uint _value, bytes memory _data) public returns (bytes4)
    {
        require(msg.sender == tokenA);
        deposits[_from] += _value;
        return 0x8943ec02;
    }
}

安全注意事项

此 Token 使用类似于普通以太币行为的模型。因此,必须考虑重放问题。

参考实现

pragma solidity ^0.8.19;

library Address {
    /**
     * @dev Returns true if `account` is a contract.
     *
     * This test is non-exhaustive, and there may be false-negatives: during the
     * execution of a contract's constructor, its address will be reported as
     * not containing a contract.
     *
     * > It is unsafe to assume that an address for which this function returns
     * false is an externally-owned account (EOA) and not a contract.
     */
    function isContract(address account) internal view returns (bool) {
        // This method relies in extcodesize, which returns 0 for contracts in
        // construction, since the code is only stored at the end of the
        // constructor execution.

        uint256 size;
        // solhint-disable-next-line no-inline-assembly
        assembly { size := extcodesize(account) }
        return size > 0;
    }
}

abstract contract IERC223Recipient {
/**
 * @dev Standard ERC-223 receiving function that will handle incoming token transfers.
 *
 * @param _from  Token sender address.
 * @param _value Amount of tokens.
 * @param _data  Transaction metadata.
 */
    function tokenReceived(address _from, uint _value, bytes memory _data) public virtual returns (bytes4);
}

/**
 * @title Reference implementation of the ERC223 standard token.
 */
contract ERC223Token {

     /**
     * @dev Event that is fired on successful transfer.
     */
    event Transfer(address indexed from, address indexed to, uint value, bytes data);

    string  private _name;
    string  private _symbol;
    uint8   private _decimals;
    uint256 private _totalSupply;
    
    mapping(address => uint256) private balances; // List of user balances.

    /**
     * @dev Sets the values for {name} and {symbol}, initializes {decimals} with
     * a default value of 18.
     *
     * To select a different value for {decimals}, use {_setupDecimals}.
     *
     * All three of these values are immutable: they can only be set once during
     * construction.
     */
     
    constructor(string memory new_name, string memory new_symbol, uint8 new_decimals)
    {
        _name     = new_name;
        _symbol   = new_symbol;
        _decimals = new_decimals;
    }

    /**
     * @dev Returns the name of the token.
     */
    function name() public view returns (string memory)
    {
        return _name;
    }

    /**
     * @dev Returns the symbol of the token, usually a shorter version of the
     * name.
     */
    function symbol() public view returns (string memory)
    {
        return _symbol;
    }

    /**
     * @dev Returns the number of decimals used to get its user representation.
     * For example, if `decimals` equals `2`, a balance of `505` tokens should
     * be displayed to a user as `5,05` (`505 / 10 ** 2`).
     *
     * Tokens usually opt for a value of 18, imitating the relationship between
     * Ether and Wei. This is the value {ERC223} uses, unless {_setupDecimals} is
     * called.
     *
     * NOTE: This information is only used for _display_ purposes: it in
     * no way affects any of the arithmetic of the contract, including
     * {IERC223-balanceOf} and {IERC223-transfer}.
     */
    function decimals() public view returns (uint8)
    {
        return _decimals;
    }

    /**
     * @dev See {IERC223-totalSupply}.
     */
    function totalSupply() public view returns (uint256)
    {
        return _totalSupply;
    }

    /**
     * @dev See {IERC223-standard}.
     */
    function standard() public view returns (string memory)
    {
        return "223";
    }

    
    /**
     * @dev Returns balance of the `_owner`.
     *
     * @param _owner   The address whose balance will be returned.
     * @return balance Balance of the `_owner`.
     */
    function balanceOf(address _owner) public view returns (uint256)
    {
        return balances[_owner];
    }
    
    /**
     * @dev Transfer the specified amount of tokens to the specified address.
     *      Invokes the `tokenFallback` function if the recipient is a contract.
     *      The token transfer fails if the recipient is a contract
     *      but does not implement the `tokenFallback` function
     *      or the fallback function to receive funds.
     *
     * @param _to    Receiver address.
     * @param _value Amount of tokens that will be transferred.
     * @param _data  Transaction metadata.
     */
    function transfer(address _to, uint _value, bytes calldata _data) public returns (bool success)
    {
        // Standard function transfer similar to ERC20 transfer with no _data .
        // Added due to backwards compatibility reasons .
        balances[msg.sender] = balances[msg.sender] - _value;
        balances[_to] = balances[_to] + _value;
        if(Address.isContract(_to)) {
            IERC223Recipient(_to).tokenReceived(msg.sender, _value, _data);
        }
        emit Transfer(msg.sender, _to, _value, _data);
        return true;
    }
    
    /**
     * @dev Transfer the specified amount of tokens to the specified address.
     *      This function works the same with the previous one
     *      but doesn't contain `_data` param.
     *      Added due to backwards compatibility reasons.
     *
     * @param _to    Receiver address.
     * @param _value Amount of tokens that will be transferred.
     */
    function transfer(address _to, uint _value) public returns (bool success)
    {
        bytes memory _empty = hex"00000000";
        balances[msg.sender] = balances[msg.sender] - _value;
        balances[_to] = balances[_to] + _value;
        if(Address.isContract(_to)) {
            IERC223Recipient(_to).tokenReceived(msg.sender, _value, _empty);
        }
        emit Transfer(msg.sender, _to, _value, _empty);
        return true;
    }
}

版权

版权和相关权利通过 CC0 放弃。

Citation

Please cite this document as:

Dexaran (@Dexaran) <dexaran@ethereumclassic.org>, "ERC-223: 具有事务处理模型的 Token," Ethereum Improvement Proposals, no. 223, May 2017. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-223.