F-2026-0003·missing-user-recovery-path

cancelPosition() escape hatch is inaccessible to the user whose funds are locked

Fixedvaultleveragedprediction-marketgithub.com/bloom-art/dripster-lend
TL;DR

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.

Severity
LOW
Impact
MEDIUM
Likelihood
LOW
Method
MManual review
CAT.
Complexity
LOW
Exploitability
LOW
02Section · Description

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.

solidity
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.

03Section · Impact

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.

04Section · Recommendation

Recommendation

Allow the position owner to call cancelPosition after the delay, while preserving admin access:

solidity
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
}
05Section · Resolution

Resolution

Fixed. Position owner can now self-cancel after cancelDelaySeconds; admin path preserved.

Status
Fixed
F-2026-0003

oog
zealynx

Smart Contract Security Digest

Monthly exploit breakdowns, audit checklists, and DeFi security research — straight to your inbox

© 2026 Zealynx