Arithmetic Over/Underflows

The Ethereum Virtual Machine specifies fixed-size data types for integers. This means that an integer variable can represent only a certain range of numbers. A uint8, for example, can only store numbers in the range [0,255]. Trying to store 256 into a uint8 will result in 0. If care is not taken, variables in Solidity can be exploited if user input is unchecked and calculations are performed that result in numbers that lie outside the range of the data type that stores them.

For further reading on arithmetic over/underflows, see “How to Secure Your Smart Contracts”, Ethereum Smart Contract Best Practices, and “Ethereum, Solidity and integer overflows: programming blockchains like 1970”.

The Vulnerability

An over/underflow occurs when an operation is performed that requires a fixed-size variable to store a number (or piece of data) that is outside the range of the variable’s data type.

For example, subtracting 1 from a uint8 (unsigned integer of 8 bits; i.e., nonnegative) variable whose value is 0 will result in the number 255. This is an underflow. We have assigned a number below the range of the uint8, so the result wraps around and gives the largest number a uint8 can store. Similarly, adding 2^8=256 to a uint8 will leave the variable unchanged, as we have wrapped around the entire length of the uint. Two simple analogies of this behavior are odometers in cars, which measure distance traveled (they reset to 000000, after the largest number, i.e., 999999, is surpassed) and periodic mathematical functions (adding 2π to the argument of sin leaves the value unchanged).

Adding numbers larger than the data type’s range is called an overflow. For clarity, adding 257 to a uint8 that currently has a value of 0 will result in the number 1. It is sometimes instructive to think of fixed-size variables as being cyclic, where we start again from zero if we add numbers above the largest possible stored number, and start counting down from the largest number if we subtract from zero. In the case of signed int types, which can represent negative numbers, we start again once we reach the largest negative value; for example, if we try to subtract 1 from a int8 whose value is -128, we will get 127.

These kinds of numerical gotchas allow attackers to misuse code and create unexpected logic flows. For example, consider the TimeLock contract in TimeLock.sol.

Example 3. TimeLock.sol

  1. contract TimeLock {
  2. mapping(address => uint) public balances;
  3. mapping(address => uint) public lockTime;
  4. function deposit() external payable {
  5. balances[msg.sender] += msg.value;
  6. lockTime[msg.sender] = now + 1 weeks;
  7. }
  8. function increaseLockTime(uint _secondsToIncrease) public {
  9. lockTime[msg.sender] += _secondsToIncrease;
  10. }
  11. function withdraw() public {
  12. require(balances[msg.sender] > 0);
  13. require(now > lockTime[msg.sender]);
  14. balances[msg.sender] = 0;
  15. msg.sender.transfer(balance);
  16. }
  17. }

This contract is designed to act like a time vault: users can deposit ether into the contract and it will be locked there for at least a week. The user may extend the wait time to longer than 1 week if they choose, but once deposited, the user can be sure their ether is locked in safely for at least a week—or so this contract intends.

In the event that a user is forced to hand over their private key, a contract such as this might be handy to ensure their ether is unobtainable for a short period of time. But if a user had locked in 100 ether in this contract and handed their keys over to an attacker, the attacker could use an overflow to receive the ether, regardless of the lockTime.

The attacker could determine the current lockTime for the address they now hold the key for (it’s a public variable). Let’s call this userLockTime. They could then call the increaseLockTime function and pass as an argument the number 2^256 - userLockTime. This number would be added to the current userLockTime and cause an overflow, resetting lockTime[msg.sender] to 0. The attacker could then simply call the withdraw function to obtain their reward.

Let’s look at another example (Underflow vulnerability example from Ethernaut challenge), this one from the Ethernaut challenges.

SPOILER ALERT: If you have not yet done the Ethernaut challenges, this gives a solution to one of the levels.

Example 4. Underflow vulnerability example from Ethernaut challenge

  1. pragma solidity ^0.4.18;
  2. contract Token {
  3. mapping(address => uint) balances;
  4. uint public totalSupply;
  5. function Token(uint _initialSupply) {
  6. balances[msg.sender] = totalSupply = _initialSupply;
  7. }
  8. function transfer(address _to, uint _value) public returns (bool) {
  9. require(balances[msg.sender] - _value >= 0);
  10. balances[msg.sender] -= _value;
  11. balances[_to] += _value;
  12. return true;
  13. }
  14. function balanceOf(address _owner) public constant returns (uint balance) {
  15. return balances[_owner];
  16. }
  17. }

This is a simple token contract that employs a transfer function, allowing participants to move their tokens around. Can you see the error in this contract?

The flaw comes in the transfer function. The require statement on line 13 can be bypassed using an underflow. Consider a user with a zero balance. They could call the transfer function with any nonzero _value and pass the require statement on line 13. This is because balances[msg.sender] is 0 (and a uint256), so subtracting any positive amount (excluding 2^256) will result in a positive number, as described previously. This is also true for line 14, where the balance will be credited with a positive number. Thus, in this example, an attacker can achieve free tokens due to an underflow vulnerability.

Preventative Techniques

The current conventional technique to guard against under/overflow vulnerabilities is to use or build mathematical libraries that replace the standard math operators addition, subtraction, and multiplication (division is excluded as it does not cause over/underflows and the EVM reverts on division by 0).

OpenZeppelin has done a great job of building and auditing secure libraries for the Ethereum community. In particular, its SafeMath library can be used to avoid under/overflow vulnerabilities.

To demonstrate how these libraries are used in Solidity, let’s correct the TimeLock contract using the SafeMath library. The overflow-free version of the contract is:

  1. library SafeMath {
  2. function mul(uint256 a, uint256 b) internal pure returns (uint256) {
  3. if (a == 0) {
  4. return 0;
  5. }
  6. uint256 c = a * b;
  7. assert(c / a == b);
  8. return c;
  9. }
  10. function div(uint256 a, uint256 b) internal pure returns (uint256) {
  11. // assert(b > 0); // Solidity automatically throws when dividing by 0
  12. uint256 c = a / b;
  13. // assert(a == b * c + a % b); // This holds in all cases
  14. return c;
  15. }
  16. function sub(uint256 a, uint256 b) internal pure returns (uint256) {
  17. assert(b <= a);
  18. return a - b;
  19. }
  20. function add(uint256 a, uint256 b) internal pure returns (uint256) {
  21. uint256 c = a + b;
  22. assert(c >= a);
  23. return c;
  24. }
  25. }
  26. contract TimeLock {
  27. using SafeMath for uint; // use the library for uint type
  28. mapping(address => uint256) public balances;
  29. mapping(address => uint256) public lockTime;
  30. function deposit() external payable {
  31. balances[msg.sender] = balances[msg.sender].add(msg.value);
  32. lockTime[msg.sender] = now.add(1 weeks);
  33. }
  34. function increaseLockTime(uint256 _secondsToIncrease) public {
  35. lockTime[msg.sender] = lockTime[msg.sender].add(_secondsToIncrease);
  36. }
  37. function withdraw() public {
  38. require(balances[msg.sender] > 0);
  39. require(now > lockTime[msg.sender]);
  40. balances[msg.sender] = 0;
  41. msg.sender.transfer(balance);
  42. }
  43. }

Notice that all standard math operations have been replaced by those defined in the SafeMath library. The TimeLock contract no longer performs any operation that is capable of under/overflow.

Real-World Examples: PoWHC and Batch Transfer Overflow (CVE-2018–10299)

Proof of Weak Hands Coin (PoWHC), originally devised as a joke of sorts, was a Ponzi scheme written by an internet collective. Unfortunately it seems that the author(s) of the contract had not seen over/underflows before, and consequently 866 ether were liberated from its contract. Eric Banisadr gives a good overview of how the underflow occurred (which is not too dissimilar to the Ethernaut challenge described earlier) in his blog post on the event.

Another example comes from the implementation of a batchTransfer() function into a group of ERC20 token contracts. The implementation contained an overflow vulnerability; you can read about the details in PeckShield’s account.