QuantAMM

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

SelfRouter can't call onAfterRemoveLiquidity because it would revert if a user has a lpTokenDepositValue 0

Summary

The onAfterRemoveLiquidity function in UpliftOnlyExample.sol contains a critical vulnerability where division by zero occurs during the calculation of lpTokenDepositValueChange. This happens because lpTokenDepositValue is initialized to zero and used as a denominator in a division operation, causing the function to revert and preventing users from removing their liquidity.

Vulnerability Details

function addLiquidityProportional(
address pool,
uint256[] memory maxAmountsIn,
uint256 exactBptAmountOut,
bool wethIsEth,
bytes memory userData
) external payable saveSender(msg.sender) returns (uint256[] memory amountsIn) {
if (poolsFeeData[pool][msg.sender].length > 100) {
revert TooManyDeposits(pool, msg.sender);
}
// Do addLiquidity operation - BPT is minted to this contract.
amountsIn = _addLiquidityProportional(
pool,
msg.sender,
address(this),
maxAmountsIn,
exactBptAmountOut,
wethIsEth,
userData
);
uint256 tokenID = lpNFT.mint(msg.sender);
//this requires the pool to be registered with the QuantAMM update weight runner
//as well as approved with oracles that provide the prices
@> uint256 depositValue = getPoolLPTokenValue( // no validation if 0
IUpdateWeightRunner(_updateWeightRunner).getData(pool),
pool,
MULDIRECTION.MULDOWN
);
poolsFeeData[pool][msg.sender].push(
FeeData({
tokenID: tokenID,
amount: exactBptAmountOut,
//this rounding favours the LP
lpTokenDepositValue: depositValue, // can be 0 if not checked
//known use of timestamp, caveats are known.
blockTimestampDeposit: uint40(block.timestamp),
upliftFeeBps: upliftFeeBps
})
);
nftPool[tokenID] = pool;
}
// UpliftOnlyExample.sol
function onAfterRemoveLiquidity(...) {
AfterRemoveLiquidityData memory localData = AfterRemoveLiquidityData({
lpTokenDepositValueNow: 0, // Initialized to 0
lpTokenDepositValue: 0, // Initialized to 0
// ...
});
// Division by zero occurs here
localData.lpTokenDepositValueChange =
(int256(localData.lpTokenDepositValueNow) - int256(localData.lpTokenDepositValue)) /
int256(localData.lpTokenDepositValue);
}
) public override onlySelfRouter(router) returns (bool, uint256[] memory hookAdjustedAmountsOutRaw) {
address userAddress = address(bytes20(userData));
AfterRemoveLiquidityData memory localData = AfterRemoveLiquidityData({
pool: pool,
bptAmountIn: bptAmountIn,
amountsOutRaw: amountsOutRaw,
minAmountsOut: new uint256[](amountsOutRaw.length),
accruedFees: new uint256[](amountsOutRaw.length),
accruedQuantAMMFees: new uint256[](amountsOutRaw.length),
currentFee: minWithdrawalFeeBps,
feeAmount: 0,
prices: IUpdateWeightRunner(_updateWeightRunner).getData(pool),
lpTokenDepositValueNow: 0,
lpTokenDepositValueChange: 0,
lpTokenDepositValue: 0,
tokens: new IERC20[](amountsOutRaw.length),
feeDataArrayLength: 0,
amountLeft: 0,
feePercentage: 0,
adminFeePercent: 0
});
// We only allow removeLiquidity via the Router/Hook itself so that fee is applied correctly.
hookAdjustedAmountsOutRaw = amountsOutRaw;
//this rounding faxvours the LP
localData.lpTokenDepositValueNow = getPoolLPTokenValue(localData.prices, pool, MULDIRECTION.MULDOWN);
FeeData[] storage feeDataArray = poolsFeeData[pool][userAddress];
localData.feeDataArrayLength = feeDataArray.length;
localData.amountLeft = bptAmountIn;
for (uint256 i = localData.feeDataArrayLength - 1; i >= 0; --i) {
localData.lpTokenDepositValue = feeDataArray[i].lpTokenDepositValue; // can have a 0
@> localData.lpTokenDepositValueChange =
@> (int256(localData.lpTokenDepositValueNow) - int256(localData.lpTokenDepositValue)) /
@> int256(localData.lpTokenDepositValue); // DOS
POC

Welcome to Chisel! Type !help to show available commands.
➜ uint256 lpTokenDepositValueNow = 0
➜ uint256 lpTokenDepositValue = 0
➜ int256 result = (int256(lpTokenDepositValueNow) - int256(lpTokenDepositValue)) / int256(lpTokenDepositValue)
Traces:
[536] 0xBd770416a3345F91E4B34576cb804a576fa48EB1::run()
└─ ← [Revert] panic: division or modulo by zero (0x12)

⚒️ Chisel Error: Failed to execute REPL contract!

Impact

Funds become locked due to function reverting

Tools Used

Chisel, Github

Recommendations

Add a zero-value check before performing the division:

function onAfterRemoveLiquidity(...) {
// ... existing code ...
if (localData.lpTokenDepositValue == 0) {
localData.lpTokenDepositValueChange = 0; // or another appropriate default value
} else {
localData.lpTokenDepositValueChange =
(int256(localData.lpTokenDepositValueNow) - int256(localData.lpTokenDepositValue)) /
int256(localData.lpTokenDepositValue);
}
}
Updates

Lead Judging Commences

n0kto Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!