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.

In order to perform the conversion, a user must deposit tokens of one standard to the Converter contract and it will automatically send tokens of another standard back.

Motivation

This proposal introduces a concept of a token standard upgrading procedure driven by a specialized smart-contract which can convert tokens of one standard to another at any time.

Currently some tokens are available on different chains in different standards, for example most exchanges support ERC-20 USDT, TRX USDT, BEP-20 USDT and all this tokens are in fact the same USDT token. This proposal is intended to introduce a concept where there can be a ERC-20 USDT and ERC-223 USDT available on Ethereum mainnet at the same time and these would be freely interchangeable.

The address of the deployed Token Converter must be described here as to solve the trust issues for the token developers and help them figure out a proper way of interacting with the Converter.

As Ethereum already has an established ecosystem of tokens and ERC-20 is the most adopted standard at the moment the lack of defined migration processes can be a bottleneck for newer standards adoption. This proposal addresses the problem of coordinating the upgrading process and addresses the backwards compatibility problems for ERC-20 and ERC-223 tokens.

The Token Converter is supposed to allow anyone to create an alternative version of an existing token implemented in a different standard. This proposal focuses on ERC-20 and ERC-223 standards and takes into account the specifics of this particular token standards. It is assumed that the most common case would be creation of ERC-223 version for an existing ERC-20 token.

The implementation of this service is an alternative to convincing each token developer to choose an alternative standard at the moment of the token deployment or during the development stage of their project. With this service there will be no need to choose one standard and stick with it as every token can be available in both concurrently.

The implementation of this Token Converter service is supposed to be a contract deployed on Ethereum mainnet once and forever. It’s address will be provided in the text of this proposal as to avoid any potential trust issues and assure the developers that the service they are interacting with is exactly the one which drives the conversion process of existing tokens.

All the ERC-223 tokens created by the Token Converter will be identical in a way that they all implement the same functions, which return the same values and there is no ambiguity there. This helps to avoid problems where a token deployed during the early stage of a token standard adoption may implement it improperly or there can be an ambiguity in the standard itself that would allow developers to implement tokens of one standard in different ways.

For example it was a common case with ERC-20 where developers could implement custom logic of the transfer function and mess the return values. The ERC-20 specification declares that a transfer function MUST return a bool value, however in practice we have three different types of ERC-20 tokens which are not compatible with each other:

  1. ERC-20 tokens that return true on success and revert on an error.
  2. ERC-20 tokens that return true on success and false on an error without reverting the transaction.
  3. ERC-20 tokens that don’t have return values and revert on an error.

Technically the third category of tokens is not compatible with ERC-20 standard. However, USDT token deployed on Ethereum mainnet at 0xdac17f958d2ee523a2206206994597c13d831ec7 address does not implement return values and it is one of the most used tokens and it is not an option to deny supporting USDT due to it’s improper implementation of the standard.

The Token Converter eliminates the issue where different development teams may implement the standard with slight modifications and result in a situation where we would have different versions of the same standard on the mainnet.

At the same time the Converter enables the concurrent token support in other smart-contracts, such as decentralized exchanges. The Converter can guarantee that a pair of two tokens one of which is a wrapper for another is in fact the same token that can be converted from one standard to another at any time. This enables the creation of liquidity pools where two different tokens are dealt with as if they were one token.

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 components:

  • Converter contract.

  • Wrapper contracts. Each original token can have exactly one wrapper of each standard.

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 ERC20 token_A the depositor MUST receive exactly 1234 units of ERC223 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

getERC20WrapperFor
function getERC20WrapperFor(address _token) public view returns (address)

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

getERC223WrapperFor
function getERC223WrapperFor(address _token) public view returns (address)

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

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

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

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

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

predictWrapperAddress
function predictWrapperAddress(address _token,
                                   bool    _isERC20 // Is the provided _token a ERC-20 or not?
                                                    // If it is set as ERC-20 then we will predict the address of a 
                                                    // ERC-223 wrapper for that token.
                                                    // Otherwise we will predict ERC-20 wrapper address.
                                  ) view external returns (address)

Wrapper contracts are deployed via CREATE2 opcode and it is possible to predict the address of a wrapper which is not yet deployed. The address of a wrapper contract depends on the bytecode therefore it is necessary to specify if the address of wrapper ERC-20 or wrapper ERC-223 must be predicted.

Providing _token address and _isERC20 = false will result in ERC-20 wrapper address being predicted.

Providing _token address and _isERC20 = true will result in ERC-223 wrapper address being predicted.

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. Reverts if _erc223Token is a wrapper created by the Converter.

The deployed contract will be a standard ERC-223 token with approve and transferFrom functions implemented for backwards compatibility.

All ERC-223 wrappers deployed by the Converter will have standard() pure returns (bytes32) function implemented which returns 223. This serves further token standard introspection as ERC-165 may not be reliable when dealing with identifying the internal logic implemented within transfer function of a token.

NOTE: This function does not verify the standard of _erc20Token because there is no reliable method of introspection available which could guarantee that the provided token implements a particular standard. As the result it is possible to create a ERC-223 wrapper for an original ERC-223 token.

createERC20Wrapper
function createERC20Wrapper(address _erc223Token) public returns (address)

Creates a new ERC-20 wrapper for a given _erc223Token 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. Reverts if _erc223Token is a wrapper created by the Converter.

NOTE: This function does not verify the standard of _erc223Token because there is no reliable method of introspection available which could guarantee that the provided token implements a particular standard. As the result it is possible to create a ERC-20 wrapper for an original ERC-20 token.

wrapERC20toERC223
function wrapERC20toERC223(address _ERC20token, uint256 _amount) public returns (bool)

Withdraws _amount of ERC-20 tokens from the transaction sender with transferFrom function. Delivers 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 wrapERC20toERC223 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.

There is no special function to unwrap ERC-223 wrappers to ERC-20 origin as this logic is implemented in the tokenReceived function of the Converter.

unwrapERC20toERC223
function unwrapERC20toERC223(address _ERC20token, uint256 _amount) public returns (bool)

Withdraws _amount of ERC-20 tokens from the transaction sender with transferFrom function. Delivers 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 wrapERC20toERC223 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.

convertERC20
function convertERC20(address _token, uint256 _amount) public returns (bool)

Automatically determines if the provided ERC-20 token is a wrapper or not. If it is a wrapper then executes unwrapERC20toERC223 function. If the provided token is an origin then executes wrapERC20toERC223 function.

This function is implemented to significantly simplify the workflow of services that integrate both versions of one token in the same contract and need to automatically convert tokens through the Converter.

isWrapper
function isWrapper(address _token) public view returns (bool)

Returns true if the provided _token address is an address of a wrapper created by the Converter.

NOTE: This function does not identify the standard of a _token. There can be exactly one origin for any wrapper created by the Converter. However an original token can have two wrappers, one of each standard.

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 sender of the token transfer.

Automatically determines

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 it is considered a ERC-223 origin and _value amount of ERC-20 wrapper tokens must be sent to the _from address. If the ERC-20 wrapper for the msg.sender token does not exist then create it first.

Returns 0x8943ec02.

extractStuckERC20
function extractStuckERC20(address _token)

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 losing their tokens. 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 wrapper 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

Support of ERC-223 original tokens

Two methods of implementing a Token Converter service were considered: (1) a converter that can only create ERC-223 versions of the existing ERC-20 tokens, and (2) a converter that can create both versions (ERC-20 and ERC-223) of any original token.

The first approach would encourage developers to always deploy an original token as ERC-20 and then create it’s ERC-223 version in the converter. If it would happen that some developers may consider ERC-223 as their original standard then they would be left with the problem of creating their custom ERC-20 version of the token. In addition, if any third party contracts like liquidity pools are using the proposed Token Converter to ensure that a token can be listed on a DEX with two versions and both can be combined within one pool - then such contract would not be able to recognize any original ERC-223 token and it’s ERC-20 version as a valid pair of contracts that represent one token available in two standards.

For that reason it was decided to go with the second approach where the Converter can create ERC-20 wrappers for original ERC-223 tokens.

Support of approve & transferFrom functions in the ERC-223 wrapper tokens

This functions are superfluous for a ERC-223 token since the transfer function can be used to deposit tokens of this standard to contracts. The current ecosystem is built for ERC-20 tokens however and there are plenty of multisig contracts that rely on accepting tokens deposited without any callback with an assumption that it is not necessary for a multisig to count the amount of tokens it stores.

There can be any other contracts and scenarios where it would be necessary to deposit a token to a contract which is relying on an assumption that tokens are deposited without invoking a callback in the recipient. As the result we can expect that any original deployed ERC-223 tokens will support this functions, as token developers strive for backward compatibility with the existing ecosystem. In order to make tokens deployed by the converter a reference implementation for developers that can be used without any modifications it was decided to support this functions in the ERC-223 wrapper contracts.

transferFrom function does not support error handling and this needs to be taken into account. It is possible to deposit tokens to a contract which is not designed to receive them by approving X tokens to your own address and then calling a transferFrom(self, contract, X). The tokens will be deposited regardless of whether the recipient contract is designed to hold/receive tokens or not. The tokens may get permanently stuck if the recipient contract did not implement the extraction functions. The approve & transferFrom function is not the default method of token transferring however and it is not directly used by any wallets and any other software that manages tokens. The transfer function (which is safe) is used instead. The transferFrom function is supposed to be invoked by a contract to pull tokens from the approver.

As the result, the approve & transferFrom transferring method must be avoided with ERC-223 tokens whenever possible.

Modified transfer events of the ERC-223 token

The pure ERC-223 token implementation has the following event emitted on a token transfer: event Transfer(address indexed _from, address indexed _to, uint256 _value, bytes _data). This events are different from ones emitted by ERC-20 tokens and may not be properly recognized by existing blockchain explorers, wallets and other services that browse token transfers history.

It was considered that events are not an important part of the standard as these do not affect the logic of the token, it’s workflow and it’s security. When developing the Converter it was decided to prioritize compatibility with existing ecosystem.

standard() function usage for the introspection

The main existing method of introspection is currently ERC-165 which inspects the signatures of functions implemented in a contract. It is not possible to differentiate an ERC-20 token from an ERC-223 token by just browsing functions that they implement without digging their internal logic.

Here is a token and it is not possible to identify if it should be dealt with as ERC-20 or ERC-223 because it depends on the actual implemenation of it’s transfer function logic.

abstract contract Token {
    function name() external virtual returns (string memory);
    function symbol() external virtual returns (string memory);
    function decimals() external virtual returns (uint8);

    function transfer(address, uint256) external virtual returns (bool);
    function approve(address, uint256) external virtual returns (bool);
    function transferFrom(address, address, uint256) external virtual returns (bool);
}

In case of this implementation the token will behave as ERC-20:

    function transfer(address _to, uint256 _amount) external virtual returns (bool)
    {
        balances[msg.sender] -= _amount;
        balances[_to] += _amount;
    }
}

In case of this implementation the token will behave as ERC-223:

    function transfer(address _to, uint256 _amount) external virtual returns (bool)
    {
        balances[msg.sender] -= _amount;
        balances[_to] += _amount;
        if(_to.isContract())
        {
            IERC223Recipient(_to).tokenReceived(msg.sender, _amount, hex"000000");
        }
    }
}

Also, there are plenty of tokens that do not implement ERC-165 introspection at all. As the result it was decided to implement a special standard() returns (uint32) function in all ERC-223 wrappers created by the Converter and assume that original ERC-223 tokens may explicityly declare themselves as ERC-223 by implementing the same function too. It is assumed that if a token does not implement this function then it is ERC-20.

This method of token standard introspection is more precise than ERC-165.

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



pragma solidity =0.8.19;

library Address {
    function isContract(address account) internal view returns (bool) {
        // This method relies on 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;
    }
}

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount) external returns (bool);
    function allowance(address owner, address spender) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

interface IERC20Metadata is IERC20 {
    /// @return The name of the token
    function name() external view returns (string memory);

    /// @return The symbol of the token
    function symbol() external view returns (string memory);

    /// @return The number of decimal places the token has
    function decimals() external view returns (uint8);
}

abstract contract IERC223Recipient {
    function tokenReceived(address _from, uint _value, bytes memory _data) public virtual returns (bytes4)
    {
        return 0x8943ec02;
    }
}

abstract contract ERC165 {
    /*
     * bytes4(keccak256('supportsInterface(bytes4)')) == 0x01ffc9a7
     */
    bytes4 private constant _INTERFACE_ID_ERC165 = 0x01ffc9a7;
    mapping(bytes4 => bool) private _supportedInterfaces;

    constructor () {
        // Derived contracts need only register support for their own interfaces,
        // we register support for ERC165 itself here
        _registerInterface(_INTERFACE_ID_ERC165);
    }
    function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) {
        return _supportedInterfaces[interfaceId];
    }
    function _registerInterface(bytes4 interfaceId) internal virtual {
        require(interfaceId != 0xffffffff, "ERC165: invalid interface id");
        _supportedInterfaces[interfaceId] = true;
    }
}

abstract contract IERC223 {
    function name()        public view virtual returns (string memory);
    function symbol()      public view virtual returns (string memory);
    function decimals()    public view virtual returns (uint8);
    function totalSupply() public view virtual returns (uint256);
    function balanceOf(address who) public virtual view returns (uint);
    function transfer(address to, uint value) public virtual returns (bool success);
    function transfer(address to, uint value, bytes calldata data) public payable virtual returns (bool success);
    event Transfer(address indexed from, address indexed to, uint value, bytes data);
}

interface standardERC20
{
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address to, uint256 value) external returns (bool);
    function allowance(address owner, address spender) external view returns (uint256);
    function approve(address spender, uint256 value) external returns (bool);
    function transferFrom(address from, address to, uint256 value) external returns (bool);
}

/**
 * @dev Interface of the ERC20 standard.
 */
interface IERC223WrapperToken {
    function name()     external view returns (string memory);
    function symbol()   external view returns (string memory);
    function decimals() external view returns (uint8);
    function standard() external view returns (string memory);
    function origin()   external  view returns (address);

    function totalSupply()                                            external view returns (uint256);
    function balanceOf(address account)                               external view returns (uint256);
    function transfer(address to, uint256 value)                      external payable returns (bool);
    function transfer(address to, uint256 value, bytes calldata data) external payable returns (bool);
    function allowance(address owner, address spender)                external view returns (uint256);
    function approve(address spender, uint256 value)                  external returns (bool);
    function transferFrom(address from, address to, uint256 value)    external returns (bool);

    function mint(address _recipient, uint256 _quantity) external;
    function burn(address _recipient, uint256 _quantity) external;
}

interface IERC20WrapperToken {
    function name()     external view returns (string memory);
    function symbol()   external view returns (string memory);
    function decimals() external view returns (uint8);
    function standard() external view returns (string memory);
    function origin()   external  view returns (address);

    function totalSupply()                                         external view returns (uint256);
    function balanceOf(address account)                            external view returns (uint256);
    function transfer(address to, uint256 value)                   external returns (bool);
    function allowance(address owner, address spender)             external view returns (uint256);
    function approve(address spender, uint256 value)               external returns (bool);
    function transferFrom(address from, address to, uint256 value) external returns (bool);

    function mint(address _recipient, uint256 _quantity) external;
    function burn(address _recipient, uint256 _quantity) external;
}

contract ERC20Rescue
{
    // ERC20 tokens can get stuck on a contracts balance due to lack of error handling.
    //
    // The author of the ERC7417 can extract ERC20 tokens if they are mistakenly sent
    // to the wrapper-contracts balance.
    // Contact [email protected]
    address public extractor = 0x01000B5fE61411C466b70631d7fF070187179Bbf;
    
    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 rescueERC20(address _token, uint256 _amount) external 
    {
        safeTransfer(_token, extractor, _amount);
    }
}

contract ERC223WrapperToken is IERC223, ERC165, ERC20Rescue
{
    address public creator = msg.sender;
    address private wrapper_for;

    mapping(address account => mapping(address spender => uint256)) private allowances;

    event Transfer(address indexed from, address indexed to, uint256 amount);
    event TransferData(bytes data);
    event Approval(address indexed owner, address indexed spender, uint256 amount);

    function set(address _wrapper_for) external
    {
        require(msg.sender == creator);
        wrapper_for = _wrapper_for;
    }

    uint256 private _totalSupply;

    mapping(address => uint256) private balances; // List of user balances.

    function totalSupply() public view override returns (uint256)             { return _totalSupply; }
    function balanceOf(address _owner) public view override returns (uint256) { return balances[_owner]; }


    /**
     * @dev The ERC165 introspection function.
     */
    function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
        return
            interfaceId == type(IERC20).interfaceId ||
            interfaceId == type(standardERC20).interfaceId ||
            interfaceId == type(IERC223WrapperToken).interfaceId ||
            interfaceId == type(IERC223).interfaceId ||
            super.supportsInterface(interfaceId);
    }

    /**
     * @dev Standard ERC223 transfer function.
     *      Calls _to if it is a contract. Does not transfer tokens to contracts
     *      which do not explicitly declare the tokenReceived function.
     * @param _to    - transfer recipient. Can be contract or EOA.
     * @param _value - the quantity of tokens to transfer.
     * @param _data  - metadata to send alongside the transaction. Can be used to encode subsequent calls in the recipient.
     */
    function transfer(address _to, uint _value, bytes calldata _data) public payable override returns (bool success)
    {
        balances[msg.sender] = balances[msg.sender] - _value;
        balances[_to] = balances[_to] + _value;
        if (msg.value > 0) 
        {
            (bool sent, bytes memory data) = _to.call{value: msg.value}("");
            require(sent);
        }
        if(Address.isContract(_to)) {
            IERC223Recipient(_to).tokenReceived(msg.sender, _value, _data);
        }
        emit Transfer(msg.sender, _to, _value, _data);
        emit Transfer(msg.sender, _to, _value); // Old ERC20 compatible event. Added for backwards compatibility reasons.

        return true;
    }

    /**
     * @dev Standard ERC223 transfer function without _data parameter. It is supported for 
     *      backwards compatibility with ERC20 services.
     *      Calls _to if it is a contract. Does not transfer tokens to contracts
     *      which do not explicitly declare the tokenReceived function.
     * @param _to    - transfer recipient. Can be contract or EOA.
     * @param _value - the quantity of tokens to transfer.
     */
    function transfer(address _to, uint _value) public override 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);
        emit Transfer(msg.sender, _to, _value); // Old ERC20 compatible event. Added for backwards compatibility reasons.

        return true;
    }

    function name() public view override returns (string memory)   { return IERC20Metadata(wrapper_for).name(); }
    function symbol() public view override returns (string memory) { return string.concat(IERC20Metadata(wrapper_for).symbol(), "223"); }
    function decimals() public view override returns (uint8)       { return IERC20Metadata(wrapper_for).decimals(); }
    function standard() public pure returns (uint32)               { return 223; }
    function origin() public view returns (address)                { return wrapper_for; }


    /**
     * @dev Minting function which will only be called by the converter contract.
     * @param _recipient - the address which will receive tokens.
     * @param _quantity  - the number of tokens to create.
     */
    function mint(address _recipient, uint256 _quantity) external
    {
        require(msg.sender == creator, "Wrapper Token: Only the creator contract can mint wrapper tokens.");
        balances[_recipient] += _quantity;
        _totalSupply += _quantity;
    }

    /**
     * @dev Burning function which will only be called by the converter contract.
     * @param _quantity  - the number of tokens to destroy. TokenConverter can only destroy tokens on it's own address.
     *                     Only the token converter is allowed to burn wrapper-tokens.
     */
    function burn(uint256 _quantity) external
    {
        require(msg.sender == creator, "Wrapper Token: Only the creator contract can destroy wrapper tokens.");
        balances[msg.sender] -= _quantity;
        _totalSupply -= _quantity;
    }

    // ERC20 functions for backwards compatibility.

    function allowance(address owner, address spender) public view virtual returns (uint256) {
        return allowances[owner][spender];
    }

    function approve(address _spender, uint _value) public returns (bool) {
        require(_spender != address(0), "ERC223: Spender error.");

        allowances[msg.sender][_spender] = _value;
        emit Approval(msg.sender, _spender, _value);

        return true;
    }

    function transferFrom(address _from, address _to, uint _value) public returns (bool) {

        require(allowances[_from][msg.sender] >= _value, "ERC223: Insufficient allowance.");

        balances[_from] -= _value;
        allowances[_from][msg.sender] -= _value;
        balances[_to] += _value;

        emit Transfer(_from, _to, _value);

        return true;
    }
}

contract ERC20WrapperToken is IERC20, ERC165, ERC20Rescue
{
    address public creator = msg.sender;
    address public wrapper_for;

    mapping(address account => mapping(address spender => uint256)) private allowances;

    function set(address _wrapper_for) external
    {
        require(msg.sender == creator);
        wrapper_for = _wrapper_for;
    }

    uint256 private _totalSupply;
    mapping(address => uint256) private balances; // List of user balances.


    function balanceOf(address _owner) public view override returns (uint256) { return balances[_owner]; }

    function name()        public view  returns (string memory) { return IERC20Metadata(wrapper_for).name(); }
    function symbol()      public view  returns (string memory) { return string.concat(IERC223(wrapper_for).symbol(), "20"); }
    function decimals()    public view  returns (uint8)         { return IERC20Metadata(wrapper_for).decimals(); }
    function totalSupply() public view override returns (uint256)       { return _totalSupply; }
    function origin()      public view returns (address)                { return wrapper_for; }

    function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
        return
            interfaceId == type(IERC20).interfaceId ||
            interfaceId == type(IERC20WrapperToken).interfaceId ||
            super.supportsInterface(interfaceId);
    }

    function transfer(address _to, uint _value) public override returns (bool success)
    {
        balances[msg.sender] = balances[msg.sender] - _value;
        balances[_to] = balances[_to] + _value;
        emit Transfer(msg.sender, _to, _value);
        return true;
    }

    function mint(address _recipient, uint256 _quantity) external
    {
        require(msg.sender == creator, "Wrapper Token: Only the creator contract can mint wrapper tokens.");
        balances[_recipient] += _quantity;
        _totalSupply += _quantity;
    }

    function burn(address _from, uint256 _quantity) external
    {
        require(msg.sender == creator, "Wrapper Token: Only the creator contract can destroy wrapper tokens.");
        balances[_from] -= _quantity;
        _totalSupply    -= _quantity;
    }

    function allowance(address owner, address spender) public view virtual returns (uint256) {
        return allowances[owner][spender];
    }

    function approve(address _spender, uint _value) public returns (bool) {

        // Safety checks.

        require(_spender != address(0), "ERC20: Spender error.");

        allowances[msg.sender][_spender] = _value;
        emit Approval(msg.sender, _spender, _value);

        return true;
    }

    function transferFrom(address _from, address _to, uint _value) public returns (bool) {

        require(allowances[_from][msg.sender] >= _value, "ERC20: Insufficient allowance.");

        balances[_from] -= _value;
        allowances[_from][msg.sender] -= _value;
        balances[_to] += _value;

        emit Transfer(_from, _to, _value);

        return true;
    }
}

contract TokenStandardConverter is IERC223Recipient
{
    event ERC223WrapperCreated(address indexed _token, address indexed _ERC223Wrapper);
    event ERC20WrapperCreated(address indexed _token, address indexed _ERC20Wrapper);

    mapping (address => ERC223WrapperToken) public erc223Wrappers; // A list of token wrappers. First one is ERC20 origin, second one is ERC223 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)
    {
        return address(erc20Wrappers[_token]);
    }

    function getERC223WrapperFor(address _token) public view returns (address)
    {
        return address(erc223Wrappers[_token]);
    }

    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 predictWrapperAddress(address _token,
                                   bool    _isERC20 // Is the provided _token a ERC20 or not?
                                                    // If it is set as ERC20 then we will predict the address of a 
                                                    // ERC223 wrapper for that token.
                                                    // Otherwise we will predict ERC20 wrapper address.
                                  ) view external returns (address)
    {
        bytes memory _bytecode;
        if(_isERC20)
        {
            _bytecode = type(ERC223WrapperToken).creationCode;
        }
        else
        {
            _bytecode = type(ERC20WrapperToken).creationCode;
        }

        bytes32 hash = keccak256(
            abi.encodePacked(
                bytes1(0xff), address(this), keccak256(abi.encode(_token)), keccak256(_bytecode)
          )
        );

        return address(uint160(uint(hash)));
    }

    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 ERC223 origin token to convert it to ERC20 wrapper
        // 2. A user deposited ERC223 wrapper token to unwrap it to ERC20 origin.

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

            erc20Supply[erc20Origins[msg.sender]] -= _value;
            safeTransfer(erc20Origins[msg.sender], _from, _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. ERC20 wrapper for the deposited token exists
        // 2. ERC20 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(!isWrapper(_token), "Error: Creating wrapper for a wrapper token");
        
        ERC223WrapperToken _newERC223Wrapper     = new ERC223WrapperToken{salt: keccak256(abi.encode(_token))}();
        _newERC223Wrapper.set(_token);
        erc223Wrappers[_token]                   = _newERC223Wrapper;
        erc20Origins[address(_newERC223Wrapper)] = _token;

        emit ERC223WrapperCreated(_token, address(_newERC223Wrapper));
        return address(_newERC223Wrapper);
    }

    function createERC20Wrapper(address _token) public returns (address)
    {
        require(address(erc20Wrappers[_token]) == address(0), "ERROR: Wrapper already exists.");
        require(!isWrapper(_token), "Error: Creating wrapper for a wrapper token");

        ERC20WrapperToken _newERC20Wrapper       = new ERC20WrapperToken{salt: keccak256(abi.encode(_token))}();
        _newERC20Wrapper.set(_token);
        erc20Wrappers[_token]                    = _newERC20Wrapper;
        erc223Origins[address(_newERC20Wrapper)] = _token;

        emit ERC20WrapperCreated(_token, address(_newERC20Wrapper));
        return address(_newERC20Wrapper);
    }

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

        _amount = IERC20(_ERC20token).balanceOf(address(this)) - _converterBalance;
        erc20Supply[_ERC20token] += _amount;

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

        safeTransfer(erc223Origins[_ERC20token], msg.sender, _amount);

        return true;
    }

    function convertERC20(address _token, uint256 _amount) public returns (bool)
    {
        if(isWrapper(_token)) return unwrapERC20toERC223(_token, _amount);
        else return wrapERC20toERC223(_token, _amount);
    }

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

    function extractStuckERC20(address _token) external 
    {
        require(msg.sender == address(0x01000B5fE61411C466b70631d7fF070187179Bbf));

        safeTransfer(_token, address(0x01000B5fE61411C466b70631d7fF070187179Bbf), IERC20(_token).balanceOf(address(this)) - erc20Supply[_token]);
    }
    
    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 and therefore require different conversion approach. This proposal focuses on ERC-20 and ERC-223 upgradeability.
  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. Therefore wrapper-ERC-20 tokens are prone to this problem as they are compatible with the ERC-20 standard. 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 risk and therefore is not recommended.
  5. There is no reliable token standard introspection method available that could guarantee that a token implements a particular token standard. 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. 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 itself 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.
  6. Token Converter does not verify the standard of a provided token when it is asked to create a wrapper for it due to the lack of reliable standard introspection method. It is possible to call createERC20Wrapper function and provide an address of an existing ERC-20 token. The Token Converter will successfully create a ERC-20 wrapper for that ERC-20 original token. It is also possible to create a ERC-223 wrapper for that exact original ERC-20 token. This doesn’t pose any threat to the workflow of the Converter but it must be taken into account that any token regardless of it’s original standard may have up to two wrappers created by the Converter, one for each standard. Any wrapper token must have exactly one origin. It is not possible to create a wrapper for a wrapper.
  7. The Token Converter only holds the original tokens that were deposited during the conversion process and it assumes that tokens do not decay over time and the token balance of the Converter does not decrease on its own. If some token implements burning logic or decaying supply and it may impact the balance of the Converter then the Converter must not be used to deploy an alternative version of that token as it will not be able to guarantee that there is enough tokens for the conversion at any time.

Copyright and related rights waived via CC0.