Fixed Point Numbers
Solidity has no floating-point type, so fractional values are stored as integers plus an agreed scale.
This document summarizes common fixed-point implementation styles.
Manual helpers
Section titled “Manual helpers”Overview
Section titled “Overview”You write the fixed-point formula directly in Solidity.
What it does
Section titled “What it does”It uses raw expressions such as (x * y) / 1e18 or (x * 1e18) / y.
Example libraries
Section titled “Example libraries”None; this is plain Solidity.
Example
Section titled “Example”// SCALE = 1e18uint256 priceWad = 2e18; // 2.0uint256 amountWad = 3e18; // 3.0
uint256 valueWad = amountWad * priceWad / 1e18; // 6e18 = 6.0- Intermediate overflow:
uint256 z = x * y / 1e18;can fail ifx * yoverflows before division. - Scale mismatch:
usdcAmount * priceWad / 1e18is wrong ifusdcAmountis still in token-native1e6scale. - Unit confusion:
assets + priceWadcompiles even though the quantities are incompatible.
Wad-style utility libraries
Section titled “Wad-style utility libraries”Overview
Section titled “Overview”Instead of repeating the scaling formula, you call helpers that assume a fixed decimal scale.
Example libraries: Solady FixedPointMathLib, Solmate FixedPointMathLib, and Aave WadRayMath.
What it does
Section titled “What it does”These libraries wrap common operations such as wad multiplication, wad division, and sometimes ray multiplication and division, with scale restoration built in.
Example
Section titled “Example”// SCALE = 1e18uint256 amountWad = 3e18; // 3.0uint256 priceWad = 2e18; // 2.0
uint256 valueWad = FixedPointMathLib.mulWad(amountWad, priceWad); // 6e18 = 6.0- Unit correctness is still manual:
mulWad(assetsWad, feeWad)only makes sense if both operands belong in that formula. - Helper names can create false confidence:
divWad(usdcAmount, totalShares)is still wrong ifusdcAmountwas never normalized. - Scale lock-in: using one decimal helper everywhere can hide places where a different precision policy is better.
Full-precision math primitives
Section titled “Full-precision math primitives”Overview
Section titled “Overview”Instead of relying on the EVM’s native 256-bit multiply, you use primitives that compute floor(a * b / denominator) with 512-bit intermediate arithmetic.
Example libraries: Uniswap FullMath, OpenZeppelin Math.mulDiv.
What it does
Section titled “What it does”These libraries provide mulDiv(a, b, denominator), which stays safe even when a * b would overflow uint256. They are not fixed-point libraries because they assume no scale, but they are a key building block for dangerous ratio paths.
Example
Section titled “Example”// shares = assets * totalShares / totalAssetsuint256 assets = 50;uint256 totalShares = 1000;uint256 totalAssets = 200;
uint256 sharesOut = FullMath.mulDiv(assets, totalShares, totalAssets); // 250- Denominator zero:
denominator = 0reverts, so callers must guard against zerototalAssetsortotalShares. - Result overflow: if the true result exceeds
uint256, the call still reverts. - False sense of safety: overflow protection only covers the intermediate multiply, not scale correctness or unit mismatches.
Fixed-point numbers
Section titled “Fixed-point numbers”These libraries define a fixed SCALE, store values as integers, and provide helpers that preserve that scale across operations.
They represent Solidity integers as scaled real numbers by agreeing on a denominator and restoring it after multiplication or division.
Fixed-point numbers store a scaled integer, if the real value is , the stored value usually is:
Common rules:
- Add/subtract: both values must use the same
SCALE - Multiply:
result = (a * b) / SCALE - Divide:
result = (a * SCALE) / b
This pattern is the same whether the scale is decimal like 1e18 or binary like 2^64.
Fixed-point libraries mainly differ in the choice of SCALE and how they help manage it.
Some expose plain helpers, while others add stronger typed wrappers; PRBMath, for example, exposes typed decimal formats such as UD60x18.
Negative values
Section titled “Negative values”Signed fixed-point values depend on the representation of the underlying stored integer, not on whether the scale is decimal or binary.
Common signed encodings include:
- Two’s complement: the most significant bit participates in the sign encoding; this is the standard modern representation.
- One’s complement: negative values are formed by inverting all bits of the positive value; it has both and .
- Sign-magnitude: the most significant bit is an explicit sign bit, and the remaining bits store the magnitude; it also has both and .
For Unsigned storage there is no sign encoding, so negative values are not representable.
Ranges
Section titled “Ranges”If an -bit stored integer
- signed, that use two’s complement,
- its integer range is to .
- its fixed points range is
- unsinged
- its integer range is
- its fixed points range is
Decimal fixed-point numbers
Section titled “Decimal fixed-point numbers”Use a decimal scale of the form 10^n, such as 1e18 or 1e27, so the representation matches how humans think about decimals.
They implement fixed-point arithmetic in a decimal system that fits token decimals, percentages, rates, and other human-readable values.
Unlike wad-style utility libraries, they define a formal SCALE abstraction and often expose typed wrappers that bind values to that scale.
Example libraries: PRBMath decimal formats such as UD60x18 (unsigned) and SD59x18 (signed)
Representation
Section titled “Representation”real_value: the actual decimal number you want to represent, such as 6.5stored_value: the scaled integer that is actually stored in memory, such as 65000n: the number of decimal places fixed to the right of the decimal point10^n: the scale factor
Typical scales:
- Wad:
10^18 - Ray:
10^27 - Typed decimal formats such as
UD60x18, which use 18 decimal places
Encoding
Section titled “Encoding”Example: Encode 6.5 in Decimal Fixed-Point (4.4):
6.5 × 10^4 = 6.5 × 10000 = 65000 → stored as binary 1111 1101 0110 1000
Decoding
Section titled “Decoding”Example: Decode 65000 in Decimal Fixed-Point (4.4):
retrieve 1111 1101 0110 1000 → 65000 ÷ 10⁴ = 6.5
Example usage
Section titled “Example usage”// UD60x18 uses 18 decimal places (60 integer bits, 18 fractional bits, scale = 10^18)
// ud() expects pre-scaled values, since we use UD60x18 we scale by e18
// Encoded:// 3.0 stored as 3 * 10^18// Full value: 3e18 (uint256 decimal) = 3000000000000000000 decimal// Integer part: 3 (scaled by 10^18)// Fractional part: 0// Decoded:// 3000000000000000000 / 10^18 = 3.0UD60x18 amount = ud(3e18);
// Encoded:// 2.0 stored as 2 * 10^18// Full value: 2e18 (uint256 decimal) = 2000000000000000000 decimal// Integer part: 2 (scaled by 10^18)// Fractional part: 0// Decoded:// 2000000000000000000 / 10^18 = 2.0UD60x18 price = ud(2e18);
// Encoded:// 6.0 stored as 6 * 10^18 (result of 3.0 * 2.0 = 6.0)// Full value: 6e18 (uint256 decimal) = 6000000000000000000 decimal// Integer part: 6 (scaled by 10^18)// Fractional part: 0// Decoded:// 6000000000000000000 / 10^18 = 6.0UD60x18 value = amount.mul(price);TradeOffs
Section titled “TradeOffs”Pros:
- It matches the human decimal system, so values like
1.5,2.75%, and token decimals are easier to reason about. - It is easier to debug because logs, tests, and documentation usually already use base-10 numbers.
- It fits naturally with ERC-20 decimals, oracle decimals, and common DeFi conventions such as wad and ray.
Cons:
- Decimal scaling does not align naturally with binary integer hardware, so scale restoration uses constants like
1e18instead of power-of-two shifts. - Operations are more expensive than binary fixed-point arithmetic.
- Scale lock-in: forcing all protocol math into one decimal scale can hide cases where a different precision level is needed.
- False confidence from readable helpers:
divWad(usdcAmount, totalShares)is still wrong ifusdcAmountwas not normalized first. - Unit correctness is still manual unless stronger typing is added on top.
Binary fixed-point numbers
Section titled “Binary fixed-point numbers”Use a binary scale of the form 2^n, so values live in a system aligned with binary integers rather than decimal notation.
Example libraries: ABDKMath64x64
Representation
Section titled “Representation”Where:
real_value: the actual number you want to represent, such as 6.5stored_value: the scaled integer stored in memoryn: the number of fractional bits2^n: the binary scale factor
Typical format is Qm.n and UQm.n, where m is the number of integer bits, n is the number of fractional bits, and UQ means unsigned while Q usually means signed.
Example ranges
Section titled “Example ranges”When you are selecting which Q format you exchange range for precision
Bits are not decimal places
- Min value:
- Max value:
- Resolution (precision):
Below you can see examples of range and precision from some Q formats
| Format | Total Bits | Min Value | Max Value | Resolution (decimal) |
|---|---|---|---|---|
| Q16.0 | 16 | −32768 | 32767 | 1 |
| Q12.4 | 16 | −2048 | 2047.9375 | 0.0625 |
| Q8.8 | 16 | −128 | ≈ 127.996 | ≈ 0.003906 |
| Q4.12 | 16 | −8 | ≈ 7.9998 | ≈ 0.000244 |
| Q1.15 | 16 | −1 | ≈ 0.99997 | ≈ 0.0000305 |
Encoding
Section titled “Encoding”store: multiply by 2^n =
<< n
Encoding Steps
- Multiply decimal by (or left-shift integer part
<<64). - Round to the nearest uintX/intX (depends on library).
- Store as an unsigned/signed X-bit value.
Example: Encode 6.5 in binary fixed-point Q4.4:
6.5 × 2^4 = 6.5 × 16 = 104 → stored as binary 0110 1000
Decoding
Section titled “Decoding”retrieve: divide by 2^n =
>> n
Decoding Steps
- Divide stored uintX/intX by
- The result is the exact decimal (or approximation if non-exact).
Example: Decode 0110 1000 in Q4.4:
retrieve 0110 1000 → 104 ÷ 2^4 = 104 ÷ 16 = 6.5
Bit Interpretation
Section titled “Bit Interpretation”Example 6.5 in Q4.4:
Q4.4
- Range: −8 to 7.9375
- Precision: 0.0625
| ◄──────── Integer ────────►| ◄─────── Fractional ──────► |
| Bit | b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 |
|---|---|---|---|---|---|---|---|---|
| Weight | -8 | 4 | 2 | 1 | 0.5 | 0.25 | 0.125 | 0.0625 |
| Stored | 0 | 1 | 1 | 0 | 1 | 0 | 0 | 0 |
The value is: 4 + 2 + 0.5 = 6.5
Example -6.5 in Q4.4:
| ◄──────── Integer ────────►| ◄─────── Fractional ──────► |
| Bit | b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 |
|---|---|---|---|---|---|---|---|---|
| Weight | -8 | 4 | 2 | 1 | 0.5 | 0.25 | 0.125 | 0.0625 |
| Stored | 1 | 0 | 0 | 1 | 1 | 0 | 0 | 0 |
The value is: -8 + 1 + 0.5 = -6.5
Example usage
Section titled “Example usage”// ABDKMath64x64 uses signed 64.64 binary fixed-point// Encoded:// 4.0 stored as 4 << 64 or 4*2^64// Full Hex: 0x00000000000000040000000000000000 (int128 hex) = 73786976294838206464 decimal// High 64 bits: 0x0000000000000004 (integer part)// Low 64 bits: 0x0000000000000000 (zero fraction)// Decoded:// 73786976294838206464 / 2^64 = 4int128 four = ABDKMath64x64.fromUInt(4);
// Encoded:// 2.5 stored as 2.5 << 64 or 2.5*2^64// Full Hex: 0x00000000000000028000000000000000 (int128 hex) = 46116860184273879040 decimal// High 64 bits: 0x0000000000000002 (integer part = 2)// Low 64 bits: 0x8000000000000000 (fraction = 0.5 = 2^{-1})// Decoded:// 46116860184273879040 / 2^64 = 2.5int128 twoPointFive = ABDKMath64x64.divu(5, 2); // 2.5
// Encoded:// 10.0 stored as 10 << 64 or 10*2^64 (result of 4.0 * 2.5 = 10.0)// Full Hex: 0x000000000000000A0000000000000000 (int128 hex) = 184467440737095516160 decimal// High 64 bits: 0x000000000000000A (integer part = 10)// Low 64 bits: 0x0000000000000000 (zero fraction)// Decoded:// 184467440737095516160 / 2^64 = 10int128 result = ABDKMath64x64.mul(four, twoPointFive); // 10.0TradeOffs
Section titled “TradeOffs”Pros:
- The scale is a power of two, so some scale-handling steps map naturally to bit shifts instead of decimal constants.
- It matches power-of-two scaling and standard binary fixed-point formats such as
Qm.n - Libraries in this family often provide richer math functionality, including exponential and other advanced functions.
Cons:
- Binary formats are less intuitive for humans working with token decimals, fee percentages, and decimal-oriented protocol specs.
- They usually require extra conversion at decimal-binary boundaries.
- Decimal-binary conversion errors: a token amount can be converted into binary fixed-point and later treated as if it were still decimal-scaled.
- Lower economic readability: binary values are harder to inspect mentally than decimal ones.
- Mixed-domain scale crossings: token decimals, oracle decimals, binary internals, and decimal outputs can drift badly after one bad conversion.