Requirement Violations Remediation
How to eliminate requirement violation vulnerabilities by correcting comparison operators, adding missing input validation guards, replacing assert with require for user-facing checks, and upgrading to Solidity 0.8.0 for built-in overflow protection.
Requirement Violations Remediation
Overview
Requirement violations are subtle correctness bugs where a contract’s validation logic fails to enforce the intended invariant. They occur in four patterns: incorrect comparison operators (<= where < is required at a boundary), missing validation guards (no zero-check before a division), assert misuse (validating user input with assert instead of require), and missing arithmetic checks (no overflow protection in Solidity versions before 0.8.0).
Each pattern creates a different class of exploitable behavior. Off-by-one comparisons allow draining of protected reserves. Missing zero checks cause unexpected reverts or division-by-zero panics that can be triggered deliberately to block protocol operations. Assert misuse wastes all remaining gas on failure (pre-EIP-3855) and behaves differently in try/catch blocks. Arithmetic overflows in Solidity 0.7.x and below silently wrap, allowing an attacker to credit or debit arbitrary amounts.
Related Detector: Access Control Detector
Recommended Fix
Before (Vulnerable)
// Solidity 0.7.x — no built-in overflow protection
contract VulnerableVault {
uint256 public balance;
uint256 public constant RESERVE = 1000; // Protected minimum
// BUG 1: Incorrect comparison — allows draining the reserve
function withdraw(uint256 amount) external {
require(amount <= balance); // Should be: balance - RESERVE
balance -= amount;
_transfer(msg.sender, amount);
}
// BUG 2: Missing zero check — division by zero if shares == 0
function distribute(uint256 total, uint256 shares) external {
uint256 perShare = total / shares; // Reverts unexpectedly when shares == 0
_distribute(perShare);
}
// BUG 3: Assert used for user input — wastes all gas on failure
function transfer(address to, uint256 amount) external {
assert(amount <= balances[msg.sender]); // Should be require()
balances[msg.sender] -= amount;
balances[to] += amount;
}
// BUG 4: No overflow protection in Solidity 0.7.x
mapping(address => uint256) public balances;
uint256 public totalDeposits;
function deposit(uint256 amount) external {
totalDeposits += amount; // Can silently overflow and wrap to zero
balances[msg.sender] += amount;
}
}
After (Fixed)
// Solidity 0.8.0+ — built-in overflow protection
contract SafeVault {
uint256 public balance;
uint256 public constant RESERVE = 1000;
mapping(address => uint256) public balances;
uint256 public totalDeposits;
uint256 public totalSupply;
// FIX 1: Correct comparison — protects the reserve
function withdraw(uint256 amount) external {
require(amount > 0, "Amount must be positive");
require(balance >= RESERVE + amount, "Insufficient balance above reserve");
balance -= amount;
_transfer(msg.sender, amount);
}
// FIX 2: Zero check before division
function distribute(uint256 total, uint256 shares) external {
require(shares > 0, "Shares must be positive");
require(total > 0, "Total must be positive");
uint256 perShare = total / shares;
_distribute(perShare);
}
// FIX 3: require() for user input; assert() reserved for true invariants
function transfer(address to, uint256 amount) external {
require(to != address(0), "Cannot transfer to zero address");
require(amount > 0, "Amount must be positive");
require(amount <= balances[msg.sender], "Insufficient balance"); // User error → require
balances[msg.sender] -= amount;
balances[to] += amount;
// True invariant — cannot fail given correct code above
assert(balances[msg.sender] + balances[to] <= totalSupply);
}
// FIX 4: Solidity 0.8.0+ throws on overflow automatically
function deposit(uint256 amount) external {
require(amount > 0, "Amount must be positive");
totalDeposits += amount; // SafeMath built-in in 0.8.0+
balances[msg.sender] += amount;
balance += amount;
}
}
Alternative Mitigations
OpenZeppelin SafeMath for legacy Solidity versions — if the contract must remain on Solidity 0.7.x or below, apply SafeMath to all arithmetic on user-controlled values:
// Solidity 0.7.x
import "@openzeppelin/contracts/math/SafeMath.sol";
contract LegacyVault {
using SafeMath for uint256;
function deposit(uint256 amount) external {
totalDeposits = totalDeposits.add(amount); // Reverts on overflow
balances[msg.sender] = balances[msg.sender].add(amount);
}
function withdraw(uint256 amount) external {
// sub() also reverts on underflow
balances[msg.sender] = balances[msg.sender].sub(amount, "Insufficient balance");
}
}
Boundary fuzz testing — for comparison operators near limits, generate test cases at boundary values (0, 1, RESERVE - 1, RESERVE, balance - RESERVE, balance - RESERVE + 1, balance, type(uint256).max). A correct comparison boundary should pass exactly the intended set:
// Test: withdraw(balance - RESERVE) should SUCCEED (withdraws all above reserve)
// Test: withdraw(balance - RESERVE + 1) should REVERT (would breach reserve)
// Test: withdraw(0) should REVERT (zero is not useful)
// Test: withdraw(type(uint256).max) should REVERT (overflow guard)
Explicit error messages on all require statements — error messages make security audits faster and help users understand why a transaction reverted. They also serve as inline documentation of the intended invariant:
require(amount > 0, "Vault: amount must be positive");
require(amount <= balances[msg.sender], "Vault: insufficient balance");
require(to != address(0), "Vault: transfer to zero address");
require(denominator != 0, "Vault: division by zero");
Custom error types (Solidity 0.8.4+) — custom errors are more gas-efficient than string messages and allow structured error data for off-chain tooling:
error InsufficientBalance(address user, uint256 requested, uint256 available);
error ReserveBreach(uint256 amount, uint256 maxAllowed);
function withdraw(uint256 amount) external {
uint256 available = balance - RESERVE;
if (amount > available) revert ReserveBreach(amount, available);
if (balances[msg.sender] < amount)
revert InsufficientBalance(msg.sender, amount, balances[msg.sender]);
// ...
}
Common Mistakes
Using assert for input validation — assert should only verify invariants that are guaranteed to hold given correct code. If an assert can be triggered by a user providing valid-looking but malicious input, it is a design error. Use require for all input and state validation.
Forgetting underflow on subtraction — in Solidity 0.8.0+, a - b where b > a throws automatically. In 0.7.x, it silently wraps to a huge number. Auditing for underflows is equally important to overflow auditing.
Incorrect reserve calculations — the pattern require(amount <= balance - RESERVE) is dangerous in Solidity 0.7.x because balance - RESERVE underflows if balance < RESERVE. Always check balance >= RESERVE + amount instead, which avoids any intermediate underflow.
Missing validation on constructor parameters — constructors that accept immutable parameters (fee rates, time locks, addresses) without validation can produce permanently broken contracts. Validate all constructor inputs the same way you would validate function inputs.