Vulnerability Due to Incorrect Rounding When the Numerator is Not a Multiple of the Denominator

In smart contracts, particularly in decentralized finance (DeFi) systems, incorrect rounding can introduce significant vulnerabilities. A common issue arises when the numerator is not a multiple of the denominator in division operations, causing rounding errors that unfairly benefit one party—usually the user. These issues can lead to lost value for liquidity providers or the protocol itself.

In this tutorial, we'll explore the vulnerability caused by rounding errors in smart contract calculations, explain why they happen when the numerator is not a multiple of the denominator, and demonstrate how to mitigate this issue.

The Vulnerability: Incorrect Rounding When the Numerator Is Not a Multiple of the Denominator

In Solidity, when performing integer division, the result is always rounded down to the nearest whole number. This behavior can create a problem in financial calculations where precision is critical. Specifically, when the numerator isn't perfectly divisible by the denominator, rounding down leads to a loss in value.

This can be particularly problematic in systems calculating token exchanges or rewards, where users may benefit from receiving more tokens or paying less than intended. If these rounding issues are not addressed, they can add up over multiple transactions and cause significant losses for the protocol or liquidity providers.

Example of the Vulnerability:

Consider a function that calculates how many base tokens are required to buy a certain amount of fractional tokens. Here’s a simplified version of such a function:

function buyQuote(uint256 outputAmount) public view returns (uint256) {
    return (outputAmount * 1000 * baseTokenReserves()) / ((fractionalTokenReserves() - outputAmount) * 997);
}

In this case:

  • outputAmount is the amount of fractional tokens the user wants to buy.

  • baseTokenReserves() is the total amount of base tokens in the contract.

  • fractionalTokenReserves() is the total amount of fractional tokens in the contract.

If the numerator (outputAmount * 1000 * baseTokenReserves()) is not a multiple of the denominator ((fractionalTokenReserves() - outputAmount) * 997), Solidity’s integer division will round down. This means the user will be required to provide slightly fewer base tokens than they should, which can lead to unfair results.


Impact: How Incorrect Rounding Affects the System

  1. Value Loss for Liquidity Providers: If users consistently pay slightly less due to rounding errors, liquidity providers (or the protocol) bear the cost. Over time, these small differences can add up, causing significant losses.

  2. Unfair Advantage for Users: Users may repeatedly exploit the rounding mechanism by making small trades, gaining more tokens than they should with each transaction, and effectively draining value from the system.

  3. Protocol Sustainability: Left unchecked, this rounding behavior could reduce the protocol’s reserves, destabilizing the system and potentially discouraging further participation from liquidity providers.


Mitigation: Fixing Rounding Errors When Numerator Isn’t a Multiple of the Denominator

There are several ways to mitigate rounding issues in smart contract calculations. Below are two common solutions to ensure fairness.

1. Add a Small Increment to Round Up

One simple way to fix the rounding issue is to add +1 to the result of the division. This forces the calculation to round up, ensuring the protocol always gets at least the correct amount of tokens, if not slightly more.

Here's the updated version of the buyQuote function:

function buyQuote(uint256 outputAmount) public view returns (uint256) {
    return ((outputAmount * 1000 * baseTokenReserves()) / ((fractionalTokenReserves() - outputAmount) * 997)) + 1;
}

By adding +1, we guarantee that the required base token amount is always rounded up, ensuring that the user pays slightly more rather than less.

2. Use Fixed-Point Arithmetic Libraries

A more technically precise solution is to use a fixed-point arithmetic library that can handle division with more precision. For example, Solmate’s FixedPointMathLib provides the mulDivUp function, which performs multiplication and division with rounding up.

Here’s how you could refactor the function using mulDivUp:

import { FixedPointMathLib } from "solmate/src/utils/FixedPointMathLib.sol";

function buyQuote(uint256 outputAmount) public view returns (uint256) {
    return FixedPointMathLib.mulDivUp(outputAmount * 1000, baseTokenReserves(), (fractionalTokenReserves() - outputAmount) * 997);
}

By using mulDivUp, you ensure that the calculation rounds up correctly, preventing users from exploiting rounding errors.


Conclusion

When the numerator is not a multiple of the denominator in smart contract calculations, rounding down can lead to value losses for liquidity providers and give users an unfair advantage. These small discrepancies can add up over time, causing significant financial imbalances within the system.

To mitigate this issue, developers should either:

  1. Add a small increment to force the result to round up.

  2. Use fixed-point arithmetic libraries like Solmate’s FixedPointMathLib to perform more accurate calculations.

By addressing these rounding vulnerabilities, smart contract developers can ensure fairer outcomes for all participants and maintain the integrity and sustainability of their protocols.

Last updated