Integer math and rounding in smart contracts
Why Solidity has no fractions, how basis points work, the order of multiplication and division, overflow checks, mulDiv and calculating shares by subtraction.
In this article
In an ordinary program 0.1 + 0.2 puzzles you and percentages come out "as they come". In a smart contract every unit of precision is real money, so all calculations are done in integers and rounding is a deliberate decision. This article shows how not to lose a cent. For context on numbers in Ethereum see accounts and state.
No fractions: everything in integers
The EVM has no float or double types. Money is stored in the smallest units of a token. If a token has decimals = 6 (like USDC), 1.5 USDC is 1500000. For ETH, 1 ETH = 10^18 wei (about tokens).
Percentages are set in basis points (bps): 1 bps = 0.01%, 10,000 bps = 100%. A 1% rate is 100 bps, 2.5% is 250 bps. Here is how it looks in the escrow contract:
uint16 public constant BPS = 10_000;
uint16 public constant PLATFORM_FEE_BPS = 100; // 1%Division rounds down
Integer division drops the remainder: 7 / 2 == 3, 1 / 3 == 0. Calculating a fee:
platformFee = (amount * PLATFORM_FEE_BPS) / BPS;For an amount of 99 units: 99 * 100 / 10000 = 0 (a fee of 0.99 rounds to 0). This is not a bug but a consequence of discreteness. For a token with 6 decimals losses in the smallest unit are immaterial, but you must be aware of them.
Multiply first, divide later
The order of operations is critical. Take an amount of 99,999 and a rate of 150 bps (1.5%):
// bad: division first loses precision
uint256 bad = (amount / BPS) * fee; // (99999 / 10000) = 9; 9 * 150 = 1350
// good: multiplication keeps precision
uint256 good = (amount * fee) / BPS; // 99999 * 150 = 14999850; / 10000 = 1499The gap is 149 units out of 1,499, that is about 10% of the fee lost to premature division. The rule: multiplication first, division second.
Overflow
Since Solidity 0.8 arithmetic is checked: if the result does not fit the type, the transaction reverts with Panic(0x11). This protects against a class of vulnerabilities where a number "wrapped around zero" (as in versions before 0.8). An unchecked { ... } block turns the checks off to save gas and fits only if you have proven overflow is impossible.
But there is an exception: explicit type conversion is not checked.
uint256 big = 2**100;
uint96 small = uint96(big); // silently truncated, no errorIn the escrow contract the amount is stored as uint96 together with an address in one storage slot. So before writing, the amount is explicitly checked, and if it is exceeded the call is rejected with the error AmountTooLarge. Without this check a seller could create an "offer" with a truncated amount.
Division by zero
Division by zero causes Panic(0x12). Watch denominators, especially if they come from external data (for example a pool's balance).
Scaled multiplication: mulDiv
The expression a * b / c can overflow at the intermediate step a * b even if the result is small. The library function mulDiv(a, b, c) computes with a 512-bit intermediate value and avoids the problem. It is used in DeFi protocols for exact calculations.
Rounding direction
When you split an amount, dividing into parts loses a remainder. Where it goes is for the developer to decide, and it is a matter of security and fairness. A general rule: round in favour of the protocol, so that "dust" cannot be squeezed out by repeated operations.
The escrow uses a different, more robust approach - subtraction:
platformFee = (amount * PLATFORM_FEE_BPS) / BPS;
uint256 net = amount - platformFee;
arbiterFee = (net * arbiterFeeBps) / BPS;
uint256 pot = net - arbiterFee;
buyerAmount = (pot * buyerShareBps) / BPS;
// The remainder is derived by subtraction: rounding down cannot create a shortfall.
sellerAmount = pot - buyerAmount;Only the fees and the buyer's share are computed by division. The seller's share is the remainder. So the equality is guaranteed:
platformFee + arbiterFee + buyerAmount + sellerAmount == amountNot a single token unit is lost or created. If both shares were computed by pot * share / BPS, together they could come to one unit less than pot, and that "dust" would be stuck in the contract. Calculations on concrete numbers: fees by example.
Checking invariants by fuzzing
How do you make sure the sums always match? Write a fuzz test: feed in thousands of random values and check the invariant "the parts equal the whole" (how contracts are tested, Foundry tutorial). Such tests find errors in edge cases: an amount of 1, the maximum possible amount, shares of 0 and 10,000.
A developer's checklist
- Money - only
uint256in smallest units. - Percentages - in bps, multiplication before division.
- Explicit type conversions - only after a range check (or
SafeCast). - For complex formulas -
mulDiv. - The remainder - by subtraction.
- The "sum of parts = whole" invariant - by fuzzing.
- Remember different
decimalsacross tokens (special tokens).
What to read next
Learn how data is packed into slots and why uint96 ended up next to an address: the storage layout. Or how to spend less gas: gas optimization.
Keep reading
- 2 · Practice
Coin-Trust fees by example - how much you pay and to whom
A breakdown of the service fees: the 1% platform fee, the guarantor's rate (base and dispute) and gas. Tables for every deal outcome on a 1000 USDC example.
4 min read - 3 · Under the hood
Storage layout and variable packing in Solidity
How a contract stores data in 32-byte slots, how variables are packed, where mappings and arrays live and how the Coin-Trust deal struct is laid out.
4 min read
Mentioned in
- Coin-Trust fees by example - how much you pay and to whom
- Ethereum accounts and state - EOAs, contracts and the world state
- A tour of the Solidity language - types, functions, errors and events by example
- ERC-20 from scratch - writing a token in Solidity and analysing every line
- Writing escrow in Solidity: the state machine and a real contract walkthrough
- Storage layout and variable packing in Solidity
- A catalogue of smart contract vulnerabilities: 12 classic mistakes and defences
- Oracles and the oracle problem - how a blockchain learns about the outside world
- Gas optimization in Solidity - 12 techniques and when they apply
- Foundry for beginners - compiling, tests, fuzzing and a local network
Ready to try it?
Create your first deal in a couple of minutes. The money is locked in a smart contract and paid out by rules you can see in advance.

