ERC-223 - Token with transaction handling model

Created 2017-05-03
Status Final
Category ERC
Type Standards Track
Authors

Abstract

The following describes an interface and logic for fungible tokens that supports a tokenReceived callback to notify contract recipients when tokens are received. This makes tokens behave identical to ether.

Motivation

This token introduces a communication model for contracts that can be utilized to straighten the behavior of contracts that interact with such tokens. Specifically, this proposal:

  1. Informs receiving contracts of incoming token transfers, as opposed to ERC-20 where the recipient of a token transfer gets no notification.
  2. Is more gas-efficient when depositing tokens to contracts.
  3. Allows for _data recording for financial transfers.

Specification

Contracts intending to receive these tokens MUST implement tokenReceived.

Token transfers to contracts not implementing tokenReceived as described below MUST revert.

Token contract

Token Methods

totalSupply
function totalSupply() view returns (uint256)

Returns the total supply of the token. The functionality of this method is identical to that of ERC-20.

name
function name() view returns (string memory)

Returns the name of the token. The functionality of this method is identical to that of ERC-20.

OPTIONAL - This method can be used to improve usability, but interfaces and other contracts MUST NOT expect these values to be present.

symbol
function symbol() view returns (string memory)

Returns the symbol of the token. The functionality of this method is identical to that of ERC-20.

OPTIONAL - This method can be used to improve usability, but interfaces and other contracts MUST NOT expect these values to be present.

decimals
function decimals() view returns (uint8)

Returns the number of decimals of the token. The functionality of this method is identical to that of ERC-20.

OPTIONAL - This method can be used to improve usability, but interfaces and other contracts MUST NOT expect these values to be present.

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

Returns the account balance of another account with address _owner. The functionality of this method is identical to that of ERC-20.

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

This function must transfer tokens, and if _to is a contract, it must call the tokenReceived(address, uint256, bytes calldata) function of _to. If the tokenReceived function is not implemented in _to (recipient contract), then the transaction must fail and the transfer of tokens must be reverted. If _to is an externally owned address, then the transaction must be sent without executing tokenReceived in _to. _data can be attached to this token transaction, but it requires more gas. _data can be empty.

The tokenReceived function of _to MUST be called after all other operations to avoid re-entrancy attacks.

NOTE: If transfer function is payable and ether was deposited then the amount of deposited ether MUST be delivered to _to address alongside tokens. If ether was sent alongside tokens in this way then ether MUST be delivered first, then token balances must be updated, then tokenReceived function MUST be called in _to if it is a contract.

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

This function must transfer tokens and invoke the function tokenReceived (address, uint256, bytes) in _to, if _to is a contract. If the tokenReceived function is not implemented in _to (recipient contract), then the transaction must fail and the transfer of tokens must not occur. If _to is an externally owned address (determined by the code size being zero), then the transaction must be sent without executing tokenReceived in _to. _data can be attached to this token transaction, but it requires more gas. _data can be empty.

NOTE: A possible way to check whether the _to is a contract or an address is to assemble the code of _to. If there is no code in _to, then this is an externally owned address, otherwise it's a contract. If transfer function is payable and ether was deposited then the amount of deposited ether MUST be delivered to _to address alongside tokens.

The tokenReceived function of _to MUST be called after all other operations to avoid re-entrancy attacks.

Events

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

Triggered when tokens are transferred. Compatible with and similar to the ERC-20 Transfer event.

ERC-223 Token Receiver

Receiver Methods

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

A function for handling token transfers, which is called from the token contract, when a token holder sends tokens. _from is the address of the sender of the token, _value is the amount of incoming tokens, and _data is attached data similar to msg.data of ether transactions. It works by analogy with the fallback function of Ether transactions and returns nothing.

NOTE: msg.sender will be a token-contract inside the tokenReceived function. It may be important to filter which tokens were sent (by token-contract address). The token sender (the person who initiated the token transaction) will be _from inside the tokenReceived function. The tokenReceived function must return 0x8943ec02 after handling an incoming token transfer. The tokenReceived function call can be handled by the fallback function of the recipient contact (and in this case it may not return the magic value 0x8943ec02).

IMPORTANT: This function must be named tokenReceived and take parameters address, uint256, bytes to match the function signature 0x8943ec02. This function can be manually called by a EOA.

Rationale

This standard introduces a communication model by enforcing the transfer to execute a handler function in the destination address. This is an important security consideration as it is required that the receiver explicitly implements the token handling function. In cases where the receiver does not implements such function the transfer MUST be reverted.

This standard sticks to the push transaction model where the transfer of assets is initiated on the senders side and handled on the receivers side. As the result, ERC-223 transfers are more gas-efficient while dealing with depositing to contracts as ERC-223 tokens can be deposited with just one transaction while ERC-20 tokens require at least two calls (one for approve and the second that will invoke transferFrom).

This standard introduces the ability to correct user errors by allowing to handle ANY transactions on the recipients side and reject incorrect or improper transfers. This tokens utilize ONE transferring method for both types of interactions with contracts and externally owned addresses which can simplify the user experience and allow to avoid possible user mistakes.

One downside of the commonly used ERC-20 standard that ERC-223 is intended to solve is that ERC-20 implements two methods of token transferring: (1) transfer function and (2) approve + transferFrom pattern. Transfer function of ERC-20 standard does not notify the receiver and therefore if any tokens are sent to a contract with the transfer function then the receiver will not recognize this transfer and the tokens can become stuck in the receivers address without any possibility of recovering them. ERC-20 standard places the burden of determining the transferring method on the user and if the incorrect method is chosen the user can lose the transferred tokens. ERC-223 automatically determines the transferring method, preventing the user from losing tokens due to chosing wrong method.

ERC-223 is intended to simplify the interaction with contracts that are intended to work with tokens. ERC-223 utilizes a "deposit" pattern, similar to that of plain Ether. An ERC-223 deposit to a contract is a simple call of the transfer function. This is one transaction as opposed to two step process of approve + transferFrom depositing.

This standard allows payloads to be attached to transactions using the bytes calldata _data parameter, which can encode a second function call in the destination address, similar to how msg.data does in an ether transaction, or allow for public logging on chain should it be necessary for financial transactions.

Backwards Compatibility

The interface of this token is similar to that of ERC-20 and most functions serve the same purpose as their analogues in ERC-20. transfer(address, uint256, bytes calldata) function is not backwards compatible with ERC-20 interface.

ERC-20 tokens can be delivered to a non-contract address with transfer function. ERC-20 tokens can be deposited to a contract address with approve + transferFrom pattern. Depositing ERC-20 tokens to the contract address with transfer function will always result in token deposit not being recognized by the recipient contract.

Here is an example of the contract code that handles ERC-20 token deposit. The following contract can accepts tokenA deposits. It is impossible to prevent deposits of non-tokenA to this contract. If tokenA is deposited with transfer function then it will result in a loss of tokens for the depositor because the balance of the user will be decreased in the contract of tokenA but the value of deposits variable in the ERC20Receiver will not be increased i.e. the deposit will not be credited. As of 5/9/2023 $201M worth of 50 examined ERC-20 tokens are already lost in this way on Ethereum mainnet.

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;
    }
}

ERC-223 tokens must be delivered to non-contract address or contract address in the same way with transfer function.

Here is an example of the contract code that handles ERC-223 token deposit. The following contract can filter tokens and only accepts tokenA. Other ERC-223 tokens would be rejected.

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;
    }
}

Security Considerations

This token utilizes the model similar to plain ether behavior. Therefore replay issues must be taken into account.

Reference Implementation

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;
    }
}

Copyright

Copyright and related rights waived via CC0.