This is the third part of the Smart Contracts series where issues about smart contracts are broken into small chunks. All the examples were run in my local blockchain using Ethereum’s remix IDE. How does an overflow really occur?
What is an Integer Overflow or Underflow
In every programming language, there is a buffer where part of the memory is allocated to execute the instructions or store data. The same applies especially for Solidity, where each extra storage on a smart contract which means more money spent. So it is common to use uint8 instead of a uint256, to save money on gas. In case the buffer has a size from 0 to 255 (like in uint8), in case 257 bytes are passed, this will overflow, go back to 0 and result to 1. The same happens when a value has a range from 0 to 255 and while it is at 3, we remove 5. This instead of -2 will result to 254, which is not what we expected at all.
What Data Types are supported by Solidity?
Solidity, the programming language primarily used for Ethereum smart contracts, features a variety of data types. These types are essential for handling data efficiently and can be categorized into value types and reference types.
Value Types
- Integers: Solidity offers both signed (
int
) and unsigned (uint
) integers in various sizes, such asuint8
,uint16
,uint256
, etc. The numeral represents the number of bits. For example,uint8
can range from 0 to 28−128−1. A significant aspect to note is the vulnerability of these types to overflow and underflow. If you increment auint8
at its maximum value (255), it will overflow and reset to 0. Similarly, decrementing auint8
at 0 will cause an underflow, making it wrap to 255. - Boolean: This type is used for representing boolean values, i.e., true or false.
- Bytes: Solidity includes fixed-size byte sequences (
bytes1
tobytes32
) and a dynamicbytes
type for variable-length data. - Address: Specifically designed for storing Ethereum addresses.
Reference Types
- Strings: Utilized for arbitrary-length UTF-8 data. It’s important to remember that strings in Solidity are not as efficient as in other high-level programming languages due to the way Ethereum Virtual Machine handles data.
How to identify and exploit Overflow vulnerabilities
These vulnerabilities are especially pertinent in contracts dealing with financial transactions, where they can be exploited to manipulate balances or token quantities. To identify such vulnerabilities, one should meticulously review all arithmetic operations, particularly those involving external inputs or critical financial calculations. Special attention should be paid to loops and recursive calls that increment or decrement variables, as well as to any math involving user-supplied data.
It’s important to note that starting with Solidity version 0.8.0, the language introduced built-in checks for arithmetic operations, effectively preventing overflows and underflows. This was a significant enhancement for the security of smart contracts. In versions prior to 0.8.0, such checks had to be manually implemented or relied upon external libraries like OpenZeppelin’s SafeMath. Therefore, when auditing or reviewing smart contracts, one must be particularly cautious with contracts compiled with Solidity versions lower than 0.8.0. These contracts might not inherently possess the same level of protection against overflow and underflow vulnerabilities and thus could be at higher risk of being exploited if adequate safeguards were not implemented by the developers.
Demo Overflow Vulnerable Smart Contract
The contract below is vulnerable by design because it allows users to add extra bytes to a 255 bytes limited buffer. As you will also see, the contract uses solidity version 0.7.6, which does not prevent overflow attacks. So let’s deploy the contract and interact with it.
// SPDX-License-Identifier: MIT pragma solidity ^0.7.6; contract VulnerableToOverflow { uint8 public count; constructor() { count = 0; } function addToCount(uint8 _value) public { count += _value; } }
After deploying the smart contract we can see that it is possible to add 150 to the count variable and when calling the count function, we can see that they were added successfully:
Next if we will add extra 107, it should normally result to 257, but because the max allowed size is 255, it will overflow, and go back to 0 instead of 256 and then to 1 instead of 257. The process can be seen in the video below.
This issue would stop being vulnerable in case the latest version of solidity was used, which if it detected that an overflow was about to occur, it reverts the transaction and the failed symbol is visible. The video below is with version 8 of solidity:
Underflow Vulnerability
The following smart contract is vulnerable to an underflow, where the value is checked based on the balance of the user and it does a check about the balance. The vulnerable part of the smart contract is in the second line of the transfer
function:
// SPDX-License-Identifier: MIT pragma solidity ^0.7.6; contract VulnerableToUnderflow { address public owner; mapping(address => uint256) public getBalance; constructor() { owner = msg.sender; } function mint(address _to, uint256 _amount) external { require(msg.sender == owner, "Not the owner"); getBalance[_to] += _amount; } function transfer(address _to, uint256 _value) public returns (bool) { require(getBalance[msg.sender] - _value >= 0, "transcations failed"); getBalance[msg.sender] -= _value; getBalance[_to] += _value; return true; } }
While the line verifies that the final result of the balance – money withdrawn is greater than 0, because the value is an uint, it underflows and the balance turns into a really large integer. Let’s try to exploit it and see the result. In this case we have three users:
User | Address |
Owner | 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 |
Attacker | 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2 |
UserA | 0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db |
In this PoC the Owner is deploying the Smart Contract and by using the mint
function, they mint 1.000 tokens to their account. So only the owner currently has 1.000 tokens.
Then, the owner transfers 100 tokens to the Attacker account. The balance of Owner is now 900 tokens and the Attacker has 100 tokens.
Now, the Attacker tries to transfer 200 tokens to the account of the UserA. Normally this should fail because the balance would be negative, but it doesn’t fail, since the uint does not offer negative values. The transaction is shown below:
Now UserA’s balance is 200 tokens as it can be seen in the image above, but the balance of the attacker is maxed out uint, like shown below:
Remediation
Remediating overflow and underflow vulnerabilities in smart contracts involves implementing checks and balances to ensure that arithmetic operations do not exceed the data type’s limits. Before Solidity version 0.8.0, this was typically achieved by using libraries like OpenZeppelin’s SafeMath, which provided secure arithmetic operations. SafeMath redefines basic operations like addition, subtraction, multiplication, and division with safety checks. These functions revert the transaction if an overflow or underflow is detected. When updating existing smart contracts or writing new ones in versions prior to 0.8.0, it’s crucial to integrate such libraries or implement similar checks manually. Additionally, conducting thorough testing and audits can help identify and rectify potential overflow and underflow issues. For contracts compiled with Solidity 0.8.0 and later, these concerns are significantly reduced, as the compiler automatically includes checks for arithmetic operations. However, it’s still vital to follow best practices in smart contract development, including rigorous testing and potentially engaging in formal verification processes to ensure the contract’s logic is sound and secure against various types of vulnerabilities.