# PositionManager's moveLiquidity can freeze funds by removing destination index even when the move was partial

PositionManager.moveLiquidity removes the fromIndex from positionIndexes even when moveQuoteToken only partially moves liquidity, permanently freezing the remaining LP position.

- Payout: Contest reward
- Severity: high
- Program: Ajna Protocol
- Credited: hyh
- Disclosed: 2023-06-29
- Write-up quality: 7/10
- Original: https://github.com/code-423n4/2023-05-ajna-findings/issues/503
- Library page: https://bugbountylibrary.com/r/c4%3A2023-05-ajna%3AH-01
- Tags: domain:web3 impact:dos impact:funds-loss kind:contest platform:code4rena sev:high tech:solidity technique:source-review vuln:accounting-error vuln:business-logic

**Target:** PositionManager.moveLiquidity / positionIndexes tracking
**Root cause:** positionIndex.remove(fromIndex) is called unconditionally before moveQuoteToken, but moveQuoteToken can be constrained by available deposit and only partially redeem LP, leaving fromPosition.lps > 0 while the index is no longer tracked.
**Steps:**
1. LP holds a position in fromIndex with LP shares; bucket has insufficient quote deposit relative to _lpToQuoteToken output.
2. Call moveLiquidity with fromIndex/toIndex; positionIndex.remove(fromIndex) executes and succeeds.
3. moveQuoteToken hits depositConstraint < scaledDepositAvailable branch, redeeming only part of LP (redeemedLP_ = wdiv(removedAmount_, exchangeRate)).
4. fromPosition.lps -= lpbAmountFrom leaves nonzero remainder, but fromIndex is no longer in positionIndexes[tokenId].
5. Remaining LP is unreachable via PositionManager since index tracking is gone; funds frozen.
**How found:** Code reading of moveLiquidity: noticed positionIndex.remove(fromIndex) precedes moveQuoteToken and is unconditional, while fromPosition.lps is only decremented by the returned lpbAmountFrom; cross-referenced LenderActions.moveQuoteToken depositConstraint branch showing partial redemption is possible.
**Exploitation:** Any LP (or griefing caller with approval) triggers moveLiquidity on a bucket where quote deposit is insufficient to cover full LP redemption; the partial move leaves residual LP orphaned from positionIndexes, permanently locking the beneficiary's remaining position.
**Insight:** When a function removes an index/set entry before an external call whose result determines the residual state, check whether the external call can partially succeed (constraint-bound) — the set removal must be conditional on full consumption of the tracked balance.
