p0n1 / badERC20Fix

Fix for the contracts which are not compatible with ERC20

Home Page:https://secbit.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

badERC20Fix License: GPL v3 Join the chat at https://gitter.im/sec-bit/Lobby PRs Welcome

Read the docs in Chinese: https://github.com/sec-bit/badERC20Fix/blob/master/README_CN.md


An enormous amount of ERC20 Token contracts do not follow EIP20 strictly [1], which undermines DApp developing significantly.

Specially, after Solidity compiler's upgrading to version 0.4.22 on April 17th, compiled Solidity code would become incompatible with a few non-standard smart contracts [2], causing difficulties in DApp programming [3].

Please refer to SECBIT team's former reports for details [4][5]. We would introduce this solution in areas of analysis, application scenarios and gas estimation.

Token Contracts with Return-Value Incompatibility

If you want to get a list of incompatible Token contracts for DApp and DEX developing, please refer to awesome-buggy-erc20-tokens repo.

Code Satisfying ERC20 Specification

contract TokenStd {
    function transfer(address _to, uint256 _value) public returns (bool success) {
        return true;
    }
    function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) {
        return true;
    } 
    function approve(address _spender, uint256 _value) public returns (bool success) {
        return true;
    }
}

Code Unsatisfying ERC20 Specification

// NEVER USE THIS
contract TokenNoStd {
    function transfer(address _to, uint256 _value) public {
    }
    function transferFrom(address _from, address _to, uint256 _value) public {
    }
    function approve(address _spender, uint256 _value) public {
    }
}

Sides Affected

Smart contracts would get affected if they are built in Solidity version >= 0.4.22 and they require calling transfer(), transferFrom() or approve() in other ERC20 Token contracts.

It mainly affects the 2 sides below:

  • Decentralized exchanges (DEX)
  • DApps using ERC20 Token

Consequences

  • Several non-standard Token contracts are not able to perform transactions normally
  • Part of tokens monitored by the contract might get locked forever

It is safe to say that a growing number of ERC20 Token API callings would fail(functions like transfer() would fail in the end due to revert conducted by EVM) as lots of DApps get upgraded.

Analysis

The incompatibility results partly from not strictly following ERC20 standard, also from the inconsistent behavior in Solidity compiler.

Take a look at the code sample:

pragma solidity ^0.4.18;

interface Token {
    function transfer(address _to, uint256 _value) returns (bool success);
}

contract DApp {
  function doTransfer(address _token, address _to, uint256 _value) public {
    require(Token(_token).transfer(_to, _value));
  }
}

This sample calls transfer() in the target ERC20 contract and compiles differently in Solidity 0.4.21 and 0.4.22:

0.4.21

0.4.21

0.4.22

0.4.22

Obviously, the bytecode compiled by 0.4.22 version checks that if RETURNDATASIZE is smaller than 32. The target contract will not pass the test without a return value, thus triggering revert. The solution is bypassing the auto-test on RETURNDATASIZE by compiler and handling manually.

Solution for DApp and DEX

function isContract(address addr) internal {
    assembly {
        if iszero(extcodesize(addr)) { revert(0, 0) }
    }
}

function handleReturnData() internal returns (bool result) {
    assembly {
        switch returndatasize()
        case 0 { // not a std erc20
            result := 1
        }
        case 32 { // std erc20
            returndatacopy(0, 0, 32)
            result := mload(0)
        }
        default { // anything else, should revert for safety
            revert(0, 0)
        }
    }
}

function asmTransfer(address _erc20Addr, address _to, uint256 _value) internal returns (bool result) {
    // Must be a contract addr first!
    isContract(_erc20Addr);  
    // call return false when something wrong
    require(_erc20Addr.call(bytes4(keccak256("transfer(address,uint256)")), _to, _value));
    // handle returndata
    return handleReturnData();
}

One approach is to apply call() to invoke transfer() and check by getting returndatasize() with inline assembly code manually.

  • Getting 0 means that the called ERC20 contract transfers successfully without a return value.

  • Getting 32 means that the contract meets with ERC20 standard. Please directly call returndatacopy() and get the return value for test by mload().

  • Else call revert.

returndatacopy() copies 0-32 Byte in RETURNDATA to 0-32 Byte in memory for mload() getting the return value afterwards.

memory[destOffset:destOffset+length] = RETURNDATA[offset:offset+length]

The full middle layer code needs to support transferFrom() and approve(). Please refer to badERC20Fix.sol for details. Forking for test and discussion is also welcomed.

Aside from this, solutions by Lukas Cremer [6] and BrendanChou [7] employs non-standard function transfer(address to, uint value) external interface for function calling.

We suggest not to follow this pattern, as the community is supposed to promote interfacing meeting with ERC20 standard.

Application Scenarios

DApp and DEX developers using Solidity 0.4.22 and above should load the encapsulated ERC20AsmFn Library in the repo and apply to the standard ERC20 contract (using ERC20AsmFn for ERC20).

Substitue asmTransfer(), asmTransferFrom() and asmApprove() for transfer(), transferFrom(), approve() when it comes to calling an ERC20 Token contract.

Please handle return values in functions above accordingly.

Each transfer() by asmTransfer() calling consumes 244 more gas than before by our computation. We can actually optimize further by calling directly with the parsed function signature.

require(_erc20Addr.call(bytes4(keccak256("transfer(address,uint256)")), _to, _value));

can change to

require(_erc20Addr.call(0xa9059cbb, _to, _value));

Thus the excessive gas cost would reduce from 244 to 96.

SECBIT team would keep an eye on this ERC20 incompatibility and offer solutions with top security and efficiency continuously. We welcome further discussions to build a better open-source security community.

Reference

License

GNU General Public License v3.0

About

Fix for the contracts which are not compatible with ERC20

https://secbit.io

License:GNU General Public License v3.0