Incomplete rescueERC1155 guard allows extraction of active position tokens after full deleverage
rescueERC1155 uses tokenTotalBorrowedUsdcUnits[tokenId] > 0 as a proxy for active positions; after a force-unwind drains borrowed capital to zero while the position retains conditional tokens, globalAdmin can drain those tokens and brick all subsequent exits.
Description
The rescueERC1155 function guards against rescuing active position tokens by checking tokenTotalBorrowedUsdcUnits[tokenId] > 0. This proxy variable tracks protocol-borrowed capital per tokenId, not whether active positions still hold tokens of that type.
function rescueERC1155(address token, uint256 tokenId, uint256 amountUnits)external nonReentrant onlyGlobalAdmin{if (token == conditionalTokens && tokenTotalBorrowedUsdcUnits[tokenId] > 0) {revert CannotRescueOperationalToken();}IERC1155(token).safeTransferFrom(address(this), globalAdmin, tokenId, amountUnits, "");}
After a position is fully deleveraged via force unwind (all borrowed capital repaid, borrowedUsdcUnits reduced to 0), tokenTotalBorrowedUsdcUnits[tokenId] reaches zero, even though the position still holds ERC1155 tokens in positionTokenUnits and remains in Opened state. A rescueERC1155 call for that tokenId succeeds, draining the vault's token balance. All subsequent exit paths (closePosition, settlePosition, liquidatePosition) revert permanently because the vault cannot transfer tokens it no longer holds. The user's accumulated pendingRefundUsdcUnits from prior unwind cycles are also permanently locked.
Recommendation
Track a separate tokenTotalPositionUnits counter that is incremented at finalizeOpen and decremented only at terminal finalization, and use it as the rescue guard:
if (token == conditionalTokens && tokenTotalPositionUnits[tokenId] > 0) {revert CannotRescueOperationalToken();}
Resolution
Fixed. New tokenTotalPositionUnits counter replaces the borrowed-capital proxy in the rescue guard. Closes the related Krait pre-audit observation on the same surface.

