ERC20 Integer Overflow Bug Explained

For those interested in the Ethereum overflow bug found in many ERC20 tokens recently, this article is a technical review of what happened.

It is common to check against Integer overflow when writing Ethereum smart contracts. An easy way to think of overflow is when the integer reaches the max value, it “wraps around” and starts from 0. Vice versa for underflow.

Beautychain ERC20 token smart contract was affected by this bug. A transaction where this happened can be found here.

Looking the transaction payload in detail:

Function: batchTransfer(address[] _receivers, uint256 _value)
MethodID: 0x83f12fec
[0]: 0000000000000000000000000000000000000000000000000000000000000040
[1]: 8000000000000000000000000000000000000000000000000000000000000000
[2]: 0000000000000000000000000000000000000000000000000000000000000002
[3]: 000000000000000000000000b4d30cac5124b46c2df0cf3e3e1be05f42119033
[4]: 0000000000000000000000000e823ffe018727585eaf5bc769fa80472f76c3d7

The methodID is the first 4 bytes of the Keccak (SHA-3) hash of the batchTransfer function signature, ie 0x83f12fec.

Parameter 0: The position of the data part of the dynamic address array, ie 4 X 16 Bytes = 64th byte

Parameter 1: The input integer value, ie 16 ** 63 * 8 = 57896044618658097711785492504343953926634992332820282019728792003956564819968

Parameter 2: The address array length is 2. Dynamic array always need to start with the array length.

Parameter 3: The first address is 0b4d30cac5124b46c2df0cf3e3e1be05f42119033

Parameter [4]: The second address is 0e823ffe018727585eaf5bc769fa80472f76c3d7

Beautychain’s original flawed smart contract can be found here and the function that allowed that to happen with commented explanation:

// inputs passed in to this function based on the parameters above.
function batchTransfer(address[] _receivers, uint256 _value) public whenNotPaused returns (bool) {

// cnt = 2
uint cnt = _receivers.length;
// the limit of uint before it overflows is 2**256-1
// which is 1.15792089237316195423570985008E+77
// so the value passed in is 5.78960446186580977117854925043E+76
// multipled by cnt(2), the amount is now overflowed, ie = 0.
uint256 amount = uint256(cnt) * _value;

// cnt fulfils this condition
require(cnt > 0 && cnt <= 20);
// _value is > 0 and balances[msg.sender] always >= 0
require(_value > 0 && balances[msg.sender] >= amount);
// sender's acct not affected after it was subtracted by 0
balances[msg.sender] = balances[msg.sender].sub(amount);
    // for loop is limited to 20. there will always be enough gas.
for (uint i = 0; i < cnt; i++) {
// both receiver address gets lots of free money here!
balances[_receivers[i]] = balances[_receivers[i]].add(_value);
Transfer(msg.sender, _receivers[i], _value);
}
return true;
}

Conclusion

The contract included the SafeMath library to help with the integer overflow protection but it wasn’t utilised on a specific line in the batchTransfer function. It must have been a human error and if there was an audit, it could have been picked up easily by any competent solidity developer. Having said that, the code also contains some other serious flaws. Can you see them?

This function wasn’t part of the standard open zeppelin library and only affected a smart number of ERC20 tokens. I felt many exchanges over-reacted by freezing all ERC20 tokens trading. If they knew what was happening, they only need to freeze the affected ones. When exchanges decide to list an ERC20 token, they should go through the smart contract in detail. The developer or company must have copied the same contract for different clients blindly. The whole event was unfortunate but could have been prevented.