QuantAMM

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

User bypass paying fees due to wrong calculation

Summary

In onAfterRemoveLiquidity function if there is a positive change in price user pays fees to the hook depending on the bptamount being removed

however, due to wrong calculation, a round down happens and the user can take profit without paying any fees.

Vulnerability Details

The equation to calculate if there is a positive change in price or not is calculated as follows

File: UpliftOnlyExample.sol
474: localData.lpTokenDepositValueChange =
475: (int256(localData.lpTokenDepositValueNow) - int256(localData.lpTokenDepositValue)) /
476: int256(localData.lpTokenDepositValue);

The equation has a severe issue as the user can take profit up to (lpTokenDepositValue * 2) - 1
ex: if the price in the deposit was 2 then at the time of removing liquidity user can remove up to 3 without paying fees and pays fees as there is no profit happened

POC

Add this test at the end of UpliftExample.t.sol contract
it has comments showing the right amount that should have been subtracted and the wrong amount that was applied

//test user can take up to near double profit without paying fees
function testRemoveLiquidityWithProtocolTakeNearDoublePositivePriceChangeWithoutPayingFees() public {
vm.prank(address(vaultAdmin));
updateWeightRunner.setQuantAMMUpliftFeeTake(0.5e18);
vm.stopPrank();
// 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();
//mock prices to be 1.9 OldPrice
int256[] memory prices = new int256[]();
for (uint256 i = 0; i < tokens.length; ++i) {
prices[i] = int256(i) * 1.9e18;
}
updateWeightRunner.setMockPrices(pool, prices);
uint256[] memory minAmountsOut = [uint256(0), uint256(0)].toMemoryArray();
BaseVaultTest.Balances memory balancesBefore = getBalances(updateWeightRunner.getQuantAMMAdmin());
vm.startPrank(bob);
upliftOnlyRouter.removeLiquidityProportional(bptAmount, minAmountsOut, false, pool);
vm.stopPrank();
BaseVaultTest.Balances memory balancesAfter = getBalances(updateWeightRunner.getQuantAMMAdmin());
// The right fee amount that must be taken is as follow
uint256 feeAmountAmountPercentpsitivechange = ((bptAmount / 2) *
((uint256(upliftOnlyRouter.upliftFeeBps()) * 1e18) / 10000)) / ((bptAmount / 2));
uint256 amountOutPositiveChange = (bptAmount / 2).mulDown((1e18 - feeAmountAmountPercentpsitivechange));
// Calculation Of Fees Incase of negative change of price
uint256 feeAmountAmountPercent = ((bptAmount / 2) *
((uint256(upliftOnlyRouter.minWithdrawalFeeBps()) * 1e18) / 10000)) / ((bptAmount / 2));
uint256 amountOut = (bptAmount / 2).mulDown((1e18 - feeAmountAmountPercent));
// Bob gets original liquidity with no fee applied because of not exceeding the double amount.
assertEq(
balancesAfter.bobTokens[daiIdx] - balancesBefore.bobTokens[daiIdx],
amountOut,
"bob's DAI amount is wrong"
);
assertEq(
balancesAfter.bobTokens[usdcIdx] - balancesBefore.bobTokens[usdcIdx],
amountOut,
"bob's USDC amount is wrong"
);
// the fees taken doesn't match the right fee amount
assertNotEq(
balancesAfter.bobTokens[daiIdx] - balancesBefore.bobTokens[daiIdx],
amountOutPositiveChange,
"bob's DAI amount is wrong"
);
assertNotEq(
balancesAfter.bobTokens[usdcIdx] - balancesBefore.bobTokens[usdcIdx],
amountOutPositiveChange,
"bob's USDC amount is wrong"
);
}

Impact

  • Loss Of funds as the wrong fee amount is being calculated.

Tools Used

manual review

Recommendations

apply this adjustment
this code applies a check if ( lpTokenDepositValueNow > lpTokenDepositValue )
to apply whether the fee for positive change or negative using the same original formula

-- localData.lpTokenDepositValueChange =
-- (int256(localData.lpTokenDepositValueNow) - ----int256(localData.lpTokenDepositValue)) /
-- int256(localData.lpTokenDepositValue);
--
-- uint256 feePerLP;
-- // if the pool has increased in value since the deposit, the fee is calculated based on the deposit value
-- if (localData.lpTokenDepositValueChange > 0) {
-- feePerLP =
-- (uint256(localData.lpTokenDepositValueChange) * (uint256(feeDataArray[i].upliftFeeBps) * 1e18)) /
-- 10000;
-- }
-- // if the pool has decreased in value since the deposit, the fee is calculated based on the base value - see wp
-- else {
-- //in most cases this should be a normal swap fee amount.
-- //there always myst be at least the swap fee amount to avoid deposit/withdraw attack surgace.
-- feePerLP = (uint256(minWithdrawalFeeBps) * 1e18) / 10000;
-- }
++ uint256 feePerLP;
++
++ // If the LP token deposit value has increased
++ if (localData.lpTokenDepositValueNow > localData.lpTokenDepositValue) {
++ // Calculate the percentage change in value
++ localData.lpTokenDepositValueChange =
++ (1e18 * (int256(localData.lpTokenDepositValueNow) - int256(localData.lpTokenDepositValue))) /
++ int256(localData.lpTokenDepositValue);
++
++ // Calculate the fee per LP based on the uplift fee
++ // 1e18 is used to maintain precision
++ feePerLP =
++ (uint256(localData.lpTokenDepositValueChange) * uint256(feeDataArray[i].upliftFeeBps)) /
++ 10000;
++ } else {
++ // If the pool value has decreased since deposit, calculate a base fee
++ // This ensures there's always at least the swap fee to avoid attacks during deposit/withdrawal cycles
++ feePerLP = (uint256(minWithdrawalFeeBps) * 1e18) / 10000;
++ }
Updates

Lead Judging Commences

n0kto Lead Judge 11 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.

Give us feedback!