Description:
The buyOrder
function in the OrderBook
contract calculates the protocolFee
using integer division, which can result in a loss of precision. In Solidity, all arithmetic is performed using integers, and any division between two integers will always round down (truncate) the result, discarding any fractional component. This means that when calculating a percentage-based fee, such as a 3% protocol fee, the actual fee collected may be slightly less than the intended amount, especially for small order values or when the fee rate is low. For example, if the order price is 10 USDC, then (10 * 3) / 100
will yield 0, not 0.3, resulting in no fee being collected for that transaction. Over many transactions, these small discrepancies can add up.
This issue is particularly relevant in DeFi protocols where fee calculations are expected to be precise and predictable. Users may also notice inconsistencies in the fees charged, which could lead to confusion or a perception of unfairness. Furthermore, attackers or sophisticated users could deliberately structure their trades to minimize fees by exploiting this rounding behavior.
Impact:
Precision loss in fee calculation can lead to the protocol collecting less in fees than expected over time. This discrepancy may accumulate, resulting in a significant loss in protocol revenue, especially as the number of small-value trades increases. Additionally, it can create inconsistencies in fee distribution, making it difficult to accurately account for protocol earnings or to distribute fees to stakeholders. In some cases, users may be able to exploit this behavior by splitting large trades into multiple small ones to avoid paying fees altogether, further reducing protocol income and potentially undermining the intended economic model.
Proof of Concept:
Add the following test into the TestOrderBook.t.sol
to demonstrate how the protocol fee can be zero for small order values due to integer division truncation:
Recommended Mitigation:
There are two main strategies to address this issue, both of which can help ensure that the protocol fee is calculated more accurately and fairly for all users:
Implement a rounding-up strategy: When calculating the protocolFee
, add a check to round up any fractional result. This can be done by adding the denominator minus one to the numerator before performing the division, or by simply adding +1
to the result if there is a remainder. This ensures that even the smallest non-zero fee is collected, and that the protocol does not systematically undercharge on small transactions. For example, the calculation could be modified as follows:
This approach ensures that any fractional part is rounded up, so the protocol always collects at least the minimum intended fee.
Use a FixedPoint math library: Integrate an external library (such as OpenZeppelin’s FixedPoint or PRBMath) or implement your own fixed-point arithmetic to handle decimal calculations with greater precision. Fixed-point math allows you to represent fractional values accurately by scaling integers, which is a common practice in Solidity to avoid the pitfalls of integer division. This method is especially useful if the protocol may need to support more complex fee structures or calculations in the future.
Both strategies have their trade-offs: rounding up is simple and gas-efficient, while fixed-point math provides more flexibility and precision at the cost of slightly higher complexity and gas usage. The choice depends on the protocol’s requirements and the desired user experience.
For further information and best practices on handling precision and rounding in Solidity, you may refer to this article, which discusses common pitfalls and recommended solutions for precision loss in smart contract arithmetic.
Protocol Suffers Potential Revenue Leakage due to Precision Loss in Fee Calculation
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.