Avoiding Integer Overflows: SafeMath Isn't Enough
This post describes what an integer overflow is and how to avoid them in smart contracts.
What is an Integer Overflow?
Fixed-size integers have a range of values they can represent. For example, an 8-bit unsigned integer can store values between 0 and 255 (28-1). When the result of some arithmetic falls outside that supported range, an integer overflow occurs. 1
On the Ethereum Virtual Machine (EVM), the consequence of an integer overflow is that the most significant bits of the result are lost. For example, when working with 8-bit unsigned integers, 255 + 1 = 0
. This is easier to see in binary, where 1111 1111 + 0000 0001
should be 1 0000 0000
, but because only 8 bits are available, the leftmost bit is lost, resulting in a value of 0000 0000
.
Intuitively, the effect of an integer overflow can be thought of as the value “wrapping around.”
Examples of Integer Overflows
Solidity uses fixed-size integers of various sizes. In the examples below, I’ve used only 256-bit integers, the largest integer types Solidity supports. Integer overflow in the EVM can occur during addition, subtraction, multiplication, and exponentiation.
Unsigned Integer Overflows
256-bit unsigned integers can store a minimum value of 0 and a maximum value of 2256-1:
uint256 u; // range: [0, 2**256)
u = 2**256 - 1;
assert(u + 1 == 0); // should be 2**256
u = 4;
assert(u - 5 == 2**256 - 1); // should be -1
u = 2**255;
assert(u * 2 == 0); // should be 2**256
u = 2**128;
assert(u**2 == 0); // should be 2**256
Signed Integer Overflows
256-bit signed integers can store a minimum value of -2255 and a maximum value of 2255-1:
int256 s; // range: [-2**255, 2**255)
s = 2**255 - 1;
assert(s + 1 == -2**255); // should be 2**255
s = -2**255;
assert(s - 1 == 2**255-1); // should be -2**255-1
s = 2**254;
assert(s * 2 == -2**255); // should be 2**255
Mitigating Integer Overflows
Unlike some computer architectures, the EVM provides no indication that an overflow has occurred. It’s up to you to write code that detects overflow conditions and handles them appropriately.
One approach to integer overflows is to perform the arithmetic, check the result, and revert the transaction if an overflow occurred. For example, here’s a safeAdd
function:
function safeAdd(uint256 a, uint256 b) internal pure returns (uint256 c) {
c = a + b;
require(c >= a);
}
If an integer overflow occurred, then c
will be less than a
, and the require
will revert the transaction.
This approach is taken by the widely-used SafeMath
library from OpenZeppelin.
SafeMath Isn’t Enough
Using functions like those provided in the SafeMath
library ensure that your contract doesn’t use the result of an integer overflow, but they might leave your contract unusable. Here’s a simple example:
using SafeMath for uint256;
uint256 price = 2 ether;
uint256 quantity = 2**200;
function purchase() public payable {
require(msg.value < price.mul(quantity));
// ...
}
Because 2 ether * 2200 overflows, the call to mul
will always revert the transaction, so no purchase can be made.
We recently encountered this difficulty in our post about periodic loans. This code in calculateCollateral
is roughly equivalent to SafeMath
’s mul
function:
uint256 product = collateralPerPayment * payment;
require(product / collateralPerPayment == payment, "payment causes overflow");
This is a good mitigation, but there’s still a problem. There’s a required minimum payment. If that payment causes an overflow, then there’s no way to pay off the loan. To avoid this situation, we added an overflow check to the constructor:
uint256 x = minimumPayment * collateralPerPayment;
require(x / collateralPerPayment == minimumPayment,
"minimumPayment * collateralPerPayment overflows");
The result of the multiplication isn’t used. The check is purely there to ensure that the contract cannot be deployed with parameters that will render it unusable.
batchOverflow
Making its rounds this week 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:
// DO NOT USE!
function batchTransfer(address[] receivers, uint256 value) public {
uint256 amount = receivers.length * value;
require(balances[msg.sender] >= amount);
balances[msg.sender] = balances[msg.sender].sub(amount);
for (uint256 i = 0; i < receivers.length; i++) {
balances[receivers[i]] = balances[receivers[i]].add(value);
}
}
The require
is meant to ensure the sender has a sufficient balance to cover the transfers, but note that amount
is the product of two values controlled by the caller. If someone were to pass 2 addresses and a value
of 2255, then 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 SafeMath
’s sub
to reduce the sender’s balance doesn’t help here because amount
is 0, so that subtraction has no overflow.
Summary
- Arithmetic using fixed-sized integers can cause overflows, resulting in mathematically incorrect results.
- To abort overflows, use a library like
SafeMath
. - To avoid overflows altogether, do parameter validation up front.
Test Your Knowledge
Now that you understand integer overflows and how to spot them, test your knowledge in the Capture the Ether “math” category. More than one of those challenges requires exploiting an integer overflow.
- Sometimes the term “integer underflow” is used when the result is specifically below the supported range. ↩