December 23, 2023
7 mins.
In Solidity we don’t have Fixed Point Arithmetic out-of-the-box, hence we rely on various libraries to make it easier and safer for us. FixedPointMathLib and ABDKMath64x64 are some of the well known libraries that handle this.
Lets see which one should you be using and why? There can be a lot of factor to support the decision of which one to use, we will go through a couple of things that matter and see which one has to be used based on the scenario that a smart contract developer has.
Precision and Range:
10**18
.Performance:
int
to 64x64
format.Use Cases:
1// SPDX-License-Identifier: MIT2pragma solidity >=0.8.0;34import "https://github.com/Vectorized/solady/blob/main/src/utils/FixedPointMathLib.sol";56import "https://github.com/abdk-consulting/abdk-libraries-solidity/blob/master/ABDKMath64x64.sol";78using FixedPointMathLib for uint256;9using FixedPointMathLib for int256;1011using ABDKMath64x64 for int128;1213contract Math {14 function mul0(uint256 a, uint256 b) public pure returns (uint256 c) {15 c = a * b;16 }1718 function mul1(int128 a, int128 b) public pure returns (int128 c) {19 c = a.mul(b);20 }2122 function mul2(uint256 a, uint256 b) public pure returns (uint256 c) {23 int128 aFixed = ABDKMath64x64.fromUInt(a);24 int128 bFixed = ABDKMath64x64.fromUInt(b);2526 // Perform the calculation using ABDKMath64x6427 int128 rawResult = aFixed.mul(bFixed);2829 // Convert the result back to uint for the return value30 c = ABDKMath64x64.toUInt(rawResult);31 }3233 function mul3(uint256 a, uint256 b) public pure returns (uint256 c) {34 c = a.mulWad(b);35 }3637 function pow0(int256 a, int256 b) public pure returns (int256 c) {38 c = a.powWad(b);39 }4041 function pow1(int128 a, uint256 b) internal pure returns (int128 c) {42 c = a.pow(b);43 }44}
This smart contract tries to make use of both the libraries to do some basic multiplication and then calculate power.
Now we will try to execute each of those and see how each performs. We only care about inputs, outputs and gas here. So rest of logs are omitted here.
1to: Math.mul0(uint256,uint256)23execution cost41049 gas (Cost only applies when called by a contract)[]56decoded input7{ "uint256 a": "120", "uint256 b": "130" }[]89decoded output10{ "0": "uint256: c 15600" }[]
This is direct multiplication in solidity without any help of libraries. Simple to understand inputs and output.
1to23Math.mul1(int128,int128) 0x9D7f74d0C41E726EC95884E0e97Fa6129e3b5E99[]45execution cost61058 gas (Cost only applies when called by a contract)[]78decoded input9{ "int128 a": "120", "int128 b": "130" }[]1011decoded output12{ "0": "int128: c 0" }[]
The output here is 0. Any idea about why is that 0?
It is because the ABDKMath64x64 library expects the number to be in the form of 64.64
bit representation. That means we need to convert the number to binary and then add 64 bit of binary 0 to make fractional part 0 for that number. Lets do that for 120 and 130
| decimal | binary (without 64 zeros for fractional part) | decimal for 64.64 |
|----------|---------|------------------|
|120 | 1111000 | 2213609288845146193920 |
|130 | 10000010 | 2398076729582241710080 |
1to23Math.mul1(int128,int128) 0x9D7f74d0C41E726EC95884E0e97Fa6129e3b5E99[]45execution cost61058 gas (Cost only applies when called by a contract)[]78decoded input9{ "int128 a": "2213609288845146193920", "int128 b": "2398076729582241710080" }[]1011decoded output12{ "0": "int128: c 287769207549869005209600" }[]
287769207549869005209600 = 111100111100000000000000000000000000000000000000000000000000000000000000000000 = 15600
1Math.mul2(uint256,uint256) 0x9D7f74d0C41E726EC95884E0e97Fa6129e3b5E99[]23execution cost41351 gas (Cost only applies when called by a contract)[]56decoded input7{ "uint256 a": "120", "uint256 b": "130" }[]89decoded output10{ "0": "uint256: c 15600" }[]
This is using ABDKMath64x64, but the catch here is we used extra helpers to convert instead of doing it all by ourselves like we did before. Hence extra gas cost.
1to23Math.mul3(uint256,uint256) 0x9D7f74d0C41E726EC95884E0e97Fa6129e3b5E99[]45execution cost6877 gas (Cost only applies when called by a contract)[]78decoded input9{ "uint256 a": "128", "uint256 b": "128" }[]1011decoded output12{ "0": "uint256: c 0" }[]
The same again, not the results we wanted to see. FixedPointMathLib expects a number with 18 decimal places i.e., we need to append 18 zeros after the number
1to23Math.mul3(uint256,uint256) 0x9D7f74d0C41E726EC95884E0e97Fa6129e3b5E99[]45execution cost6877 gas (Cost only applies when called by a contract)[]789decoded input10{ "uint256 a": "120000000000000000000", "uint256 b": "130000000000000000000" }[]1112decoded output13{ "0": "uint256: c 15600000000000000000000" }[]
Now we get the correct output but with 18 zeros appended to it.
1to23Math.pow0(int256,int256) 0x9D7f74d0C41E726EC95884E0e97Fa6129e3b5E99[]45execution cost62723 gas (Cost only applies when called by a contract)[]78decoded input9{ "int256 a": "2000000000000000000", "int256 b": "10000000000000000000" }[]1011decoded output12{ "0": "int256: c 1023999999999999995727" }[]
Uses the FixedPointMathLib. I would expect 2 ** 10
to be 1024
. But the value here is 1023.999.....5727
which is not correct but close when we round up.
Users the ABDKMath64x64. For conversion
2
- 10
in binary and when converted to int with 64 bit decimal places, it is 18446744073709551616
10
- we give this directly as the power is expected in int128
but not 64.64
1to23Math.pow1(int128,uint256) 0xddaAd340b0f1Ef65169Ae5E41A8b10776a75482d[](https://remix.ethereum.org/#)45execution cost672302 gas (Cost only applies when called by a contract)[](https://remix.ethereum.org/#)89input10110x9ea...0000a[](https://remix.ethereum.org/#)1213decoded input1415{ "int128 a": "36893488147419103232", "uint256 b": "10" }[](https://remix.ethereum.org/#)1617decoded output1819{ "0": "int128: c 18889465931478580854784" }[](https://remix.ethereum.org/#)
The output when converted to binary will give us 10000000000+64 zeros
. Those 64 zeros can be omitted to get the integral part which then converts to 1024
, the exact output for 2 ** 10
ABDKMath64x64 | FixedPointMathLib | |
---|---|---|
mul | 1058 | 877 |
pow | 2302 | 2723 |
If we include gas to convert for ABDKMath64x64 it would add around 300
ABDKMath64x64 is gas efficient for complex calculations and very precise and accurate for smaller integral numbers with large fractional part. FixedPointMathLib is good with large numbers and has less precision for more complex calculations, but highly gas efficient for basic math.
So, if you are building something that include complex formulae like bonding curve, exponential farming or anything that requires high precision and complex calculations one should go for ABDKMath64x64, else if you require basic math with low precision which is mostly used in transfer, ERC20 related, more user facing applications and functions one should go for FixedPointMathLib, as for user facing you don’t have to put in extra effort to convert it to 64.64
format when compared to WAD
.
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!