December 23, 2023
10 mins.
In the realm of blockchain and smart contract development, efficient and accurate arithmetic operations are crucial. Solidity inherently supports integer arithmetic. However, there are scenarios where we require more precision, and this is where fixed point arithmetic enters the fray.
Fixed point arithmetic allows for fractional calculations, bridging the gap between integer arithmetic’s limitations and the necessity for precision in certain calculations.
Let’s delve into the FixedPointMathLib, one of the the libraries that facilitate fixed point arithmetic in Solidity , available on GitHub, source file located here. To understand its core functionalities and how you can leverage it in your smart contract projects. We’ll try to understand what Fixed Point Arithmetic is and also go through some code in the library aforementioned.
In fixed point arithmetic, numbers are represented as integers, but a predefined scale factor indicates the decimal point’s position. For instance, if the scale factor is 100
, the number 12345
represents 123.45
. This method allows for fractional computations while utilising integer arithmetic operations, thus maintaining a balance between precision and performance. The scale factor can change depending on our requirement for precision.
If you’re familiar with other programming languages then you would have already known that most of the languages support floating point and fixed point numbers by default with some data types. For example you can see float
and double
types in C/C++. If you can remember we have no such data types in Solidity. That is for a reason, to put it simple it is due to the inconsistencies in the handling and implementation of fractional numbers on machines. Due to these inconsistencies, we might end up in multiple forks of the chain due to minor changes in data on chain.
Need to know more? Your rabbit hole starts here
Let us look into some functions what this library offers us.
The mulWad
function is designed to multiply two fixed point numbers. Here’s the function signature:
1function mulWad(uint256 x, uint256 y) internal pure returns (uint256 z)
Parameters:
a
and b
: The fixed point numbers to be multiplied.If you observe, we have something called Wad
in function name. That basically means that the scale factor here is 10**18
(alleged origin of wad
comes from here)
The function employs Yul to perform the multiplication operation efficiently. By leveraging Yul, the mulWad
function ensures that the multiplication is carried out with a low gas cost, which is crucial for blockchain operations.
1/// @solidity memory-safe-assembly2assembly {3 // Equivalent to `require(y == 0 || x <= type(uint256).max / y)`.4 if mul(y, gt(x, div(not(0), y))) {5 mstore(0x00, 0xbac65e5b) // `MulWadFailed()`.6 revert(0x1c, 0x04)7 }8 z := div(mul(x, y), WAD)9}
The mulWad
function first computes the product of a
and b
using integer multiplication, then adjusts the result for the scale factor by dividing the product by 10 ** 18
(in the file at L51 WAD
constant is defined). This way, the function yields the correct fixed point representation of the product.
1if mul(y, gt(x, div(not(0), y)))
If you observe the code checks for the overflow case and makes sure to revert the execution with error.
not(0)
- bit wise negation results in the max int value. Ex: 0x00
will be 0xFF
when negated - let us assume this result as maxInt
. Here it is actually max value a uint256
can take.div(not(0), y))
- now this will be div(maxInt, y)
- lets assume this as quotient
gt(x, div(not(0), y))
- this will check if x > quotient
. Simple check to make sure that x * y < maxInt
. Lets assume this result as check
, which can have a value of 0
and 1
iszero(mul(y, iszero(mul(WAD, gt(x, div(not(0), WAD))))))
- simply means that verifying if y * check
is 0
or notThis makes it safe to avoid unexpected behaviour of the smart contracts.
1mstore(0x00, 0xbac65e5b) // `MulWadFailed()`.2revert(0x1c, 0x04)
mstore(0x00, 0xbac65e5b)
: This line is storing the value 0xbac65e5b
at memory location 0x00
. The value 0xbac65e5b
is likely the hash of the error message MulWadFailed()
. Solidity uses the first 4 bytes of the keccak-256
hash of the function signature to identify it. This is a common practice to save gas instead of storing the entire string error message.revert(0x1c, 0x04)
: This line reverts the transaction. The revert
opcode takes two parameters: the first is the starting position in memory of the data to return, and the second is the size of this data in bytes. In this case, 0x1c
is the position in memory where the data starts, and 0x04
is the size of the data to return. It means that when the transaction is reverted, it will return 4 bytes of data starting from the 28th byte (0x1c
in hexadecimal) of memory.This is similar to mulWad
but the result is rounded up. For the other case it is rounded down
1z := add(iszero(iszero(mod(mul(x, y), WAD))), div(mul(x, y), WAD))
iszero(mod(mul(x, y), WAD))
- check if (x * y) % WAD == 0
- If it is zero, it means the product is perfectly divisible by WAD
and there’s no fractional part to consider for roundingiszero(iszero(mod(mul(x, y), WAD)))
: This double-negative check turns a true condition (non-zero modulus) into a false one and vice versa. So, if there is a non-zero fractional part, the iszero
function will return 0
for non-zero and then iszero
again will turn that 0
into 1
.add(iszero(iszero(mod(mul(x, y), WAD))), div(mul(x, y), WAD))
: Adds the result of the double iszero
check to the quotient of the product divided by WAD
. This effectively rounds up the result if there was a non-zero fractional part.Similarly, the divWad
function facilitates division operations between fixed point numbers. Here’s the function signature:
1function divWad(uint256 x, uint256 y) internal pure returns (uint256 z)
Parameters are similar to mulWad
, with a
and b
being the operands
There are checks here as well, just like we do in other functions to make sure the results are not inconsistence or overflowed or underflowed.
1assembly {2 // Equivalent to `require(y != 0 && (WAD == 0 || x <= type(uint256).max / WAD))`.3 if iszero(mul(y, iszero(mul(WAD, gt(x, div(not(0), WAD)))))) {4 mstore(0x00, 0x7c5f487d) // `DivWadFailed()`.5 revert(0x1c, 0x04)6 }7 z := div(mul(x, WAD), y)8}
mul(WAD, gt(x, div(not(0), WAD)))
: This performs a check to ensure that the value of x
is not too large to be multiplied by the scaling factor WAD
without causing an overflow. The gt
function checks if x
is greater than the largest uint256
divided by WAD
. If x
is indeed greater, gt(x, div(not(0), WAD))
would return 1
indicating an overflow. If x
is not greater, the result would be 0
. iszero(mul(WAD, gt(x, div(not(0), WAD))))
: This is checking the result of the multiplication for 0
. If x <= uint256.max / WAD
, then gt(x, div(not(0), WAD))
would be 0
, and multiplying by WAD
would still be 0
, resulting in iszero
returning 1
. If x
were too large, the result of gt(x, div(not(0), WAD))
would be 1
, and mul(WAD, 1)
would be WAD
, making iszero
return 0
.mul(y, iszero(mul(WAD, gt(x, div(not(0), WAD)))))
: This multiplies y
with the result of the previous iszero
check. If the previous result was 1
, it implies that there is no risk of overflow when x
is multiplied by WAD
, and it proceeds to multiply y
by 1
(effectively leaving y
unchanged). If the result was 0
, it means that there was a risk of overflow, and multiplying y
by 0
would yield 0
.iszero(...)
: If y
was 0
or if x
was safe to multiply by WAD
without causing an overflow, then the entire multiplication expression would evaluate to 0
, and iszero
would return 1
. If there was a potential overflow detected, the multiplication result would not be 0
, and iszero
would return 0
.This is making sure that we make the calculations in the bounds and makes sure no unexpected behaviour happens if there is an overflow or underflow.
This is similar to divWad
but the result is rounded up. For the other case it is rounded down. A similar approach to round up used in mulWadUp
is being used here but with a different values.
These functions are indispensable when dealing with financial calculations, simulations, or any scenario demanding precision beyond integer arithmetic in your smart contracts. By understanding and utilising fixed point arithmetic through the FixedPointMathLib, you can ensure that your smart contracts operate accurately and efficiently, making your dApps more robust and reliable. Examples as follows:
x * y = k
) for liquidity pools.Also other examples include Insurance Contracts, Bonding Curves, Interest Rate Models, Stablecoin Pegs, Financial Derivatives, Scientific Computations, Gaming and Gambling Contracts, Fractional NFTs
In conclusion, FixedPointMathLib’s functions are vital tools in the toolkit of a Solidity developer aiming to tackle complex arithmetic scenarios in smart contract development. Through a deep understanding of these functions and fixed point arithmetic, you’re well on your way to mastering the mathematical underpinnings of Solidity and developing sophisticated smart contracts for the Ethereum blockchain.
By Krushi Raj Tula A developer, geek, enthusiast, who loves to solve problems and fix things with technology. You can reach out to him on Twitter!