Fixed Point Float
Float library is implemented in FunC for the consideration of gas efficiency. Our implementation takes int128 as input and shifts it left by 64 bits, resulting a fixed bit space for storing decimals. Take number as example, when user transforms integer into float format, the output value will be .
Overview
Same as solidity, both FunC and Tact do not support float datatype. However, what if our DeFi application requires to compute some interest rate in a float format? To address this issue, early developers in Solidity adopt a fixed point floating point method, which is relatively easy and gas efficient in comparison with IEEE 754
format. The basic idea is multiplying a number by a large constant to simulate a decimal points, effectively shifting the decimal place to the right.
For example, if we need to represent in smart contract, we could easily multiply it by (shifting the decimals three place to the right), to get as an integer. This approach simplifies computations while maintaining precision, as long as the developers carefully manage the scaling factor throughout the calculations. It is noteworthy we can substitude the constant for any arbitrary number, thus, we can use binary shift operator to further reduce the computation complexity. In order to preserve a sufficient range to represent almost all rates in developing, we choose for the constant, which is equivalent to shift the origin number 64 bit left by the operator <<
.
Case Study
To explain the arithmetic (especially multiplication and division) in a simple term, we will introduce our usage step by step with elaboration. We take number , and scaling factor for example in this section.
Multiplication
Multiply by is quite easy, we directly compute . However, since we are talking about fixed point arithmetic, we need to process and into fixed point format before performing the multiplication. First, we convert these numbers into a fixed point representation by multiplying them with the scaling factor . In this case, becomes and becomes . These converted numbers are now in a format that allows for integer-based calculations while simulating floating-point precision.
Next, we perform the multiplication. In fixed point arithmetic, when we multiply two numbers, the result's scale is the square of the original scale. So, when we multiply the fixed point representations of and , the calculation is equivalent to . This results in a number that is scaled by .
However, we want our final answer to be in the same scale as our original inputs, which is . To achieve this, we divide our result by the scaling factor . This step readjusts the scale of the result back to our intended scale. The final computation is therefore , which gives us the correctly scaled result of in fixed point format.
By leveraging our implementation, you can simply do the above procedure in Tact Language:
import "./packages/math/float.fc";
@name(safeMul)
extends native safeMul(self: Int, b: Int): Int;
contract MathExample with Deployable {
x: Int = 2;
y: Int = 7;
get fun safeMul(): Int {
let a: Int = self.x.toFloat();
let b: Int = self.y.toFloat();
return a.safeMul(b);
}
}
Division
Division in fixed point arithmetic follows a logic similar to multiplication, but with a few differences to accommodate the nature of division. Using the same numbers , , and scaling factor as in the multiplication example, let's break down the process of division.
First, like in multiplication, we convert our numbers into fixed point format using the scaling factor . So, becomes and becomes . These numbers are now ready for arithmetic operations in fixed point format.
When performing division in fixed point arithmetic, it's important to maintain the scale of the result consistent with the scale of the inputs. To divide by in fixed point format, we calculate . Here, the scaling factors in the numerator and denominator cancel each other out.
However, this direct division would lead to a loss of precision since . To address this, we can multiply the numerator by an additional scaling factor before performing the division, and divide it at the end of computation. This means we calculate . Now, the result of this division will align with the scale of our inputs.
In Tact Language, this division process can be implemented as follows:
import "./packages/math/float.fc";
@name(safeDiv)
extends native safeDiv(self: Int, b: Int): Int;
contract MathExample with Deployable {
x: Int = 2;
y: Int = 7;
get fun safeDiv(): Int {
let a: Int = self.x.toFloat();
let b: Int = self.y.toFloat();
return a.safeDiv(b);
}
}