Precision Errors Remediation
How to eliminate precision loss in Solidity financial calculations by reordering operations, using scaling factors, and applying explicit rounding direction.
Precision Errors Remediation
Overview
Precision errors in Solidity arise from integer division truncating fractional bits before a subsequent multiplication, or from insufficient scaling factors in financial calculations. The remediation requires reordering operations to multiply before dividing, using 1e18-scaled fixed-point arithmetic, and applying explicit rounding direction in vault implementations.
Related Detector: Precision Errors
Recommended Fix
Before (Vulnerable)
// Division before multiplication — systematic precision loss
function calculateFee(uint256 amount) public pure returns (uint256) {
// For amount = 999: (999 / 1000) = 0, then 0 * 3 = 0 (fee evaded)
return (amount / 1000) * FEE_RATE;
}
After (Fixed)
// Multiplication before division — correct precision
function calculateFee(uint256 amount) public pure returns (uint256) {
// For amount = 999: 999 * 3 = 2997, then 2997 / 1000 = 2 (correct)
return (amount * FEE_RATE) / FEE_BASIS;
}
Alternative Mitigations
OpenZeppelin Math.mulDiv with explicit rounding for ERC-4626 vaults:
import "@openzeppelin/contracts/utils/math/Math.sol";
contract SafeVault {
uint256 public totalShares;
uint256 public totalAssets;
// Round DOWN for deposits (user gets fewer shares — protocol-favorable)
function convertToShares(uint256 assets) public view returns (uint256) {
return Math.mulDiv(assets, totalShares + 1, totalAssets + 1, Math.Rounding.Floor);
}
// Round DOWN for withdrawals (user gets fewer assets — protocol-favorable)
function convertToAssets(uint256 shares) public view returns (uint256) {
return Math.mulDiv(shares, totalAssets + 1, totalShares + 1, Math.Rounding.Floor);
}
}
The +1 virtual offset on both totalShares and totalAssets defeats the inflation attack: even when the vault is empty, the ratio is 1:1, not 0:0.
Fixed-point libraries for complex financial math:
// PRBMath library for 18-decimal fixed-point arithmetic
import { UD60x18, ud } from "@prb/math/src/UD60x18.sol";
function calculateInterest(UD60x18 principal, UD60x18 rate, UD60x18 time) public pure returns (UD60x18) {
return principal.mul(rate).mul(time); // All operations maintain 18-decimal precision
}
Common Mistakes
Using SafeMath with division before multiplication — SafeMath prevents overflow but does not fix operation ordering. Division before multiplication loses precision even with SafeMath.
Assuming uniform 18 decimals — USDC uses 6 decimals, not 18. Mixing 6-decimal and 18-decimal tokens without explicit scaling produces systematic errors.
Symmetric rounding direction — vaults that round the same direction for both deposits and withdrawals can be drained through repeated deposit/withdraw cycles.