-
Notifications
You must be signed in to change notification settings - Fork 3
Description
Summary
Add multi-outcome event markets (NegRisk) to TRUF Network's prediction market system. Multiple binary markets are linked to a single event where exactly one outcome is true. A convert/merge operation enables capital efficiency - users don't need to lock collateral per-market when positions net out across the event.
This builds on top of the existing orderbook (migrations 030-042) with minimal changes to existing code. Bracket-style markets and split/merge operations already exist - NegRisk adds the event linkage layer and a cross-market convert operation.
Motivation
Economic indicator markets are naturally multi-outcome. Examples on TRUF data:
| Event | Outcomes |
|---|---|
| "CPI YoY range for March 2026" | <2.0%, 2.0-2.5%, 2.5-3.0%, 3.0-3.5%, >3.5% |
| "Next Fed rate decision" | Cut 50bp, Cut 25bp, Hold, Hike 25bp |
| "Unemployment rate bracket Q2" | <3.5%, 3.5-4.0%, 4.0-4.5%, >4.5% |
| "Which month does CPI peak?" | Jan, Feb, Mar, Apr, May, Jun |
Without NegRisk, each outcome is an independent market. A market maker providing liquidity on 5 outcomes needs 5x the collateral. With NegRisk, they need 1x - the same $1 backs all outcomes since exactly one wins.
Capital efficiency comparison (5-outcome event, $10K per market):
| Model | Collateral needed | LP capital efficiency |
|---|---|---|
| Independent markets | $50,000 | 1x |
| NegRisk linked | $10,000 | 5x |
Current Architecture (unchanged)
For reference, the existing schema this proposal extends:
ob_queries - Market definitions (hash, settle_time, bridge, etc.)
ob_positions - Unified orderbook + holdings (price encoding: <0=buy, 0=hold, >0=sell)
ob_participants - Wallet ID mapping
ob_rewards - LP reward snapshots
Key existing operations:
place_split_limit_order- Mints YES+NO pair for $1, holds YES, sells NOmatch_mint- Two complementary buys (YES@P1 + NO@P2, P1+P2=100) mint new pairsmatch_burn- Two complementary sells destroy pairs, unlock collateral- Collateral:
amount * price * 10^16wei for buys,amount * 10^18for splits
None of these change. NegRisk adds a layer on top.
Proposed Schema Changes
New Table: ob_events
Links multiple ob_queries markets to a single event.
CREATE TABLE IF NOT EXISTS ob_events (
id INT PRIMARY KEY,
hash BYTEA NOT NULL UNIQUE, -- Deterministic hash of event params
name TEXT NOT NULL, -- Human-readable description
num_outcomes INT NOT NULL, -- Number of linked markets
settled BOOLEAN DEFAULT false NOT NULL,
winning_query_id INT, -- Which market's TRUE won
settle_time INT8 NOT NULL,
created_at INT8 NOT NULL,
creator BYTEA NOT NULL,
bridge TEXT NOT NULL,
CONSTRAINT chk_ob_events_outcomes CHECK (num_outcomes >= 2 AND num_outcomes <= 20),
FOREIGN KEY (winning_query_id) REFERENCES ob_queries(id)
);
CREATE INDEX IF NOT EXISTS idx_ob_events_settled ON ob_events(settled);
CREATE INDEX IF NOT EXISTS idx_ob_events_settle_time ON ob_events(settle_time);Modified Table: ob_queries
Add nullable FK to link markets to events. NULL = standalone (backwards compatible).
ALTER TABLE ob_queries ADD COLUMN event_id INT REFERENCES ob_events(id);
ALTER TABLE ob_queries ADD COLUMN outcome_index INT; -- 0-based position in event
CREATE INDEX IF NOT EXISTS idx_ob_queries_event ON ob_queries(event_id);
-- Constraint: if event_id is set, outcome_index must be set
ALTER TABLE ob_queries ADD CONSTRAINT chk_event_outcome
CHECK ((event_id IS NULL AND outcome_index IS NULL) OR
(event_id IS NOT NULL AND outcome_index IS NOT NULL AND outcome_index >= 0));New Table: ob_event_collateral
Tracks the number of "complete sets" minted per participant for an event. A complete set = 1 TRUE share of every outcome.
CREATE TABLE IF NOT EXISTS ob_event_collateral (
event_id INT NOT NULL,
participant_id INT NOT NULL,
complete_sets INT8 NOT NULL DEFAULT 0, -- Number of full sets held
PRIMARY KEY (event_id, participant_id),
FOREIGN KEY (event_id) REFERENCES ob_events(id) ON DELETE CASCADE,
FOREIGN KEY (participant_id) REFERENCES ob_participants(id),
CONSTRAINT chk_complete_sets CHECK (complete_sets >= 0)
);New Actions
create_linked_event
Creates an event with N binary markets atomically. Each market shares the same bridge and settle_time.
CREATE OR REPLACE ACTION create_linked_event(
$bridge TEXT,
$name TEXT,
$outcome_names TEXT[], -- ["<2.0%", "2.0-2.5%", "2.5-3.0%", ">3.0%"]
$settle_time INT8,
$max_spread INT,
$min_order_size INT8
) PUBLIC RETURNS (event_id INT)Logic:
- Validate bridge, params,
ARRAY_LENGTH($outcome_names) >= 2 - Create
ob_eventsrow - For each outcome name, create an
ob_queriesrow withevent_idset and uniquequery_componentsderived from event hash + outcome index - Charge creation fee (2 TRUF per market, or 2 TRUF total - TBD)
- Return event_id
split_event (Cash -> Complete Set)
Deposit $1 per set, receive 1 TRUE share of every outcome in the event.
CREATE OR REPLACE ACTION split_event(
$event_id INT,
$amount INT8 -- Number of complete sets to mint
) PUBLICLogic:
- Validate event exists, not settled
- Lock collateral:
$amount * 10^18wei (= $1.00 per set) - For each market in event: INSERT holding (price=0, outcome=TRUE, amount=$amount)
- Update
ob_event_collateral.complete_sets += $amount - Maintain binary parity: also mint FALSE shares (see Parity section below)
merge_event (Complete Set -> Cash)
Burn 1 TRUE share of every outcome, receive $1 per set.
CREATE OR REPLACE ACTION merge_event(
$event_id INT,
$amount INT8 -- Number of complete sets to burn
) PUBLICLogic:
- Validate event exists, not settled
- Verify participant holds >= $amount TRUE shares (price=0) in EVERY market of the event
- For each market: DELETE or reduce TRUE holding by $amount
- Unlock collateral:
$amount * 10^18wei - Update
ob_event_collateral.complete_sets -= $amount - Also burn corresponding FALSE shares
settle_event
Settle all linked markets at once. Exactly one outcome wins.
CREATE OR REPLACE ACTION settle_event(
$event_id INT
) PUBLICLogic:
- Validate event not settled, past settle_time
- For each market in event: check attestation result
- Exactly one market's attestation must return TRUE - error if 0 or >1
- Mark winning market, mark event as settled
- Call
process_settlementon each market:- Winning market: TRUE holders get 98 cents (2% fee), FALSE holders lose
- Losing markets: FALSE holders get 98 cents, TRUE holders lose
- Distribute collected fees to LPs (per-event pool)
Binary Parity in NegRisk
The existing system enforces total_true_shares == total_false_shares per market. NegRisk must maintain this.
Split creates both TRUE and FALSE shares:
When splitting, we mint TRUE shares the user holds, plus FALSE shares that are effectively "the other side." For NegRisk:
- Split 1 set of 4-outcome event:
- Market 1: +1 TRUE (user holds), +1 FALSE (to event collateral pool)
- Market 2: +1 TRUE (user holds), +1 FALSE (to event collateral pool)
- Market 3: +1 TRUE (user holds), +1 FALSE (to event collateral pool)
- Market 4: +1 TRUE (user holds), +1 FALSE (to event collateral pool)
The FALSE shares back the system's obligation: when the event settles, the losing TRUE shares are worthless, but the FALSE shares of losing markets pay out. The math works because exactly one TRUE wins ($0.98) and N-1 FALSE shares win ($0.98 each), but the total payout = 1 complete set = $1 minus the 2% fee.
Alternative (simpler): Don't create FALSE shares at split. Instead, modify validate_market_collateral to account for complete sets. Complete sets are self-collateralizing since exactly one wins. This avoids phantom FALSE shares cluttering the orderbook.
Recommendation: The simpler approach. Complete sets tracked in ob_event_collateral provide the collateral backing. Individual market binary parity only needs to hold for independently-traded shares (from place_split_limit_order on individual markets).
Validation Changes
validate_event_collateral (new)
CREATE OR REPLACE ACTION validate_event_collateral($event_id INT)
PUBLIC VIEW RETURNS (
valid BOOL,
num_markets INT,
total_complete_sets INT8,
vault_balance NUMERIC(78, 0),
expected_collateral NUMERIC(78, 0)
)Logic:
- Expected collateral = (complete_sets * 10^18) + SUM(individual market collateral for non-event positions)
- Vault balance from bridge.info()
- Must hold across all unsettled events sharing the same bridge
Modify validate_market_collateral
Add awareness of event-linked markets. When calculating expected collateral, exclude complete sets (they're covered by ob_event_collateral).
LP Rewards for NegRisk
Two options - needs team input:
Option A: Per-market (current behavior, no change)
Each market in the event has its own LP reward scoring. LPs get rewards for providing liquidity on individual markets. Simple, no code changes to sample_lp_rewards.
Pro: No changes needed.
Con: LPs must provide liquidity on each market separately. Doesn't reward event-level market making.
Option B: Per-event (new)
LP rewards scored at the event level. If you provide liquidity across all outcomes of an event, you get a bonus multiplier.
Pro: Incentivizes full event coverage.
Con: More complex scoring, new code in sample_lp_rewards.
Attestation for NegRisk Events
Current binary attestation actions (040) support:
price_above_threshold- "CPI > 2.5%"price_below_threshold- "CPI < 2.5%"value_in_range- "CPI between 2.5% and 3.0%"value_equals- "CPI = 2.5% +/- 0.1%"
For NegRisk bracket events, each outcome uses value_in_range with non-overlapping ranges:
| Outcome | Attestation |
|---|---|
| CPI <2.0% | price_below_threshold(stream, 2.0) |
| CPI 2.0-2.5% | value_in_range(stream, 2.0, 2.5) |
| CPI 2.5-3.0% | value_in_range(stream, 2.5, 3.0) |
| CPI >3.0% | price_above_threshold(stream, 3.0) |
Validation: settle_event must verify exactly one attestation returns TRUE. If the ranges are properly non-overlapping, this is guaranteed by the data. But edge cases (value exactly on boundary) need defined behavior - suggest exclusive lower bound, inclusive upper: [min, max).
New attestation action needed: None. Existing actions cover all bracket patterns. The event settlement logic just iterates each market's attestation.
Migration Plan (incremental PRs)
| PR | Migration | What | Risk |
|---|---|---|---|
| 1 | 043 | ob_events table + event_id/outcome_index on ob_queries |
Low - additive schema |
| 2 | 044 | ob_event_collateral table + create_linked_event action |
Low - new action |
| 3 | 045 | split_event + merge_event actions |
Medium - collateral logic |
| 4 | 046 | settle_event action |
Medium - settlement changes |
| 5 | 047 | validate_event_collateral + modify existing validation |
Medium - consensus-critical |
| 6 | - | Go extension: add event settlement to tn_settlement |
High - extension change |
Backwards compatible: All changes are additive. Standalone markets (event_id=NULL) work exactly as before. No existing actions are modified in PRs 1-4.
Dependency chain: 1 → 2 → 3 → 4 → 5 → 6. Each PR is independently mergeable and functional up to that point.
PR 1 - Schema Foundation
- Add
ob_eventstable (id, hash, name, num_outcomes, settled, settle_time, etc.) - Add
event_id+outcome_indexnullable columns toob_queries - Add indexes and constraints
- Pure DDL - no logic, no actions. Nothing references these new columns yet, so nothing can break. Existing standalone markets are completely unaffected (event_id defaults to NULL).
- Testing: Run migration, verify all existing markets still query, trade, and settle normally. Verify new columns exist with correct types and constraints.
PR 2 - Event Creation
- Add
ob_event_collateraltable - Add
create_linked_eventaction - takes a bridge, name, array of outcome names, and atomically creates oneob_eventsrow + Nob_queriesrows linked to it - Each child market gets
event_idset and a uniqueoutcome_index(0-based) - Testing: Call
create_linked_eventwith 4 outcomes, verify 1 event + 4 markets created with correctevent_idandoutcome_index. Verify standalonecreate_querystill works independently. Verify constraint rejectsnum_outcomes < 2and> 20.
PR 3 - Split + Merge (Collateral Operations)
split_event- lock $1 per set, mint 1 TRUE share in every market of the event, track complete sets inob_event_collateralmerge_event- burn 1 TRUE share from every market, unlock $1 per set, decrement complete sets- This is where collateral logic gets real. Must handle: insufficient holdings for merge, settled events (reject), partial sets (reject - must hold TRUE in ALL markets)
- Testing: Split 100 sets → verify 100 TRUE shares in each market + collateral locked. Merge 50 back → verify 50 remain + collateral partially unlocked. Attempt merge of more than held → must fail. Attempt split on settled event → must fail. Verify standalone
place_split_limit_orderstill works independently on non-event markets.
PR 4 - Event Settlement
settle_event- iterate each market's attestation, verify exactly one returns TRUE, pay winners, distribute fees- Winning market: TRUE holders get $0.98 (2% fee), FALSE holders lose
- Losing markets: TRUE holders lose, FALSE holders get $0.98
- Refund all open orders on all markets in the event
- Testing: Create event, split sets, trade shares between participants, trigger attestation, settle. Verify payouts are correct for winners and losers. Test edge cases: 0 attestations return TRUE (must error), 2+ attestations return TRUE (must error). Verify standalone market settlement via
process_settlementis unchanged.
PR 5 - Validation Update
- New
validate_event_collateralaction - verifies bridge holds enough for all complete sets + individual positions - Modify existing
validate_market_collateralto skip complete set collateral for event-linked markets (those are covered by event-level validation). This is a conditional branch: ifevent_id IS NULL, existing logic runs unchanged. - Consensus-critical - if validation is wrong, the network rejects blocks
- Testing: Extensive. Validate after splits, merges, trades, partial merges, settlements. Verify standalone market validation is byte-for-byte identical to pre-change behavior. Run against existing test suite to confirm no regressions.
PR 6 - Go Extension (Co-authored with TRUF Team)
- Modify the
tn_settlementGo extension to recognize events and broadcastsettle_eventinstead ofsettle_marketfor event-linked markets (the extension currently broadcastssettle_market, which internally callsprocess_settlement) - This extension runs automatically every ~5 minutes in the Kwil settlement loop
- Highest risk - touches the Go extensions that run inside the Kwil node itself. Should be co-authored or at minimum reviewed closely by the TRUF core team.
- Testing: Deploy to testnet, create event markets, let settlement extension trigger automatically. Verify correct settlement behavior. Verify standalone markets still settle on their normal schedule.
Open Questions for TRUF Team
- Creation fee: 2 TRUF per market in the event, or flat 2 TRUF per event?
- Max outcomes: Proposed cap of 20 per event - is this reasonable?
- LP rewards: Per-market (Option A) or per-event (Option B) to start?
- Boundary behavior (critical for NegRisk correctness): Current
value_in_rangeuses[min, max](inclusive both ends). For adjacent brackets (e.g., 2.0-2.5% and 2.5-3.0%), a value landing exactly on 2.5% would match BOTH, violating the "exactly one outcome" invariant. Should boundaries be[min, max)or(min, max]? - Indexer: Does the prediction market indexer need event-level endpoints? e.g.,
GET /v0/prediction-market/events/{id} - Convert naming: Polymarket calls these "split" and "merge." TRUF already has
place_split_limit_order. Should we use different terminology to avoid confusion? Suggestions:split_event/merge_eventormint_set/burn_set.
Example: CPI Bracket Market
Event: "CPI YoY for March 2026"
Bridge: hoodi_tt2
Outcomes: 4
Market 1 (query_id=50): CPI < 2.0% → price_below_threshold(cpi_stream, 2.0)
Market 2 (query_id=51): CPI 2.0-2.5% → value_in_range(cpi_stream, 2.0, 2.5)
Market 3 (query_id=52): CPI 2.5-3.0% → value_in_range(cpi_stream, 2.5, 3.0)
Market 4 (query_id=53): CPI > 3.0% → price_above_threshold(cpi_stream, 3.0)
User flow:
- Alice calls
split_event(event_id=1, amount=100)→ locks $100, gets 100 TRUE shares of each market - Alice sells TRUE shares on markets 1, 3, 4 (she thinks CPI will be 2.0-2.5%)
- Alice keeps 100 TRUE shares on market 2
- Bob buys Alice's sells on markets 1, 3, 4
- March CPI comes in at 2.3% → attestation for market 2 returns TRUE
settle_event(1)pays market 2 TRUE holders $0.98/share, refunds open orders, distributes 2% fees to LPs
Alice P&L: Sold shares on 3 markets (revenue) + won market 2 ($0.98 * 100) - initial $100 cost
Capital used: $100 (not $400 as independent markets would require)