Skip to main content
Sigvex

Unchecked Subtraction Remediation

How to prevent integer underflow in Solidity by using Solidity 0.8+ default arithmetic, adding explicit balance checks before subtraction, and using SafeMath in legacy contracts.

Unchecked Subtraction Remediation

Overview

Unchecked subtraction underflows wrap a zero balance to 2^256 - 1, giving an attacker unlimited funds. In Solidity 0.8+, arithmetic is checked by default and reverts on underflow. The remediation for pre-0.8 contracts is to add explicit require guards or use SafeMath. For 0.8+ contracts, avoid placing user-controlled subtraction inside unchecked {} blocks.

Related Detector: Unchecked Subtraction

Before (Vulnerable)

// Pre-0.8 Solidity — no protection
pragma solidity ^0.6.0;

contract VulnerableToken {
    mapping(address => uint256) public balances;

    function transfer(address to, uint256 amount) external {
        // VULNERABLE: wraps to 2^256 - 1 if balances[msg.sender] < amount
        balances[msg.sender] -= amount;
        balances[to] += amount;
    }
}

After (Fixed)

// Option 1: Upgrade to Solidity 0.8+ (reverts on underflow automatically)
pragma solidity ^0.8.0;

contract SafeToken {
    mapping(address => uint256) public balances;

    function transfer(address to, uint256 amount) external {
        // 0.8+: automatically reverts on underflow — no action needed
        balances[msg.sender] -= amount;
        balances[to] += amount;
    }
}

// Option 2: Explicit guard for pre-0.8 contracts
pragma solidity ^0.6.0;

contract SafeToken {
    mapping(address => uint256) public balances;

    function transfer(address to, uint256 amount) external {
        require(balances[msg.sender] >= amount, "Insufficient balance");
        balances[msg.sender] -= amount; // Safe after require
        balances[to] += amount;
    }
}

Alternative Mitigations

SafeMath library (for legacy pre-0.8 contracts where upgrading the compiler is not possible):

pragma solidity ^0.6.0;
import "@audited/math/SafeMath.sol";

contract SafeToken {
    using SafeMath for uint256;
    mapping(address => uint256) public balances;

    function transfer(address to, uint256 amount) external {
        // sub() reverts on underflow
        balances[msg.sender] = balances[msg.sender].sub(amount, "Insufficient");
        balances[to] = balances[to].add(amount);
    }
}

Safe use of unchecked {} in 0.8+ contracts — only use unchecked when underflow is provably impossible:

pragma solidity ^0.8.0;

contract GasOptimized {
    mapping(address => uint256) public balances;

    function transfer(address to, uint256 amount) external {
        require(balances[msg.sender] >= amount, "Insufficient");
        unchecked {
            // SAFE: require above proves balances[msg.sender] >= amount
            balances[msg.sender] -= amount;
            balances[to] += amount;
        }
    }

    // SAFE: loop counter cannot underflow because i >= 0 always
    function process(uint256 n) external {
        for (uint256 i = 0; i < n; ) {
            _process(i);
            unchecked { ++i; } // Gas optimization
        }
    }
}

Common Mistakes

Placing fee subtraction inside unchecked without validating the fee — if both amount and fee are user-controlled and subtracted together inside unchecked, a crafted fee value can cause underflow even if amount alone is bounded.

Relying on type boundsuint8, uint16, etc., have smaller maximums but still wrap on underflow unless the compiler version is ≥0.8.

References