QuantAMM

QuantAMM
49,600 OP
View results
Submission Details
Severity: medium
Valid

Rounding Error in feePerLP Calculation Allow Users to Pay Minimum Fee Instead of Correct Uplift Fee

Summary

In the calculation of feePerLP, the contract determines lpTokenDepositValueChange. However, due to rounding errors in this calculation, small price increases (less than double) are not accounted for correctly. As a result, users pay the minimum withdrawal fee (minWithdrawalFeeBps) instead of the higher uplift fee (upliftFeeBps) that should apply.


Vulnerability Details

In UpliftOnlyExample::onAfterRemoveLiquidity, the following calculation is performed:

localData.lpTokenDepositValueChange = (
int256(localData.lpTokenDepositValueNow) - int256(localData.lpTokenDepositValue)
) / int256(localData.lpTokenDepositValue); // @audit rounding issue here
uint256 feePerLP;
if (localData.lpTokenDepositValueChange > 0) {
feePerLP = (
uint256(localData.lpTokenDepositValueChange) * (uint256(feeDataArray[i].upliftFeeBps) * 1e18)
) / 10000;
}

Issue

  • The calculation divides lpTokenDepositValueNow - lpTokenDepositValue by lpTokenDepositValue before multiplying.

  • Solidity truncates the result of integer division, which can lead to the lpTokenDepositValueChange rounding down to zero for small price increases.

  • If lpTokenDepositValueChange is zero, users pay the minimum fee (minWithdrawalFeeBps) instead of the uplift fee (upliftFeeBps).

Impact

Users withdrawing after small price increases (e.g., 1.5x) bypass the correct uplift fee and pay only the minimum withdrawal fee. This results in reduced fees collected by the protocol and allows users to exploit the rounding issue.

Proof of Concept

The provided test demonstrates the issue:

function testRounding() public {
// Add liquidity so Bob has BPT to remove liquidity.
uint256[] memory maxAmountsIn = [dai.balanceOf(bob), usdc.balanceOf(bob)].toMemoryArray();
vm.prank(bob);
upliftOnlyRouter.addLiquidityProportional(pool, maxAmountsIn, bptAmount, false, bytes(""));
vm.stopPrank();
int256[] memory prices = new int256[]();
for (uint256 i = 0; i < tokens.length; ++i) {
prices[i] = int256(i) * 15e17; // Price increases by 1.5x
}
updateWeightRunner.setMockPrices(pool, prices);
uint256[] memory minAmountsOut = [uint256(0), uint256(0)].toMemoryArray();
BaseVaultTest.Balances memory balancesBefore = getBalances(bob);
vm.startPrank(bob);
upliftOnlyRouter.removeLiquidityProportional(bptAmount, minAmountsOut, false, pool);
vm.stopPrank();
BaseVaultTest.Balances memory balancesAfter = getBalances(bob);
uint256 feeAmountPercent =
((bptAmount / 2) * ((uint256(upliftOnlyRouter.minWithdrawalFeeBps()) * 1e18) / 10000)) / (bptAmount / 2);
uint256 amountOut = (bptAmount / 2).mulDown((1e18 - feeAmountPercent));
// Assert that Bob pays the minimum fee instead of the uplift fee.
assertEq(
balancesAfter.bobTokens[daiIdx] - balancesBefore.bobTokens[daiIdx], amountOut, "Bob's DAI amount is wrong"
);
}

Tools Used

Manual review and unit testing.

Recommendations

To mitigate rounding errors, perform multiplication before division:

- localData.lpTokenDepositValueChange = (
- int256(localData.lpTokenDepositValueNow) - int256(localData.lpTokenDepositValue)
- ) / int256(localData.lpTokenDepositValue);
+ localData.lpTokenDepositValueChange = (
+ (int256(localData.lpTokenDepositValueNow) - int256(localData.lpTokenDepositValue)) * 1e18
+ ) / int256(localData.lpTokenDepositValue);
uint256 feePerLP;
if (localData.lpTokenDepositValueChange > 0) {
- feePerLP = (
- uint256(localData.lpTokenDepositValueChange) * (uint256(feeDataArray[i].upliftFeeBps) * 1e18)
- ) / 10000;
+ feePerLP = (
+ uint256(localData.lpTokenDepositValueChange) * uint256(feeDataArray[i].upliftFeeBps)
+ ) / 10000;
}

Benefits of the Fix

  • Ensures accurate fee calculations for small price increases.

  • Prevents users from bypassing uplift fees, preserving protocol revenue integrity.

Updates

Lead Judging Commences

n0kto Lead Judge 9 months ago
Submission Judgement Published
Validated
Assigned finding tags:

finding_onAfterRemoveLiquidity_lpTokenDepositValueChange_rounding_error_100%_minimum

Likelihood: High, every call to the function (withdraw) Impact: Low/Medium, uplift fees will be applied only when the price of one asset is doubled but fixed fees will still be collected.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.