--- name: Arithmetic & Precision Vulnerability Patterns description: Use this skill when writing or auditing Solidity arithmetic — especially in reward distribution, interest accrual, or token accounting — to prevent overflow, underflow, and precision loss bugs that silently drain or lock funds. ecosystem: ethereum type: security-guide source: community confidence: high version: 1.0.0 time_sensitivity: evergreen tags: - arithmetic - precision - overflow - underflow - fixed-point - smart-contract-security - solidity updated_at: 2026-03-26T00:00:00.000Z --- # Arithmetic & Precision Vulnerability Patterns Distilled from 300+ Code4rena audit findings. Arithmetic bugs are among the hardest to spot during review because the logic looks correct at a glance — the error only manifests at boundary conditions or with unusual input values. These patterns explain where precision loss and overflow/underflow most commonly appear in DeFi protocols. ## Overflow & Underflow ### 1. Unchecked Subtraction (Underflow) **Pattern:** A balance or counter is decremented without verifying it is >= the amount being subtracted. In Solidity ≥0.8.0, this reverts — but code inside `unchecked {}` blocks bypasses the check. Older Solidity silently wraps to `2^256 - 1`. **Example (vulnerable, Solidity 0.8 with unchecked):** ```solidity unchecked { balances[user] -= amount; // no require(balances[user] >= amount) } ``` **Real-world context:** Repayment functions in lending protocols. If a user repays more than their debt (off-by-one in interest calculation), the debt underflows, treating the user as having an astronomically large debt or zero debt depending on how the value is used. **Fix:** ```solidity require(balances[user] >= amount, "insufficient balance"); balances[user] -= amount; ``` ### 2. Accumulation Overflow **Pattern:** A value accumulates through repeated additions without a ceiling check. If the accumulator overflows `uint256` (2^256 − 1 ≈ 1.16 × 10^77), it wraps to 0, silently resetting accounting. **Real-world context:** Locked token accumulators in veToken contracts. If `totalLocked` wraps to zero, the protocol believes no tokens are locked, potentially allowing users to exit without burning lock positions. **Fix:** Add `require(totalLocked + amount <= MAX_TOTAL_LOCKED)` or use SafeMath patterns for critical accumulators even in Solidity 0.8. ### 3. Type Casting Truncation **Pattern:** A `uint256` value is cast to a smaller type (e.g., `uint128`, `uint64`) without checking whether the value fits. The upper bits are silently dropped. **Example (vulnerable):** ```solidity uint64 timestamp = uint64(block.timestamp); // safe until year 2554 uint64 amount = uint64(largeAmount); // DANGEROUS if largeAmount > type(uint64).max ``` **Fix:** Use OpenZeppelin's `SafeCast` library for all downcast operations. --- ## Precision Loss (Fixed-Point Arithmetic) ### 1. Division Before Multiplication **Pattern:** Dividing before multiplying causes truncation of intermediate results because Solidity performs integer division (floor). The correct order is always multiply first, divide last. **Example (vulnerable):** ```solidity uint256 fee = (amount / 10000) * bps; // precision lost in first division ``` **Fix:** ```solidity uint256 fee = (amount * bps) / 10000; // multiply first ``` **Rule of thumb:** In any expression `(a / b) * c`, the precision loss is up to `(b-1) * c / b`. For large `c`, this matters. ### 2. Scaling Factor Misuse in Reward Distribution **Pattern:** A reward-per-token calculation uses an incorrect scaling factor that either causes integer division to zero (when the weight is large) or creates huge multipliers (when the weight is small). **Real-world example:** ```solidity // Vulnerable: gaugeWeight is an absolute token amount, not a fraction uint256 rewardPerGauge = (totalReward * 1e18) / gaugeWeight; ``` If `gaugeWeight` is 1 wei and `totalReward` is 1e18, `rewardPerGauge = 1e36` — which exceeds available funds and corrupts accounting. **Fix:** Normalize gauge weights to percentages (0–10000 bps) before using them as divisors. Validate that the result is within expected bounds with `require`. ### 3. Rounding Direction Bias **Pattern:** When computing how much a user owes (debt) or how much protocol earns (fees), always round *against the user* and *in favor of the protocol* to prevent dust accumulation or insolvency. Rounding the wrong direction, even by 1 wei per operation, can drain a protocol over millions of transactions. **Fix:** - For fees/debts owed *by* user: round **up** (`(a + b - 1) / b`) - For amounts claimable *by* user: round **down** (standard integer division) ### 4. `shares`/`assets` Discrepancy in ERC-4626 Vaults **Pattern:** ERC-4626 `convertToShares` and `convertToAssets` must be inverses. If the protocol rounds in the same direction in both functions, arbitrage is possible: deposit 1000 assets → get N shares → redeem N shares → get 1001 assets. **Fix:** Follow ERC-4626 spec: `convertToShares` rounds down; `convertToAssets` rounds down (for withdrawal, round in vault's favor). --- ## Compound Interest & Time-Based Precision ### 5. Block Timestamp vs. Block Number **Pattern:** Using `block.number` for time-based calculations assumes fixed block times. On L2s and sidechains, block times differ significantly from Ethereum mainnet (12s). Code originally written for ETH on BSC (3s blocks) runs 4× faster, compressing intended time windows. **Fix:** Use `block.timestamp` for durations. If block counts are required (e.g., voting periods), make them configurable at deployment time per chain. ### 6. Exponential Interest Rate Precision **Pattern:** Compound interest rates represented as `(1 + r)^n` in integer math lose precision when `n` is large or `r` is small. Using linear approximation `(1 + r*n)` instead of compound interest introduces systematic undercharging. **Fix:** Use a well-tested interest accrual library (e.g., Aave's `MathUtils.calculateCompoundedInterest`) with ray-precision (1e27) rather than implementing from scratch. --- ## Audit Checklist - [ ] Every subtraction: is there a `require(a >= b)` or equivalent guard? - [ ] Every addition to a running total: is there an overflow ceiling check? - [ ] Every downcast (`uint256 → uint128`): is `SafeCast` used? - [ ] Every division: does multiplication precede it in the expression? - [ ] Reward/fee calculations: are scaling factors (1e18 vs absolute values) consistent? - [ ] Rounding direction: do user-favoring paths round down, protocol-favoring paths round up? - [ ] Time-based code: does it use `block.timestamp`, and are constants chain-aware? - [ ] ERC-4626 vaults: are `convertToShares` / `convertToAssets` round-trip safe?