Solidity use Fixed Point Arithmetic, that mean it doesn't support decimal value.
As a result, any non-integer value is truncated downward.
This characteristic of Solidity can lead to precision loss during numerical operations, especially when division is performed before multiplication, adversely affecting the accuracy of calculations.
For Example in Solidity3/2=1;1/2=0;
Different Kind of Division Precision Loss
Division precision loss can manifest in several ways within Solidity.
This article focuses on the two most prevalent issues:
Division Before Multiplication
Rounding Down To Zero
Division Before Multiplication
Solidity truncates any non-integer result to the nearest lower integer.
If a division occurs before a multiplication, the operation may result in precision loss due to truncation.
For ExampleThe expected result is 55.Solidity make the first calculation 11/2=5 due to trucationThen proceed to multiplication.uint a =11;uint b =2;uint c =10;a / b * c =50 instead of55
This is a common rule to follow:
"Always Multiply Before Dividing"
Although many developers follow this rule, "Hidden Precision Loss" can still occur, resulting from complex calculations across different functions or contracts.
These scenarios are trickier to identify but pose a significant risk if overlooked.
Let's take an example of the USSD Contest on C4:
At first glance, the calculation appears correct, let's take a look at the BuyUSSDSellCollateral function
But the BuyUSSDSellCollateral function multiplies the input by 1e12, leading to potential precision loss.
So it will first do the calculation inside the parenthesis (USSDamount - DAIamount / 1e12)/2 Then call the 'BuyUSSDSellCollateral' function and multiply the result by 1e12.
But mutiply a number that might has been round down by 1 Trillion seems not to be a good idea.
In Solidity, due to the same feature, if the Numerator is Lower that the Denominator, the result will be 0
In regular math:
If $A < B$ with $A, B > 0$
Then $\frac{A}{B} < 1$
So here, the common rule is:
"Always make sure that the Numerator is greater than the Denominator"
Here's an example in the Cooler Contest on Sherlock:
If loanCollaterral * repaid < loanAmount -> decollateralized == 0 That's bad, we don't want that to happen.
It can be tricky sometime to always make sure of that rule. That how Security Reasearchers came up with a sanity check.
While division rounding errors might seem minor, they can lead to significant fund risks if overlooked.
This overview only scratches the surface of common division rounding errors in Solidity. Researcher are encouraged to delve deeper into the subject to understand and mitigate potential precision losses in their audit.
If we adjust our example above and change c to 1e12,
The expected result is 5.5e12. However:
uint a = 11;
uint b = 2;
uint c = 1e12;
a / b * c = 5e12
5.5e12 - 5e12 = 0.5e12;
The difference is 0.5e12, indicating a half-a-trillion error. π€―
function errorRepay(uint repaid) external {
console.log("PrecisionLoss.errorRepay()");
// If repaid small enough, decollateralized will round down to 0,
// allowing loan to be repaid without changing collateral
uint decollateralized = loanCollateral * repaid / loanAmount;
loanAmount -= repaid;
loanCollateral -= decollateralized;
}
+ require(decollateralized != 0, "Round down to zero");