Precision Errors
Detects division-before-multiplication and rounding errors in financial calculations that cause systematic fund loss or enable economic attacks through precision manipulation.
Precision Errors
Overview
Remediation Guide: How to Fix Precision Errors
The precision errors detector identifies arithmetic operations in smart contracts where integer division discards fractional bits before a subsequent multiplication, causing systematic rounding loss that an attacker can exploit. Solidity performs all arithmetic as integer operations with no floating-point support: division truncates toward zero, discarding remainders. When division occurs before multiplication — or when scaling factors are absent or too small — rounding errors accumulate and can be abused to avoid fees, extract value from vaults, or trigger the ERC-4626 inflation attack.
The detector scans the intermediate representation for DIV instructions followed by MUL on the same value path, and for share-calculation patterns in vault-like contracts where the numerator precision is insufficient relative to the denominator.
Why This Is an Issue
Precision loss is not random — it is systematic and directional, which makes it exploitable. An attacker who understands the rounding behavior can craft transaction inputs that always round to zero fees or always extract more value than entitled. Unlike most vulnerabilities, precision attacks are repeatable at low cost: each individual transaction may only extract a fraction of a wei, but batched across thousands of transactions the aggregate loss is significant.
Vault share inflation attacks (ERC-4626 first-depositor attack) are particularly severe: by depositing 1 wei and then donating assets directly to the vault, an attacker inflates the share price so that subsequent depositors receive 0 shares due to rounding — their entire deposit is stolen.
The Balancer composable stable pool incident (2023) exposed how precision errors in invariant calculations could be systematically exploited through carefully sequenced trades, with a potential exposure of $128M.
How to Resolve
Always multiply before dividing. Use scaling factors of at least 1e18 for financial calculations. Use Math.mulDiv with explicit rounding direction for ERC-4626 and similar vault implementations.
// Before: Vulnerable — division before multiplication
function calculateFee(uint256 amount) public pure returns (uint256) {
// For amount = 999: (999 / 1000) = 0, then 0 * 3 = 0 (no fee paid)
return (amount / 1000) * FEE_RATE;
}
// After: Fixed — multiply before divide
function calculateFee(uint256 amount) public pure returns (uint256) {
// For amount = 999: 999 * 3 = 2997, then 2997 / 1000 = 2 (correct fee)
return (amount * FEE_RATE) / FEE_BASIS;
}
Examples
Vulnerable Code
// Precision loss pattern 1: division before multiplication
contract VulnerableFee {
uint256 public constant FEE_RATE = 3; // 0.3% expressed as 3/1000
function calculateFee(uint256 amount) public pure returns (uint256) {
// VULNERABLE: division first — attacker uses amounts just below 1000 to pay no fee
return (amount / 1000) * FEE_RATE;
}
}
// Precision loss pattern 2: vault inflation attack surface
contract VulnerableVault {
uint256 public totalShares;
uint256 public totalAssets;
function previewDeposit(uint256 assets) public view returns (uint256 shares) {
if (totalShares == 0) return assets;
// VULNERABLE: no offset guards against inflation attack
// Attacker deposits 1 wei, donates 10 ETH directly to vault,
// next depositor gets 0 shares (their 5 ETH rounds to 0)
return (assets * totalShares) / totalAssets;
}
}
Fixed Code
import "@openzeppelin/contracts/utils/math/Math.sol";
// Fix 1: Multiply before divide with explicit basis
contract SafeFee {
uint256 public constant FEE_RATE = 3;
uint256 public constant FEE_BASIS = 1000;
function calculateFee(uint256 amount) public pure returns (uint256) {
return (amount * FEE_RATE) / FEE_BASIS;
}
}
// Fix 2: ERC-4626 with rounding direction and virtual offset
contract SafeVault {
uint256 public totalShares;
uint256 public totalAssets;
// Virtual offset of 1 share = 1 asset defeats the inflation attack
function convertToShares(uint256 assets) public view returns (uint256) {
// Round DOWN for deposits — user gets fewer shares (protocol-favorable)
return Math.mulDiv(assets, totalShares + 1, totalAssets + 1, Math.Rounding.Floor);
}
function convertToAssets(uint256 shares) public view returns (uint256) {
// Round DOWN for withdrawals — user gets fewer assets (protocol-favorable)
return Math.mulDiv(shares, totalAssets + 1, totalShares + 1, Math.Rounding.Floor);
}
}
Sample Sigvex Output
{
"detector_id": "precision-errors",
"severity": "critical",
"confidence": 0.85,
"description": "Division at offset 0x4c (DIV slot_0x01 1000) precedes multiplication at offset 0x58 (MUL result 3) in function calculateFee(uint256). This ordering discards the fractional remainder before scaling, enabling systematic fee evasion.",
"location": {
"function": "calculateFee(uint256)",
"offset": 76
}
}
Detection Methodology
The detector analyzes the HIR for precision-losing patterns:
- Division-before-multiplication: Tracks the data flow from
DIVinstructions. If the result of aDIVis used as an operand in a subsequentMULwithin the same function, the pattern is flagged. - Vault share calculations: Identifies the pattern
(A * B) / CwhereAis a user-supplied deposit amount andB/Cis a ratio of two storage variables, then checks whether a virtual offset (+1) is present on both numerator and denominator. - Rounding direction: For ERC-4626 compatible functions (detected by function selector matching), verifies that deposit calculations round down for shares and withdrawal calculations round down for assets.
Confidence is High for division-before-multiplication patterns where the division operand is a constant. Confidence is Medium for vault share patterns where the rounding direction must be inferred from function semantics.
Limitations
False positives:
- Integer division followed by multiplication in contexts where the truncation is intentional (e.g., aligning to a block boundary) may be flagged. Review flagged instances for business logic intent.
- Division inside
uncheckedblocks for gas optimization may produce false positives if the arithmetic is known to be exact.
False negatives:
- Precision errors that require multiple transactions to exploit (cumulative rounding over time) may not be detected because the detector analyzes single-function execution paths.
- Cross-token decimal mismatch (e.g., mixing 6-decimal USDC with 18-decimal ETH without scaling) is detected separately by the
token-decimal-mismatchdetector.
Related Detectors
- Integer Overflow — detects arithmetic overflow and underflow
- Unchecked Subtraction — detects subtraction without underflow protection