Smart contracts are programs stored on the blockchain that execute automatically when triggered. As lines of code that handle and transfer significant financial value, smart contracts must be thoroughly secured against vulnerabilities.
One major class of security flaws in smart contracts stems from arithmetic underflow and overflow conditions. These bugs can enable attackers to wrongly take funds, corrupt data, or break application logic.
In this comprehensive guide, we’ll cover everything developers and auditors need to know about finding, preventing, and fixing arithmetic underflow and overflow vulnerabilities in Ethereum and blockchain smart contracts programmed in languages like Solidity.
What is Arithmetic Underflow and Overflow?
Arithmetic underflow and overflow refer to exceptional conditions that occur when integer values are incremented or decremented past the minimum or maximum bounds allowed for their bit storage.
For example, an 8-bit unsigned integer variable has a range from 0 to 255. If it tries to go below 0, an arithmetic underflow occurs as it circles back around to the maximum 255 value.
Conversely, incrementing from 255 back to 0 signifies arithmetic overflow as it exceeds the 8-bit storage.
These conditions violate the mathematical logic and open the door for incorrect calculations or abused behaviors. Underflow and overflow conditions can occur in any programming language but pose heightened risks for smart contracts managing finances and sensitive data.
Next let’s explore how underflow and overflow manifest specifically in the Solidity language for writing Ethereum smart contracts.
Arithmetic Underflow/Overflow in Solidity
The Solidity language provides a uint
unsigned integer data type for storing positive numbers. For example:
uint8
stores values from 0 to 255uint16
stores values from 0 to 65,535uint256
stores values from 0 to 2^256 – 1
If a uint
is decreased below 0, underflow occurs and the number circles round from 0 back to the maximum value.
For instance, decrementing a uint8
holding 0 would underflow to 255. This corrupts the logical value.
The flipside, arithmetic overflow, happens when a uint
exceeds its maximum bound. A uint8
incrementing from 255 flips back to 0 due to overflow.
This underflow/overflow behavior for uint
types enables incorrect state in contracts that could have security consequences. Developers must proactively guard against these flaws.
Real World Example: Underflow Bug in Smart Contract
To see how arithmetic underflow bugs can lead to disastrous issues in practice, consider the high profile case of the ProxyOverflow exploit:
Smart contract platform Furucombo suffered an underflow vulnerability that allowed attackers to create an absurd amount of FURU tokens out of thin air, devaluing the true supply.
The issue stemmed from a subtraction operation on the userBalance
variable of type uint256
:
solidityCopy code
function deposit(uint amount) external { userBalance[msg.sender] -= amount; }
When userBalance
equaled 0, underflow occurred on - amount
resulting in the maximum uint256 value (2^256 – 1).
This wrongly allowed the attacker to mint themselves trillions in FURU tokens and extract $15 million in value.
This example highlights the dire real-world consequences arithmetic underflow and overflow can cause in improperly coded smart contracts.
Next let’s cover how to mitigate these vulnerabilities through secure programming techniques.
How To Prevent Arithmetic Underflow and Overflow Bugs
Here are 8 techniques Solidity and blockchain developers should follow to write underflow/overflow-resistant code:
1. Use SafeMath Library
Include OpenZeppelin’s SafeMath library which replaces math operators with functions that revert on under/overflow.
2. Explicitly Check Before Operations
Manually check that values are within expected bounds before performing math operations.
3. Use Require Statements
Include require() validation statements before math operations to revert execution if false.
4. Build in Redundancy Checks
Add redundant conditional checks that operation results are within bounds.
5. Leverage Type Invariance
Use structs with specific data types like uint32 rather than generic uints.
6. Adopt Defensive Coding
Program defensively assuming invariants can be broken by bad actors.
7. Use Time-Tested Frameworks
Build on established battle-tested smart contract frameworks like OpenZeppelin.
8. Conduct Regular Audits
Continuously audit code line-by-line and use formal verification tools to mathematically prove correctness.
Taking measures like these during development will strengthen smart contracts against arithmetic vulnerabilities that could lead to disastrous outcomes.
Next let’s walk through a simple example to demonstrate overflow/underflow protection in practice.
Solidity Example: Preventing Arithmetic Overflows
Here is a simplified Solidity contract vulnerable to arithmetic overflows:
solidityCopy code
contractOverflowVulnerable { uint public tokenBalance; function addToBalance(uint amount) external { tokenBalance += amount; } }
tokenBalance
is a generic uint
type which could cause overflow on increments.
Here is an improved version using OpenZeppelin’s SafeMath library to prevent overflows on addition:
solidityCopy code
import "@openzeppelin/contracts/utils/math/SafeMath.sol"; contract OverflowPrevented { using SafeMath for uint; // Apply SafeMath functions uint public tokenBalance; function addToBalance(uint amount) external { tokenBalance = tokenBalance.add(amount); // Uses safe add function } }
SafeMath replaces the += operator with a call to .add()
that will revert on overflows rather than incorrectly wrapping. This technique generalizes to preventing both underflows and overflows.
Let’s look at a couple more examples of arithmetic vulnerabilities and secure implementations.
Example: Batch Transfer Underflow Bug
Here is a contract with a batch payout function vulnerable to underflow:
solidityCopy code
contract BatchPayout { mapping(address => uint) public balances; function batchPayout(address[] calldata recipients, uint amount) external { for(uint i = 0; i < recipients.length; i++) { address recipient = recipients[i]; balances[recipient] -= amount; // Potential underflow } } }
If a balances[recipient]
is 0, it will incorrectly underflow to the max uint value.
Here is a fixed version checking for sufficient balance first:
solidityCopy code
contract BatchPayoutSecure { // ... function batchPayout(address[] calldata recipients, uint amount) external { for(uint i = 0; i < recipients.length; i++) { address recipient = recipients[i]; require(balances[recipient] >= amount, "Insufficient balance"); balances[recipient] -= amount; } } }
The explicit require
prevents the payouts from causing invalid state due to arithmetic underflow.
Example: Using SafeMath’s .Sub Method
For another example, here is a contract with a vulnerable subtraction operation:
solidityCopy code
contract MathVulnerable { uint public balance; function withdraw(uint amount) external { balance -= amount; } }
An attacker could provide a large amount
to cause underflow when subtracted from a smaller balance
.
Here is a more robust version using SafeMath’s .sub()
method:
solidityCopy code
import "@openzeppelin/contracts/utils/math/SafeMath.sol"; contract MathSecure { using SafeMath for uint; uint public balance; function withdraw(uint amount) external { balance = balance.sub(amount); } }
By adopting defensive programming techniques like input validation and established libraries, arithmetic vulnerabilities can be eliminated.
Auditing for Arithmetic Under/Overflow Bugs
In addition to secure development practices, rigorous code auditing is essential to catch arithmetic vulnerabilities before contracts are deployed.
When reviewing Solidity smart contracts, watch for these common bug patterns:
- Direct math operations on generic
uint
types, especially increments/decrements. - Subtractions against user input without sufficient input validation.
- Math operations inside loops where bound checks are bypassed.
- Lack of validation on return values from external contract calls.
- Implicit type casting that may cause unexpected overflows.
- Math operations performed after user input instead of before.
Thorough line-by-line manual reviews combined with automated analysis tools like Slither, Oyente, and MythX can strengthen detection of arithmetic vulnerabilities.
Conclusion
Arithmetic underflow and overflow represent some of the most dangerous yet avoidable vulnerabilities in smart contract programming. By mastering security practices like input validation, safe libraries, defensive coding, and rigorous auditing, blockchain developers can eliminate this major source of potential bugs.
Robust