Starting with Solidity version 0.4.22, people are experiencing unexpected reverts especially in connection with calling functions on some ERC20 tokens. This article would like to explain why this is happening and tries to suggest that this might be better than how it behaved before.

When did it happen?

With 0.4.22, we enabled the use of the Byzantium opcodes. More generally, Solidity now has a setting to select which EVM version is targeted. The two opcodes returndatasize and returndatacopy finally enabled accessing dynamic arrays being returned from function calls. The ABI encoding of dynamic arrays contains pointers and in order to protect against wild pointers being returned from potentially malicious contracts, we enabled a check that these pointers are not out of bounds. Together with this check, we thought it would be a good idea to do that in general: Whenever we decode returned data, we check that we do actually read from the returned data and not from some other part of the memory which could contain garbage.

This check is only possible on post-Byzantium EVMs, because it needs the returndatasize opcode.

I hope you now think that this is a good thing to do, but what does this have to do with your code?

Where does it happen?

Code that experiences this behaviour does not correctly adhere to the required interface specifications. As an example, let us take a look at the ERC20 token interface. All ERC20 tokens have to contain the following function:

function transfer(address to, uint amount) public

returns (bool success) { ... }

The important part here is the bool success . The function is supposed to return true only if the transfer actually happened (i.e. if the sender had enough funds). Some tokens chose to implement this function by either returning true or reverting the call altogether. This is fine and might even be better in some situations. Because of that, the return value was deemed unnecessary and contracts were written that had the following function:

function transfer(address to, uint amount) public { ... }

These contracts are not ERC20 contracts.

If you implement a contract C that inherits from a contract or an interface that is identical to the ERC20 specification, but contains the transfer function without return parameters, the compiler will complain that you did not properly override the function.

Why did it work before?

Calling the transfer function on a contract that is not ERC20 compliant because it returns the wrong type worked before Solidity 0.4.22. The reason is that before byzantium, smart contracts have no feasible way to check the length of the returned data. Because of that, the caller just has to assume that the callee provided enough data to be decoded in the given types. The caller decoded 32 bytes somewhere in memory and assumed it was the representation of a bool variable. Usually, callers of transfer check that it returns true . This check worked, even though the called contract did not return anything, because there was garbage written in memory at that point and most of the time, this garbage is not 0, but some of the other 2²⁵⁶-1 values (0 is the only representation of false ).

What can we do?

Always be careful if you make assumptions about the type of a contract you do not know. Explicit type conversions between contracts are allowed in Solidity, but the compiler has to trust the user that this is the right thing to do. Most of the time, you pass in an address from the user interface anyway, where the compiler is not even involved in the checking.

In my opinion, checking the size of the returned data is the correct thing to do. It might have been better to wait for version 0.5.0, but there is an easy workaround if you still want to use the old behaviour:

The compiler has a switch solc --evm-version homestead to use the old version of the EVM which does not have returndatasize . This way, the check about the size of the returned data is not performed. You will decode garbage which can lead to security problems, but at least it will still work.

If you have a contract that uses the wrong ERC20 interface, it is probably best to implement a proxy contract that is an actual ERC20-compliant contract and which just forwards all calls to the original contract.

Discussion: https://github.com/ethereum/solidity/issues/4116

Acknowledgements

This behaviour was noticed, among others, by Dean Eigenmann, Matthew Di Ferrante, Ivo Georgiev and Yaron Velner.