QuantAMM

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

Malicious user can rekt the protocol by removing more liquidity than they own.

Summary

The UpliftOnlyExample.sol contract is vulnerable to an exploit that allows a malicious user to drain the entire protocol by removing more liquidity than they provided. This is due to improper validation in the removeLiquidityProportional() function, allowing attackers to pass more bptAmountIn and bypass balance checks.

Vulnerability Details

  1. Liquidity Addition:

    • When users add liquidity using addLiquidityProportional(), BPT tokens are minted to the protocol contract address instead of the user's address.

    • Users only receive lpNFT as a representation of their liquidity.

  2. Liquidity Removal:

    • In removeLiquidityProportional(), the function accepts bptAmountIn and minAmountsOut parameters.

    • The Balancer vaults calculates amountsOut and calls the onAfterRemoveLiquidity() hook for custom logic.

  3. Exploit:

    • The onAfterRemoveLiquidity() function iterates through the user's lpNFT fee data and burns or adjusts their liquidity without validating whether the user has sufficient BPT.

    • An attacker can provide a single small liquidity deposit to bypass the initial ownership check:

      uint depositLength = poolsFeeData[pool][msg.sender].length;
      if (depositLength == 0) {
      revert WithdrawalByNonOwner(msg.sender, pool, bptAmountIn);
      }
    • They can then call removeLiquidityProportional() with an inflated bptAmountIn, causing the protocol to burn more BPT than they own.

    • onAfterRemoveLiquidity() hook loops through all the deposits of the user and burns the lpNFT but does not check this user burned equal amount of LpNFT which is equal to the value of bptAmountIn.

    • For loop loops over all the data and then takes only upLift fee for that one deposit in our case.

    • As a result, the excess BPT required for the operation is effectively taken from the protocol’s reserves.

    • Attacker gets more fund than they have/provided.

Impact

  1. Complete Protocol Drain

Tools Used

Manual Review

Recommendations

  1. Checks user has enough BPT amount to burn.

  2. After burning all the tokens check user burned enough tokens

function onAfterRemoveLiquidity(
address router,
address pool,
RemoveLiquidityKind,
uint256 bptAmountIn,
uint256[] memory,
uint256[] memory amountsOutRaw,
uint256[] memory,
bytes memory userData
) public override onlySelfRouter(router) returns (bool, uint256[] memory hookAdjustedAmountsOutRaw) {
..SNIP
for (uint256 i = localData.feeDataArrayLength - 1; i >= 0; --i) { //@audit-issue - what if this loops end but still money is not paid which is asked?
localData.lpTokenDepositValue = feeDataArray[i].lpTokenDepositValue;
localData.lpTokenDepositValueChange =
(int256(localData.lpTokenDepositValueNow) - int256(localData.lpTokenDepositValue)) /
int256(localData.lpTokenDepositValue);
uint256 feePerLP;
// if the pool has increased in value since the deposit, the fee is calculated based on the deposit value
if (localData.lpTokenDepositValueChange > 0) {
feePerLP =
(uint256(localData.lpTokenDepositValueChange) * (uint256(feeDataArray[i].upliftFeeBps) * 1e18)) /
10000;
}
// if the pool has decreased in value since the deposit, the fee is calculated based on the base value - see wp
else {
//in most cases this should be a normal swap fee amount.
//there always myst be at least the swap fee amount to avoid deposit/withdraw attack surgace.
feePerLP = (uint256(minWithdrawalFeeBps) * 1e18) / 10000;
}
// if the deposit is less than the amount left to burn, burn the whole deposit and move on to the next
if (feeDataArray[i].amount <= localData.amountLeft) {
uint256 depositAmount = feeDataArray[i].amount;
localData.feeAmount += (depositAmount * feePerLP);
localData.amountLeft -= feeDataArray[i].amount;
lpNFT.burn(feeDataArray[i].tokenID);
delete feeDataArray[i];
feeDataArray.pop();
if (localData.amountLeft == 0) {
break;
}
} else {
feeDataArray[i].amount -= localData.amountLeft;
localData.feeAmount += (feePerLP * localData.amountLeft);
++ localData.amountLeft = 0;
break;
}
}
++ require(localData.amountLeft == 0,"Not Enough tokens to burn");
..SNIP
return (true, hookAdjustedAmountsOutRaw);
}
Updates

Lead Judging Commences

n0kto Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

invalid_removing_liquidity_never_check_if_amount_is_owned_by_the_sender

Read bugs with that tag: invalid_onAfterRemoveLiquidity_loop_underflow Because of that implementation, trying to remove more will lead to an underflow.

Support

FAQs

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

Give us feedback!