Unchecked Subtraction
Detects subtraction operations without underflow protection that can wrap around to a large value, bypassing balance checks and enabling fund theft.
Unchecked Subtraction
Overview
Remediation Guide: How to Fix Unchecked Subtraction
The unchecked subtraction detector identifies integer subtraction operations that can underflow — wrapping from zero to the maximum uint256 value — without any prior balance check or overflow protection. In Solidity versions prior to 0.8.0, all arithmetic is unchecked by default: 0 - 1 produces 2^256 - 1 rather than reverting. In Solidity 0.8+, code inside unchecked {} blocks intentionally disables this protection and reintroduces the risk.
Sigvex detects this by identifying SUB opcodes in the bytecode where the subtrahend is user-controlled (derived from function parameters or SLOAD) and no preceding comparison instruction validates that the minuend is sufficient. Pre-0.8 contracts are always analyzed for this pattern; 0.8+ contracts are analyzed specifically for unchecked {} scopes.
Why This Is an Issue
An underflow wraps a zero balance to 2^256 - 1 — approximately 1.16 × 10^77. An attacker who achieves this can withdraw unlimited funds, transfer unlimited tokens, or bypass any downstream balance check. The vulnerability is binary: either the subtraction is protected or it is not. There is no partial exploitation — a single underflowing subtraction gives the attacker an astronomical balance.
The attack is low cost and does not require special privileges. Any user-accessible transfer or withdrawal function that subtracts from a balance before checking is sufficient.
The BEC Token incident (April 2018) demonstrated the full impact: an integer underflow in batchTransfer allowed an attacker to generate an astronomical token quantity, collapsing the token’s market value to near zero. Numerous DEX and DeFi protocols using pre-0.8 Solidity without SafeMath suffered similar attacks between 2018 and 2020.
How to Resolve
Use Solidity 0.8+ (which checks arithmetic by default) and avoid putting user-controlled subtraction inside unchecked {} blocks. For pre-0.8 contracts, use OpenZeppelin’s SafeMath or add explicit require guards before every subtraction.
// Before: Vulnerable — no underflow protection (pre-0.8 Solidity)
pragma solidity ^0.6.0;
function transfer(address to, uint256 amount) external {
balances[msg.sender] -= amount; // Wraps to 2^256 - 1 if balance < amount
balances[to] += amount;
}
// After: Fixed — explicit guard before subtraction
pragma solidity ^0.6.0;
function transfer(address to, uint256 amount) external {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount; // Safe — require above prevents underflow
balances[to] += amount;
}
Examples
Vulnerable Code
// Pre-0.8 Solidity — always vulnerable to underflow
pragma solidity ^0.6.0;
contract VulnerableToken {
mapping(address => uint256) public balances;
function transfer(address to, uint256 amount) external {
// VULNERABLE: no balance check — underflows to 2^256 - 1
balances[msg.sender] -= amount;
balances[to] += amount;
}
}
// Solidity 0.8+ with unchecked block — explicitly vulnerable
pragma solidity ^0.8.0;
contract VulnerableVault {
mapping(address => uint256) public balances;
function withdrawWithFee(uint256 amount, uint256 fee) external {
unchecked {
// VULNERABLE: attacker passes fee > balance — underflows
balances[msg.sender] -= amount + fee;
}
payable(msg.sender).transfer(amount);
}
}
Fixed Code
pragma solidity ^0.8.0;
// Solidity 0.8+ checks by default — no special action needed
contract SafeToken {
mapping(address => uint256) public balances;
function transfer(address to, uint256 amount) external {
// In 0.8+, this reverts automatically on underflow
balances[msg.sender] -= amount;
balances[to] += amount;
}
}
// Safe use of unchecked — only for provably safe arithmetic
contract GasOptimizedToken {
mapping(address => uint256) public balances;
function transfer(address to, uint256 amount) external {
require(balances[msg.sender] >= amount, "Insufficient balance");
unchecked {
// SAFE: require above guarantees balances[msg.sender] >= amount
balances[msg.sender] -= amount;
balances[to] += amount; // Also safe: addition cannot overflow here
}
}
// SAFE: loop counter increment cannot overflow uint256 in practice
function processAll() external {
for (uint256 i = 0; i < items.length; ) {
_process(items[i]);
unchecked { ++i; }
}
}
}
Sample Sigvex Output
{
"detector_id": "unchecked-subtraction",
"severity": "critical",
"confidence": 0.88,
"description": "SUB opcode at offset 0x3e in function transfer() subtracts user-controlled parameter (stack position 1) from storage slot 0x00. No preceding comparison validates that the storage value is >= the subtrahend. Underflow to 2^256 - 1 is possible.",
"location": {
"function": "transfer(address,uint256)",
"offset": 62
}
}
Detection Methodology
Sigvex identifies unchecked subtraction through dataflow analysis of the decompiled bytecode:
- Compiler version detection: Identifies whether the contract was compiled with Solidity <0.8 (all arithmetic is unchecked) or ≥0.8 (arithmetic is checked by default).
- Unchecked scope identification: For 0.8+ contracts, identifies
unchecked {}scopes by detecting the compiler’s generated pattern (which skips overflow checks by using rawSUBinstead of the checked variant). - User-controlled operand detection: Taint-tracks function parameters and SLOAD-derived values. Marks any subtraction where the subtrahend is tainted as a candidate.
- Preceding guard check: For each candidate
SUB, checks whether aLT,GT, orEQcomparison on the same operands appears in a preceding basic block with a conditional branch to a revert path. - Finding emission: Reports any
SUBwhere the subtrahend is user-controlled and no guard is found.
Limitations
False positives:
- Subtraction where the operand is constrained by prior arithmetic to be provably smaller than the minuend (e.g., a modulo result) may be flagged if the constraint is not explicitly expressed as a comparison.
- Library calls that internally protect against underflow but are inlined at the bytecode level may produce false positives if the protection pattern is not recognized.
False negatives:
- Underflows that require multiple transactions to construct (e.g., depositing a small amount then subtracting a larger amount through a different path) may not be detected if the multi-path constraint is not inferred.
Related Detectors
- Integer Overflow — detects overflow in addition and multiplication operations
- Unchecked Call — detects external call return values that are not checked