Skip to content

Instantly share code, notes, and snippets.

@patrickodacre
Last active April 28, 2021 20:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save patrickodacre/04aed815eaa12fabc467d9b8aeb622a8 to your computer and use it in GitHub Desktop.
Save patrickodacre/04aed815eaa12fabc467d9b8aeb622a8 to your computer and use it in GitHub Desktop.
Error in user YAML: (<unknown>): could not find expected ':' while scanning a simple key at line 7 column 1
---
eip: <to be assigned>
title: ERC20Receiver Interface
author: Witek Radomski <witek@enjin.io>, Patrick O'Dacre <patrick@patrickwho.me>
discussions-to: <URL>
status: Draft
type: Standards Track
category Interface or ERC ??
created: 2021-04-28
requires: ERC165, ERC20
---

Simple Summary

This EIP defines a minimal, contract-friendly interface for ERC20 Token Contracts and Contract ERC20 Token Receivers.

Abstract

The following standard allows for ERC20 Token contracts to safely transfer tokens to contract accounts with a single call. This specification simply seeks to standardize a pattern for ERC20 Tokens that is already established with ERC721 and ERC1155 contracts.

Motivation

Sending tokens to a contract (e.g.: contract wallet) can result in tokens being "stuck" in the receiving contract if that contract isn't itself set up to transfer tokens it holds.

The typical solution is to have the receiving contract request the funds with a call to transferFrom. This approach requires that the token owner first approve() the receiving contract. This 2-step process is cumbersome for both the sender and the receiver.

Contract developers that implement the ERC20Receiver interface will make it easy for their target users to send tokens to their contract.

Token developers that implement the corresponding ERC20SafeContractTransfer interface will give their users more confidence when transferring their tokens to contract recipients.

This interface is well-established with ERC721 and ERC1155 standards, and will be familiar to most experienced developers. Even newer developers should fine the interfaces simple and straightforward to implement.

Specification

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.

ERC20 Tokens

ERC20 Smart contracts implementing this standard MUST implement all functions in this interface.

Note: the calls to super.transfer(recipient, amount); or super.transferFrom(sender, recipient, amount); MUST be calls to the ERC20-compliant functions.

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/Address.sol";

interface ERC20SafeContractTransfer is IERC20 {
    
    /// @notice safely transfers tokens to externally-owned accounts or contracts
    /// MUST call ERC20 compliant version of `transfer`
    /// MUST revert if call to `transfer` does not return true
    /// MUST call _doSafeTransferAcceptanceCheck
    /// MUST revert if _doSafeTransferAcceptanceCheck fails
    /// MUST send empty '' as _doSafeTransferAcceptanceCheck `data` arg
    /// MUST revert on any other error
    /// @param recipient recipient address
    /// @param amount number of tokens to transfer
    /// @return true if successful
    function safeTransfer(address recipient, uint256 amount) public returns (bool) {}

    /// @notice safely transfer tokens to externally-owned accounts or contracts
    /// @dev for transfers that include arbitrary data for the recipient
    /// MUST call ERC20 compliant version of `transfer`
    /// MUST revert if call to `transfer` does not return true
    /// MUST call _doSafeTransferAcceptanceCheck
    /// MUST revert if _doSafeTransferAcceptanceCheck fails
    /// MUST revert on any other error
    /// @param recipient recipient address
    /// @param amount number of tokens to transfer
    /// @param data arbitrary data for the recipient
    /// @return true if successful
    function safeTransfer(address recipient, uint256 amount, bytes memory data) public returns (bool) {}

    /// @notice safely transfer tokens from one account to another externally-owned account or contract
    /// @dev Caller must be approved to manage the tokens being transferred out of the `_from` account
    /// MUST call ERC20 compliant version of `transferFrom`
    /// MUST revert if call to `transfer` does not return true
    /// MUST call _doSafeTransferAcceptanceCheck
    /// MUST revert if _doSafeTransferAcceptanceCheck fails
    /// MUST revert on any other error
    /// MUST send empty '' as _doSafeTransferAcceptanceCheck `data` arg
    /// @param recipient recipient address
    /// @param amount number of tokens to transfer
    /// @return true if successful
    function safeTransferFrom(address sender, address recipient, uint256 amount) public returns (bool) {}

    /// @notice safely transfer tokens from one account to another externally-owned account or contract
    /// @dev Caller must be approved to manage the tokens being transferred out of the `_from` account
    /// @dev for transfers that include arbitrary data for the recipient
    /// MUST call ERC20 compliant version of `transferFrom`
    /// MUST revert if call to `transfer` does not return true
    /// MUST call _doSafeTransferAcceptanceCheck
    /// MUST revert if _doSafeTransferAcceptanceCheck fails
    /// MUST revert on any other error
    /// @param recipient recipient address
    /// @param amount number of tokens to transfer
    /// @param data arbitrary data for the recipient
    /// @return true if successful
    function safeTransferFrom(address sender, address recipient, uint256 amount, bytes memory data) public returns (bool) {}

    /// @notice check that recipient contract account implements onERC20Received
    /// MUST revert if receiver has not implemented onERC20Received function
    /// MUST revert if function does not return onERC20Received selectedId
    /// MUST revert for any other error
    /// MUST NOT revert if `to` is not a contract
    /// @param operator the msg.sender
    /// @param from transfer from account
    /// @param to transfer to account
    /// @param amount number of tokens to transfer
    /// @param data arbitrary data for the recipient
    function _doSafeTransferAcceptanceCheck(address operator, address from, address to, uint256 amount, bytes memory data) private {}
        
}

Example implementation:

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/utils/Address.sol";

contract NewToken is ERC20 {

    using Address for address;
    
    /// @notice safely transfer tokens to externally-owned accounts or contracts
    /// @param recipient recipient address
    /// @param amount number of tokens to transfer
    /// @return true if successful
    function safeTransfer(address recipient, uint256 amount)
        public
        returns (bool)
    {
        super.transfer(recipient, amount);

        address operator = msg.sender;

        _doSafeTransferAcceptanceCheck(
            operator,
            operator,
            recipient,
            amount,
            ""
        );

        return true;
    }

    /// @notice safely transfer tokens to externally-owned accounts or contracts
    /// @dev for transfers that include arbitrary data for the recipient
    /// @param recipient recipient address
    /// @param amount number of tokens to transfer
    /// @param data arbitrary data for the recipient
    /// @return true if successful
    function safeTransfer(
        address recipient,
        uint256 amount,
        bytes memory data
    ) public returns (bool) {
        super.transfer(recipient, amount);

        address operator = msg.sender;

        _doSafeTransferAcceptanceCheck(
            operator,
            operator,
            recipient,
            amount,
            data
        );

        return true;
    }

    /// @notice safely transfer tokens from one account to another externally-owned account or contract
    /// @param recipient recipient address
    /// @param amount number of tokens to transfer
    /// @return true if successful
    function safeTransferFrom(
        address sender,
        address recipient,
        uint256 amount
    ) public returns (bool) {
        super.transferFrom(sender, recipient, amount);

        address operator = msg.sender;

        _doSafeTransferAcceptanceCheck(operator, sender, recipient, amount, "");

        return true;
    }

    /// @notice safely transfer tokens from one account to another externally-owned account or contract
    /// @dev for transfers that include arbitrary data for the recipient
    /// @param recipient recipient address
    /// @param amount number of tokens to transfer
    /// @param data arbitrary data for the recipient
    /// @return true if successful
    function safeTransferFrom(
        address sender,
        address recipient,
        uint256 amount,
        bytes memory data
    ) public returns (bool) {
        super.transferFrom(sender, recipient, amount);

        address operator = msg.sender;

        _doSafeTransferAcceptanceCheck(
            operator,
            sender,
            recipient,
            amount,
            data
        );

        return true;
    }

    /// @notice check that recipient contract account implements onERC20Received
    /// @param operator the msg.sender
    /// @param from transfer from account
    /// @param to transfer to account
    /// @param amount number of tokens to transfer
    /// @param data arbitrary data for the recipient
    function _doSafeTransferAcceptanceCheck(
        address operator,
        address from,
        address to,
        uint256 amount,
        bytes memory data
    ) private {
        if (to.isContract()) {
            try
                IERC20Receiver(to).onERC20Received(operator, from, amount, data)
            returns (bytes4 response) {
                if (response != IERC20Receiver(to).onERC20Received.selector) {
                    revert("ERC20: ERC20Receiver rejected tokens");
                }
            } catch Error(string memory reason) {
                revert(reason);
            } catch {
                revert("ERC20: transfer to non ERC20Receiver implementer");
            }
        }
    }
}

ERC20 Token Receiver

Smart contracts implementing the IERC20Receiver interface standard MUST implement the ERC165 supportsInterface function and MUST return the constant value true if bytes4(keccak256("onERC20Received(address,address,uint256,bytes)")) / 0x4fc35859 is passed through the interfaceID argument.

NOTE: The Token Receiver SHOULD implement some API for transferring the tokens it receives. As with ERC721 and ERC1155, it is left to contract developers to implement this as best suits their use case. That said, it is recommended that a function invoking a safeTransfer be used. See Open Zeppelin's Safe ERC20.

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/utils/introspection/IERC165.sol";

/** 
    @dev Implement on the receiving contract.
*/
interface IERC20Receiver is IERC165 {
    /**
        @dev Handles the receipt of a single ERC20 token type. This function is
        called at the end of a `safeTransfer` or `safeTransferFrom` after the balance has been updated.
        To accept the transfer, this must return
        `bytes4(keccak256("onERC20Received(address,address,uint256,bytes)"))`
        (i.e. its own function selector).
        /// MUST return 0x4fc35859 (this.onERC20Received.selector) to accept tokens
        /// MAY return false, revert, or a message to reject tokens
        /// MAY ignore all or some parameters        
        @param operator The address which initiated the transfer (i.e. msg.sender)
        @param from The address which previously owned the token
        @param value The amount of tokens being transferred
        @param data Additional data with no specified format
        @return `bytes4(keccak256("onERC20Received(address,address,uint256,bytes)"))` / `0x4fc35859` if transfer is allowed
    */
    function onERC20Received(address operator, address from, uint256 value, bytes calldata data) external returns (bytes4) {};
}

IERC165 for reference:

pragma solidity ^0.8.0;

/**
 * @dev Interface of the ERC165 standard, as defined in the
 * https://eips.ethereum.org/EIPS/eip-165[EIP].
 *
 * Implementers can declare support of contract interfaces, which can then be
 * queried by others ({ERC165Checker}).
 *
 * For an implementation, see {ERC165}.
 */
interface IERC165 {
    /**
     * @dev Returns true if this contract implements the interface defined by
     * `interfaceId`. See the corresponding
     * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
     * to learn more about how these ids are created.
     *
     * This function call must use less than 30 000 gas.
     */
    function supportsInterface(bytes4 interfaceId) external view returns (bool);
}

Example ERC20Receiver Implementation

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "./ERC20Receiver.sol";

/**
    @dev example contract to receive ERC20 tokens
*/
contract ERC20Holder is ERC20Receiver {
    using SafeERC20 for IERC20;
    
    address private _owner;
    
    constructor() {
        _owner = msg.sender;
    }

    /// @notice onERC20Received may be called by ERC20 Token Contract `safeTransfer` to ensure this contract can receive tokens
    /// @param address IGNORED/NOT NEEDED the operator / msg.sender
    /// @param address IGNORED/NOT NEEDED the owner of the tokens
    /// @param uint256 IGNORED/NOT NEEDED the amount of tokens to be transferred
    /// @param bytes IGNORED/NOT NEEDED arbitrary data
    /// @return bytes return the function selectorId to accept the token transfer
    function onERC20Received(
        address,
        address,
        uint256,
        bytes memory
    ) public virtual override returns (bytes4) {
        return this.onERC20Received.selector;
    }
    
    /// @notice As an Owner, transfer any tokens belonging to this contract
    /// @param _token address of the token to transfer
    /// @param _to address of the receiver
    /// @param _amount amount of tokens to transfer
    /// @return true if successful
    function withdrawTokens(
        address _token,
        address _to,
        uint256 _amount
    ) external returns (bool) {
        require(_owner == msg.sender, "Only Owner");

        IERC20(_token).safeTransfer(_to, _amount);

        return true;
    }
}
pragma solidity ^0.8.0;

import "./IERC20Receiver.sol";
import "@openzeppelin/contracts/utils/introspection/ERC165.sol";

/**
    @dev Inherit this abstract contract in your receiver contract e.g.: wallet.
*/
abstract contract ERC20Receiver is ERC165, IERC20Receiver {
    /**
     * @dev See {IERC165-supportsInterface}.
     */
    function supportsInterface(bytes4 interfaceId)
        public
        view
        virtual
        override(ERC165, IERC165)
        returns (bool)
    {
        return
            interfaceId == type(IERC20Receiver).interfaceId ||
            super.supportsInterface(interfaceId);
    }
}

Safe Transfer Rules

This standard seeks to only extend ERC20, so contracts implementing the ERC20SafeContractTransfer interface MUST adhere to all ERC20 standards.

Scenarios

Scenario #1 : The recipient is not a contract.

If the recipient is not a contract the transfer will complete as any other ERC20 transfer would. The safety check in this standard will only execute if the recipient is a contract.

Scenario #2 : The transaction is not a mint/transfer of a token.

onERC20Received MUST NOT be called outside of a mint or transfer process.

Scenario #3 : The receiver does not implement the necessary ERC20TokenReceiver interface function(s).

The transfer MUST be reverted.

Scenario #4 : The receiver implements the necessary ERC20TokenReceiver interface function(s) but returns any value other than the proper function selectorId.

The transfer MUST be reverted.

Rationale

These interfaces are nearly an exact port of interfaces already used in ERC721 and ERC1155 standards. Familiar, battle-tested patterns are more likely to be adopted, and adopting these interfaces will simplify applications, and protect users from the common error of transferring tokens to a contract where they will be lost forever.

It was also a conscious decision to not collide with the ERC20 interface; this standard is fully compatible with the ERC20 standard, and should be painless to implement.

Related EIPs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment