Assert Violation Remediation
Overview
assert() is meant for internal invariants that the contract’s own logic guarantees, while require() validates external inputs and state. A reachable assertion failure means the contract can reach a state the developer believed impossible, which signals a logic bug and, on pre-0.8 compilers, burns all remaining gas. The fix is to guard inputs with require() and keep assert() only for conditions that must hold by construction.
Related Detector: Assert Violation
Recommended Fix
Before (Vulnerable)
function withdraw(uint256 amount) external {
balances[msg.sender] -= amount;
assert(totalSupply >= amount); // Can fail if balances and totalSupply diverge
totalSupply -= amount;
payable(msg.sender).transfer(amount);
}
After (Fixed)
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount, "Insufficient balance");
require(totalSupply >= amount, "Supply underflow");
balances[msg.sender] -= amount;
totalSupply -= amount;
payable(msg.sender).transfer(amount);
assert(totalSupply == _computeExpectedSupply()); // True invariant check
}
Moving the input and state preconditions into require() rejects bad calls cleanly with a refund and a message, before any state change. The remaining assert() now only checks a property that should hold whenever the logic is correct, so a failure genuinely indicates a bug.
Alternative Mitigations
- Compile with Solidity 0.8+ so a failed assertion reverts with
Panic(0x01)and refunds unused gas instead of consuming it all. - Use custom errors with
require/revertfor cheaper, more descriptive input validation. - Add invariant tests or fuzzing so genuine
assert()conditions are exercised before deployment.
Common Mistakes
- Using
assert()to validate user input or token balances, which are external conditions. - Mutating state before checking the precondition, so the contract is left inconsistent on failure.
- Leaving reachable assertions in pre-0.8 code where failures are a gas-griefing vector.