Build / Rules

Settlement

Cash settlement lifecycle for dated futures contracts.

Settlement

This document explains how futures positions are cash-settled at maturity in HPDX Hashprice Futures.

Cash settlement model (contract 3.0.0). Positions are unilateral aggregates per (user, expirationAt). There is no physical hashrate delivery, no escrow, no validator role, and no destination URL. At maturity, a single settlement price is pinned per expiration; each user's aggregate on that expiration is marked to that price independently, and PnL is routed through the insurance fund.


Settlement Lifecycle

flowchart LR
  A[Order placed<br/>±qty, price, maturity] --> B[Matched FIFO<br/>updates both nets]
  B --> C[Aggregate held<br/>offset / liquidate]
  C --> D[Cash settled<br/>at / after maturity]
Phase When What happens
Order Before maturity Place signed-qty orders
Match Before maturity FIFO fills update each side's (netQuantity, netEntryValue)
Hold Before maturity Offset via opposite orders, or liquidate if underwater
Settle At/after expirationAt Permissionless mark-to-pinned-price

Before Maturity

Before an aggregate matures, the holder can:

  • Offset: Place opposite orders to reduce netQuantity toward zero and realize PnL
  • Be liquidated: Permissionlessly reduced via liquidatePosition(user, expirationAt, closeQty) if undercollateralized (orders must be cleared first)

No payment, escrow, or destination endpoint is required. The aggregate waits until expirationAt and is then cash-settled.


Settlement at Maturity

Once block.timestamp >= expirationAt, a user's non-zero aggregate at that expiry can be settled at the expiration's pinned settlement price.

Who Can Settle

Settlement is permissionless. Anyone — typically an off-chain keeper — can call:

function settlePosition(address user, uint256 expirationAt) public;

To settle several user+expiry pairs in one transaction:

function settlePositions(address[] calldata users, uint256[] calldata expirationAts) external;

Lengths must match (ArrayLengthMismatch otherwise). Keepers should pre-filter to pairs where block.timestamp >= expirationAt and getUserPosition(user, expirationAt).netQuantity != 0.

Settlement Price Pinning

All aggregates sharing the same expirationAt settle against one price, recorded the first time anyone settles (or explicitly pins) that expiration at/after maturity.

sequenceDiagram
  participant Caller
  participant Futures
  participant Oracle

  Caller->>Futures: recordSettlementPrice(expirationAt)<br/>or first settlePosition
  alt price not yet pinned
    Futures->>Oracle: getMarketPrice()
    Futures->>Futures: settlementPrice[expirationAt] = mark
    Futures-->>Caller: SettlementPriceRecorded
  else already pinned
    Futures-->>Caller: reuse stored price
  end
  Caller->>Futures: settlePosition(user, expirationAt)
  Futures-->>Caller: PositionSettled(..., settlementPrice, ...)

Anyone can pin without settling:

function recordSettlementPrice(uint256 expirationAt) external;

recordSettlementPrice reverts with SettlementDateNotReached before maturity and is idempotent once set. Readable via settlementPrice(expirationAt) (0 until recorded).

event SettlementPriceRecorded(uint256 indexed expirationAt, uint256 price, address recordedBy);

Collateral Is Held Until Settlement

A matured-but-unsettled aggregate still carries margin until settled. This prevents a losing party from withdrawing collateral after maturity but before settlement. Once pinned, market risk is frozen; on settlement the held collateral funds realized PnL.

How PnL Is Computed

Each whole contract settles one unit of price (no duration multiplier):

pnl = settlementPrice × netQuantity − netEntryValue

Example — long 1 contract, entry 4.10, pinned settlement 4.30:

netQuantity   = +1
netEntryValue = 4.10
pnl = 4.30 × 1 − 4.10 = +0.20 USDC

Short of 1 at the same entry realizes −0.20. PnL routes through the insurance fund. After settlement the user's aggregate at that expirationAt is cleared (netQuantity = 0, removed from active dates).

Settlement Event

PositionSettled(user, expirationAt, closedQuantity, pnl, settlementPrice, settledBy)

Each call settles one user's aggregate. Settling Alice does not settle Bob — call once per (user, expirationAt).

There Is No Settlement Window

A matured aggregate can be settled any time after expirationAt. There is no expiry window or late-settlement penalty; it always settles at the pinned price.


Complete Settlement Flow

sequenceDiagram
  participant Alice
  participant Bob
  participant Keeper
  participant Futures

  Note over Alice,Bob: Pre-maturity: Alice long +2, Bob short −2<br/>(independent aggregates, same expirationAt)
  Keeper->>Futures: recordSettlementPrice(expirationAt)
  Futures-->>Keeper: SettlementPriceRecorded
  Keeper->>Futures: settlePosition(Alice, expirationAt)
  Futures-->>Keeper: PositionSettled(Alice, ...)
  Keeper->>Futures: settlePosition(Bob, expirationAt)
  Futures-->>Keeper: PositionSettled(Bob, ...)<br/>same pinned settlementPrice

Read next

Sourced from futures-marketplace/docs/gitbook/05.Delivery-Settlement.md @ dev. Regenerated on every site build.