Summary
Due to invalid formula used for localData.lpTokenDepositValueChange
calculation, the following logic is prone to underflowing.
Vulnerability Details
Excerpt from UpliftOnlyExample.sol - onAfterRemoveLiquidity
...
localData.lpTokenDepositValueChange =
(int256(localData.lpTokenDepositValueNow) - int256(localData.lpTokenDepositValue)) /
int256(localData.lpTokenDepositValue);
...
We'll focus on price increase, since decrease will simply ignore the change in further execution.
The above math results in the localData.lpTokenDepositValueChange
to be equivalent of "How many TIMES value changed minus one".
And further used in feePerLp calculation:
...
if (localData.lpTokenDepositValueChange > 0) {
feePerLP =
(uint256(localData.lpTokenDepositValueChange) * (uint256(feeDataArray[i].upliftFeeBps) * 1e18)) /
10000;
}
...
In the tests upliftFeeBps is set to 2% (200). Here we already can see that if the value increased 51 times (lpTokenDepositValueChange = (51 - 1) = 50) the result will be feePerLP = 1e18. Which is equal to a 100% fee and any further increase will cause it to go over 100% here:
...
if (feeDataArray[i].amount <= localData.amountLeft) {
uint256 depositAmount = feeDataArray[i].amount;
localData.feeAmount += (depositAmount * feePerLP);
...
} else {
feeDataArray[i].amount -= localData.amountLeft;
localData.feeAmount += (feePerLP * localData.amountLeft);
break;
}
}
localData.feePercentage = (localData.feeAmount) / bptAmountIn;
...
And in turn eventually cause an underflow here:
for (uint256 i = 0; i < localData.amountsOutRaw.length; i++) {
uint256 exitFee = localData.amountsOutRaw[i].mulDown(localData.feePercentage);
...
hookAdjustedAmountsOutRaw[i] -= exitFee;
...
Impact
In crypto industry it's not rare for tokens to shoot up in price. This will cause fee to become more than the total depositAmount and underflow. Adding the fact that deposits are stored and later cleared using FILO - the user deposit can stay uncleared for long periods of time which only increases the probably to hit the underflow. This will result in the inability to withdraw liquidity.
Tools Used
Manual Review + Foundry Test
Below is a modified testRemoveLiquidityDoublePositivePriceChange
import "forge-std/console2.sol";
...
function testRemoveLiquidity52TimesPriceIncrease() public {
uint256[] memory maxAmountsIn = [dai.balanceOf(bob), usdc.balanceOf(bob)].toMemoryArray();
vm.prank(bob);
upliftOnlyRouter.addLiquidityProportional(pool, maxAmountsIn, bptAmount, false, bytes(""));
vm.stopPrank();
console2.log("bptAmount", bptAmount / 1e18);
assertEq(upliftOnlyRouter.getUserPoolFeeData(pool, bob).length, 1, "bptAmount mapping should be 1");
assertEq(upliftOnlyRouter.getUserPoolFeeData(pool, bob)[0].amount, bptAmount, "bptAmount mapping should be 0");
assertEq(
upliftOnlyRouter.getUserPoolFeeData(pool, bob)[0].blockTimestampDeposit,
block.timestamp,
"bptAmount mapping should be 0"
);
assertEq(
upliftOnlyRouter.getUserPoolFeeData(pool, bob)[0].lpTokenDepositValue,
500000000000000000,
"should match sum(amount * price)"
);
assertEq(upliftOnlyRouter.getUserPoolFeeData(pool, bob)[0].upliftFeeBps, 200, "fee");
int256[] memory prices = new int256[]();
for (uint256 i = 0; i < tokens.length; ++i) {
prices[i] = int256(i) * 52 * 1e18;
}
updateWeightRunner.setMockPrices(pool, prices);
uint256 nftTokenId = 0;
uint256[] memory minAmountsOut = [uint256(0), uint256(0)].toMemoryArray();
BaseVaultTest.Balances memory balancesBefore = getBalances(bob);
vm.startPrank(bob);
console2.logString("--------------------------------Removing liquidity--------------------------------");
upliftOnlyRouter.removeLiquidityProportional(bptAmount, minAmountsOut, false, pool);
console2.logString("--------------------------------Liquidity removed--------------------------------");
vm.stopPrank();
}
The pring "---Liquidity removed---" is never reached because of the underflow. If the "TIMES multiplier" is decreased it will again pass.
Recommendations
If the current fee scaling is intended which sponsors confirmed. Then the only way to limit the underflow is limit the maximum feePerLp.
So something along the lines of:
if (localData.lpTokenDepositValueChange > 0) {
feePerLP = (uint256(localData.lpTokenDepositValueChange) * (uint256(feeDataArray[i].upliftFeeBps) * 1e18)) /
10000;
feePerLP = feePerLP > 1e18 ? 1e18 : fePerLP;
}