onAfterRemoveLiquidity() is used to remove liquidity. The issue arises because amountLeft (the liquidity to be removed) is not checked to ensure it is 0. This oversight allows users to withdraw more tokens than intended.
onAfterRemoveLiquidity() is implemented as folows:
The function iterates through the array to withdraw the requested liquidity (amountLeft) by subtracting the liquidity deposited in each NFT until amountLeft == 0. However, the issue is that this condition is only used to break the loop, assuming that all necessary NFTs were burned to extract the requested liquidity. If amountLeft != 0, the transaction does not revert, allowing users to withdraw more tokens than intended.
This happens because there is no check to ensure that amountLeft == 0, which would confirm that the combined liquidity of the NFTs is sufficient to meet the withdrawal request.
Finally, the function returns hookAdjustedAmountsOutRaw, representing the adjusted amount after applying fees. This flaw allows users to withdraw more liquidity than their NFTs actually hold, leading to unintended behavior.
Users can withdraw more than intended stealing funds from the contract.
To better understand the issue, let's use an example:
Bob owns 1 NFT with amount = 100.
onAfterRemoveLiquidity() is called by the router with amountsOutRaw = 1000 and bptAmountIn = 1000.
The function iterates to calculate the fees. Since there is only 1 NFT available to burn, the calculation results in localData.amountLeft = 1000 - 100 = 900 and the fee is determined to be 50.
The loop finishes, leaving localData.amountLeft = 900, meaning there is still 900 liquidity left to be withdrawn. However, because there is no check to ensure amountLeft == 0, the execution continues despite the insufficient NFTs.
The amountsOutRaw are adjusted, resulting in hookAdjustedAmountsOutRaw = 1000 - 10 = 990.
As a result, the user receives 990 tokens but only burns 100 worth of liquidity, effectively taking more tokens than they should.
Manual review.
To resolve the issue, add a check to ensure that localData.amountLeft == 0. This will verify that the NFTs provide sufficient liquidity to cover the requested removal amount; otherwise, revert the transaction.
Read bugs with that tag: invalid_onAfterRemoveLiquidity_loop_underflow Because of that implementation, trying to remove more will lead to an underflow.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.