F-2026-0005·missing-precondition-check

createPosition accepts user funds without checking capital availability or limits

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

createPosition transfers user collateral and origination fee before checking capital pool availability or per-user/market/token/global caps; if openPosition later reverts, funds remain locked in Created state until cancellation.

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

Description

When a user calls createPosition(), their USDC (collateral plus origination fee) is transferred into the contract at L503 without any check on whether the protocol can fund the requested leverage. Both the capital pool balance check (capitalPoolBalanceUsdcUnits < protocolCapital at L542) and the four capital limit checks (validateCapitalLimits at L547, per-user, per-market, per-token, and global caps) only occur later when the admin calls openPosition() in a separate transaction that the user has no visibility into.

If any of these checks fail, openPosition reverts on the admin side. The user receives no on-chain feedback, their funds remain locked in Created state. Recovery requires a manual cancelPosition call (currently admin-only per L-01) after cancelDelaySeconds elapses (default 10 minutes).

The capital pool is funded manually via addCapital() by the globalAdmin (2-of-3 cold wallet multisig). There is no automated replenishment, no minimum reserve requirement, and no utilization circuit breaker. During demand surges the pool can drain to zero through normal usage, causing all new position opens to fail simultaneously while multiple users have funds locked.

03Section · Recommendation

Recommendation

Add capital availability and limit checks to createPosition before transferring user funds:

solidity
// In createPosition(), before the safeTransferFrom:
uint256 protocolCapitalUsdcUnits = notionalUsdcUnits - collateralUsdcUnits;
if (capitalPoolBalanceUsdcUnits < protocolCapitalUsdcUnits) {
revert InsufficientCapital();
}
PositionLib.validateCapitalLimits(
userTotalBorrowedUsdcUnits,
marketTotalBorrowedUsdcUnits,
tokenTotalBorrowedUsdcUnits,
totalProtocolBorrowedUsdcUnits,
maxUserCapitalUsdcUnits,
maxMarketCapitalUsdcUnits,
maxTokenCapitalUsdcUnits,
maxTotalCapitalUsdcUnits,
msg.sender,
marketId,
tokenId,
protocolCapitalUsdcUnits
);

This does not replace the checks in openPosition, both are needed since pool balance and limits can change between calls. The early checks prevent the common case where a user locks funds into a position that is already unfundable at creation time.

04Section · Resolution

Resolution

Acknowledged. Backend pre-flight is the primary gate; recovery available via L-01 user self-cancel.

F-2026-0005

oog
zealynx

Smart Contract Security Digest

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

© 2026 Zealynx