Skip to main content

Market Lifecycle

Signals v1 markets run on a timestamped state machine. Every market has a trading window, a settlement window keyed by a fixed settlement timestamp, and a claim gate. The point of the lifecycle is not presentation; it is a hard on-chain schedule that makes timing rules explicit and makes invalid actions revert.

The lifecycle is easiest to understand as two coupled systems:

  • A trading schedule keyed by startTimestamp and endTimestamp.
  • A settlement schedule keyed by settlementTimestamp (denoted TsetT_\text{set}) plus global window parameters.

The trading schedule decides when positions can change. The settlement schedule decides when oracle samples are admissible, when settlement can be finalized, and when claims can be executed.

State machine at a glance

Market lifecycle state machine

Figure: settlement finality has a primary path (candidate-based) and a secondary path (manual value after explicit failure).

Pre-Tset -> SettlementOpen -> PendingOps -> Settled
\\-> FailedPendingManual -> Settled

The settlement schedule is exposed by a derived helper (getMarketState) with labels:

  • Trading (pre-TsetT_\text{set} stage)
  • SettlementOpen
  • PendingOps
  • FinalizedPrimary (timeline stage where primary finalization is permitted)
  • FailedPendingManual

In the current release, the helper does not reliably distinguish primary vs secondary settlement after a market is settled. The distinction is surfaced by events emitted at finalization, not by the derived label.

Time model

Each market defines three timestamps:

  • startTimestamp (TstartT_\text{start}): trading becomes valid.
  • endTimestamp (TendT_\text{end}): trading closes (trades revert after this).
  • settlementTimestamp (TsetT_\text{set}): settlement schedule anchor and day key.

Creation enforces the relationship:

Tstart<TendTsetT_\text{start} < T_\text{end} \le T_\text{set}

Settlement uses three global window lengths:

  • SettlementOpen window Δsettle\Delta_\text{settle} (sample submissions)
  • PendingOps window Δops\Delta_\text{ops} (finalize or mark failed)
  • Claim delay Δclaim\Delta_\text{claim} with the enforced relationship Δclaim=Δsettle+Δops\Delta_\text{claim}=\Delta_\text{settle}+\Delta_\text{ops}

The four derived boundaries returned by getSettlementWindows are:

settleEnd=Tset+ΔsettleopsEnd=Tset+Δsettle+ΔopsclaimOpenTime=Tset+Δclaim\begin{aligned} \text{settleEnd} &= T_\text{set} + \Delta_\text{settle} \\ \text{opsEnd} &= T_\text{set} + \Delta_\text{settle} + \Delta_\text{ops} \\ \text{claimOpenTime} &= T_\text{set} + \Delta_\text{claim} \end{aligned}

The claim gate is time-based, but claimability is still conditional on finalization. A claim can only execute after:

  • the market has been finalized on-chain (market.settled == true), and
  • the block timestamp is at or after claimOpenTime.

Lifecycle semantics

This section treats the lifecycle as "what transitions are admitted at a given time", ignoring UI flow. The contract enforces these rules through reverts.

Creation and seeding

A market is created with its tick grid, timestamps, depth α\alpha, a fee policy, and a prior encoded as seed factors stored in a SeedData contract. At creation time, the market's pricing tree is initialized and the prior is recorded, but the market is not yet tradable.

Seeding is performed in chunks via seedNextChunks. Each call applies a slice of the prior factors to the segment tree and advances a cursor. When the cursor reaches the market's bin count, market.isSeeded flips to true.

Trading entrypoints require market.isSeeded == true. This makes seeding an explicit prerequisite rather than an implicit background step.

Trading window

Open, increase, decrease, and close share the same trading gate:

  • market exists and has not been settled
  • market is seeded
  • tTstartt \ge T_\text{start}
  • tTendt \le T_\text{end}

The important detail is that the settlement schedule does not replace the trading schedule. The derived settlement label Trading means "pre-TsetT_\text{set}", not "trades are currently valid". Trade validity is always checked against TstartT_\text{start} and TendT_\text{end} at the point of execution.

SettlementOpen window

SettlementOpen is the window where oracle samples are admissible for candidate selection:

t[Tset,Tset+Δsettle)t \in [T_\text{set}, T_\text{set} + \Delta_\text{settle})

During this window, settlement samples can be submitted permissionlessly. Each sample contains:

  • a scaled settlement value, and
  • the oracle's price timestamp (seconds).

The oracle module maintains a single candidate per market. A new sample replaces the candidate only if it is strictly closer to TsetT_\text{set} in absolute distance, with an explicit tie-break rule toward the past:

tnewTset<tcandTsetor(tnewTset=tcandTset    tnew<tcand)|t_\text{new} - T_\text{set}| < |t_\text{cand} - T_\text{set}| \quad\text{or}\quad \left(|t_\text{new} - T_\text{set}| = |t_\text{cand} - T_\text{set}| \;\wedge\; t_\text{new} < t_\text{cand}\right)

Samples outside the oracle timing constraints (future tolerance and maximum distance) revert and do not update the candidate.

PendingOps window and failure flag

PendingOps begins when SettlementOpen ends:

tTset+Δsettlet \ge T_\text{set} + \Delta_\text{settle}

During PendingOps, settlement is decided by privileged transactions:

  • primary settlement can be finalized using the current candidate, or
  • the market can be explicitly marked as failed.

Marking a market as failed clears any candidate and flips market.failed to true. The failure flag indicates that primary settlement should not be used for that market day.

After PendingOps ends, the allowed failure behavior tightens: failure can only be marked if there was no candidate at all. This separates two classes of outcomes:

  • a candidate existed and was not rejected during the decision window, or
  • no candidate existed and the market needs a manual settlement value.

Finalization

Finalization is the act that makes the market settled. Both primary and secondary finalization write the same end object:

  • a scaled settlement value,
  • a settlement tick τ\tau on the market's grid, and
  • a settlementFinalizedAt timestamp (when the finalization transaction was mined).

Tick mapping clamps and aligns the settlement tick to the market's grid:

  • convert value to a raw tick by dividing by 10610^6,
  • clamp to [minTick, maxTicktickSpacing][\text{minTick},\ \text{maxTick}-\text{tickSpacing}],
  • align to tickSpacing from minTick.

Finalization also starts the day boundary accounting work. It computes maker-side P&L, records gross fees, escrows a payout reserve for open positions, and marks the market as resolved for the daily batch keyed by TsetT_\text{set}.

In addition, settlement snapshot work can be chunked. A settled market with a large number of positions can request settlement chunks. Each request emits SettlementChunkRequested events up to a bounded per-transaction limit and advances snapshotChunkCursor until snapshotChunksDone is true.

Claims and post-settlement behavior

Claiming a payout is not a view-level computation. It is a state transition that burns the position token and transfers ctUSD from escrow.

Two gates apply:

  1. The market must already be settled.
  2. The block timestamp must be at or after Tset+ΔclaimT_\text{set} + \Delta_\text{claim}.

Formally:

tTset+Δclaimmarket.settled=truet \ge T_\text{set} + \Delta_\text{claim} \quad\wedge\quad \text{market.settled} = \text{true}

Claims draw from the payout reserve escrowing performed at finalization. This is an accounting constraint: payouts are not sourced from future maker value or from later batches. The reserve is decremented on each claim, and the system reverts if a claim would exceed the remaining reserve.

Edge cases

This list focuses on boundary conditions that are easy to miss when reading the state machine as a UI flow.

  • Trading end at the boundary: market creation allows Tend=TsetT_\text{end} = T_\text{set}. In that boundary case, a block timestamp equal to TsetT_\text{set} is inside the trade gate (tTendt \le T_\text{end}) and inside SettlementOpen (tTsett \ge T_\text{set}). Most deployments avoid this equality, but the code permits it.
  • No candidate: if no admissible oracle sample is submitted during SettlementOpen, the candidate is missing. Primary finalization reverts in this case. After PendingOps ends, failure can still be marked explicitly, which enables secondary finalization.
  • Candidate rejected: if a candidate exists but failure is marked during PendingOps, the candidate is discarded and primary finalization is no longer available for that market day.
  • Time passed without finalization: claimOpenTime is derived from timestamps, but claims remain impossible until finalization writes market.settled = true. This is a liveness property: timestamps define admissibility, and mined transactions realize the transition.
  • Primary vs secondary observability: primary and secondary finalization emit distinct events. For settled markets, those events are the reliable surface for the finalization path.

Related sections: