Clumsy Pink Otter
Medium
Liquidations are temporarily blocked if user's pending position close amount is greater than the latest position size.
A new feature in v2.4 is guaranteed position change via Intents: since the price is provided in Intent itself, it doesn't need corresponding epoch oracle price to be valid. This makes it possible to close positions even when position opening in still pending (if all orders are from Intents), as such pending negative (pending position closure) is allowed to be greater than latest position size for intent orders:
if (
context.pendingLocal.invalidation != 0 && // pending orders are partially invalidatable
context.pendingLocal.neg().gt(context.latestPositionLocal.magnitude()) // total pending close is greater than latest position
) revert IMarket.MarketOverCloseError();
The issue is that liquidation order is always a "normal" order (invalidation == 1), thus this condition is always enforced for liquidations. However, if pending negative is already greater than latest position magnitude, it's impossible to create liquidation order which will satisfy this condition (order can only increase pending negative and can't influence latest position). In such situations liquidations will temporarily be impossible until some pending order becomes commited and the condition can be satisfied.
Incorrect validation for protected orders in case pendingLocal.neg > latestPosition.magnitude()
:
https://github.com/sherlock-audit/2025-01-perennial-v2-4-update/blob/main/perennial-v2/packages/core/contracts/libs/InvariantLib.sol#L118-L122
Notice, that there is a special handling for the case of crossing zero (when pending order closes long and opens short or vice versa): in such case liquidation order must be empty (since crossing zero is prohibited for non-empty non-intent orders). But there is no such handling for the case of large pending position closure.
- User has more pending close than latest position at the last commited oracle price (for example, user fully closes long position, then opens long again, then closes again).
None.
- User has some position open (for example,
long = 10
) - User uses Intent to fully close position
- Shortly after that (order to close still pending) user uses Intent to open position again (for example,
long = 10
). - Then immediately user uses Intent again to partially close position of
size = 1
- Shortly after that (all user orders are still pending) the price sharply drops and user becomes liquidatable.
- At this time, user's latest position = 10, pending positive (open) = 10, pending negative (close) = 11.
- All liquidations will revert, because regardless of liquidation order size, pending negative will remain greater than latest position (10).
- Once pending order to close is settled, liquidation is possible again, but might be too late as user might already accumulate bad debt.
Result: User can not be liquidated in time and is liquidated much later, when his position is already in bad debt, which is a funds loss for all market users or a risk of a bank run as the last to withdraw won't have enough funds in the market to pay out.
All liquidation attempts revert although the user should be liquidatable, thus liquidation happens later than it should, potentially creating bad debt and loss of funds for all market users.
Similar to crossing zero, include special check when liquidating - and if pending negative is greater than latest position, require liquidation order to be empty.