Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Termination & Settlement

Every Bazaar market can be wound down without anyone’s permission: when its feed is decommissioned, its asset is delisted, or its price series breaks. This page covers the SDK side of that: reading where a market stands, proposing termination through UMA or an insurer vote, fixing the settlement price (including fetching and proving the archived Pyth Pro updates it needs), settling positions, finalizing, and withdrawing what a terminated market pays.

Use it for a market-status banner, a “propose termination” or “vote” screen for insurers, a withdrawal screen on an ended market, or a bot that settles markets for their tips. The rules themselves (why the cutoff must be 12 hours out, how the chain rule picks its five prices, the settlement waterfall) are in Bazaar’s Termination & Settlement page; this page links to it rather than repeating it.

Everything here is on bz.termination (with bz connected as in Connecting). The termination namespace export has the same functions with a context first, plus the pure helpers and constants (reasonProblem, TerminationCategory, insurerProposalId, …).

How a Market Ends

There are four ways in, and every one of them goes through the same two stages.

PathWho starts itSDK
UMA claimAnyone, with a USDC bond (at least 1,000 USDC) and a 12-hour UMA livenessproposeTermination, settleTerminationProposal
Insurer voteInsurers whose matured shares reach 60% of the vote’s snapshotproposeInsurerTermination, voteForInsurerTermination, executeInsurerTermination
Stale oracleAnyone, once the market has stored no price for 21 daysterminateStalePair
AutonomousThe pair’s own insolvency checksnothing to call
  1. The cutoff. A UMA claim or an insurer vote schedules a cutoff. Trading, deposits and withdrawals stop 6 seconds before it (LIMBO_LEAD): the market is in limbo.
  2. Fixing the price. After the cutoff, anyone calls terminateScheduledPair with the archived Pyth Pro updates that prove the settlement price (the stale-oracle path fixes it directly at the last stored price). This opens the settlement window.
  3. Settling. In the window every open position is settled at the fixed price (liquidate on the pair, permissionless, tipped $0.05 per position settled for someone else).
  4. Finalizing. Once every position is settled, or 14 days after the fix, anyone calls finalizeTermination. The payout ratios freeze and withdrawals open for good.

Costed Calls

Every builder on this page returns a CostedCall (except terminateScheduledPair, which returns a plan holding one). A CostedCall is a PreparedCall (simulate it and send it as one) with four more fields:

FieldMeaning
costsEvery bond, fee, levy and share lock the call takes or puts at risk, each a Cost. Empty for a call that costs only gas.
approvalsERC-20 approvals to send first, as PreparedCalls, each for exactly the amount (never unlimited). Empty when the allowance already covers it. The SDK never assumes an approval exists.
requiresPreconditions in words for the UI to show: an allowance to send first, a balance short of the bond, a cap the contract enforces.
notesWhat will happen, in plain language, and warnings.

A Cost:

FieldMeaning
kind"bond", "seed", "fee", "levy" or "lock".
labelWhat it is, in a trader’s words (“UMA termination bond”).
amountIn unit’s own precision: USDC with 6 decimals, ETH in wei, shares with 18 decimals, "token" in the token’s units.
unit"USDC", "ETH", "shares" or "token" (an ERC-20 other than USDC; see token and decimals).
tokenThe ERC-20 it is paid in; null for ETH and shares.
decimalsFor "token": the token’s decimals.
payeeWho takes or holds it (the contract pulling a bond, the oracle), or null when nothing moves now.
returnedWhen and how it comes back, if ever.
contingenttrue for a worst case that depends on what happens later (a levy that depends on a verdict).

Note the units: a bond’s amount is in USDC’s 6 decimals, while the call’s pullsUsdc is the same bond in 18 decimals, like every other pullsUsdc. Format costs with uma.usdcText, uma.ethText and uma.sharesText, which print the exact amount.

A helper that shows a costed call, sends its approvals, simulates it and sends it:

/** A cost as text, in its own unit. */
function costText(c: uma.Cost): string {
  if (c.unit === "USDC") return uma.usdcText(c.amount);
  if (c.unit === "ETH") return uma.ethText(c.amount);
  if (c.unit === "shares") return uma.sharesText(c.amount);
  return `${formatDecimal(c.amount, c.decimals ?? 18)} of token ${c.token}`;
}

/** Show what a costed call takes, send its approvals, simulate it, then send it. */
async function sendCosted(call: uma.CostedCall): Promise<Receipt> {
  console.log(call.summary);
  for (const c of call.costs) {
    console.log(`${c.label}: ${costText(c)}${c.contingent ? " (worst case)" : ""}. ${c.returned}`);
  }
  for (const r of call.requires) console.log(`Needs: ${r}`);
  for (const n of call.notes) console.log(n);
  // Approvals first (each for the exact amount): the call itself reverts until they land.
  for (const a of call.approvals) await bz.waitForReceipt(await bz.send(window.ethereum, a));
  const sim = await bz.simulate(call);
  if (!sim.ok) throw new CallError(sim.error!.message, sim.error);
  return bz.waitForReceipt(await bz.send(window.ethereum, call));
}

Simulate after the approvals have landed: the call pulls the bond with transferFrom and reverts until the allowance is there. When the wallet already allows the spender more than the bond, no approval is returned, and a note warns that the contract takes what it charges when the call runs, up to that allowance; to cap it at the quote, approve exactly the bond first.

A bond quote read from the chain that is below the contract’s own floor (an RPC answering 0, for example) cannot be right: the builder throws uma.BondQuoteError and builds nothing, since an approval sized to it would be wrong.

Reading Where a Market Stands

const s = await bz.termination.terminationStatus(aapl.address);
console.log(`phase: ${s.phase}`); // live | scheduled | limbo | settling | terminated
if (s.scheduledTerminationTs) console.log(`cutoff ${uma.timeText(s.scheduledTerminationTs)}; trading halts ${uma.timeText(s.haltsAt!)}`);
console.log(s.settlementRule.key, "-", s.settlementRule.meaning);
if (s.settlementPrice !== null) console.log(`settlement price ${formatUnits18(s.settlementPrice)}`);
if (s.finalizeOpensAt !== null) console.log(`finalize opens ${uma.timeText(s.finalizeOpensAt)}`);
for (const tp of s.openProposals.termination) console.log(`UMA claim ${tp.assertionId}: ${tp.categoryName}, cutoff ${uma.timeText(tp.lastTradingTs)}`);
for (const ip of s.openProposals.insurer) console.log(`vote ${ip.id}: ${ip.state}, ${uma.sharesText(ip.yesShares)} yes`);
// Without a subgraph, find the ids in the terminator's logs instead: terminationStatus(pair, { fromBlock }).

terminationStatus(pair, search?) answers in one call. phase is:

PhaseMeaning
liveNo cutoff scheduled.
scheduledA cutoff is scheduled; trading continues until 6 seconds before it.
limboWithin 6 seconds of the cutoff, or past it, with no settlement price yet: nothing trades, deposits or withdraws until terminateScheduledPair fixes the price.
settlingThe settlement price is fixed; positions settle at it until finalizeTermination.
terminatedFinalized: withdrawals pay at the frozen ratios.
FieldMeaning
nowThe chain time the status was judged at.
scheduledTerminationTs, haltsAt, cutoffPassedThe cutoff, when trading halts (the cutoff less 6 s), and whether it has passed.
settlementPriceFixedTs, settlementPriceWhen the price was fixed and the price (18 decimals), or null.
isPairTerminatedFinalized.
settlementRuleHow terminateScheduledPair would settle now (see below).
acceptedTerminationAssertionIdThe accepted UMA claim whose cutoff is in force, or null.
umaCutoffCapWhile an insurer vote holds the schedule: the UMA cutoff that caps its settlement samples, or null.
insurerScheduled, insurerScheduleIdWhether an insurer vote set the cutoff, and which.
openInterest{ long, short }: finalize waits for both to reach zero, or for the 14-day escape hatch.
finalizeOpensAtWhen finalizeTermination can be called: now once every position is settled, else 14 days after the fix; null before the fix and after finalize.
openProposalsUnresolved UMA claims and insurer votes on the market.

The terminator keeps no list of proposals, so openProposals (also callable on its own as openProposals(pair, search?)) finds their ids, then reads each one on chain, so the answer reflects the chain rather than the index. Its source says where the ids came from:

sourceWhen
"subgraph"The connection has a subgraph: every page of pending and disputed claims and open votes, each id checked (32 bytes, strictly ascending, at most MAX_OPEN_PROPOSALS = 1,000 of each kind). An answer that breaks any of these throws SubgraphError rather than being half-used.
"logs"No subgraph, and search.fromBlock is given: one eth_getLogs over the terminator from fromBlock to search.toBlock (default: the latest block).
"ids"Only search.ids were given. Ids you pass are always read, whatever else runs.
"none"Nothing to search: both lists are empty.

Other reads:

FunctionReturns
settlementRule(pair)The terminator’s settlementRule, decoded (fields below).
terminationProposal(assertionId)A UMA claim: pair, proposer, lastTradingTs (its cutoff), reason, proposalTs, livenessEndsAt, resolved, category, categoryName; or null.
insurerProposal(id)An insurer vote (see Reading Votes), or null.
requiredTerminationBond()The bond a UMA claim posts now, USDC units: the larger of 1,000 USDC and UMA’s minimum.
terminatorConstants()The terminator’s constants read from the chain: category, outcome, verdict and rule numbers, the bond floor and the live bond, the liveness, the reason length, the sample count and spacing.

A market snapshot also carries the termination stamps (marketSnapshot(...).termination: the cutoff, the fixed price, the ratios, the bad debt) and a status; see Markets.

Ending a Market Through UMA

Proposing

// Validate as the user types: null means the terminator accepts the text.
const reason = "Delisting announced for 2026-12-01: https://example.com/notice?id=42";
const problem = termination.reasonProblem(reason);
if (problem) console.log(problem); // e.g. 'The character "[" is not allowed: ...'

// The four grounds, with a sentence each for a picker.
for (const [id, info] of Object.entries(termination.TERMINATION_CATEGORY_INFO)) {
  console.log(id, info.name, "-", info.description);
}
try {
  const now = await bz.uma.chainNow(); // the chain's clock, not the browser's
  const call = await bz.termination.proposeTermination({
    pair: aapl.address,
    category: termination.TerminationCategory.AssetCessation,
    lastTradingTs: now + 3 * 86_400, // at least 12 hours out, so UMA's acceptance alone enforces it
    reason,
    from: user!,
  });
  const receipt = await sendCosted(call);
  const { assertionId, insurerProposalId } = bz.termination.proposalIdsFromReceipt(receipt);
  console.log(`claim ${assertionId}; its insurer vote has the same id: ${insurerProposalId}`);
} catch (e) {
  console.error(e instanceof uma.BondQuoteError ? e.message : errorMessage(e));
}

proposeTermination(args) posts a claim to UMA’s Optimistic Oracle that the market should end.

ArgumentMeaning
pairThe market.
categoryOne of the four grounds, 1 to 4 (termination.TerminationCategory).
lastTradingTsThe proposed cutoff, unix seconds.
reasonThe grounds and evidence, 1 to 1,000 characters of a restricted charset.
fromThe proposer, who posts the bond.
slackSecondsHow long the transaction may take to land: the 2-hour minimum is checked this far ahead. Default 60.

The categories (termination.TERMINATION_CATEGORY_INFO has a name and a description of each, for a picker):

ValueNameGrounds
1 OracleDecommissionOracle decommissionThe market’s Pyth Pro feed (either leg of a composite) has stopped or is scheduled to stop publishing.
2 AssetCessationAsset cessationThe asset has been or will be delisted (delisting, merger, bankruptcy, an indefinite halt), once officially confirmed with a fixed date.
3 StructuralChangeStructural changeAn officially scheduled event breaks the price series: a split, spinoff, rights offering, special distribution over 5%, redenomination, index re-basing.
4 PythVerifierMigrationPyth verifier migrationThe Pyth Pro verifier contract the market’s oracle uses is deprecated, being replaced, or no longer verifies the updates Pyth distributes.

Market moves of any size are not grounds.

The reason. The terminator splices reason into the claim UMA’s voters read, between fences that its charset makes unforgeable, so it accepts only letters, digits, space and . , & / - : ? = # % _ ~ + (enough for evidence URLs). Every other character, including square brackets and anything non-ASCII, is refused. termination.reasonProblem(reason) returns why a reason would be refused, or null, so a form can check it as the user types. The claim names the market by its pair id and feed ids; the reason cannot change which market it is about.

The bond. The terminator pulls requiredTerminationBond() (at least 1,000 USDC) from the proposer and posts it to UMA as the assertion bond. UMA returns it when the claim is accepted, and pays it to the disputer if UMA’s voters reject the claim. The call’s costs has the bond, and its approvals the USDC approval for the terminator when the allowance is short. If UMA’s acceptance schedules the market, the proposer also earns 0.1% of its insurance fund, at most $100, claimable on the market with claimFees.

The cutoff. It must be at least 2 hours out (plus slackSeconds). Acceptance alone enforces it as proposed only when it is at least 12 hours out (UMA’s liveness), so prefer that. A nearer cutoff is met as proposed only if insurers vote it through an hour before it; otherwise the claim is accepted after it and the cutoff moves to 7 seconds after acceptance (MIN_CUTOFF_LEAD), and the call’s notes say so. For an event that has already happened, propose the earliest cutoff accepted. Once a UMA cutoff is in force, a new claim must name an earlier one. Filing also opens an insurer vote on the same cutoff, under the claim’s assertion id (its window opens 14 days before the cutoff, or at filing).

proposalIdsFromReceipt(receipt) reads the claim’s assertionId (and the insurer vote it opened, insurerProposalId, which is the same id) from the receipt’s terminator events.

The builder refuses up front, with an InputError: a market the terminator does not know, one already ended or with its price fixed, a category outside 1 to 4, a cutoff too soon or not earlier than the UMA cutoff in force, an invalid reason, and UMA’s identifier being off its live whitelist (claims are closed until Bazaar’s identifier upgrade completes; see UMA Disputes & Governance). The insurer-vote and stale-oracle paths still work then.

Settling the Claim

async function settleClaim(assertionId: `0x${string}`): Promise<string> {
  const st = await bz.uma.assertionStatus(assertionId, { home: "terminator" });
  console.log(uma.ASSERTION_PHASE_TEXT[st.phase]);
  if (st.phase === "liveness") return `It can be settled from ${uma.timeText(st.canSettleAt!)}.`;
  if (st.phase !== "settleable" && st.phase !== "disputed") return "Nothing to settle.";
  const call = await bz.termination.settleTerminationProposal({ assertionId, from: user! });
  // A disputed claim settles only once UMA's voters have ruled: the simulation says whether they have.
  await sendCosted(call);
  const tp = await bz.termination.terminationProposal(assertionId);
  return tp?.resolved ? "Settled." : "Not resolved yet.";
}

settleTerminationProposal({ assertionId, from? }) settles the assertion on the oracle it was made on and applies the verdict: an accepted claim schedules the market (or pulls its cutoff earlier, or caps an insurer schedule’s samples); a rejected one leaves the market alone and ends the claim’s vote. It costs only gas, and anyone may send it.

It refuses an unknown or resolved claim, and one still inside UMA’s liveness. A disputed claim settles only after UMA’s voters have ruled; until then the call reverts, so simulate it. Use bz.uma.assertionStatus(id, { home: "terminator" }) to show where the claim stands (liveness with canSettleAt, settleable, disputed, settledTrue, settledFalse), and see UMA Disputes & Governance for disputing a claim and previewing a DVM ruling.

Insurer Votes

An insurer vote ends a market when yes-votes from matured insurance shares reach 60% of the vote’s share snapshot, and someone executes it at least one hour before its cutoff. Every UMA claim opens one; an insurer can also file a bare proposal with no claim behind it.

A Bare Proposal

// A bare proposal: a cutoff 2 hours to 14 days out, a fixed 1,000 USDC bond, no UMA claim behind it.
const now = await bz.uma.chainNow();
const proposal = await bz.termination.proposeInsurerTermination({ pair: aapl.address, cutoffTs: now + 2 * 86_400, from: user! });
const receipt = await sendCosted(proposal);
const id = bz.termination.proposalIdsFromReceipt(receipt).insurerProposalId!;

proposeInsurerTermination({ pair, cutoffTs, from, slackSeconds? }) posts a fixed 1,000 USDC bond (INSURER_TERMINATION_BOND_USDC), held by the terminator until the vote resolves: refunded if the vote schedules the market, is preempted by an earlier cutoff, is voided by a drained fund, or fails having reached 60%; forfeited to the market’s insurance fund if it fails short of 60%.

The cutoff must be 2 hours to 14 days out and earlier than any cutoff the market already carries, and the proposer must hold shares deposited at least 4 days ago. Proposing does not vote the proposer’s shares. A bare vote settles under the chain rule, so its yes-voters pay a levy (below) unless the vote meets a cutoff UMA accepted. The id is fixed by the second the proposal lands; read it with proposalIdsFromReceipt(receipt).insurerProposalId (or compute it with termination.insurerProposalId(pair, proposer, timestamp)).

Voting

try {
  const vote = await bz.termination.voteForInsurerTermination({ pair: aapl.address, id, shares: parseUnits18("100"), from: user! });
  // costs: the lock on the voted shares, and (contingent) the worst-case levy if this vote ends the market.
  for (const c of vote.costs) console.log(`${c.label}: ${uma.sharesText(c.amount)}${c.contingent ? " at worst" : ""}. ${c.returned}`);
  await sendCosted(vote);
} catch (e) {
  console.error(errorMessage(e));
}

voteForInsurerTermination({ pair, id, shares, from }) votes yes with shares (18 decimals). Votes are never withdrawn. The same shares may back several proposals, up to 8 live ones per wallet on a market (INSURER_MAX_LIVE_VOTES, enforced by the contract and listed in requires).

Only shares held now that were deposited at least 4 days before the vote window opened count (termination.maturityCutoff(windowOpenTs) gives that instant), less what the wallet has already voted on this proposal. The builder refuses more than that, with the limit in the message.

The call’s costs describe what voting puts at stake:

  • a lock: the voted shares cannot be withdrawn from the insurance fund until the proposal’s cutoff or the market’s, whichever is first (a failed proposal releases them sooner only while the market has no cutoff); if this vote ends the market and its outcome is costed, until 13 days after the settlement price is fixed (INSURER_POST_FIX_LOCK);
  • a levy, contingent, at its worst case: shares forfeited at redemption to Bazaar’s bug bounty address if this vote ends the market.
Outcome (stamped at the fix)What the yes-voters owe
true: UMA upheld the claim behind the voteNothing; it settles under UMA’s rule.
false: UMA rejected the claimThe 13-day lock, and half of every voted share (INSURER_LEVY_BP = 5,000).
unjudged: a bare vote, or no verdict within 14 days of the claimThe 13-day lock, and half the dissent fraction of the voted shares, at least a tenth (termination.unjudgedLevyBp(snapshot, yes)), unless the vote met a cutoff UMA had accepted.

The notes also say how many shares the vote needs, how many it has with yours, and whether yours meet the threshold. termination.OUTCOME_TEXT has a sentence for each outcome.

Executing

async function executeVote(voteId: Hex): Promise<string> {
  const call = await bz.termination.executeInsurerTermination({ pair: aapl.address, id: voteId, from: user! });
  // outcome: "schedule" | "preempted" | "voided" | "failedRefund" | "failedForfeit"
  console.log(call.outcome, call.notes[0]);
  await sendCosted(call);
  return call.outcome;
}

console.log(await executeVote(id).catch(errorMessage));
// The levy an unjudged outcome would charge, for a vote that ended at 70% yes:
console.log(`${termination.unjudgedLevyBp(1_000n, 700n)} bp`); // 1500 bp: half the 30% that did not vote yes

A vote takes effect only when someone executes it. executeInsurerTermination({ pair, id, from? }) resolves it; anyone may send it, and it costs only gas. Its extra outcome field (and notes[0]) says what it will do now:

outcomeWhenA bare vote’s bond
scheduleAt or above 60% before the latch closes: the market is scheduled to stop at the cutoff, or its schedule pulled earlier.Refunded
preemptedThe price is already fixed, or a cutoff at or before this one is already scheduled.Refunded
voidedThe fund was drained and recapitalized since the snapshot.Refunded
failedRefundThe latch closed (one hour before the cutoff) without scheduling, having reached 60%.Refunded
failedForfeitThe latch closed short of 60%.Forfeited to the insurance fund

A claim-backed vote’s bond is UMA’s, so none of these moves one. The builder refuses an unknown or resolved vote, and one still open below its threshold (the contract reverts until the vote reaches 60% or its latch closes).

Reading Votes

const p = await bz.termination.insurerProposal(id);
if (p) {
  console.log(`${p.state}: ${uma.sharesText(p.yesShares)} of ${uma.sharesText(p.thresholdShares)} needed`);
  console.log(`voting ${uma.timeText(p.windowOpenTs)} to ${uma.timeText(p.votingClosesAt)}; cutoff ${uma.timeText(p.cutoffTs)}`);
  console.log(p.claimBacked ? "Backed by a UMA claim." : "A bare vote: it settles under the chain rule.");
}
const mine = await bz.termination.getLockedShares({ pair: aapl.address, user: user! });
console.log(`your shares locked by live votes: ${uma.sharesText(mine)}`);

insurerProposal(id) returns the vote with its state judged at the chain’s time:

stateMeaning
upcomingIts window has not opened (a claim filed more than 14 days before its cutoff).
votingOpen for votes and, once at the threshold, for execution.
expiredThe latch closed without the vote scheduling the market; executing it now resolves it as failed.
scheduledIt reached the threshold and set the market’s cutoff.
rejectedUMA rejected the claim behind it before it scheduled anything.
endedResolved otherwise (failed, preempted or voided); the events say which.

Its other fields: proposer, proposalTs, shareEpoch, cutoffTs, windowOpenTs, votingClosesAt (the cutoff less one hour), yesShares, snapshotTotalShares and snapshotTaken (the snapshot is taken at the first touch once the window is open), thresholdShares and thresholdMet, claimBacked, resolved, scheduled, costed, verdict/verdictName, outcome/outcomeName/outcomeText, and levyBp (stamped with the outcome).

getInsurerVote({ pair, voter, id }) is the shares a voter has in one vote, and getLockedShares({ pair, user }) the shares voted into live proposals, which an insurance withdrawal cannot take (the exit plan reports them as lockedForVoting).

Fixing the Settlement Price

Stale Oracle

try {
  // Only for a market whose oracle has been silent for more than 21 days.
  await simulateAndSend(await bz.termination.terminateStalePair({ pair: aapl.address, from: user! }));
} catch (e) {
  console.log(errorMessage(e)); // e.g. "pair: This market stored a price within the last 21 days: ..."
}

terminateStalePair({ pair, from? }) ends a market that has stored no price for more than 21 days (ORACLE_DEAD_THRESHOLD) at its last stored price, or at a nominal price of 1 when it never stored one (then aged from its creation). Anyone may call it; it pays nothing and costs only gas. The pair’s own stored price is the evidence: a pair stores one whenever a call lands on it with a fresh update, so one refreshPrice restarts the 21 days.

It refuses a market the terminator does not know, one with its price fixed, one whose vote-set cutoff has passed (that market settles through terminateScheduledPair, whose own fallback reaches the stored price), and one that stored a price within 21 days, with the time the path opens.

At a Scheduled Cutoff

const plan = await bz.termination.terminateScheduledPair({ pair: aapl.address, from: user! });
if (plan.call) {
  console.log(`settles under ${plan.under} at ${formatUnits18(plan.settlementPrice!)} with ${plan.updates.length} update(s)`);
  for (const n of plan.call.notes) console.log(n);
  // Already simulated as `from`; its value is the Pyth fee (plan.call.costs), any excess refunded.
  await bz.waitForReceipt(await bz.send(window.ethereum, plan.call));
} else {
  console.log(plan.reason);
  for (const a of plan.attempts) console.log(`${a.under}: ${a.ok ? "accepted" : a.reason}`);
  if (plan.retryAt !== null) console.log(`try again from ${uma.timeText(plan.retryAt)}`);
}

terminateScheduledPair({ pair, from, fetchTick?, estimateGas? }) fixes a scheduled market’s price with archived Pyth Pro updates. Pyth keeps no prices on chain, so every settlement price is submitted as a signed update and verified on the spot. All of them come from Pyth’s one-second channel (fixed_rate@1000ms), which issues one update per second, stamped on the second, and whose archive serves the update for any past second: the connection’s price source fetches them (atTick; without a Pyth Pro source the builder throws ConnectError). It returns a ScheduledSettlementPlan whose call is one the contract accepted in simulation as from, or null with the reason.

It reads settlementRule(pair), whose fields are:

FieldMeaning
rule, key0 nothing, 1 tick, 2 chain, 3 awaitVerdict.
meaningA sentence for the rule (termination.SETTLEMENT_RULE_TEXT).
firstTargetThe cutoff: the second whose update settles the market under UMA’s rule, the first sample’s target under the chain rule.
latestPublishTsUnder UMA’s rule the cutoff again; under the chain rule the latest second a sample’s update may be stamped with, null when unbounded.
lastResortTsAfter this a call with no updates settles at the stored price; null when not applicable.
verdictDeadlineWhile waiting for a verdict: when the vote stops waiting and settles unjudged.

and builds the set the rule needs:

RuleUpdatesFee
tick (UMA’s rule)The update stamped at the cutoff, carrying every feed leg: one update for both legs of a composite, or one per leg when the archive will not answer for them together. The market settles at the price it carries, whenever that price was generated: a quiet feed’s update carries its last price forward.getUpdateFee(updates.length)
chainFive samples. Each is the earliest second whose update carries a price generated at or after the sample’s target: the target’s own update when it does, otherwise two updates, the quiet second before and that second. The first target is the cutoff, each later one 600 s past the second of the sample before (the later leg’s, for a composite); every sample’s second no later than latestPublishTs. Per sample the base leg’s one or two updates come first, then the quote leg’s.getUpdateFee(updates.length)
awaitVerdictNothing yet: the vote that set the cutoff waits for UMA’s verdict on its claim.
nothingNothing to settle (no cutoff, or the price is fixed).

Finding a chain sample. termination.chainSample fetches the update stamped at the target. If its price was generated at or after the target, that one update is the sample. If not, the feed was quiet there, and since a price’s generation time never goes backwards from one second to the next, the first second with a new price is found by doubling the step from the target and then bisecting: a reopen days later takes a few dozen requests. The targets cannot be computed ahead, so the five samples are searched in sequence, and the last exists only once its price does, at least 40 minutes after the cutoff. A sample that would land past latestPublishTs, or that the feed has not produced yet, makes the builder return no call and say so.

Proving the set. Each set is verified by calling the pair’s oracle exactly as the terminator does (fetchPriceAtTick or fetchChainedMedian, as from, with the fee), and only a set the oracle accepts is offered. The price it returns is the plan’s settlementPrice. This matters once the stored-price fallback is open: the terminator then settles at the stored price instead of reverting on a refused set, so the contract’s simulation alone would accept bad updates and the call would be labelled with a price it does not fix.

Verdicts. settlementRule cannot see a verdict UMA’s DVM has reached on the claim behind a vote until someone records it, but terminateScheduledPair records it itself. For a disputed claim the builder previews the DVM’s ruling (uma.previewSettlement) and builds that rule’s set: UMA’s rule if the claim was upheld (whose 12-hour grace starts at this recording, so no fallback yet), the chain rule if not. When the ruling cannot be previewed, it builds both sets and returns the one the contract accepts. A claim still in its liveness returns no call, with retryAt set to the end of the liveness.

The last resort. Past lastResortTs, a call with no updates settles the market at its stored price, for good (12 hours after the cutoff or the verdict under UMA’s rule; 21 days after the cutoff under the chain rule, or 12 hours after a capping UMA cutoff, termination.chainLastResort). It is tried only when no proven set is accepted, and when it is offered depends on storedPrice:

storedPriceThe stored-price call is offered
"whenAbsent" (default)Only when every set failed because Pyth’s archive holds no usable update (none for the second, no price in it, no chain under its bound or none yet), and the archive is shown to work: it must serve ETH/USD’s one-second update from a minute before the chain’s time, or its “none” is not believed. A request that timed out or was refused, an archive at a wrong URL, a signer check that failed, and a set the oracle refuses may all hide a proven price, so the call is withheld and the plan says why.
"always"Whenever no proven set is accepted, for a caller who has decided. It needs no price source.
"never"Never.

The plan:

FieldMeaning
callThe CostedCall to send, or null. Its value is the Pyth fee in ETH (in costs; any excess is refunded); its notes state the price and the rule.
under"tick", "chain" or "storedPrice".
updatesThe call’s priceUpdate.
printsThe updates as read, feed by feed (feedId, update, tick, priced, price, generatedUs, publishTime): in calldata order under the chain rule, one per leg under UMA’s rule.
settlementPriceThe price the call fixes (18 decimals).
attemptsEach candidate tried: under, ok, reason, kind ("absent": the archive showed no proven settlement exists now; "unreachable": it could not be asked or relied on; "refused": the oracle or the contract refused the set; null when it was accepted), the decoded error, price.
reasonWhy there is no call.
retryAtWhen to try again, when known (the verdict, the fallback).
storedPriceWithheldWhy the stored-price call is not offered although its last resort is open; null when it is offered or not open.
rule, scheduledTerminationTs, nowWhat it was judged against.

The builder throws an InputError for a market the terminator does not know, one with its price fixed, one with no cutoff, and one whose cutoff has not passed yet. Updates are fetched with termination.tickFetcher(source); pass fetchTick (a TickFetcher: (feedIds, second) => Promise<ProUpdate | null>) to use another archive. The building blocks are exported too: tickPrints, chainSample, chainPrints, settlementPrint, generatedAtOrAfter, and SettlementPrintError for an update that cannot be fetched or a sample that cannot be proven, whose kind is "absent" or "unreachable" as in attempts. A price at or below zero, at the cutoff or in a chain sample, cannot settle the market: the oracle refuses it.

No keeper job fixes prices on its own; the keeper’s operator can run terminateStalePair and terminateScheduledPair as manual operations (see Manual Operations).

The Settlement Window

Once the price is fixed, trading, ADL, deposits and all withdrawals stay frozen while every open position is settled at it. Settling is the pair’s liquidate(accounts, []): anyone may call it, with no price update and no ETH, and settling someone else’s position earns a $0.05 tip, claimable with claimFees. Settling your own is free (a position also settles on its owner’s first withdrawal after finalize).

The SDK has no builder for this call; encode it with the pair’s codec:

// The SDK has no builder for settling positions in the settlement window: encode the pair's
// liquidate(accounts, []) directly. No price update and no ETH; each position you settle for
// someone else earns a $0.05 tip, claimable with claimFees.
async function settleOpenPositions(): Promise<void> {
  const accounts: Address[] = [];
  let after: string | null = null;
  do {
    const page = await bz.history.positions({ pair: aapl.address, openOnly: true, first: 1000, after });
    accounts.push(...page.items.map((p) => p.account));
    after = page.next;
  } while (after);
  for (let i = 0; i < accounts.length; i += 100) {
    const batch = accounts.slice(i, i + 100); // the keeper's default batch for this job
    await simulateAndSend({
      action: "liquidate",
      to: aapl.address,
      data: pairCodec().encode("liquidate", batch, []),
      value: 0n,
      from: user!,
      summary: `Settle ${batch.length} position(s) at the settlement price`,
    });
  }
}
if (s.phase === "settling") await settleOpenPositions();

The sample finds open positions in Bazaar’s subgraph (bz.history.positions, see History). The keeper’s terminalSettlement job does the same from its own index, then finalizes; see The Jobs.

Finalizing

try {
  await simulateAndSend(await bz.termination.finalizeTermination({ pair: aapl.address, from: user! }));
} catch (e) {
  console.log(errorMessage(e)); // e.g. positions still open before the 14-day escape hatch
}

finalizeTermination({ pair, from? }) retires the vault’s inherited inventory and freezes the payout ratios. Anyone may call it once every position is settled (open interest is zero) or, as the escape hatch, 14 days after the fix (TERMINAL_SETTLEMENT_ESCAPE); it costs only gas. It refuses a market with no price fixed, one already finalized, and one with positions still open before the escape hatch (with the time it opens).

Finalizing through the escape hatch with positions open is allowed, but a bankrupt position settled afterwards can lower the ratios for everyone still in, and insurance redemptions wait until those positions are settled. Settle first when you can.

Withdrawing After Termination

const ent = await bz.termination.getTerminalEntitlement({ pair: aapl.address, account: user! });
if (!ent.finalized) {
  console.log("Not finalized yet: the payout ratios are not frozen.");
} else if (ent.payout > 0n) {
  console.log(`principal ratio ${formatUnits18(ent.principalRatio * 100n, 2)}%, profit ratio ${formatUnits18(ent.profitRatio * 100n, 2)}%`);
  console.log(`withdrawing everything pays ${formatUnits18(ent.payout, 2)} USDC`);
  // withdrawCollateral takes collateral units; the largest amount it accepts is the preview's maximum.
  const w = await bz.previews.previewWithdrawal(aapl.address, user!);
  await simulateAndSend(await bz.trade.withdrawCollateral(aapl.address, user!, w.maxWithdrawable));
}
// Tips and rewards (settlement tips, a UMA proposer's reward) are claimed separately:
await simulateAndSend(await bz.trade.claimFees(aapl.address, user!));

getTerminalEntitlement({ pair, account }) reads what the market pays an account:

FieldMeaning
collateralThe account’s collateral.
registeredClaimIts profit claim (a winner settled before finalize).
principalRatio, profitRatioCash per unit of collateral and of claim (1e18 = 100%); 0 before finalize.
payoutcollateral × principal ratio + claim × profit ratio: what withdrawing everything pays now.
finalizedtrue once finalize has run.

Before finalize the ratios, and so the payout, read 0: show that as “not frozen yet”, not “worthless”. A position still open is not included until it is settled.

Withdraw with bz.trade.withdrawCollateral(pair, user, amount) as on a live market. The amount is in collateral units, the first withdrawal credits the registered claim into collateral, and the wallet receives the amount times the principal ratio; the call’s summary says what it pays, and a withdrawal that would pay nothing is refused. bz.previews.previewWithdrawal(pair, user) returns the largest amount accepted (maxWithdrawable) and the entitlement (terminal). Settlement tips and a UMA proposer’s reward are separate, in the fee balance (claimFees). Insurance LPs redeem through the insurance exit.

Events

termination.decodeTerminatorLog(log) decodes a terminator log (or returns null for another contract’s), and termination.TERMINATOR_EVENT_TEXT has a sentence for each event, for an activity feed. Every UMA claim ends in exactly one of TerminationProposalAccepted, TerminationScheduleRefused, TerminationProposalSuperseded or TerminationProposalRejected. For indexed history (terminationProposals, insurerProposals, insurerVotes, insurerLevies, pairTerminations, terminalRatioUpdates) see History. Live Updates reads the terminator’s logs alongside each watched market’s.

Constants

ConstantValue
TERMINATION_PROPOSAL_BOND_USDC1,000 USDC (the floor; the bond posted is requiredTerminationBond())
TERMINATION_PROPOSAL_LIVENESS12 hours
MAX_REASON_LENGTH1,000
MIN_CUTOFF_LEAD7 s (a late acceptance’s cutoff)
INSURER_TERMINATION_MIN_LEAD2 hours
INSURER_TERMINATION_LATCH_LEAD1 hour
INSURER_TERMINATION_MAX_LEAD14 days
INSURER_TERMINATION_THRESHOLD_BP6,000 (60%)
INSURER_TERMINATION_BOND_USDC1,000 USDC
INSURER_SHARE_MATURITY_PERIOD4 days
INSURER_MAX_LIVE_VOTES8
INSURER_LEVY_BP, INSURER_LEVY_FLOOR_BP5,000 and 1,000
CLAIM_VERDICT_GRACE14 days
INSURER_POST_FIX_LOCK13 days
SCHEDULED_TERMINATION_GRACE12 hours
NEVER_PRICED_SETTLEMENT_PRICE1, the smallest price in 18 decimals (a market that never stored a price)

These are on the termination namespace; LIMBO_LEAD, ORACLE_DEAD_THRESHOLD, TERMINAL_SETTLEMENT_ESCAPE, SETTLEMENT_SAMPLE_COUNT and SETTLEMENT_SAMPLE_SPACING are exported at the top level. See Units & Constants.

What Can Go Wrong

Builder refusals are InputErrors naming the argument; errorMessage(e) gives the sentence. A BondQuoteError, a SettlementPrintError, a SubgraphError or a ConnectError carries its own message: show that (errorMessage does not recognize them). Reverts a simulation can show:

ErrorMeaning
BazaarPairTerminator__LastTradingTsTooSoon, InsurerCutoffTooSoon, InsurerCutoffTooLateThe cutoff is outside its bounds; the time moved on while the user decided. Rebuild.
BazaarPairTerminator__CutoffNotEarlier, InsurerCutoffNotEarlierAn earlier or equal cutoff is already in force.
BazaarPairTerminator__UmaIdentifierNotLiveUMA claims are closed until the identifier upgrade.
BazaarPairTerminator__InsurerNoMatureShares, InsurerInsufficientUnlockedSharesNot enough shares held 4 days before the window opened.
BazaarPairTerminator__InsurerThresholdNotMetThe vote is below 60% and still open.
BazaarPairTerminator__InsurerTooManyLiveVotesThe wallet backs 8 live votes on this market.
BazaarPairTerminator__InsurerVotingClosed, InsurerVoteNotOpenOutside the vote’s window.
BazaarPairTerminator__InsurerVerdictPendingThe claim behind the vote has no verdict yet.
BazaarPairTerminator__OracleNotStaleEnoughThe market stored a price within 21 days.
BazaarPairTerminator__TerminationTimeNotReachedThe cutoff has not passed.
BazaarPairTerminator__InsufficientPythFeeNot enough ETH for the updates’ fee. Rebuild.
BazaarOracle__SampleCountMismatchThe chain set does not hold one update per leg for each of five samples.