Skip to content

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 == EMPTY
  • assets <= idleReserve - (aggMarketNAV * reserveTargetBps / 10000) — reserve constraint uses aggMarketNAV as the TVL basis, not idleReserve alone
  • adapter is a valid non-zero deployed contract address
  • maturity > block.timestamp

Behavior:

  1. Deducts assets from idleReserve
  2. 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
  3. Sets position struct:
    position.adapter         = adapter
    position.entryPrice      = executionPrice   // from adapter — operator does not supply this
    position.startTime       = block.timestamp
    position.maturity        = maturity
    position.allocatedAssets = assets
    position.status          = ACTIVE
    position.lastRebase      = 0
    
  4. 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

function markSettling(uint256 slotIndex) external

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 == ACTIVE
  • adapter.isSettled() == true

Behavior:

  1. Sets positions[slotIndex].status = SETTLING
  2. 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): aggModeledNAV drops slightly to meet market value.
  • If market price < modeled accrual price (gap exists): the gap on this slot collapses to zero — aggModeledNAV drops to meet aggMarketNAV for this slot.
  • In either case, the gap contribution of this slot is eliminated from the moment of the call forward.

closePosition

function closePosition(uint256 slotIndex) external

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 == SETTLINGnot ACTIVE. markSettling() must be called first.

Behavior:

  1. Calls adapter.claimSettlement() — adapter returns USDC to vault
  2. Adds returned USDC to idleReserve
  3. Sets all position fields to zero:
    position = Position(address(0), 0, 0, 0, 0, EMPTY, 0)
    
  4. Emits PositionClosed(slotIndex, settledValue)

Capital is now available for redeployment via openPosition().


writeOff

function writeOff(uint256 slotIndex) external

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:

  1. Sets positions[slotIndex].entryPrice = 0
  2. Sets positions[slotIndex].status = WRITTEN_OFF
  3. Preserves adapter, allocatedAssets, and maturity in storage — retained for audit trail
  4. 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 0 to both aggregateModeledNav() and aggregateMarketNav()
  • 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

function rebasePosition(
    uint256 slotIndex,
    uint256 newEntryPrice,
    uint256 newMaturity
) external

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:

  1. Sets positions[slotIndex].entryPrice = newEntryPrice
  2. Sets positions[slotIndex].maturity = newMaturity
  3. Sets positions[slotIndex].startTime = block.timestampaccrual restarts from the new base
  4. Sets positions[slotIndex].lastRebase = block.timestamp
  5. 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

function reclaimSlot(uint256 slotIndex) external

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_OFF
  • adapter.isSettled() == true — cannot be called while the underlying market is still live

Behavior:

  1. Full zero-out: positions[slotIndex] = Position(address(0), 0, 0, 0, 0, EMPTY, 0)
  2. Emits SlotReclaimed(slotIndex)

emergencyLiquidate

function emergencyLiquidate(uint256 slotIndex, uint256 maxShares) external

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 ACTIVE or SETTLING

Behavior:

  1. 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, so actualShares may be less than maxShares.
  2. Adds usdcReceived to idleReserve
  3. Adjusts positions[slotIndex].allocatedAssets -= usdcReceived — reduced by actual USDC returned, NOT by the modeled value of shares sold. This keeps allocatedAssets as a cash-in / cash-out ledger. The slippage loss is embedded in the position and realised at settlement as the difference between allocatedAssets and actual proceeds.
  4. If adapter.positionSize() == 0 after the sell: zero all position fields and set slot to EMPTY
  5. Emits EmergencyLiquidation(slotIndex, actualShares, usdcReceived, slippageBps)
  6. System unpauses automatically if idleReserve >= dailyCap AND aggregateGapBps < 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.