Alert Source Discuss
⚠️ Draft Standards Track: ERC

ERC-7417: Token Converter

Smart-contract service that converts token of one ERC version to another

Authors Dexaran (@Dexaran) <dexaran@ethereumclassic.org>
Created 2023-07-27
Discussion Link https://ethereum-magicians.org/t/token-standard-converter/15252
Requires EIP-20, EIP-165, EIP-223

Abstract

There are multiple token standards on Ethereum chain currently. This EIP introduces a concept of cross-standard interoperability by creating a service that allows ERC-20 tokens to be upgraded to ERC-223 tokens anytime. ERC-223 tokens can be converted back to ERC-20 version without any restrictions to avoid any problems with backwards compatibility and allow different standards to co-exist and become interoperable and interchangeable.

To perform the conversion, the user must send tokens of one standard to the Converter contract and he will automatically receive tokens of another standard.

Motivation

When an ERC-20 contract is upgraded, finding the new address introduces risk. This proposal creates a service that performs token conversion and prevents potentially unsafe implementations from spreading.

Specification

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

The Token Converter system comprises two main parts:

  • Converter contract

  • ERC-223 wrapper contract for each ERC-20 token

Converter contract can deploy new ERC-223 wrapper contracts for any ERC-20 token that does not have a ERC-223 wrapper currently. There MUST be exactly one ERC-223 wrapper for each ERC-20 token.

Converter contract MUST accept deposits of ERC-20 tokens and send ERC-223 tokens to the depositor at 1:1 ratio. Upon depositing 1234 units of ERC-20 token_A the depositor MUST receive exactly 1234 units of ERC-223 token_A. This is done by issuing new ERC-223 tokens at the moment of ERC-20 deposit. The original ERC-20 tokens MUST be frozen in the Converter contract and available for claiming back.

Converter contract MUST accept deposits of ERC-223 tokens and send ERC-20 tokens to the depositor at 1:1 ratio. This is done by releasing the original ERC-20 tokens at the moment of ERC-223 deposit. The deposited ERC-223 tokens must be burned.

Token Converter

Conver contract methods

getWrapperFor
function getWrapperFor(address _erc20Token) public view returns (address)

Returns the address of the ERC-223 wrapper for a given ERC-20 original token. Returns 0x0 if there is no ERC-223 version for the provided ERC-20 token address. There MUST be exactly one wrapper for any given ERC-20 token address created by the Token Converter contract.

getOriginFor
function getOriginFor(address _erc223Token) public view returns (address)

Returns the address of the original ERC-20 token for a given ERC-223 wrapper. Returns 0x0 if the provided _erc223Token is not an address of any ERC-223 wrapper created by the Token Converter contract.

createERC223Wrapper
function createERC223Wrapper(address _erc20Token) public returns (address)

Creates a new ERC-223 wrapper for a given _erc20Token if it does not exist yet. Reverts the transaction if the wrapper already exist. Returns the address of the new wrapper token contract on success.

convertERC20toERC223
function convertERC20toERC223(address _erc20token, uint256 _amount) public returns (bool)

Withdraws _amount of ERC-20 token from the transaction senders balance with transferFrom function. Sends the _amount of ERC-223 wrapper tokens to the sender of the transaction. Stores the original tokens at the balance of the Token Converter contract for future claims. Returns true on success. The Token Converter must keep record of the amount of ERC-20 tokens that were deposited with convertERC20toERC223 function because it is possible to deposit ERC-20 tokens to any contract by directly sending them with transfer function.

If there is no ERC-223 wrapper for the _ERC20token then creates it by calling a createERC223Wrapper(_erc20toke) function.

If the provided _erc20token address is an address of a ERC-223 wrapper reverts the transaction.

tokenReceived
function tokenReceived(address _from, uint _value, bytes memory _data) public override returns (bytes4)

This is a standard ERC-223 transaction handler function and it is called by the ERC-223 token contract when _from is sending _value of ERC-223 tokens to address(this) address. In the scope of this function msg.sender is the address of the ERC-223 token contract and _from is the initiator of the transaction.

If msg.sender is an address of ERC-223 wrapper created by the Token Converter then _value of ERC-20 original token must be sent to the _from address.

If msg.sender is not an address of any ERC-223 wrapper known to the Token Converter then revert the transaction thus returning any ERC-223 tokens back to the sender.

This is the function that MUST be used to convert ERC-223 wrapper tokens back to original ERC-20 tokens. This function is automatically executed when ERC-223 tokens are sent to the address of the Token Converter. If any arbitrary ERC-223 token is sent to the Token Converter it will be rejected.

Returns 0x8943ec02.

rescueERC20
function rescueERC20(address _token) external

This function allows to extract the ERC-20 tokens that were directly deposited to the contract with transfer function to prevent users who may send tokens by mistake from permanently freezing their assets. Since the Token Converter calculates the amount of tokens that were deposited legitimately with convertERC20toERC223 function it is always possible to calculate the amount of “accidentally deposited tokens” by subtracting the recorded amount from the returned value of the balanceOf( address(this) ) function called on the ERC-20 token contract.

Converting ERC-20 tokens to ERC-223

In order to convert ERC-20 tokens to ERC-223 the token holder should:

  1. Call the approve function of the ERC-20 token and allow Token Converter to withdraw tokens from the token holders address via transferFrom function.
  2. Wait for the transaction with approve to be submitted to the blockchain.
  3. Call the convertERC20toERC223 function of the Token Converter contract.

Converting ERC-223 tokens back to ERC-20

In order to convert ERC-20 tokens to ERC-223 the token holder should:

  1. Send ERC-223 tokens to the address of the Token Converter contract via transfer function of the ERC-223 token contract.

Rationale

TBD

Backwards Compatibility

This proposal is supposed to eliminate the backwards compatibility concerns for different token standards making them interchangeable and interoperable.

This service is the first of its kind and therefore does not have any backwards compatibility issues as it does not have any predecessors.

Reference Implementation

    address public ownerMultisig;

    mapping (address => ERC223WrapperToken) public erc223Wrappers; // A list of token wrappers. First one is ERC-20 origin, second one is ERC-223 version.
    mapping (address => ERC20WrapperToken)  public erc20Wrappers;

    mapping (address => address)            public erc223Origins;
    mapping (address => address)            public erc20Origins;
    mapping (address => uint256)            public erc20Supply; // Token => how much was deposited.

    function getERC20WrapperFor(address _token) public view returns (address, string memory)
    {
        if ( address(erc20Wrappers[_token]) != address(0) )
        {
            return (address(erc20Wrappers[_token]), "ERC-20");
        }

        return (address(0), "Error");
    }

    function getERC223WrapperFor(address _token) public view returns (address, string memory)
    {
        if ( address(erc223Wrappers[_token]) != address(0) )
        {
            return (address(erc223Wrappers[_token]), "ERC-223");
        }

        return (address(0), "Error");
    }

    function getERC20OriginFor(address _token) public view returns (address)
    {
        return (address(erc20Origins[_token]));
    }

    function getERC223OriginFor(address _token) public view returns (address)
    {
        return (address(erc223Origins[_token]));
    }

    function tokenReceived(address _from, uint _value, bytes memory _data) public override returns (bytes4)
    {
        require(erc223Origins[msg.sender] == address(0), "Error: creating wrapper for a wrapper token.");
        // There are two possible cases:
        // 1. A user deposited ERC-223 origin token to convert it to ERC-20 wrapper
        // 2. A user deposited ERC-223 wrapper token to unwrap it to ERC-20 origin.

        if(erc20Origins[msg.sender] != address(0))
        {
            // Origin for deposited token exists.
            // Unwrap ERC-223 wrapper.

            safeTransfer(erc20Origins[msg.sender], _from, _value);

            erc20Supply[erc20Origins[msg.sender]] -= _value;
            //erc223Wrappers[msg.sender].burn(_value);
            ERC223WrapperToken(msg.sender).burn(_value);
            
            return this.tokenReceived.selector;
        }
        // Otherwise origin for the sender token doesn't exist
        // There are two possible cases:
        // 1. ERC-20 wrapper for the deposited token exists
        // 2. ERC-20 wrapper for the deposited token doesn't exist and must be created.
        else if(address(erc20Wrappers[msg.sender]) == address(0))
        {
            // Create ERC-20 wrapper if it doesn't exist.
            createERC20Wrapper(msg.sender);
        }
        
        // Mint ERC-20 wrapper tokens for the deposited ERC-223 token
        // if the ERC-20 wrapper didn't exist then it was just created in the above statement.
        erc20Wrappers[msg.sender].mint(_from, _value);
        return this.tokenReceived.selector;
    }

    function createERC223Wrapper(address _token) public returns (address)
    {
        require(address(erc223Wrappers[_token]) == address(0), "ERROR: Wrapper exists");
        require(getERC20OriginFor(_token) == address(0), "ERROR: 20 wrapper creation");
        require(getERC223OriginFor(_token) == address(0), "ERROR: 223 wrapper creation");

        ERC223WrapperToken _newERC223Wrapper     = new ERC223WrapperToken(_token);
        erc223Wrappers[_token]                   = _newERC223Wrapper;
        erc20Origins[address(_newERC223Wrapper)] = _token;

        return address(_newERC223Wrapper);
    }

    function createERC20Wrapper(address _token) public returns (address)
    {
        require(address(erc20Wrappers[_token]) == address(0), "ERROR: Wrapper already exists.");
        require(getERC20OriginFor(_token) == address(0), "ERROR: 20 wrapper creation");
        require(getERC223OriginFor(_token) == address(0), "ERROR: 223 wrapper creation");

        ERC20WrapperToken _newERC20Wrapper       = new ERC20WrapperToken(_token);
        erc20Wrappers[_token]                    = _newERC20Wrapper;
        erc223Origins[address(_newERC20Wrapper)] = _token;

        return address(_newERC20Wrapper);
    }

    function depositERC20(address _token, uint256 _amount) public returns (bool)
    {
        if(erc223Origins[_token] != address(0))
        {
            return unwrapERC20toERC223(_token, _amount);
        }
        else return wrapERC20toERC223(_token, _amount);
    }

    function wrapERC20toERC223(address _ERC20token, uint256 _amount) public returns (bool)
    {
        // If there is no active wrapper for a token that user wants to wrap
        // then create it.
        if(address(erc223Wrappers[_ERC20token]) == address(0))
        {
            createERC223Wrapper(_ERC20token);
        }
        uint256 _converterBalance = IERC20(_ERC20token).balanceOf(address(this)); // Safety variable.
        safeTransferFrom(_ERC20token, msg.sender, address(this), _amount);
        
        erc20Supply[_ERC20token] += _amount;

        require(
            IERC20(_ERC20token).balanceOf(address(this)) - _amount == _converterBalance,
            "ERROR: The transfer have not subtracted tokens from callers balance.");

        erc223Wrappers[_ERC20token].mint(msg.sender, _amount);
        return true;
    }

    function unwrapERC20toERC223(address _ERC20token, uint256 _amount) public returns (bool)
    {
        require(IERC20(_ERC20token).balanceOf(msg.sender) >= _amount, "Error: Insufficient balance.");
        require(erc223Origins[_ERC20token] != address(0), "Error: provided token is not a ERC-20 wrapper.");

        ERC20WrapperToken(_ERC20token).burn(msg.sender, _amount);
        IERC223(erc223Origins[_ERC20token]).transfer(msg.sender, _amount);

        return true;
    }

    function isWrapper(address _token) public view returns (bool)
    {
        return erc20Origins[_token] != address(0) || erc223Origins[_token] != address(0);
    } 

/*
    function convertERC223toERC20(address _from, uint256 _amount) public returns (bool)
    {
        // If there is no active wrapper for a token that user wants to wrap
        // then create it.
        if(address(erc20Wrappers[msg.sender]) == address(0))
        {
            createERC223Wrapper(msg.sender);
        }
        
        erc20Wrappers[msg.sender].mint(_from, _amount);
        return true;
    }
*/

    function rescueERC20(address _token) external {
        require(msg.sender == ownerMultisig, "ERROR: Only owner can do this.");
        uint256 _stuckTokens = IERC20(_token).balanceOf(address(this)) - erc20Supply[_token];
        safeTransfer(_token, msg.sender, IERC20(_token).balanceOf(address(this)));
    }

    function transferOwnership(address _newOwner) public
    {
        require(msg.sender == ownerMultisig, "ERROR: Only owner can call this function.");
        ownerMultisig = _newOwner;
    }
    
    // ************************************************************
    // Functions that address problems with tokens that pretend to be ERC-20
    // but in fact are not compatible with the ERC-20 standard transferring methods.
    // EIP20 https://eips.ethereum.org/EIPS/eip-20
    // ************************************************************
    function safeTransfer(address token, address to, uint value) internal {
        // bytes4(keccak256(bytes('transfer(address,uint256)')));
        (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value));
        require(success && (data.length == 0 || abi.decode(data, (bool))), 'TransferHelper: TRANSFER_FAILED');
    }

    function safeTransferFrom(address token, address from, address to, uint value) internal {
        // bytes4(keccak256(bytes('transferFrom(address,address,uint256)')));
        (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value));
        require(success && (data.length == 0 || abi.decode(data, (bool))), 'TransferHelper: TRANSFER_FROM_FAILED');
    }

Security Considerations

  1. While it is possible to implement a service that converts any token standard to any other standard - it is better to keep different standard convertors separate from one another as different standards may contain specific logic. Therefore this proposal focuses on ERC-20 to ERC-223 conversion methods.
  2. ERC-20 tokens can be deposited to any contract directly with transfer function. This may result in a permanent loss of tokens because it is not possible to recognize this transaction on the recipients side. rescueERC20 function is implemented to address this problem.
  3. Token Converter relies on ERC-20 approve & transferFrom method of depositing assets. Any related issues must be taken into account. approve and transferFrom are two separate transactions so it is required to make sure approval was successful before relying on transferFrom.
  4. This is a common practice for UI services to prompt a user to issue unlimited approval on any contract that may withdraw tokens from the user. This puts users funds at high risk and therefore not recommended.
  5. It is possible to artificially construct a token that will pretend it is a ERC-20 token that implements approve & transferFrom but at the same time implements ERC-223 logic of transferring via transfer function in its internal logic. It can be possible to create a ERC-223 wrapper for this ERC-20-ERC-223 hybrid implementation in the Token Converter. This doesn’t pose any threat for the workflow of the Token Converter but it must be taken into account that if a token has ERC-223 wrapper in the Token Converter it does not automatically mean the origin is fully compatible with the ERC-20 standard and methods of introspection must be used to determine the origins compatibility with any existing standard.

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Dexaran (@Dexaran) <dexaran@ethereumclassic.org>, "ERC-7417: Token Converter [DRAFT]," Ethereum Improvement Proposals, no. 7417, July 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7417.