How to Secure Your Smart Contracts (Part 2)

Arithmetic overflows and underflows

Alex Roan
Alex Roan
Mar 26 · 3 min read
Photo by Antoine Dautry on Unsplash

Prerequisites: A basic understanding of the Ethereum blockchain and smart contracts.


Introduction

Here, we’ll go through arithmetic overflows and underflows, a type of logic weakness that can sometimes creep into our code. We’ll describe what they mean, examples of how they might appear, and how to prevent them from appearing in our smart contracts.


What Are They?

Ethereum Virtual Machine (EVM) integers are always of a fixed size. For example, unit8 can only store values between (and including) 0 and 255. Trying to store the value 256 in a uint8 variable will result in a value of 0. This is ripe for exploitation if no checks are made before execution.

Underflows

uint8 myValue = 2;
uint8 subValue = 3;
uint8 result = myValue - subValue;

Here, our result variable would not equal -1 as we think it should; result would equal 255.

Counting backward from 2, the EVM goes 1 … 0 … 255. This is known as an underflow.

Overflows

uint8 myValue = 254;
uint8 addValue = 3;
uint8 result = myValue + addValue;

Here, our result variable does not equal 257 as we think it should. It gets calculated to 1 because 255 is the maximum value of uint8.

Counting upward from 254, the EVM goes … 255 … 0 … 1. This is known as an overflow.


Example

mapping(address => uint) balances;function withdraw(uint _value) external {
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
msg.sender.transfer(_value);
}

The require statement requires that the balance of the sender minus the withdraw value is greater than or equal to 0. This makes sense to us logically since we don’t want to allow anyone to withdraw more than they have in their balance. However, this is vulnerable to underflow exploitation.

If the attacker had two Ether stored in the contract and attempted to withdraw ten, the require statement would allow it. This is because the subtraction would cause the result of balances[msg.sender] — _value to be greater than or equal to 0 by underflowing to the maximum. Because the integer is unsigned, the require statement will always pass as long as the msg.sender has a balance.

By this logic, anyone who has deposited a balance at any point can completely rinse the contract of funds.


Preventative Measures

The library is thoroughly scrutinized by the community before being released, so we can have confidence in its ability to prevent arithmetic logic bugs. Here’s an example of how to use SafeMath’s sub() function instead of using the minus arithmetic operator in our smart contract:

using SafeMath for uint;function withdraw(uint _value) external {
require(balances[msg.sender].sub(_value) >= 0);
balances[msg.sender] -= _value;
msg.sender.transfer(_value);
}

In this scenario, the require statement would fail if we attempted to withdraw more than our balance. This is because the sub() function requires the balance be greater than or equal to _value.


Better Programming

Advice for programmers.

Alex Roan

Written by

Alex Roan

Blockchain Developer. Writer for The Startup, Better Programming, Coinmonks, and more. Editor of Sail. Owner of BlockCentric. Personal site: alexroan.co.uk

Better Programming

Advice for programmers.

More From Medium

More from Better Programming

More from Better Programming

More from Better Programming

Fun Side Projects That You Can Build Today

3.1K

More from Better Programming

More from Better Programming

The Zero-Dollar Infrastructure Stack

1.1K

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade