SettleKit docs

Settlement spec

The deterministic resolution contract — precise enough to audit.

Settlement is deterministic (a pure function of on-chain accounts and a supplied proof), permissionless (no privileged signer), and immutable once written.

What "settled" means

A market is settled when a MatchFacts or Verdict account exists that a Merkle proof of the relevant stat produced. No account, no settlement. There is no code path — for any signer, including the deployer — that sets an outcome without a proof.

The proof chain

TxODDS accumulates score events into 5-minute batches and publishes a Merkle root of each batch to its txoracle program on Solana. A single stat connects to that root through three levels:

ScoreStat ──statProof──▶ eventStatRoot ──subTreeProof──▶ fixture events root ──mainTreeProof──▶ batch root (on-chain)
 (leaf)                  (per event)                     (per fixture)                          (per 5-min batch)

Verified hashing scheme

Reproduced byte-for-byte from a real full-time proof (locked by reference tests):

  • Leaf: sha256(borsh(ScoreStat)) where borsh(ScoreStat) = key:u32-LE ‖ value:i32-LE ‖ period:i32-LE (12 bytes). An already-computed sub-tree root is fed in raw (not re-hashed).
  • Node: at each proof step, is_right_sibling ? sha256(node ‖ sibling) : sha256(sibling ‖ node).

The final level (batch root inside the roots account) is delegated to the oracle CPI; SettleKit independently verifies the two documented levels (dual attestation).

The canonical Question

Every verdict answers a canonical Question:

Question {
  version, fixture_id,
  stat_keys: [u32],        // soccer: 1/2 goals, 3/4 yellows, 5/6 reds, 7/8 corners
  predicates: [
    Single { stat_index, threshold, cmp: Gt|Lt|Eq } |
    Binary { index_a, index_b, op: Add|Subtract, threshold, cmp }   // e.g. corners A+B > 10
  ],
  phase_req,               // required ScoreStat.period; 5 = FT, 10 = after ET, 13 = after pens
  batch_bound: None | AtOrBefore(ts) | AtOrAfter(ts),
}

question_hash = sha256(borsh(Question)) keys the Verdict PDA, and the oracle strategy is always derived on-chain from the question. A verdict account therefore provably corresponds to exactly the predicate that was verified.

The gate sequence

post_verdict / post_match_facts execute these in order; each failure has a dedicated error code:

#GateRuleError
1G-HASHquestion_hash == sha256(borsh(question))QuestionHashMismatch
2shapeversion supported; 1–8 stat keys; predicates non-empty; indices in rangeQuestionShapeInvalid
3G-FIXTUREpayload fixture + stat keys match the questionFixtureMismatch / StatKeyMismatch
4G-PHASEif phase_req != ANY, every proven ScoreStat.period == phase_reqPhaseGateFailed
5G-TIMEpayload.ts == summary.min_timestamp; batch_bound satisfiedBatchTsMismatch / BatchTooLate / BatchTooEarly
6G-MONOtime-bounded questions restricted to monotone stats (keys 1–8)StatNotMonotone
7G-ROOTroots account = canonical txoracle PDA for the epoch day; owner = txoracleOracleRootAccountInvalid
8CPIinvoke(txoracle::validate_stat_v2) with the derived strategy—
9G-RETURNreturn data present, from txoracle, 1 byte ≤ 1OracleReturn…

outcome = (return byte == 1). For post_verdict, false is stored as data (a failure-to-prove, not a proven negation). For post_match_facts, a FALSE proof aborts — MatchFacts is only ever written from a TRUE proof at a final phase.

Why G-PHASE is load-bearing. Verdict PDAs are init-once. Without the phase gate an attacker could post a valid-but-premature FALSE verdict at minute 10 and permanently squat the PDA the true full-time score needs. Because ScoreStat.period lives inside the Merkle-authenticated leaf, a mid-match proof carries period 4 and fails the gate.

Dual attestation

attest_native upgrades a CPI-verified verdict to DUAL. When a verdict is posted, the same transaction persists a BatchAnchor PDA holding the events_sub_tree_root the oracle CPI just validated (written only on a TRUE validation). attest_native then independently recomputes — with its own sha256 syscalls — the fixture events sub-tree root straight from the raw stat leaf, and requires it to equal that anchor. No caller-supplied reference value appears anywhere in the instruction: two independent verifiers meet at an on-chain, oracle-authenticated commitment. Failure-to-prove verdicts leave their anchor rootless and can never be attested.

Market resolution (pure functions of the proof)

WinnerFT:        home>away → Home | home<away → Away | level at FT → Draw
                 level after ET/pens → VOID (goal facts cannot name a pens winner)
TotalGoalsOU:    (home+away)*10 > param → Over | else Under   (half-lines ⇒ no push)
BTTS:            home≥1 && away≥1 → Yes | else No
DrawNoBet:       home/away win → that side | draw → VOID (refund)
CorrectScore:    exact scoreline bucket | "Any other"
GoalsBands:      0–1 / 2–3 / 4+     OddEven: (home+away) parity
SumThreshold:    a TRUE Binary Verdict "statA + statB > K at FT" → Yes | "< K+1" → No
ThresholdBefore: in-play — YES proven AtOrBefore(T), NO proven AtOrAfter(T)

Payouts, solvency, and the no-oracle backstop

  • Claim is permissionless (any cranker) but funds only move to the position owner's token account. Winning shares redeem at 1 USDC; voided markets unwind at seed prices.
  • Solvency is structural. LMSR seeds are normalized so min(q0) = 0; the required subsidy ceil(b·ln(1/p_min)) is prefunded at creation, so the vault always covers the worst-case payout. Property-tested over 2,048 random trade sequences.
  • No-oracle backstop. If no proof ever arrives, anyone can void the market 48h after kickoff and holders refund at seed prices. The refund path touches no oracle.