Position Management Functions¶
Position management functions are split between two roles: operator (daily operations) and governance (emergencies only). All lifecycle functions — openPosition through reclaimSlot — are operator-only. Only emergencyLiquidate remains governance-only and requires the system to be paused.
State Transition Summary¶
EMPTY → ACTIVE : openPosition()
ACTIVE → SETTLING : markSettling()
SETTLING → EMPTY : closePosition() // NO wins — USDC returned
ACTIVE → WRITTEN_OFF : writeOff() // YES wins from ACTIVE
SETTLING → WRITTEN_OFF : writeOff() // YES wins from SETTLING
WRITTEN_OFF → EMPTY : reclaimSlot() // after market fully settled
openPosition¶
function openPosition(
uint256 slotIndex,
address adapter,
uint256 assets,
uint256 maturity
) external
Access: Operator only
Description: Deploy capital from idleReserve into a new prediction market position.
Pre-conditions (all enforced on-chain):
positions[slotIndex].status == EMPTYassets <= idleReserve - (aggMarketNAV * reserveTargetBps / 10000)— reserve constraint usesaggMarketNAVas the TVL basis, notidleReservealoneadapteris a valid non-zero deployed contract addressmaturity > block.timestamp
Behavior:
- Deducts
assetsfromidleReserve - Calls
(sharesAcquired, executionPrice) = IMarketAdapter(adapter).buyNoShares(assets)— adapter acquires NO shares and returns both the shares acquired and the weighted average execution price as a named tuple - Sets position struct:
- Emits
PositionOpened(slotIndex, adapter, assets, entryPrice, maturity)
Entry price source
entryPrice is assigned from the executionPrice field of the tuple returned by adapter.buyNoShares() — the operator does not supply this value. This eliminates the ability to set an artificially optimistic accrual baseline.
markSettling¶
Access: Operator only
Description: Transition a slot from ACTIVE to SETTLING once the underlying market has resolved. This is the mandatory first step in the settlement path — closePosition() requires the slot to be SETTLING.
Pre-conditions (enforced on-chain):
positions[slotIndex].status == ACTIVEadapter.isSettled() == true
Behavior:
- Sets
positions[slotIndex].status = SETTLING - Emits
PositionMarkedSettling(slotIndex)
Field mutation: Only status is changed. entryPrice is not modified — it retains its original value in storage. However, _modeledPrice() checks status == SETTLING first and returns adapter.currentPrice() directly, without reading entryPrice. The retained entryPrice is effectively ignored for all pricing from this point forward.
NAV effect — immediate on call:
From this point forward, positionModeledValue() for this slot returns adapter.positionValue() instead of linear accrual. The gap contribution of this slot collapses to zero. Linear accrual is frozen permanently.
Direction of the NAV effect
- If market price > modeled accrual price (e.g. $0.99 vs $0.97):
aggModeledNAVdrops slightly to meet market value. - If market price < modeled accrual price (gap exists): the gap on this slot collapses to zero —
aggModeledNAVdrops to meetaggMarketNAVfor this slot. - In either case, the gap contribution of this slot is eliminated from the moment of the call forward.
closePosition¶
Access: Operator only
Description: Claim settlement proceeds after a market resolves NO. Returns USDC to idle reserve. Requires the slot to be SETTLING — call markSettling() first.
Pre-conditions:
positions[slotIndex].status == SETTLING— notACTIVE.markSettling()must be called first.
Behavior:
- Calls
adapter.claimSettlement()— adapter returns USDC to vault - Adds returned USDC to
idleReserve - Sets all position fields to zero:
- Emits
PositionClosed(slotIndex, settledValue)
Capital is now available for redeployment via openPosition().
writeOff¶
Access: Operator only
Description: Write a position to zero. Callable from ACTIVE or SETTLING. Used when the underlying market resolves YES, rendering NO shares worthless.
Behavior:
- Sets
positions[slotIndex].entryPrice = 0 - Sets
positions[slotIndex].status = WRITTEN_OFF - Preserves
adapter,allocatedAssets, andmaturityin storage — retained for audit trail - Emits
PositionWrittenOff(slotIndex, previousModeledValue)
NAV impact differs by prior status:
| Called from | Prior positionModeledValue |
Drop on write-off |
|---|---|---|
ACTIVE |
Linear accrual value (above market) | Full accrual gap drops instantly |
SETTLING |
Already adapter.positionValue() (gap = 0) |
Small drop — slot was already using market value |
Effects (both cases):
- That slot contributes
0to bothaggregateModeledNav()andaggregateMarketNav() - No cooldown required — write-offs are always time-sensitive
- If aggregate gap now exceeds 1500 bps, the system pauses automatically
Slot recovery
After the write-off, the slot status is WRITTEN_OFF. Call reclaimSlot() after the underlying market actually settles to reset the slot to EMPTY.
rebasePosition¶
Access: Operator only
Description: Rebase a single ACTIVE position downward. Not callable on SETTLING positions — once settling, the slot uses market value directly and rebase has no meaning.
Constraints (all enforced on-chain):
require(positions[slotIndex].status == ACTIVE); // SETTLING excluded
require(newEntryPrice <= _modeledPrice(position)); // can only rebase down
require(
newEntryPrice >= adapter.currentPrice()
|| newEntryPrice == 0 // zero = write-off path
);
require(
block.timestamp >= position.lastRebase + REBASE_COOLDOWN
|| newEntryPrice == 0 // cooldown waived for write-off
);
Behavior:
- Sets
positions[slotIndex].entryPrice = newEntryPrice - Sets
positions[slotIndex].maturity = newMaturity - Sets
positions[slotIndex].startTime = block.timestamp— accrual restarts from the new base - Sets
positions[slotIndex].lastRebase = block.timestamp - Emits
PositionRebased(slotIndex, oldEntryPrice, newEntryPrice, newMaturity)
Effect: Accrual restarts from the new lower entryPrice with startTime reset to now. Aggregate modeled NAV decreases. The gap may narrow enough to unpause the system.
reclaimSlot¶
Access: Operator only
Description: Reset a WRITTEN_OFF slot to EMPTY so it can be reused. Only callable after the underlying market has actually settled.
Pre-conditions (both enforced on-chain):
positions[slotIndex].status == WRITTEN_OFFadapter.isSettled() == true— cannot be called while the underlying market is still live
Behavior:
- Full zero-out:
positions[slotIndex] = Position(address(0), 0, 0, 0, 0, EMPTY, 0) - Emits
SlotReclaimed(slotIndex)
emergencyLiquidate¶
Access: Governance only; system must be paused (enforced on-chain)
Last Resort Only
This is the last resort. It should never be used in normal operation. It exists for one specific scenario: the system is paused, the idle reserve is depleted, the HouseBuffer cannot top up, no positions are near settlement, and investors are locked with no path to liquidity.
Pre-conditions (enforced on-chain):
paused() == true— the function reverts if the system is not paused- Slot must be
ACTIVEorSETTLING
Behavior:
- Calls
(actualShares, usdcReceived) = adapter.sellNoShares(maxShares)— sells NO shares at market price, returns both the actual shares sold and USDC received. The adapter caps at its actual position size, soactualSharesmay be less thanmaxShares. - Adds
usdcReceivedtoidleReserve - Adjusts
positions[slotIndex].allocatedAssets -= usdcReceived— reduced by actual USDC returned, NOT by the modeled value of shares sold. This keepsallocatedAssetsas a cash-in / cash-out ledger. The slippage loss is embedded in the position and realised at settlement as the difference betweenallocatedAssetsand actual proceeds. - If
adapter.positionSize() == 0after the sell: zero all position fields and set slot toEMPTY - Emits
EmergencyLiquidation(slotIndex, actualShares, usdcReceived, slippageBps) - System unpauses automatically if
idleReserve >= dailyCapANDaggregateGapBps < PAUSE_GAP_BPS
Slippage protection: The adapter's sellNoShares must implement a minimum output check. Recommended maximum slippage: 200 bps. Sell in smaller increments if market depth is insufficient.
See Emergency Liquidation Flow for full operational guidance.