top of page
  • Writer's pictureJeffrey Scholz

Fixed Point Arithmetic in Solidity (Using Solady, Solmate, and ABDK as Examples)

Updated: 6 days ago

A fixed-point number is an integer that stores only the numerator of a fraction — while the denominator is implied.

This type of arithmetic is not necessary in most programming languages because they have floating point numbers. It is necessary in Solidity because Solidity only has integers, and we often need to perform operations with fractional numbers.

Fixed point numbers are found in most DeFi smart contracts, so understanding them is a must.

For example, if the “implied denominator” is 100, then a fixed-point number holding “10” is interpreted as 0.1.

The most common fixed point number in Solidity is 10¹⁸: the number of “decimals” Ethereum and most ERC-20 tokens have. When we read the balance of an Ethereum address, we implicitly divide the number by 10¹⁸ to determine their Ether amount. For example, an address that has a balance of 10¹⁹ is interpreted to have 10 Ether — because the division by 10¹⁸ is implied.

The fixed point number with a denominator of 10¹⁸ is so common that engineers in the Solidity community refer to it as a “Wad” (the name was first introduced by MakerDAO). Sometimes, an 18-digit fixed-point number is interpreted as having the rightmost 18 digits allocated for the decimals, for example, the number “10” is shown below:

10 ^ 18 with an emphasis on the 18 zeros

However, we have found this mental model makes understanding fixed-point arithmetic harder to learn, so this article will use the mental model of the fixed-point number holding the numerator, and the 10¹⁸ denominator being implied.

In this article we will learn how to do arithmetic with fixed point numbers and explain how popular fixed point libraries work.

Converting an integer to a fixed point number

To convert an integer to a fixed point number, multiply the integer by the implied denominator. For example, “2 ether” is 2 × 10¹⁸, so converting the integer 2 to “2 ether” we multiply it by 10¹⁸. The implied denominator of 10¹⁸ cancels out the 10¹⁸.

Multiplying Fixed Point Numbers

To multiply two fixed-point numbers together, we follow the rules of multiplying fractions:

  1. multiply the numerators together

  2. multiply the denominators together

  3. simplify the result.

For example:

Fraction multiplication of 3/4 and 2/3

However, we can optimize this calculation in practice because the denominator is always the same with fixed point numbers.

Now let’s consider a different set of fractions that have a common denominator:

Fraction multiplication with common denominatiors

However, we do not want to return a result with an implied denominator of 𝑑 because that would not be compatible with the implied denominator we selected. Therefore, we need to divide the numerator and denominator by 𝑑 to return a fixed point number consistent with our choice of denominator.

Fraction multiplication and division relationship

Thus, if 𝑥 and 𝑦 are fixed point numbers with an implied denominator 𝑑² we can compute their product as (𝑥 × 𝑦)/𝑑.

Code example of multiplying fixed points

The Solady library has a mulWad math operation to multiply two fixed point numbers together with an implied Wad denominator (10¹⁸). Below, we show the code and then explain how it relates to our previous discussion:

solady mulwad function code

The core algorithm is at the bottom of the screenshot (inside the green box). We compute (𝑥 × 𝑦)/𝑑 there where 𝑑 is WAD or 10¹⁸ (as shown at the top of the screenshot where WAD is declared).

Real world example

Suppose a user has 1 DAI (which has 18 decimals) and we wish to compute their balance assuming their deposit has earned 15% interest. This is a clear example of the need for fixed-point arithmetic, since we cannot directly multiply a number by 1.15 in Solidity.

example contract using the solady mulwad library

The output is 1.15 after dividing by 1e18. Of course, we cannot actually divide by 1e18 because that would erase the decimals. We need a fixed-point representation because 1.15 cannot be represented as an integer. The code above can be tested on Remix here.

Multiplying a Fixed-Point Number by an Integer

Multiplying a fraction x by an integer y is the same as multiplying x by y/1:

Fraction and integer multiplication relationship

Thus, when we multiply a fixed-point number by an integer, we do not need any extra steps. We simply interpret the return value as a fixed-point number with the denominator unchanged.

Dividing Fixed Point Numbers

To divide fractions, we “flip” the second fraction and multiply them together. For example:

Fraction division to multiplication conversion calculation

Now let’s consider an example where they have the same denominator:

Fraction division between 6/10 and 3/10

Note that the common denominator of 10 got cancelled. If we want to represent 2 with an implied denominator of 10 (i.e. as a fixed point number with denominator 10), we need to multiply it by 10 again:

2 equals 2 times 10 divided by 10 fraction

Thus, for a general 𝑥 and 𝑦 having a common denominator 𝑑, if we want to express the output with an implied denominator of 𝑑, we must do the following:

derivation of the implied denominator equation ((x*d)/y)/d)

Thus, if 𝑥 and 𝑦 are fixed point numbers with an implied denominator 𝑑 we can compute their quotient as (𝑥 × 𝑑)/𝑦.

solady divwad function

If we put mulWad() and divWad() side-by-side, we can see the only difference between them (at the computation step, not the overflow check), is that the div case is multiplying by an inverted fraction.

solady mulwad() and muldiv() code side by side

Dividing a Fixed Point Number by an Integer

Suppose we want to divide 2.5 by 2 (or some fraction by an integer in general). It is not necessary to convert 2 to a fixed-point number via (2 × 𝑑)/𝑑.

Dividing a fraction x by an integer y is the same as dividing the numerator of x by y.

example of fraction division only effecting the numerator

Note that 35 ÷ 3 = 11, not 11.666, because we are using integer division, not floating points. We simply divide the fixed-point number by the integer and interpret the result as a fixed-point number. As with multiplying a fixed-point number by an integer, the denominator remains the same.

Adding and Subtracting Fixed Point Numbers

Adding and subtracting fractions that have the same denominator are simply added together and the denominator is ignored. We interpret the sum as a fixed-point number with the same implied denominator as the summands. For example,

fraction addition formula with the same denominator

Therefore, when adding fixed point numbers of the same denominator, we simply add the numbers together like we would with regular integers.

Consider the example with an implied denominator of 100:

Fraction subtraction with a denominator of 100

To compute it, we simply do 50 - 40 = 10, we don’t need to incorporate 100 into our computation.

Binary vs Decimal Fixed Point Number

A binary fixed point number is a fixed point number where the denominator can be expressed as 2ⁿ. Binary fixed point numbers are usually denoted with Q notation. For example, UQ112x112 uses 2¹¹² as the denominator. The U means “unsigned.” The datatype used to hold UQ112x112 would be 224. Another way to interpret this is that the “fractional part after the decimal” is held in the rightmost 112 bits and the “whole number part” is held in the left 112 bits.

As another example, UQ64x64 (or UQ64.64) is a uint128 that holds the “fractional part” in the least significant 64 bits and the “whole number” in the most significant bits. This can still be interpreted as having an implied denominator of 2⁶⁴ as we will see next.

The advantage of a binary fixed point number is we can use a gas-efficient left bit shift instead of multiplying by the denominator, (when converting an integer to a fixed point number, or doing a right bit shift when dividing.

As a basic example, consider that:

(1) 2 has a binary representation of 10

(2) 16 has a binary representation of 10000

(3) 16 = 2 × 2³

(4) binary(1000) = binary(10) << 3

Note that 3 is the exponent in (3) and the amount we leftshift the bits by in (4).

The relationship between the bitshift by amount e and multiplying by 2ᵉ holds in general. The following operations are equivalent:

// x * 2¹¹² equals x left bitshifted by 112 bits
x * 2 ** 112 == x << 112

// x / 2¹¹² equals x right bitshifted by 112 bits
x / 2 ** 112 == x >> 112

x can be an arbitrary number as long as it fits in the unsigned integer.

The ABDK library converts unsigned integers to fixed point numbers (with an implied 2⁶⁴ denominator) using the following function:

ABDK library fromUInt function code

The require statement ensures that x is less than type(int64).max, since the ABDK library uses signed fixed point numbers. The leftshift by 64 is equivalent to multiplying by 2⁶⁴.

Similarly, when ABDK does a multiplication, instead of dividing the product of x and y by 2⁶⁴, it does a rightshift by 64 bits:

ABDK mul function code

Uniswap V2 Fixed Point Library

The fixed point library of Uniswap V2 is quite simple because the only operations Uniswap V2 does with fixed point numbers is addition and division of a fixed point number by an integer.

Uniswap v2 uq112x112 library

The encode() function converts a uint112 to a fixed point number stored in a uint224. Uniswap V2 uses an implied denominator of 2**112. It could be more gas efficient if it used a bitshift instead of a multiplication (this is probably a mistake by the Uniswap developers).

The fixed point number is stored in a uint224 which is twice the size of the uint112 it will interact with. During the encode operation, the bits of the uint112 number are effectively shifted to the most significant 112 bits of the uint224.

This “encode” operation is easier to visualize with smaller uint sizes. Let’s use a hypothetical fixed-point number with a 2⁸ denominator. Below, we show what happens when we encode a uint8 to a fixed point number with a 2⁸ denominator:

Starting with the number 125 which has binary representation 01111101, if we multiply it by 2⁸, the product is 32000, which when stored in a 16 bit uint is represented as 0111110100000000. Note that multiplying 125 by 2⁸ has the same effect as shifting left by 8 bits.

The uqdiv() function simply does division of a fixed point number by an integer, which needs no extra steps.

Uniswap uses this library to accumulate the prices for the TWAP oracle below. The TWAP adds the latest price every time an update happens into an accumulator (which is used to calculate the average price with additional steps, something outside outside the scope of this article). Since prices are represented as fractions, fixed point numbers are an ideal way to represent them.

The variables _reserve0 and _reserve1 hold the most recent token balances of the pool and are uint112. price0CumulativeLast and price1CumulativeLast are UQ112x112 (fixed point numbers with an implied denominator of 2¹¹²). The code below from Uniswap V2 converts the numerator to a fixed point number (UQ112x112) and divides it by an integer (the denominator is not converted to a UQ112x112). The result is a fixed point number.

_update() function in uniswap using the UQ112X112 encode function

Rounding up vs rounding down

Fixed-point libraries commonly have the option to round up when dividing. For example, Solady has:

  • mulWadUp — multiply two fixed point numbers, but when dividing by d, round up. Recall, the formula for multiplying two fixed point numbers is (x × y) / d.

  • mulDivUp — divide two fixed point numbers, but round up when dividing

Solidity division always rounds down, for example 10 / 3 = 3. However, if we were to round up, 10 / 3 would be equal to 4. When calculating credits or prices, one should always round in favor of the protocol and against the user. For example, if we are computing how much a user should pay for a fixed amount of another asset, we should round the price up.

For example:

  • 10 / 3 rounding down is 3.3333

  • 10 / 3 rounding up is 3.3334 (depending on the size of our denominator)

Rounding up simply means adding 1 to the result if the remainder is non-zero. For example, 9 / 3 = 3 exactly, so we should not return 4. However, 10 / 3 and 11 / 3 have a remainder of 1 and 2 respectively, so we should add 1 to the result of the division.

Here is how the Solmate library does it:

Solmate mulDivUp function

In the green underline, the code checks if the modulo is greater than zero. If it is, then 1 is added to the result (rounding up), otherwise 0 is added (no round up).

337 views1 comment

Recent Posts

See All

1 Comment

4 days ago

fireboy and watergirl

I'm concerned about the repercussions. I would like to investigate the potential consequences of what you have suggested.

bottom of page