input-arithmetic-safety
SKILL.md
Input & Arithmetic Safety
Detect input validation failures (the #1 direct exploitation cause at 34.6% of all contract exploits) and arithmetic vulnerabilities that persist even with Solidity 0.8+ checked math — precision loss, rounding exploitation, unsafe casting, and share price manipulation.
When to Use
- Auditing any contract with public/external functions accepting user-supplied parameters
- Reviewing DeFi protocols with fee calculations, share pricing, or exchange rates
- Analyzing vault/staking contracts for rounding or first-depositor attacks
- Checking contracts with
uncheckedblocks for overflow/underflow risks - Verifying arithmetic in token minting, burning, and distribution logic
When NOT to Use
- Access control analysis (use semantic-guard-analysis)
- Reentrancy detection (use reentrancy-pattern-analysis)
- Full multi-dimensional audit (use behavioral-state-analysis)
Part 1: Input Validation Analysis
Critical Missing Validations
Zero Address Check:
// VULNERABLE: No zero address check
function setAdmin(address newAdmin) external onlyOwner {
admin = newAdmin; // Can set admin to address(0) — locking out admin forever
}
// SAFE
function setAdmin(address newAdmin) external onlyOwner {
require(newAdmin != address(0), "Zero address");
admin = newAdmin;
}
Zero Amount Check:
// VULNERABLE: Allows zero-amount operations
function deposit(uint256 amount) external {
balances[msg.sender] += amount;
emit Deposit(msg.sender, amount);
// Zero deposit: wastes gas, pollutes events, may affect accounting
}
// SAFE
function deposit(uint256 amount) external {
require(amount > 0, "Zero amount");
balances[msg.sender] += amount;
}
Array Length Validation:
// VULNERABLE: No length check
function batchTransfer(address[] calldata recipients, uint256[] calldata amounts) external {
for (uint i = 0; i < recipients.length; i++) {
transfer(recipients[i], amounts[i]); // Out-of-bounds if arrays differ in length
}
}
// SAFE
function batchTransfer(address[] calldata recipients, uint256[] calldata amounts) external {
require(recipients.length == amounts.length, "Length mismatch");
require(recipients.length <= MAX_BATCH_SIZE, "Batch too large");
// ...
}
Bounds Checking:
// VULNERABLE: No upper bound on fee
function setFee(uint256 newFee) external onlyOwner {
fee = newFee; // Owner can set 100% fee, stealing all user funds
}
// SAFE
function setFee(uint256 newFee) external onlyOwner {
require(newFee <= MAX_FEE, "Fee too high"); // e.g., MAX_FEE = 1000 (10%)
fee = newFee;
}
Input Validation Detection Algorithm
For each public/external function F:
For each parameter P:
1. Is P an address? → Check for require(P != address(0))
2. Is P an amount/value? → Check for require(P > 0) if zero is invalid
3. Is P an array? → Check for length validation and max size
4. Is P a percentage/rate? → Check for upper bound
5. Is P used as an index? → Check for bounds checking
6. Is P a deadline/timestamp? → Check for require(P > block.timestamp)
Flag any parameter without appropriate validation as:
- CRITICAL if parameter controls fund flow or access
- HIGH if parameter affects protocol state
- MEDIUM if parameter affects non-critical functionality
Part 2: Arithmetic Vulnerability Analysis
Pattern 1: Division-Before-Multiplication (Precision Loss)
// VULNERABLE: Division first truncates, then multiplication amplifies error
uint256 result = (amount / totalShares) * price;
// If amount = 100, totalShares = 3: 100/3 = 33 (truncated from 33.33)
// 33 * price = less than expected
// SAFE: Multiply first, then divide
uint256 result = (amount * price) / totalShares;
// 100 * price / 3 = more precise (only one truncation at the end)
Detection:
For each arithmetic expression:
If division (/) appears BEFORE multiplication (*) in the same expression:
→ PRECISION LOSS: division-before-multiplication
Exception: If the division result is stored and intentionally used as a floored value
Pattern 2: Rounding Direction Exploitation
In financial protocols, rounding direction determines who benefits:
Protocol-favorable rounding:
- Deposits: round DOWN shares (user gets fewer shares)
- Withdrawals: round DOWN assets (user gets fewer assets)
- Fees: round UP fee amount (protocol collects more)
User-favorable rounding (VULNERABLE to extraction):
- Deposits: round UP shares → user gets more than entitled
- Withdrawals: round UP assets → user extracts more than entitled
- Fees: round DOWN → protocol collects less
// VULNERABLE: Rounds in user's favor on withdrawal
function withdraw(uint256 shares) external returns (uint256 assets) {
assets = (shares * totalAssets()) / totalSupply(); // Rounds DOWN — correct for withdrawal
// BUT if this rounds UP somehow (e.g., via ceiling division):
assets = (shares * totalAssets() + totalSupply() - 1) / totalSupply(); // Rounds UP — BAD
}
// SAFE: Use mulDiv with explicit rounding direction
assets = shares.mulDiv(totalAssets(), totalSupply(), Math.Rounding.Down); // For withdrawals
shares = assets.mulDiv(totalSupply(), totalAssets(), Math.Rounding.Up); // For deposits
Pattern 3: ERC4626 Vault Share Inflation Attack
// Attack on first deposit
contract VulnerableVault is ERC4626 {
function totalAssets() public view returns (uint256) {
return asset.balanceOf(address(this)); // Manipulable via donation!
}
// No virtual shares offset
function _convertToShares(uint256 assets) internal view returns (uint256) {
uint256 supply = totalSupply();
return supply == 0 ? assets : assets.mulDiv(supply, totalAssets());
}
}
Attack Sequence:
1. Vault is empty (totalSupply = 0, totalAssets = 0)
2. Attacker deposits 1 wei → receives 1 share
3. Attacker donates 1000 tokens directly to vault (not via deposit)
4. totalAssets = 1000e18 + 1, totalSupply = 1
5. Victim deposits 500 tokens:
shares = 500e18 * 1 / (1000e18 + 1) = 0 (rounds to zero!)
6. Victim gets ZERO shares, their 500 tokens are trapped
7. Attacker withdraws 1 share → gets all 1500+ tokens
Detection:
For ERC4626 vaults:
1. Does totalAssets() use balanceOf(address(this))? → Donation-attackable
2. Is there a virtual shares/assets offset? → Missing = VULNERABLE
3. Is there a minimum first deposit? → Missing = VULNERABLE
4. Does the vault use OpenZeppelin's _decimalsOffset()? → Present = Mitigated
Pattern 4: Unsafe Integer Casting
// VULNERABLE: Silent truncation
uint256 largeValue = 2**200;
uint128 smallValue = uint128(largeValue); // Truncated! No revert in 0.8+
// VULNERABLE: Signed/unsigned confusion
int256 negative = -1;
uint256 converted = uint256(negative); // = type(uint256).max in 0.8+
// SAFE: Use SafeCast
uint128 smallValue = SafeCast.toUint128(largeValue); // Reverts if overflow
Detection:
For each type cast operation:
If casting from larger to smaller type (e.g., uint256 → uint128):
Check if preceded by bounds validation
If no bounds check → UNSAFE CASTING
If casting between signed and unsigned:
Check if value can be negative
If possible → SIGN CONFUSION
Pattern 5: Unchecked Block Risks
// Solidity 0.8+: checked math by default, but unchecked{} disables it
unchecked {
// VULNERABLE: Overflow/underflow silently wraps
uint256 result = a - b; // If b > a: wraps to huge number
uint256 sum = a + b; // If a + b > type(uint256).max: wraps to small number
}
// SAFE use of unchecked (when overflow is impossible):
unchecked {
++i; // In a bounded for loop — i cannot overflow uint256
}
Detection:
For each unchecked block:
For each arithmetic operation inside:
1. Can the operation overflow/underflow?
2. Is there a pre-condition that guarantees safety?
3. If no guarantee → UNCHECKED OVERFLOW/UNDERFLOW risk
Common safe patterns (don't flag):
- Loop counter increment: unchecked { ++i; } in for loop with bounded length
- Post-require subtraction: require(a >= b); unchecked { a - b; }
Pattern 6: Dust Amount Exploitation
// VULNERABLE: Tiny amounts bypass fee logic
function swap(uint256 amountIn) external {
uint256 fee = amountIn * FEE_BPS / 10000;
// If amountIn = 1 and FEE_BPS = 30: fee = 30/10000 = 0
// Zero fee! Attacker makes many tiny swaps to avoid fees
uint256 amountOut = amountIn - fee;
}
Detection:
For each fee/tax calculation:
If fee = amount * rate / denominator:
Can amount * rate < denominator? (making fee = 0)
If yes → DUST AMOUNT EXPLOITATION: zero-fee transactions possible
Workflow
Task Progress:
- [ ] Step 1: Audit all public/external function parameters for missing validation
- [ ] Step 2: Find division-before-multiplication patterns
- [ ] Step 3: Verify rounding direction in share/price calculations (protocol-favorable)
- [ ] Step 4: Check ERC4626 vaults for inflation attack protection
- [ ] Step 5: Identify all type casting operations and verify bounds
- [ ] Step 6: Analyze all unchecked blocks for overflow/underflow risks
- [ ] Step 7: Check fee calculations for dust amount exploitation
- [ ] Step 8: Score findings and generate report
Output Format
## Input & Arithmetic Safety Report
### Finding: [Title]
**Function:** `functionName()` at `Contract.sol:L42`
**Category:** [Missing Validation | Precision Loss | Rounding | Inflation | Unsafe Cast | Unchecked | Dust]
**Severity:** [CRITICAL | HIGH | MEDIUM | LOW]
**Issue:**
[Description of the input validation or arithmetic vulnerability]
**Vulnerable Code:**
[Code snippet showing the issue]
**Exploit Scenario:**
1. [Step-by-step exploitation]
**Mathematical Proof:**
Input: [values]
Expected: [correct result]
Actual: [incorrect result due to precision/rounding]
Difference: [loss amount]
**Recommendation:**
[Specific fix — add validation, reorder operations, use SafeCast, add rounding]
Quick Detection Checklist
- Do all public functions validate address parameters against
address(0)? - Do all amount parameters check for
> 0where zero is invalid? - Are array parameters checked for equal lengths and maximum size?
- Do all percentage/rate parameters have upper bounds?
- Is division always performed AFTER multiplication (not before)?
- Does rounding favor the protocol (down on deposits, down on withdrawals of assets)?
- Do ERC4626 vaults use virtual shares/assets offset against inflation?
- Are all downcasts (uint256 → smaller) protected by SafeCast or bounds checks?
- Are
uncheckedblocks only used where overflow/underflow is mathematically impossible? - Can fee calculations produce zero for small but valid amounts?
For precision patterns, see {baseDir}/references/precision-patterns.md. For validation checklist, see {baseDir}/references/validation-checklist.md.
Rationalizations to Reject
- "Solidity 0.8+ has checked math" →
uncheckedblocks exist; precision loss and rounding are NOT overflow - "The fee is too small to matter" → Millions of small transactions compound; zero-fee dust swaps are profitable
- "No one would deposit 1 wei" → ERC4626 inflation attack uses exactly this; front-runners are automated
- "The admin wouldn't set a bad value" → Admin key compromise + no bounds = instant parameter manipulation
- "Rounding errors are just 1 wei" → 1 wei per transaction × millions of transactions = significant loss
- "Zero address can't sign transactions" → But setting admin to zero address locks out all admin functions permanently
Weekly Installs
1
Repository
quillai-network…s_skillsGitHub Stars
62
First Seen
11 days ago
Security Audits
Installed on
amp1
cline1
openclaw1
opencode1
cursor1
kimi-cli1