Alert Source Discuss
⚠️ Review Standards Track: SRC

SRC-7417: Token Converter

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

Authors Dexaran (@Dexaran) <dexaran@silaclassic.org>
Created 2023-07-27
Requires SIP-20, SIP-165, SIP-223

Abstract

There are multiple token standards on Sila chain currently. This SIP introduces a concept of cross-standard interoperability by creating a service that allows SRC-20 tokens to be upgraded to SRC-223 tokens anytime. SRC-223 tokens can be converted back to SRC-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 SRC-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 SRC-20 USDT and SRC-223 USDT available on Sila sila-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 Sila already has an established ecosystem of tokens and SRC-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 SRC-20 and SRC-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 SRC-20 and SRC-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 SRC-223 version for an existing SRC-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 Sila sila-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 SRC-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 SRC-20 where developers could implement custom logic of the transfer function and mess the return values. The SRC-20 specification declares that a transfer function MUST return a bool value, however in practice we have three different types of SRC-20 tokens which are not compatible with each other:

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

Technically the third category of tokens is not compatible with SRC-20 standard. However, USDT token deployed on Sila sila-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 sila-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 SRC-223 wrapper contracts for any SRC-20 token that does not have a SRC-223 wrapper currently. There MUST be exactly one SRC-223 wrapper for each SRC-20 token.

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

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

Token Converter

Conver contract methods

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

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

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

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

getSRC20OriginFor
function getSRC20OriginFor(address _src223Token) public view returns (address)

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

getSRC223OriginFor
function getSRC223OriginFor(address _src20Token) public view returns (address)

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

predictWrapperAddress
function predictWrapperAddress(address _token,
                                   bool    _isSRC20 // Is the provided _token a SRC-20 or not?
                                                    // If it is set as SRC-20 then we will predict the address of a 
                                                    // SRC-223 wrapper for that token.
                                                    // Otherwise we will predict SRC-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 SRC-20 or wrapper SRC-223 must be predicted.

Providing _token address and _isSRC20 = false will result in SRC-20 wrapper address being predicted.

Providing _token address and _isSRC20 = true will result in SRC-223 wrapper address being predicted.

createSRC223Wrapper
function createSRC223Wrapper(address _src20Token) public returns (address)

Creates a new SRC-223 wrapper for a given _src20Token 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 _src223Token is a wrapper created by the Converter.

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

All SRC-223 wrappers deployed by the Converter will have standard() pure returns (bytes32) function implemented which returns 223. This serves further token standard introspection as SRC-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 _src20Token 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 SRC-223 wrapper for an original SRC-223 token.

createSRC20Wrapper
function createSRC20Wrapper(address _src223Token) public returns (address)

Creates a new SRC-20 wrapper for a given _src223Token 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 _src223Token is a wrapper created by the Converter.

NOTE: This function does not verify the standard of _src223Token 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 SRC-20 wrapper for an original SRC-20 token.

wrapSRC20toSRC223
function wrapSRC20toSRC223(address _SRC20token, uint256 _amount) public returns (bool)

Withdraws _amount of SRC-20 tokens from the transaction sender with transferFrom function. Delivers the _amount of SRC-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 SRC-20 tokens that were deposited with wrapSRC20toSRC223 function because it is possible to deposit SRC-20 tokens to any contract by directly sending them with transfer function.

If there is no SRC-223 wrapper for the _SRC20token then creates it by calling a createSRC223Wrapper(_src20toke) function.

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

unwrapSRC20toSRC223
function unwrapSRC20toSRC223(address _SRC20token, uint256 _amount) public returns (bool)

Withdraws _amount of SRC-20 tokens from the transaction sender with transferFrom function. Delivers the _amount of SRC-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 SRC-20 tokens that were deposited with wrapSRC20toSRC223 function because it is possible to deposit SRC-20 tokens to any contract by directly sending them with transfer function.

If there is no SRC-223 wrapper for the _SRC20token then creates it by calling a createSRC223Wrapper(_src20toke) function.

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

Automatically determines if the provided SRC-20 token is a wrapper or not. If it is a wrapper then executes unwrapSRC20toSRC223 function. If the provided token is an origin then executes wrapSRC20toSRC223 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 SRC-223 transaction handler function and it is called by the SRC-223 token contract when _from is sending _value of SRC-223 tokens to address(this) address. In the scope of this function msg.sender is the address of the SRC-223 token contract and _from is the sender of the token transfer.

Automatically determines

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

If msg.sender is not an address of any SRC-223 wrapper known to the Token Converter then it is considered a SRC-223 origin and _value amount of SRC-20 wrapper tokens must be sent to the _from address. If the SRC-20 wrapper for the msg.sender token does not exist then create it first.

Returns 0x8943ec02.

extractStuckSRC20
function extractStuckSRC20(address _token)

This function allows to extract the SRC-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 convertSRC20toSRC223 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 SRC-20 token contract.

Converting SRC-20 tokens to SRC-223

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

  1. Call the approve function of the SRC-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 convertSRC20toSRC223 function of the Token Converter contract.

Converting SRC-223 wrapper tokens back to SRC-20

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

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

Rationale

Support of SRC-223 original tokens

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

The first approach would encourage developers to always deploy an original token as SRC-20 and then create it’s SRC-223 version in the converter. If it would happen that some developers may consider SRC-223 as their original standard then they would be left with the problem of creating their custom SRC-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 SRC-223 token and it’s SRC-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 SRC-20 wrappers for original SRC-223 tokens.

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

This functions are superfluous for a SRC-223 token since the transfer function can be used to deposit tokens of this standard to contracts. The current ecosystem is built for SRC-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 SRC-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 SRC-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 SRC-223 tokens whenever possible.

Modified transfer events of the SRC-223 token

The pure SRC-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 SRC-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 SRC-165 which inspects the signatures of functions implemented in a contract. It is not possible to differentiate an SRC-20 token from an SRC-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 SRC-20 or SRC-223 because it depends on the actual implementation 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 SRC-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 SRC-223:

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

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

This method of token standard introspection is more precise than SRC-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 ISRC20 {
    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 ISRC20Metadata is ISRC20 {
    /// @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 ISRC223Recipient {
    function tokenReceived(address _from, uint _value, bytes memory _data) public virtual returns (bytes4)
    {
        return 0x8943ec02;
    }
}

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

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

abstract contract ISRC223 {
    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 standardSRC20
{
    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 SRC20 standard.
 */
interface ISRC223WrapperToken {
    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 ISRC20WrapperToken {
    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 SRC20Rescue
{
    // SRC20 tokens can get stuck on a contracts balance due to lack of error handling.
    //
    // The author of the SRC7417 can extract SRC20 tokens if they are mistakenly sent
    // to the wrapper-contracts balance.
    // Contact dexaran@silaclassic.org
    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 rescueSRC20(address _token, uint256 _amount) external 
    {
        safeTransfer(_token, extractor, _amount);
    }
}

contract SRC223WrapperToken is ISRC223, SRC165, SRC20Rescue
{
    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 SRC165 introspection function.
     */
    function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
        return
            interfaceId == type(ISRC20).interfaceId ||
            interfaceId == type(standardSRC20).interfaceId ||
            interfaceId == type(ISRC223WrapperToken).interfaceId ||
            interfaceId == type(ISRC223).interfaceId ||
            super.supportsInterface(interfaceId);
    }

    /**
     * @dev Standard SRC223 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)) {
            ISRC223Recipient(_to).tokenReceived(msg.sender, _value, _data);
        }
        emit Transfer(msg.sender, _to, _value, _data);
        emit Transfer(msg.sender, _to, _value); // Old SRC20 compatible event. Added for backwards compatibility reasons.

        return true;
    }

    /**
     * @dev Standard SRC223 transfer function without _data parameter. It is supported for 
     *      backwards compatibility with SRC20 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)) {
            ISRC223Recipient(_to).tokenReceived(msg.sender, _value, _empty);
        }
        emit Transfer(msg.sender, _to, _value, _empty);
        emit Transfer(msg.sender, _to, _value); // Old SRC20 compatible event. Added for backwards compatibility reasons.

        return true;
    }

    function name() public view override returns (string memory)   { return ISRC20Metadata(wrapper_for).name(); }
    function symbol() public view override returns (string memory) { return string.concat(ISRC20Metadata(wrapper_for).symbol(), "223"); }
    function decimals() public view override returns (uint8)       { return ISRC20Metadata(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;
    }

    // SRC20 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), "SRC223: 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, "SRC223: Insufficient allowance.");

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

        emit Transfer(_from, _to, _value);

        return true;
    }
}

contract SRC20WrapperToken is ISRC20, SRC165, SRC20Rescue
{
    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 ISRC20Metadata(wrapper_for).name(); }
    function symbol()      public view  returns (string memory) { return string.concat(ISRC223(wrapper_for).symbol(), "20"); }
    function decimals()    public view  returns (uint8)         { return ISRC20Metadata(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(ISRC20).interfaceId ||
            interfaceId == type(ISRC20WrapperToken).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), "SRC20: 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, "SRC20: Insufficient allowance.");

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

        emit Transfer(_from, _to, _value);

        return true;
    }
}

contract TokenStandardConverter is ISRC223Recipient
{
    event SRC223WrapperCreated(address indexed _token, address indexed _SRC223Wrapper);
    event SRC20WrapperCreated(address indexed _token, address indexed _SRC20Wrapper);

    mapping (address => SRC223WrapperToken) public src223Wrappers; // A list of token wrappers. First one is SRC20 origin, second one is SRC223 version.
    mapping (address => SRC20WrapperToken)  public src20Wrappers;

    mapping (address => address)            public src223Origins;
    mapping (address => address)            public src20Origins;
    mapping (address => uint256)            public src20Supply; // Token => how much was deposited.

    function getSRC20WrapperFor(address _token) public view returns (address)
    {
        return address(src20Wrappers[_token]);
    }

    function getSRC223WrapperFor(address _token) public view returns (address)
    {
        return address(src223Wrappers[_token]);
    }

    function getSRC20OriginFor(address _token) public view returns (address)
    {
        return (address(src20Origins[_token]));
    }

    function getSRC223OriginFor(address _token) public view returns (address)
    {
        return (address(src223Origins[_token]));
    }

    function predictWrapperAddress(address _token,
                                   bool    _isSRC20 // Is the provided _token a SRC20 or not?
                                                    // If it is set as SRC20 then we will predict the address of a 
                                                    // SRC223 wrapper for that token.
                                                    // Otherwise we will predict SRC20 wrapper address.
                                  ) view external returns (address)
    {
        bytes memory _bytecode;
        if(_isSRC20)
        {
            _bytecode = type(SRC223WrapperToken).creationCode;
        }
        else
        {
            _bytecode = type(SRC20WrapperToken).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(src223Origins[msg.sender] == address(0), "Error: creating wrapper for a wrapper token.");
        // There are two possible cases:
        // 1. A user deposited SRC223 origin token to convert it to SRC20 wrapper
        // 2. A user deposited SRC223 wrapper token to unwrap it to SRC20 origin.

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

            src20Supply[src20Origins[msg.sender]] -= _value;
            safeTransfer(src20Origins[msg.sender], _from, _value);

            SRC223WrapperToken(msg.sender).burn(_value);

            return this.tokenReceived.selector;
        }
        // Otherwise origin for the sender token doesn't exist
        // There are two possible cases:
        // 1. SRC20 wrapper for the deposited token exists
        // 2. SRC20 wrapper for the deposited token doesn't exist and must be created.
        else if(address(src20Wrappers[msg.sender]) == address(0))
        {
            // Create SRC-20 wrapper if it doesn't exist.
            createSRC20Wrapper(msg.sender);
        }

        // Mint SRC-20 wrapper tokens for the deposited SRC-223 token
        // if the SRC-20 wrapper didn't exist then it was just created in the above statement.
        src20Wrappers[msg.sender].mint(_from, _value);
        return this.tokenReceived.selector;
    }

    function createSRC223Wrapper(address _token) public returns (address)
    {
        require(address(src223Wrappers[_token]) == address(0), "ERROR: Wrapper exists");
        require(!isWrapper(_token), "Error: Creating wrapper for a wrapper token");
        
        SRC223WrapperToken _newSRC223Wrapper     = new SRC223WrapperToken{salt: keccak256(abi.encode(_token))}();
        _newSRC223Wrapper.set(_token);
        src223Wrappers[_token]                   = _newSRC223Wrapper;
        src20Origins[address(_newSRC223Wrapper)] = _token;

        emit SRC223WrapperCreated(_token, address(_newSRC223Wrapper));
        return address(_newSRC223Wrapper);
    }

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

        SRC20WrapperToken _newSRC20Wrapper       = new SRC20WrapperToken{salt: keccak256(abi.encode(_token))}();
        _newSRC20Wrapper.set(_token);
        src20Wrappers[_token]                    = _newSRC20Wrapper;
        src223Origins[address(_newSRC20Wrapper)] = _token;

        emit SRC20WrapperCreated(_token, address(_newSRC20Wrapper));
        return address(_newSRC20Wrapper);
    }

    function wrapSRC20toSRC223(address _SRC20token, uint256 _amount) public returns (bool)
    {
        // If there is no active wrapper for a token that user wants to wrap
        // then create it.
        if(address(src223Wrappers[_SRC20token]) == address(0))
        {
            createSRC223Wrapper(_SRC20token);
        }
        uint256 _converterBalance = ISRC20(_SRC20token).balanceOf(address(this)); // Safety variable.
        safeTransferFrom(_SRC20token, msg.sender, address(this), _amount);

        _amount = ISRC20(_SRC20token).balanceOf(address(this)) - _converterBalance;
        src20Supply[_SRC20token] += _amount;

        src223Wrappers[_SRC20token].mint(msg.sender, _amount);

        return true;
    }

    function unwrapSRC20toSRC223(address _SRC20token, uint256 _amount) public returns (bool)
    {
        require(ISRC20(_SRC20token).balanceOf(msg.sender) >= _amount, "Error: Insufficient balance.");
        require(src223Origins[_SRC20token] != address(0), "Error: provided token is not a SRC-20 wrapper.");

        SRC20WrapperToken(_SRC20token).burn(msg.sender, _amount);

        safeTransfer(src223Origins[_SRC20token], msg.sender, _amount);

        return true;
    }

    function convertSRC20(address _token, uint256 _amount) public returns (bool)
    {
        if(isWrapper(_token)) return unwrapSRC20toSRC223(_token, _amount);
        else return wrapSRC20toSRC223(_token, _amount);
    }

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

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

        safeTransfer(_token, address(0x01000B5fE61411C466b70631d7fF070187179Bbf), ISRC20(_token).balanceOf(address(this)) - src20Supply[_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 SRC-20 and SRC-223 upgradeability.
  2. SRC-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-SRC-20 tokens are prone to this problem as they are compatible with the SRC-20 standard. rescueSRC20 function is implemented to address this problem.
  3. Token Converter relies on SRC-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 SRC-20 token that implements approve & transferFrom but at the same time implements SRC-223 logic of transferring via transfer function. It can be possible to create a SRC-223 wrapper for this SRC-20-SRC-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 SRC-223 wrapper in the Token Converter it does not automatically mean the origin is fully compatible with the SRC-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 createSRC20Wrapper function and provide an address of an existing SRC-20 token. The Token Converter will successfully create a SRC-20 wrapper for that SRC-20 original token. It is also possible to create a SRC-223 wrapper for that exact original SRC-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.

Citation

Please cite this document as:

Dexaran (@Dexaran) <dexaran@silaclassic.org>, "SRC-7417: Token Converter [REVIEW]," Sila Improvement Proposals, no. 7417, July 2023. Available: https://sips.sila.org/SIPS/sip-7417.