Intro to Smart Contract Security Audits | Overflow

SlowMist
6 min readDec 29, 2021

--

Last week we wrote an introduction about Reentrancy vulnerabilities, this week we will discuss another common vulnerability called Overflow.

let’s take a look at what an overflow is:

There are two types of Flow, Overflow, and Underflow. The so-called overflow refers to the fact that when a single numerical calculation is run, the result of the calculation is greater than the capacity limit that the register or memory can store or represent. For example, in Solidity, the range that uint8 can represent is 256 numbers from 0 to 255. When the uint8 type is used to calculate 255 + 1 in the actual operation, an overflow will occur, so the calculated result is 0, the minimum value that the uint8 type can represent. Similarly, underflow is when the calculation result is minimal, less than the capacity limit that the register or memory can store or represent. For example, in Solidity, when the uint8 type is used to calculate 0–1, it will produce underflow, so the computed value is 255, which is the maximum value that the uint8 type can represent.

If a contract has an overflow loophole, it can significantly differ between the actual result of the calculation and the expected result. This will affect the normal logic of the contract and cause the loss of funds in the contract. However, there are version restrictions for overflow vulnerabilities. In Solidity versions <0.8, overflow will not report an error, but in versions >= 0.8, an overflow will cause an error. So when we see the 0.8 version of the contract, we should note that this contract may arise with overflow vulnerabilities.

Example

After reading about Overflows, let’s take a look at an example with codes:

// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;

contract TimeLock {
mapping(address => uint) public balances;
mapping(address => uint) public lockTime;

function deposit() external payable {
balances[msg.sender] += msg.value;
lockTime[msg.sender] = block.timestamp + 1 weeks;
}

function increaseLockTime(uint _secondsToIncrease) public {
lockTime[msg.sender] += _secondsToIncrease;
}

function withdraw() public {
require(balances[msg.sender] > 0, “Insufficient funds”);
require(block.timestamp > lockTime[msg.sender], “Lock time not expired”);

uint amount = balances[msg.sender];
balances[msg.sender] = 0;

(bool sent, ) = msg.sender.call{value: amount}(“”);
require(sent, “Failed to send Ether”);
}
}

Vulnerability analysis

We can see that the TimeLock contract acts as a time vault. Users can deposit and lock funds into the contract through the deposit function to be locked for at least one week. Of course, the user can still increase the storage time through the increaseLockTime function. The user cannot withdraw the tokens locked in the TimeLock contract before the set storage period expires.

While looking at the increaseLockTime function and deposit function in this contract, we can see that it contained arithmetic functions. The version supported by the contract is 0.7.6 upward compatibility, so this contract will not report an error when the arithmetic overflows. Let’s analyze the two functions, increaseLockTime and the deposit function. We can start by examining the range of influence of the parameters in these two functions and then decide how to launch an attack.

1. The deposit function has two operations. The first one affects the balances deposited by the user. The parameters passed here are controllable, so there is a risk of overflow. The other is to affect the user’s lock time. The calculation logic here is that each time the deposit is called to deposit tokens, lockTime will be added by one week. Since the parameters here are fixed, there is no risk of overflow in this calculation.

2. The increaseLockTime function performs calculations based on the _secondsToIncrease parameter passed in by the user to change the lock time of the user’s deposited tokens. Since the _secondsToIncrease parameter is controllable, there is a risk of overflow.

Let’s take a closer look at the balances parameter. A substantial amount of funds(²²⁵⁶ to be exact) needs to be deposited into our account to create an overflow. This will cause the balances in our account to overflow and be reduced to zero, making it seem like there is nothing there. Due to the nature of this vulnerability, you can see why not many would consider this an option.

Now let’s focus on the _secondsToIncrease parameter. This parameter is passed in when we call the increaseLockTime function to increase the storage time. This parameter can determine when we deposit and lock the funds in the contract. It is directly calculated with the lockTime corresponding with the account. We can manipulate the _secondsToIncrease parameter to cause an overflow and return to zero, allowing us to withdraw the balance before the expiration date.

Let’s take a look at the attack contract:

contract Attack {
TimeLock timeLock;

constructor(TimeLock _timeLock) {
timeLock = TimeLock(_timeLock);
}

fallback() external payable {}

function attack() public payable {
timeLock.deposit{value: msg.value}();
timeLock.increaseLockTime(
type(uint).max + 1 — timeLock.lockTime(address(this))
);
timeLock.withdraw();
}
}

Here we will deploy the exploit contract to deposit the Eth first and then use the contract’s overflow vulnerability to extract the funds we deposited.

1. Deploy the TimeLock contract first.

2. Deploy the exploit contract and pass in the address of the TimeLock contract.

3. Call the Attack.attack function; it then calls the TimeLock.deposit function to deposit Eth into the TimeLock contract (at this time, the Eth will be locked by TimeLock for a week). Next, it calls the TimeLock.increaseLockTime function again. It passes in the maximum value that can be represented by the uint type (²²⁵⁶-1) plus one minus the lock time recorded in the current TimeLock contract. At this time, the result of lockTime in the TimeLock.increaseLockTime function is the value of ²²⁵⁶. Since the number of ²²⁵⁶ overflows, the returned value will be 0. At this time, we just saved the value in the TimeLock contract, and the returned lock time for the contract becomes 0.

4. At this time, Attack.attack calls TimeLock again. The withdraw function will successfully pass block.timestamp> lockTime[msg.sender] This check allows us to successfully remove in advance when the storage time has not expired.

The following is the function call flow chart of the attacker:

Repair suggestions

Now that we understand overflow vulnerabilities better, we can learn how to protect against them. We will learn how to prevent overflow vulnerabilities and quickly spot them from the perspective of developers and auditors:

As a developer

1. Use SafeMath to prevent overflow;

2. Use Solidity 0.8 and above to develop contracts and use unchecked with caution because there is no overflow check for parameters in unchecked modified code blocks;

3. It is necessary to use variable-type coercion with caution. For example, forcing a parameter of uint256 type to uint8 type may cause overflow due to the different value ranges of the two types.

As an Auditor

1. First, check whether the contract version is below Solidity 0.8 or an unchecked modified code block. If there is, check the overflow of parameters first and determine the scope of influence.

2. If the contract version is below Solidity 0.8, you need to check whether the contract references SafeMath.

3. If SafeMath is used, we need to pay attention to whether there is a mandatory type conversion in the contract. If there is, there may be a risk of overflow.

4. If SafeMath is not used and there are arithmetic operations in the contract, we can conclude that this contract may have an overflow risk. In an actual audit, we should also consider the existing code.

--

--

SlowMist
SlowMist

Written by SlowMist

SlowMist is a Blockchain security firm established in 2018, providing services such as security audits, security consultants, red teaming, and more.