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
Recommended Fix
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 "@openzeppelin/contracts/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 bounds — uint8, uint16, etc., have smaller maximums but still wrap on underflow unless the compiler version is ≥0.8.