Arithmetic Underflow and Overflow Overview
The Ethereum Virtual Machine (EVM) specifies fixed-size data types for integers. This means that an integer variable only has a certain range of numbers it can represent. A uint8, for example, can only store integers between 0 and 255. Trying to store the value 256 into a uint8 will result in zero. If care is not taken, variables in Solidity can be exploited when user input is unchecked and calculations that produce numbers outside the range of the data type that stores them are performed.
This sort of vulnerability is certainly not new: an integer overflow, which arises from the fact that most computer languages work with integers that have a limited range. Perform a calculation where the result falls outside that range and the result of the operation is no longer accurate, violating the common-sense assumption. For example two very large numbers added together can produce a very small number. Or two positive values multiplied may yield a negative product. These types of bugs are very well known in the realm of low-level languages such as C and C++. Combined with weak type-safety, lack of range checking and manual memory management, such flaws often provide a starting point for building a full remote-code execution exploit. Ethereum virtual machine (EVM) design and the Solidity language resurrected a vulnerability class from low-level systems programming in the context of a wildly different environment.
In Solidity, you can perform many different operations with numbers. One example is arithmetic operations, which can have the associated problem of an integer overflow, if care is not taken wit the code.
There are two types of integers in Solidity:
- uint: unsigned integers - positive numbers ranging from 0 to (2²⁵⁶ - 1) [UINT_MIN, UINT_MAX]
- int: signed integers - both positive and negative numbers ranging from -2²⁵⁵ to (2²⁵⁵ - 1), [INT_MIN, INT_MAX]
Image 1 Source: https://valid.network/post/integer-overflow-in-ethereum
In signed int, the LMB (leftmost bit) represents the sign of the number and thus signed int. 0 in the LMB stands for positive numbers, and 1 stands for negative numbers. Therefore, the number of bits in a number value is decreased from 256 to 255. Initially it can be seen that in the unsigned number circle there can be either the addition of 2 numbers that overflow to a smaller value, or the subtraction of 2 numbers that underflow to a greater value. However in the signed circle, due to the sign, both overflow and underflow can occur within the same operation.
An over/under flow occurs when an operation is performed that requires a fixed size variable to store a number that is outside the range of the variable's data type.
For example, subtracting 1 from a uint8 (unsigned integer of 8 bits) variable that stores 0 as it's value, will result in the number 255. This is an underflow. A number has been assigned below the range of the uint8, 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 the entire length of the uint has been wrapped around. Adding numbers larger than the data type's range is called an overflow. For clarity, adding 257 to a uint8 that currently has a zero value will result in the number 1. It's sometimes helpful to think of fixed type variables as cyclic, where they start again from zero if numbers are added above the largest possible stored number, and vice-versa for zero (where it starts counting down from the largest number, as more is subtracted from 0).
In the case of uint (the alias for uint256) Solidity can handle up to 256 bit numbers (up to 2^256 -1), so incrementing by 1 would result into 0. Likewise, in the inverse case, when the number is unsigned, decrementing will underflow the number, resulting in the maximum possible value. In Solidity, cryptocurrency accounts have their balances defined as uint256 type, meaning that using this vulnerability attackers could flip an empty wallet to maximum value, and a wealthy person could overflow their wallet with an erroneous transaction.
Overflow and underflow in the uint data type can be more clearly understood with the following contract in Remix:
Image 2 and 3: Overflow.sol in Remix
Once deployed, the blue buttons can be used to check that the value of zero is 0 and the value of max is 2^256-1 = 115792089237316195423570985008687907853269984665640564039457584007913129639935. Now the function underflow can be called (by pressing the orange button with the function name) to subtract 1 from the uint variable zero whose value is 0. As a result, the variable zero will underflow and now have the maximum value for the type, of 2^256-1. Next, the function overflow can be called, to add 1 to the uint variable max whose value is 2^256-1. As a result, the variable max will overflow and now have the minimum value for the type, of 0.
Image 3 and 4: Overflow.sol in Remix
Examples of the Vulnerability
The most straightforward example is a function that does not check for integer underflow, allowing a very large amount of tokens to be withdrawn (Solidity versions prior to 0.8.0):
Image 5: Function withdraw
The second example (spotted during the Underhanded Solidity Coding Contest) is an off-by-one error facilitated by the fact that an array's length is represented by an unsigned integer (Solidity versions prior to 0.8.0):
Image 6: Function popArrayOfThings
The third example is a single transaction overflow, where the result of a multiplication on two unsigned integers is an unsigned integer (Solidity versions prior to 0.8.0):
Image 7: Contract IntegerOverflowMul
The fourth example is the same file overflow.sol that was used with Remix, but in Solidity 0.8.13 using unchecked blocks. This contract can be used to show that unchecked blocks are not tested for integer overflow and underflow and the vulnerability persists:
Image 8: Using unchecked
These kinds of numerical caveats allow attackers to misuse code and create unexpected logic flows. In the fifth example, consider the time locking contract below: https://github.com/ylevalle/SolidityOverflow/blob/main/timelock.sol
Image 9: Timelock contract
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 could 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 extract 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). Calling this value userLockTime, they could then call the increaseLockTime function and pass as an argument the number 2^256 - userLockTime. The 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.
For the last example, there is an integer overflow bug that has been dubbed “batchOverflow.” The following is a slight simplification of the erroneous code found in several ERC20 token contracts. This function allows a token holder to send tokens to multiple recipients:
Image 10: batchOverflow
The require is meant to ensure the sender has a sufficient balance to cover the transfers, but note that the amount is the product of two values controlled by the caller. If someone were to pass 2 addresses and a value of 2^255, then the amount would overflow to 0. The require would verify that the sender’s balance was at least 0, and the recipients’ token balances would be increased.
Note that the use of SafeMaths’s sub to reduce the sender’s balance doesn’t help here because amount is 0, so that subtraction has no overflow.
More examples of this vulnerability can be found in the different arithmetic operations and in different solidity versions on my github: https://github.com/ylevalle/SolidityOverflow
While in other software languages there is an indication of arithmetic integer overflow (for example, Overflow flag in Assembly), that is not the case for EVM. There is no indication that an overflow has occurred during an execution of a transaction on the EVM. In some cases, it can be deduced that an overflow has occurred from the values that are stored after the execution of the transaction. Although most probably, the transaction will have to be re-run to expose overflows using different heuristics.
Integer overflow/underflow can occur after the addition or subtraction of 2 numbers. However, because the multiplication operation is based on addition and exponentiation is based on multiplication, they can cause overflow as well. So specifically, to EVM, the following opcodes that can all cause an integer overflow: ADD, SUB, MUL, EXP.
Newer versions of Solidity (Solidity 0.8 and higher) throw an error for overflow/underflow, but all unchecked blocks should still be analysed, as seen in the previous examples. In older solidity versions all arithmetic functions should be checked to make sure that they are using SafeMath for every addition, subtraction, exponentiation and multiplication. Some of the smart contracts security tools, like MythX and Mythril, can also help detecting integer overflows and underflows.
1) The easiest way to prevent this bug is to use at least a 0.8 version of the Solidity compiler. In Solidity 0.8, the compiler will automatically take care of checking for overflows and underflows. However, be sure the code is designed properly to avoid Denial of Service attacks based on integer overflow. Running the same example https://github.com/ylevalle/SolidityOverflow/blob/main/overflow.sol in Remix but with Solidity 0.8.0. When the new version is deployed, trying to hit "overflow" or "underflow" will generate an error in the Remix console and the transaction will be reverted:
Image 11: Overflow.sol in Solidity 0.8.0
2) The preventative technique used to guard against under/overflow vulnerabilities in older versions is to use or build mathematical libraries which replace the standard math operators; addition, subtraction, exponentiation and multiplication (division is excluded as it doesn't cause over/under flows and the EVM reverts on division by 0). In June 2017, OpenZeppelin created the SafeMath.sol library aiming to tackle the underflow and overflow issues, SafeMath is now a well-known library used in many contracts. It provides safe addition, subtraction, and multiplication, by carefully comparing the operators and operands before the operation, but can also check the preconditions and postconditions to understand whether an overflow has occurred. When such an error is detected, the library fails the execution of the transaction and updates the status of the transaction as ‘Reverted’.
To demonstrate how this library is used in Solidity, the Timelock.sol contract can corrected using the SafeMath library. The overflow-free version of the contract is:
Image 12: Timelock.sol using SafeMath
Notice that all standard math operations have been replaced by those defined in the SafeMath library. The Timelock contract no longer performs any operation capable of under or overflow.
Phantom or False Overflows
Given that Solidity does not handle fractions, how can 3% of the variable x be calculated in older versions of Solidity? In most circumstances, x*3/100 will be enough, but what if x is so large that x*3 will overflow? The answer is by using SafeMath as shown in the preventative techniques section: mul (x, 3) / 100. This later version is more secure as it reverts, whereas the former version returns an incorrect result. But why could calculating 3% of something ever overflow when 3% of something is always smaller than the original value? This is what is called "phantom overflow" or "false overflow": when the final computation result will fit inside the result data type, but an intermediate action overflows.
Phantom overflows are much harder to detect and address than real overflows. One solution for this problem is to use wider integer types or even floating-point type for intermediate values. Another is to refactor the expression in order to make phantom overflow impossible.
Real Life Examples and Attacks using arithmetic overflows and underflows
1) BeautyChain (BEC) contract is a great example of using an integer overflow as a vulnerability to perform an attack on a contract. The attacker used the behaviour of integer overflow to overcome some security checks and stole a huge amount of BEC tokens.
- The BEC contract: https://etherscan.io/address/0xc5d105e63711398af9bbff092d4b6769c82f793d#code
- The ‘batchTransfer’ transaction that performed the attack: https://etherscan.io/tx/0xad89ff16fd1ebe3a0a7cf4ed282302c06626c1af33221ebe0d3a470aba4a660f
- Detailed post describing the attack: https://medium.com/secbit-media/a-disastrous-vulnerability-found-in-smart-contracts-of-beautychain-bec-dbf24ddbc30e
2) A 4chan group decided it was a great idea to build a ponzi scheme on Ethereum, written in Solidity. They called it the Proof of Weak Hands Coin (PoWHC). Unfortunately it seems that the author(s) of the contract hadn't seen over/under flows before and consequently, 866 ether was liberated from its contract. A good post describing the attack can be found here: Eric Banisadar's post.
3) The mintToken function of a smart contract implementation for Coinstar (CSTR), an Ethereum token, has an integer overflow that allows the owner of the contract to set the balance of an arbitrary user to any value. https://github.com/VenusADLab/EtherTokens/blob/master/MyAdvancedToken/MyAdvancedToken.md
Many people do not fully understand the implication of software flaws and how they enable vulnerabilities to exist. Understanding how numbers are represented by computers, what signed and unsigned numbers are, and what an integer overflow attack is, are all critical to understanding the full scope of these vulnerabilities. What may seem like a simple problem can have catastrophic consequences leading to exploitable situations, as can be seen in the real life examples and attacks.
It is also important to recognize that overflow and underflow can happen in different arithmetic operations and in different versions of Solidity, it even remains possible in version 0.8 for operations inside unchecked blocks. Additionally, it is important to pay attention to possible logic and arithmetic errors in batch operations that can lead to an underflow or overflow and to the existence of phantom or false overflows in the code.
Fortunately though, there are detection and prevention techniques to help prevent and detect these vulnerabilities before launch, and so avoid unnecessary financial and time expense.
References and Useful Links: