Overflow and Underflow in Solidity: A Deep Dive
If you have ever tried to operate on numbers and gotten a counter-intuitive result then you have most likely experienced an overflow or an underflow.
The concept of Overflow and Underflow in Solidity results from a fundamental concept in computer science. In this post, I intend to guide you through the fundamentals to help you understand the why behind it.
I assume you understand the basics of Solidity and the basics of how binary numbers work.
Copy and paste the code below on remix to replicate an Overflow and an Underflow. Deploy the code and let's walk through the code.
- Line 2: I used version 0.7.0 of solidity because overflow and underflow checking is implemented by default in versions 0.8.0 and above. Using that, the compiler would throw an error when we try to call the functions below. Moreover, It's advised you use 0.8.0 or above when writing production code so you won't have to use libraries like SafeMath or your implementation to do the checking. This is purely experimental.
- Line 6: I declared the variable balance which is set to 0 by default.
- Line 8 to Line 10: We have the underflow function. It uses line 9 to decrease the value of balance by 1. Call this function once and then check the new value of the balance; it's 255. i.e 0 minus 1 just gave us 255. That's an underflow.
- Line 12 to Line 15: We have the overflow function. It sets the value of balance to 255. And then, increments it by 1. Intuitively, the new balance value should be 256 but it's 0. That's an overflow.
Haven replicated an underflow and overflow situation. Let's go through the fundamentals.
Fundamentals of Overflow and Underflow
Binary is at the core of how computers store and process data. An 8-bit memory would only be able to store at most 8-bit representations of any form of data at any given time.
Programming languages also try to emulate this pattern to handle storage more efficiently. For example, if you wanted to store data that you know for sure won't exceed the 8-bit space why then would you create a 256-bit storage space for it? It's a waste of storage space among other disadvantages and that's why languages like solidity give us the ability to store data in different storage spaces based on data size. For integers, we could store integers from the 8 bits up to 256 bits in steps of 8 using int8/uint8 to int256/uint256 respectively.
Imagine declaring an unsigned integer and setting the storage to 8-bit storage as illustrated below in solidity.
But, at the time of assignment, an integer exceeding the 8-bit storage space is passed to the variable a. What happens then? That's what overflow and underflow are all about. Overflows and underflows are a result of data exceeding the predefined storage space created for them.
So, how do you determine the minimum and maximum integer a bit size can store? Here's the simple formula:
- For unsigned integers: 0 to (2 ^ n ) - 1 where n is the number of bits. For example, for 8 bits: 0 to (2 ^ 8) - 1 = 0 to (256) - 1 = 0 to 255
- For signed integers: (-2 ^ n -1) to (2 ^ n - 1) - 1 where n is the number of bits. For example, for 8 bits: (-2 ^ 8 -1) to (2 ^ 8 - 1) - 1 = (-2 ^ 7) to (2 ^ 7) - 1 = -128 to 127
Or, using Solidity:
To understand this better, think of it as how modulo arithmetic works. Values of arithmetic operations are wrapped around the range of numbers they can store. For example:
- 25 + 120 in signed 8-bit(int8) is -111. 25 plus 120 is 145 but the highest number that can be represented is 127 so instead of the next number being 128 it starts all over from the least value -128 and counts up to -127 then -126, -125 up to -111
- 40 + 240 in unsigned 8-bit(uint8) is 24. 40 plus 240 is 280 but the highest number that can be represented is 255 so instead of the next number being 256 it starts all over from the least value 0 then counts up to 1 then 2, 3 up to 24
- 0 - 1 in unsigned 8-bit(uint8) is 255. 0 minus 1 is -1 but the least number that can be represented is 0 so instead of the next number being -1 it goes over to the top at 255.
- -128 - 5 in signed 8-bit(int8) is 123. -128 minus 5 is -133 but the least number that can be represented is -128 so instead of the following number being -129 it starts all over from the top at 127 and counts down to 126 then 125, 124 down to 123
At this point, it is safe to say that the values in the range are like numbers on a clock and every number revolves around them.
Let's go a bit more in-depth by using binary numbers to explain the examples above.
Before we move on, here are a few things to take note of when solving in binary:
- In signed binary arithmetic, if the addition of two positive numbers gives a -ve number then an overflow/underflow has occurred.
- In signed binary arithmetic, if the addition of two negative numbers gives a +ve number then an overflow/underflow has occurred.
- In unsigned binary arithmetic, if there is a carry from the MSB(i.e for the most significant bit. it's the numbers on the left-most part of the binary digit. For example, in 00011001, 0 is the most significant bit and in 10000000, 1 is the most significant bit) then an overflow has occurred.
- In signed binary arithmetic, the MSB signifies if a number is a -ve number or +ve number. If the MSB is 0, it's a +ve number while if the MSB is 1, it's a -ve number.
- A simple trick to know if an overflow/underflow has occurred in signed arithmetic addition is if the carry into the MSB and the carry out of the MSB are different. If they are the same, an overflow/underflow has not occurred. For example, 1 & 1 and 0 & 0 mean no overflow/underflow while 0 & 1 and 1 & 0 mean there is an overflow/underflow. While for unsigned arithmetic, if there is a carry-out of the MSB then there is an overflow/underflow.
The above is quite dense but I assure you you can understand the next part without understanding it. Feel free to go through it multiple times to understand it better but for now, you can just move to the next part. The calculations:
- 25 + 120 in signed 8-bit: 25 is 00011001 and 120 is 01111000. 00011001 + 01111000 is 10010001. 10010001 is -111 in signed binary arithmetic.
- 40 + 240 in unsigned 8-bit: 40 is 00101000 and 240 is 11110000. 00101000 + 11110000 is 00011000. 00011000 is 24 in unsigned binary arithmetic.
- 0 - 1 in unsigned 8-bit: 0 is 00000000 and 1 is 00000001. 00000000 + 11111111(i.e this is the 2's complement of 00000001 which is 1. We can't perform 0 - 1 rather we do 0 + (-1) and -1 is 11111111) is 11111111. 11111111 is 255 in unsigned binary arithmetic.
- -128 - 5 in signed 8-bit: -128 is 10000000 and -5 is 11111011. Using -128 + (-5) then 10000000 + 11111011 is 01111011. 01111011 is 123 in signed binary arithmetic.
Use a binary calculator to try these out yourself. That's a wrap ✌️
As you might have noticed, this post is not about how to prevent an overflow or underflow rather it's about why you run into them. That being said, overflows and underflows are security threats and have been the cause of hacks in the past so make sure you prevent them by using solidity version 0.8.0 and above or using the SafeMath library when writing your solidity code.
This is quite complex to grasp right away so feel free to ask questions in the comment below. Please share if you found this helpful. Happy learning.