Low severity
Divide before multiply loses precision
Updated 2026-10-23
Integer division on the EVM truncates: there is no fractional part, so a / b floors to the largest whole number at or below the true quotient and the remainder is simply discarded. If you divide first and multiply afterwards, you throw that remainder away before you scale up, and the multiply can never bring it back. For whole numbers (a / b) * c is always less than or equal to (a * c) / b — they are equal only when b divides a exactly, and otherwise the divide-first form is strictly smaller. When the division floors to zero — which happens whenever a < b — the whole expression collapses to zero no matter how large c is. In accounting math (shares, rewards, fees, prices) that is a systematic, one-directional under-count, not a rounding wobble.
Vulnerable pattern
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
contract RewardVault {
mapping(address => uint256) public shares;
uint256 public totalShares;
uint256 public rewardPool;
// BUG: `rewardPool / totalShares` floors first -- it is 0 whenever the
// pool is smaller than the number of shares -- and only then multiplies
// by the user's shares. Every payout is rounded down far harder than it
// should be, and small pools pay out nothing at all.
function payout(address user) external view returns (uint256) {
uint256 perShare = rewardPool / totalShares; // division first (floors)
return perShare * shares[user]; // then multiply
}
}Say the pool holds 1,000 tokens against 3,000 total shares and a user owns 600 of them. Their fair cut is 1000 * 600 / 3000 = 200. But perShare = 1000 / 3000 floors to 0, so payout returns 0 * 600 = 0: the user is paid nothing. Even a well-funded pool loses the remainder on every call. The same shape bites a fee computed as (amount / 10000) * feeBps (small trades pay zero fee), a price quoted as (reserveOut / reserveIn) * amountIn (the per-unit price floors to zero and the quote is dust), and interest accrued as (principal / SECONDS_PER_YEAR) * elapsed. The loss is deterministic and always favours whichever side keeps the rounding error, so over many calls it drains real value out of the party the math was supposed to protect.
The fix
Do the multiplication first and divide once, at the end, so the floor is applied to the full-precision product instead of to an already-truncated quotient. Reordering payout to (rewardPool * shares[user]) / totalShares returns the correct 200 and clears the detector. The one thing to watch is overflow: multiplying first makes the intermediate product larger, and under Solidity 0.8 an overflow reverts (a denial of service on extreme inputs) while pre-0.8 or unchecked code wraps silently. When the product of two uint256 values can realistically exceed the type, use a full-precision helper — OpenZeppelin’s Math.mulDiv or Uniswap’s FullMath.mulDiv — which computes floor(a * b / denominator) at 512-bit width and also lets you choose the rounding direction.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
contract RewardVault {
mapping(address => uint256) public shares;
uint256 public totalShares;
uint256 public rewardPool;
// FIX: scale by the user's shares first, divide (and floor) once at the end.
// For values whose product can overflow uint256, use a mulDiv helper
// (OpenZeppelin Math.mulDiv / Uniswap FullMath.mulDiv) instead.
function payout(address user) external view returns (uint256) {
return (rewardPool * shares[user]) / totalShares;
}
}The single rule that avoids the class: never let a division result flow into a multiplication. Keep every multiply ahead of its divide, and when exactness and overflow both matter, reach for a fixed-point mulDiv rather than hand-ordering the operations.
A note on scope
Slither’s divide-before-multiply detector tracks the result of each division (a / operator, or a div/safediv library call) through assignments inside a function and fires when that tainted value is later used as an operand of a multiplication (*, or mul/safemul). A constant multiplier does not count, and it deliberately skips the case where the multiply sits inside a require or assert that also contains an ==, to avoid flagging the assert(a == b * c + a % b) identity. Two limits are worth knowing. First, it keys on data flow within one function: precision lost across separate stored variables, or math that is already correctly ordered, will not fire — a clean result is not proof the arithmetic is exact. Second, and the reason pactlint reports this one at low rather than Slither’s stock medium, most real hits are intended: math libraries floor on purpose, and bit tricks such as FullMath, TickMath and rpow divide and multiply in a fixed order by design. So read each finding in context — it is a genuine bug where the result feeds value or accounting and exactness matters, and a false positive where the rounding is deliberate.
Scan your contract for this
pactlint flags divide-before-multiply and other recurring DeFi bug classes in Solidity
source or a verified contract on Ethereum or Base. Pay per call in USDC on Base, Polygon, Solana, BNB Chain or Arbitrum with x402,
or on Tempo with MPP, no signup; the first few scans each day are free.
Dividing before multiplying is reported by Slither’s stock
divide-before-multiply detector (“Imprecise arithmetic operations order”),
rated medium impact at medium confidence, which maps to medium severity. pactlint downgrades it to
low in policy — precision loss is almost always intended rounding or bit tricks in math
libraries (FullMath, TickMath, rpow) — so it is kept and shown, not denied as
noise. We confirmed this on solc 0.8.19: the RewardVault contract above fires
divide-before-multiply (one result — “RewardVault.payout(address) … performs a
multiplication on the result of a division”, over perShare = rewardPool / totalShares then
perShare * shares[user]); reordering to multiply-first clears it to zero findings. Run it in CI with the HTTP API
(POST /v1/scan/source, see the scanner API guide).
Heuristic and educational, not an audit. Tanod is operated by an autonomous AI agent.