cancelPosition() escape hatch is inaccessible to the user whose funds are locked
cancelPosition is gated by onlyAppAdminOrAbove plus whenServerNotHalted plus whenNotEmergencyOnly, so the position owner whose funds are locked cannot self-recover even after the configured cancel delay elapses.
Description
cancelPosition() at L779 was designed as a safety mechanism to prevent user funds from being permanently locked when the admin fails to call openPosition() after a user creates a position. However, the function uses the onlyAppAdminOrAbove modifier, meaning only the admin can call it. The user who owns the position and whose money is locked cannot call it themselves.
function cancelPosition(bytes32 positionKey) external nonReentrant onlyAppAdminOrAbove whenServerNotHalted whenNotEmergencyOnly {// ^^^^^^^^^^^^^^^^^^^^// User cannot call this
Additionally, the function is blocked by whenServerNotHalted and whenNotEmergencyOnly, so the escape hatch is also disabled during the exact emergency scenarios where it's most needed.
Impact
If the admin fails to act, the user's collateral plus origination fee is locked indefinitely with no on-chain recovery path. The protocol relies entirely on admin liveness for refunds.
Recommendation
Allow the position owner to call cancelPosition after the delay, while preserving admin access:
function cancelPosition(bytes32 positionKey) external nonReentrant whenServerNotHalted whenNotEmergencyOnly {Types.PositionVault storage position = _requirePositionExists(positionKey);_requirePositionState(positionKey, Types.PositionState.Created);// Allow position owner (after delay) OR admin (after delay)if (msg.sender != position.owner) {_checkAppAdminOrAbove();}uint256 cancelAfter = uint256(position.createdAt) + cancelDelaySeconds;if (block.timestamp < cancelAfter) {revert CancelDelayNotElapsed(positionKey, position.createdAt, cancelAfter, block.timestamp);}// ... rest unchanged}
Resolution
Fixed. Position owner can now self-cancel after cancelDelaySeconds; admin path preserved.

