article · December 23, 2023 · 10 min read
Mastering Fixed Point Arithmetic in Solidity
An article that explores on why we would need a library to handle fixed point arithmetic in Solidity.
Mastering Fixed Point Arithmetic in Solidity
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.
Understanding Fixed Point Arithmetic
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.
Why even use a Library for this?
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
FixedPointMathLib
Let us look into some functions what this library offers us.
mulWad
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:
aandb: 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:0x00will be0xFFwhen negated - let us assume this result asmaxInt. Here it is actually max value auint256can take.div(not(0), y))- now this will bediv(maxInt, y)- lets assume this asquotientgt(x, div(not(0), y))- this will check ifx > quotient. Simple check to make sure thatx * y < maxInt. Lets assume this result ascheck, which can have a value of0and1iszero(mul(y, iszero(mul(WAD, gt(x, div(not(0), WAD))))))- simply means that verifying ify * checkis0or not
This 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 value0xbac65e5bat memory location0x00. The value0xbac65e5bis likely the hash of the error messageMulWadFailed(). Solidity uses the first 4 bytes of thekeccak-256hash 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. Therevertopcode 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,0x1cis the position in memory where the data starts, and0x04is 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 (0x1cin hexadecimal) of memory.
mulWadUp
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 byWADand 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, theiszerofunction will return0for non-zero and theniszeroagain will turn that0into1.add(iszero(iszero(mod(mul(x, y), WAD))), div(mul(x, y), WAD)): Adds the result of the doubleiszerocheck to the quotient of the product divided byWAD. This effectively rounds up the result if there was a non-zero fractional part.
divWad
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 ofxis not too large to be multiplied by the scaling factorWADwithout causing an overflow. Thegtfunction checks ifxis greater than the largestuint256divided byWAD. Ifxis indeed greater,gt(x, div(not(0), WAD))would return1indicating an overflow. Ifxis not greater, the result would be0.iszero(mul(WAD, gt(x, div(not(0), WAD)))): This is checking the result of the multiplication for0. Ifx <= uint256.max / WAD, thengt(x, div(not(0), WAD))would be0, and multiplying byWADwould still be0, resulting iniszeroreturning1. Ifxwere too large, the result ofgt(x, div(not(0), WAD))would be1, andmul(WAD, 1)would beWAD, makingiszeroreturn0.mul(y, iszero(mul(WAD, gt(x, div(not(0), WAD))))): This multipliesywith the result of the previousiszerocheck. If the previous result was1, it implies that there is no risk of overflow whenxis multiplied byWAD, and it proceeds to multiplyyby1(effectively leavingyunchanged). If the result was0, it means that there was a risk of overflow, and multiplyingyby0would yield0.iszero(...): Ifywas0or ifxwas safe to multiply byWADwithout causing an overflow, then the entire multiplication expression would evaluate to0, andiszerowould return1. If there was a potential overflow detected, the multiplication result would not be0, andiszerowould return0.
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.
divWadUp
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.
How would this help?
Pros:
- Precision: It offers higher precision for fractional numbers compared to integer arithmetic, which is crucial for financial computations that require decimal points.
- Gas Efficiency: The library uses Yul, an intermediate language that compiles down to EVM bytecode, often leading to more gas-efficient contract execution than using Solidity alone.
- Safety: It includes checks to prevent overflows and underflows, common pitfalls in smart contract development that can lead to significant vulnerabilities.
- Convenience: Developers can perform arithmetic operations without implementing their own fixed-point math system, saving time and reducing the risk of errors.
- Interoperability: It uses a standard format (wad) that is widely recognized in the Ethereum ecosystem, making it easier to integrate with other contracts and systems.
- Transparency: The operations are transparent and verifiable, ensuring that contract behavior can be predicted and validated, which is essential for trust in DeFi applications.
Cons:
- Complexity: For developers unfamiliar with fixed-point arithmetic, there may be a learning curve to understand how to properly use the library functions.
- Code Size: Including an external library can increase the size of the contract bytecode, potentially leading to higher deployment costs.
- Overhead: While the library is optimized for gas, using any library still adds a layer of complexity and gas overhead compared to native operations.
- Limited Range: Fixed-point numbers have a limited range compared to floating-point numbers, which may be a limitation for some applications.
- Specificity: The library is designed for a specific scaling factor (wad), which may not be suitable for all use cases requiring different levels of precision.
- Upgradability: If the library is found to have a bug or requires an update, depending on how it’s integrated, it might be difficult to upgrade the smart contracts that use it.
Practical Applications
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:
- DeFi Yield Farming Platforms: Calculating rewards based on staking durations and amounts often requires precision to ensure users receive an accurate share of the yield.
- Automated Market Makers (AMMs): AMMs use complex formulas like the constant product formula (
x * y = k) for liquidity pools. - Token Vesting Contracts: When tokens are vested, they might be released continuously over time, which could require division to calculate the fraction of tokens to release at any given point.
- Oracles: Price oracles that feed external data (like currency exchange rates) into the blockchain often require high precision. When these rates are used to determine transaction values or contract terms, fixed-point arithmetic helps maintain accuracy.
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.
