The Smart Contract Weakness Classification and Test Cases (SWC) Registry is a set of Web3 vulnerabilities to avoid when writing smart contracts. It may seem daunting to understand every issue so I’ll do my best to demystify every issue and try my best to explain each issue with real-world examples. This write-up is intended for those who are just starting out in solidity development and smart contract auditing.
SWC-101: Integer Overflow and Underflow
(Note: After Solidity version 0.8.0, the integer reverts instead when facing an over/underflow issue. This means that this issue will not be a problem if the smart contract is using a solidity version greater than 0.8.0. The current version of solidity is 0.8.19, but most contracts do not update to the latest version. For example, Uniswap’s V2 uses version 0.5.16, which may be susceptible to over/underflow. Do note the version of solidity when auditing/developing smart contracts)
To understand the issue, we must understand what is a uint, an int, and the different types of uint values, eg uint8, uint16, uint256. We must also understand what a literal ‘overflow’ means. In this write-up, we will be going through these four bullets.
Can there be a maximum number in solidity?
What is an overflow/underflow and how does it work?
How to circumvent the issue
Summary
Can there be a maximum number in solidity?
Yes, there can be a maximum number if the developer intends to have it. There can also be negative numbers (if we are talking about debt), but most of the time, solidity works with positive numbers (balance in a contract/wallet).
When a contract declares an integer variable, you will see a data type associated with the variable.
The ‘u’ in uint means unsigned. The ‘int’ in uint means Integer. Unsigned integers only contain positive numbers (or zero). Also, if you see a uint, it is equivalent to writing uint256. In the above image, the maximum number that totalSupply can go to is 0 - 2 ** 256. 2 ** 256 is an extremely large number, which is actually
115792089237316195423570985008687907853269984665640564039457584007913129639936.
Sometimes, developers don’t like to use large numbers in their protocol. For example, if my variable is about storing whitelisted members, I probably only want about 100 whitelisted people. Thus, I don’t need to write
uint public membersListed;
I can write this instead:
uint8 public membersListed;
This means that the maximum value that membersListed can go to is 2 ** 8, which is 256.
Right now, imagine a contract that increases the value of membersListed by 1 every time a person clicks a button. (By the way, if no value is assigned to a variable, the default value is 0, so membersListed starts from 0)
Imagine you are the 255th person to go to a secret website to click the button. You log into the page, wait for the page to refresh, see the button, then click the button. The value of membersListed goes up from 255 to 256.
What is an overflow/underflow and how does it work?
Assuming that there is no validation check in the contract, ie if the number reaches 256, then the button stops appearing. Now, your friend happens to get the link to the secret website as well. He is the 256th person and he clicks the button. What happens? Does the variable value go up to 257? Does the function revert?
Recall that the maximum value of membersListed is set to uint8, which is 256. If the contract is using a solidity version below 0.8.0, then the counter will go to 0. In other words, if the maximum number is reached, and the value is still being added, the value will loop back to the minimum number, which is 0. This is called an overflow, because the new value exceeds the maximum value that the variable is supposed to have.
Underflow works the opposite way. Let’s say that there’s a way to reduce the number of membersListed by one each time the button is clicked. The admin clicks the button 255 times until the value of membersListed is 0. When you click the button again after 0, the value loops back to 256, because the minimum value is reached and the function still attempts to decrease the value.
How to circumvent the issue
There are three simple ways to eliminate the risk of overflow/underflow.
Firstly, update the solidity version to 0.8.0 and above. Solidity version 0.8.0 onwards introduces a revert when the value goes above the maximum value of a variable. Instead of overflowing or underflowing, the function stops working. Going back to the example above, if your friend is the 256th person and he clicks the button, then the function will revert and the value of membersListed will not increase.
Secondly, create validation checks. In the function, make sure that if the max number is reached, do not attempt to increase the number again. The validation check can be something like
if(membersListed == type(uint8).max){revert};
If membersListed reaches a max value of uint256, do not increase anymore.
Lastly, import external code like OpenZeppelin’s SafeMath library to prevent overflow/underflow vulnerabilities.
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/SafeMath.sol
Usually, developers do these three mitigation steps together, and not in isolation. They will make sure that the version is up to date, add additional validation checks, and add external imports to create many layers of security.
Summary
Integer values in solidity have a maximum value
The default value for an integer variable, if not set, will be 0
The simplest check to prevent overflow/underflow is to update the Solidity version to 0.8.0 or higher
I have not seen a real-world contract that got hacked because of this vulnerability. Nevertheless, it is still an important concept to note.