Decimals in Solidity: A Fixed-Point of view

Juan Xavier Valverde
CoinsBench
Published in
10 min readNov 21, 2023

--

Introduction

In the world of blockchain and smart contract development, precision is vital. For Solidity developers, fixed-point arithmetic is a crucial concept that ensures accuracy in financial and mathematical operations within decentralized applications and smart contracts. In this article, we'll explore fixed-point arithmetic in Solidity, its significance, and practical use cases. This guide is designed for intermediate Solidity developers ready to dive into the world of fixed-point numbers and operations.

For the purpose of this article, which is to provide an overview of the usefulness about Fixed Point math libraries, the functions we’ll be revisiting are in this library FixedPoint, extracted from Solady’s FixedPointMathLib.

The FixedPointMathLib is an all-round library with operations for fixed-point numbers. It includes functions to calculate maximums, minimums, absolute values, exponentiations, square roots and more. Particularly, we'll take a look at four functions to understand how they work and how they can be used in your next project: mulWad, mulWadUp, divWad, divWadUp.

What is Fixed-Point Arithmetic?

One of Solidity's downsides are the limitation in representing and operating with decimal values. Fixed-point arithmetic comes in as a solution to overcome this issue. By utilizing fixed-point arithmetic, we are able to work with numbers that have (or need to have) decimal places. Typically, fixed-point numbers are implemented as unsigned integers with a predetermined number of decimal places.

This approach ensures precise calculations by multiplying and or dividing by a factor or precision. By employing this technique, developers can perform mathematical operations involving decimal values more accurately. It is an essential tool when dealing with financial transactions or any scenario that requires precise calculations involving fractions, particularly in DeFi applications.

Fixed-Point Precision

The level of precision in fixed-point arithmetic depends on the number of decimal places used. In most blockchain applications, the choice is 18 decimal places, which makes perfect sense as it's the decimal relation between ETH and wei. This is often represented as WAD (probably short for "wei as decimal", but the origin looks unclear), and it serves as the base for the operations we'll explore.

You may find some applications that use a RAY value instead, which is 27 decimal places.

uint256 internal constant WAD = 1e18;

We’ll be using WAD repeatedly for ensuring that all fixed-point operations are consistent and adhere to the wanted precision.

Hands on

Now, let's explore the Solidity functions tailored for fixed-point arithmetic operations, all designed to maintain precision based on the 18 decimal places defined by WAD.

In regards to calculation, and also, for the sake of simplicity and to work with less amount of zeros, we'll work with a `WAD` of `100` (1e2). This would mean the rounding would be done on a `100` basis.

mulWad()

function mulWad(uint256 x, uint256 y) public pure returns (uint256 z) {
assembly {
// Equivalent to `require(y == 0 || x <= type(uint256).max / y)`.
if mul(y, gt(x, div(not(0), y))) {
mstore(0x00, 0xbac65e5b) // `MulWadFailed()`.
revert(0x1c, 0x04)
}
z := div(mul(x, y), WAD)
}
}

Note: not(0) == type(uint256).max

The mulWad function calculates the product of two fixed-point numbers, x and y (which we'll refer to as xy from this point), and then divides the result by WAD while rounding down to the nearest integer (by doing no extra computation since Solidity rounds down out of the shelf when working with remainders).

The function also includes a safety check to ensure that dividing xy by WAD won't lead to integer overflows issues. If the division is safe, the function proceeds with the calculation.

Basically, it will revert if x > (type(uint256).max / y) as this would mean a potential overflow in the result.

Examples:

  • In regards to safety, let's say, for the sake of simplicity, that type(uint256).max is 100. If inputs x = 5 and y = 25, the function would revert as 5 > (100 / 25), revealing an overflow.
  • If inputs x = 5e2 (500) and y = 25e2 (2500) the function would return 12500, which translates to 125.00.
  • If we wanted to multiply, let’s say 0.2 (tokens) * 25.11 (price), we would input x = 20 and y = 2511, and get 502, which translates to 5.02.

mulWadUp()

function mulWadUp(uint256 x, uint256 y) public pure returns (uint256 z) {
assembly {
// Equivalent to `require(y == 0 || x <= type(uint256).max / y)`.
if mul(y, gt(x, div(not(0), y))) {
mstore(0x00, 0xbac65e5b) // `MulWadFailed()`.
revert(0x1c, 0x04)
}
z := add(iszero(iszero(mod(mul(x, y), WAD))), div(mul(x, y), WAD))
}
}

The mulWadUp function operates similarly to mulWad in the way that it calculates the product of two fixed-point numbers, xy and subsequently divides the result by WAD. Just like mulWad the function also includes a safety check that will also make the function revert if x > type(uint256).max / y. However, it employs a distinct rounding strategy, rounding up to the nearest integer.

In terms of rounding, what sets mulWadUp apart from mulWad is its utilization of the mod operation to calculate the remainder when xy is divided by WAD. If the remainder isn't zero, it implies that rounding up is required to uphold precision in fixed-point arithmetic. The function then adds 1 to the outcome of dividing xy by WAD, ensuring that the final result is rounded up to the nearest integer whenever necessary (considering the WAD precision point).

If we wanted to translate the assembly code to Solidity, it would look like this:

if ((x * y) % WAD > 0) {
z = ((x * y) / WAD) + 1
}

Examples:

  • With inputs x = 5 and y = 25 the function would return us 2. This is because the result, 125, is higher than 100. So in this case it will round up to 200, thus the result 2.
  • With inputs x = 50 and y = 25, the return result would be 13, as the result of 50 * 25 = 1250, so it rounds up to 1300.
  • If we wanted to multiply 0.2 (tokens) * 25.11 (price), we would input x = 20 and y = 2511, and we would get 503, which is rounded up from the 5.02 value we obtained from a previous example of `mulWad`.

divWad()

function divWad(uint256 x, uint256 y) public pure returns (uint256 z) {
assembly {
// Equivalent to `require(y != 0 && (WAD == 0 || x <= type(uint256).max / WAD))`.
if iszero(mul(y, iszero(mul(WAD, gt(x, div(not(0), WAD)))))) {
mstore(0x00, 0x7c5f487d) // `DivWadFailed()`.
revert(0x1c, 0x04)
}
z := div(mul(x, WAD), y)
}
}

The divWad function offers precise division of fixed-point numbers. Just like mulWad and mulWadUp, it also includes a safety check to ensure that the division won't lead to integer underflow issues.

As always in math, the denominator in a division can't be 0, so the function includes a safety check to ensure that for y . Besides that, the code checks that if x > type(uint256).max / WAD we shall revert, as this would mean an overflow when multiplying x times WAD.

The division is then carried out by multiplying x by WAD to maintain the required precision and then dividing by y. As mentioned previously, there's no need for extra computation to round down as Solidity works this way.

Examples:

  • With inputs x = 28 and y = 5 the function would return 560, since (28 * 100) / 5 = 560. This would translate to 5.60.
  • With inputs x = 25 and y = 50, the function would return 50, since (25 * 100) / 50 = 50`. This would translate to 0.5, as we're (obviously) taking into account the WAD precision point.
  • With inputs x = 10 and y = 6 the function would return 166, since (10*100) / 6 = 166.6. This would translate to 166, rounding down.

divWadUp()

function divWadUp(uint256 x, uint256 y) public pure returns (uint256 z) {
assembly {
// Equivalent to `require(y != 0 && (WAD == 0 || x <= type(uint256).max / WAD))`.
if iszero(mul(y, iszero(mul(WAD, gt(x, div(not(0), WAD)))))) {
mstore(0x00, 0x7c5f487d) // `DivWadFailed()`.
revert(0x1c, 0x04)
}
z := add(iszero(iszero(mod(mul(x, WAD), y))), div(mul(x, WAD), y))
}
}

The divWadUp function offers precise division of fixed-point numbers, but rounding up to the nearest integer.

As in the previous function, safety checks for overflows/underflows are included and the code is the same as in divWad.

The division is carried out by multiplying x by WAD to maintain the required precision and then dividing by y. Similarly to mulWadUp, the function also includes a mod operation to calculate the remainder when xy is divided by y. If the remainder isn't zero, it implies that rounding up is required to uphold precision in fixed-point arithmetic. The function then adds 1 to the outcome of dividing xy by y, ensuring that the final result is rounded up to the nearest integer whenever necessary (again, considering the WAD precision point).

Examples:

  • With inputs x = 10 and y = 6, the function would return 167, since ((10 * 100) / 6 ) + 1 = 167. This would translate to 1.67, which is the rounded-up version of the divWad example.
  • With inputs x = 100 and y = 300, the function would return 34, since ((100 * 100) / 300 ) + 1 = 34. This would translate to 0.34 (1 divided by 3 rounded up).
  • With inputs x = 21e2 and y = 4e2 the function would return 525, since ((2100 * 100) / 400 ) = 525. This would translate to 5.25, which is the same non-rounded version that the divWad would return.

Significance of Fixed-Point Arithmetic

Fixed-point arithmetic is crucial in blockchain and smart contract development, especially in decentralized finance (DeFi) and financial applications. Here's why it's significant:

- Precision: Fixed-point arithmetic enables precise representation and manipulation of non-integer values, which is critical in financial applications where even the smallest discrepancies can have significant consequences.

- Consistency: Using a fixed number of decimal places ensures consistency across different operations and smart contracts. In DeFi, where interoperability and trust are vital, this consistency is crucial.

- Security: Preventing overflows and handling division by zero is crucial for smart contract security. Fixed-point arithmetic provides mechanisms to address these issues, reducing the risk of vulnerabilities.

- Reliability: Fixed-point arithmetic minimizes rounding errors and imprecisions, making smart contracts more reliable and accurate in financial calculations.

Use Cases of Fixed-Point Arithmetic

Now that the significance of fixed-point arithmetic is explained, let's explore some key use cases in the web3 ecosystem:

- Lending and Borrowing Platforms: DeFi platforms like Aave and Compound use fixed-point arithmetic libraries for interest rate calculation, loan to value ratio, and liquidation calculations. These calculations require precise arithmetic to ensure accurate and fair transactions.

- Decentralized Exchanges (DEXs): Fixed-point arithmetic libraries are used in DEXs to perform calculations for trading pairs, liquidity provision, and yield farming. DEXs like Uniswap and Sushiswap use similar libraries to calculate the amount of tokens to be swapped and the amount of tokens to be received.

- Decentralized Autonomous Organizations (DAOs) and stablecoin protocols: DAOs often require precise financial calculations for proposals, voting mechanisms, and fund management. MakerDAO use fixed-point arithmetic libraries to maintain the stability of their stablecoin.

- Token Staking and Liquidity Provision: DeFi applications use fixed-point arithmetic libraries to calculate yields and rewards for liquidity providers. These calculations require precision to ensure that users receive the correct amount of rewards proportional to their stake.

- NFTs and Gaming DApps: In gaming DApps and NFT platforms, fixed-point arithmetic libraries can be used for calculating game mechanics, tokenomics, and rewards distribution. For example, they can be used to calculate the probability of getting a rare item from a loot box or the rewards earned by a player in a game.

Conclusion

Essentially, any mathematical calculation that requires decimal precision can benefit from this library. By using fixed-point arithmetic, we can ensure that all calculations are consistent and robust.

Fixed-point arithmetic is a fundamental concept for Solidity developers, especially in the realm of DeFi applications. The functions provided for fixed-point arithmetic operations ensure precision, consistency, and security in financial and mathematical calculations.

For Solidity developers, understanding fixed-point arithmetic and how to use these functions is a valuable skill that enhances the creation of reliable and secure smart contracts. Whether you're working on DeFi platforms, stablecoins, or other financial applications, and even games, fixed-point arithmetic is a tool that enhances the math capabilities of your smart contracts are both functional and trustworthy.

References:

If you found this article useful, feel free to show your support through a donation to my address or via Kofi:

Your contribution would be greatly appreciated!

--

--