Architecture
Four core contracts, pluggable resolver modules, no proxies, no admin.
Four core contracts. Pluggable resolver modules. No proxies. No upgradeable storage. No admin. Deploy once.
External
┌─────────┐ ┌──────────────┐ ┌──────────────┐
│ USDC │ │ Gnosis CTF │ │ UMA OOv3 │
└──┬──┬───┘ └──┬──┬────┬───┘ └──────┬───────┘
│ │ │ │ │ │
│ │ Core │ │ │ Modules│
┌──▼──┼────┐ ┌────▼──▼──┐ │ ┌──────▼───────┐
│Treasury │ │OrderBook │ │ │ UMAResolver │
└──────────┘ └────┬─────┘ │ ├──────────────┤
│ │ │KalshiResolver│
┌─────▼───────▼┐ ├──────────────┤
│ MarketFactory│ │PolymarketResolver│
└──────┬───────┘ ├──────────────┤
│ │ Anyone's │
┌──────▼───────┐ └──────────────┘
│NegRiskAdapter│
└──────────────┘
OrderBook ←→ USDC (collateral transfers, fee payments)
OrderBook ←→ CTF (splitPosition, token transfers)
Factory ←→ CTF (prepareCondition, reportPayouts)
Resolvers → Factory (resolveMarket, notify callbacks)
Resolvers ←→ UMA/Polymarket/etc (oracle-specific logic)Core contracts
Immutable. Deployed once. No admin functions. No governance.
MarketFactory
The registry and resolution authority. Every market is created and tracked here. The factory is the CTF oracle - it calls CTF.reportPayouts() directly.
createMarket(question, outcomeSlotCount, deadline, resolver)- creates a market with a per-market resolver. Calls CTF to prepare the condition. No whitelist - any address can be a resolver.resolveMarket(conditionId, payouts)- called by the market's resolver to report payouts. Only that market's resolver can call this. Factory forwards payouts to CTF.notifyResolutionInitiated(conditionId)- resolver calls this when an assertion/resolution attempt begins. IncrementspendingResolutionCount.notifyResolutionFinalized(conditionId)- resolver calls this when an assertion settles or is disputed. DecrementspendingResolutionCount.forceVoid(conditionId)- 90-day timeout after deadline, only ifpendingResolutionCount == 0. 180-day emergency override ignores pending count. Reports equal payouts (pro-rata refund).isTrading(conditionId)- true if market exists and not yet resolved.
conditionId: Deterministic. CTF.getConditionId(factory, questionId, outcomeSlotCount). The factory address is baked in as the oracle - all markets share the same oracle address regardless of resolver.
Security model: Trust is per-market, not per-protocol. A bad resolver can only affect markets created with it. Core contracts are unaffected by resolver choice. Frontends decide which resolvers to trust and display.
OrderBook
The matching engine. Holds USDC and outcome tokens in escrow until orders are filled or cancelled. Implements ERC-1155 receiver (required by CTF).
Order types:
placeBuyOrder- deposit USDC, want outcome tokensplaceSellOrder- deposit outcome tokens (via CTF approval), want USDC
Fill types:
fillBuyVsBuy(buyId0, buyId1, fillAmount)- two BUY orders on opposite outcomes. Their combined USDC splits into outcome tokens via CTF. Each side gets their tokens.fillBuyVsSell(buyId, sellId, fillAmount)- a BUY and SELL on the same outcome. Buyer's USDC goes to seller (minus fee). Seller's tokens go to buyer.
Matching is permissionless. Anyone calls fill functions. Frontends and keepers discover matching orders off-chain, execute on-chain. No privileged operator.
Fees: 0.1% on every fill, sent to Treasury. Rounds up (ceil division) - no zero-fee trades.
Cancellation: Makers cancel their own orders anytime. After resolution, anyone can cancel stale orders to free locked collateral.
Treasury
Receives fees. That's it.
FEE_BPS = 10(0.1%)RECIPIENTis immutable - set at deploy, never changeswithdraw(token)- anyone can call, sends full balance to RECIPIENTcalculateFee(amount)- pure function, rounds up
NegRiskAdapter
Converts multi-outcome questions into binary markets.
"Who wins the election?" with candidates [A, B, C] becomes:
- "Who wins the election? | A" - YES/NO
- "Who wins the election? | B" - YES/NO
- "Who wins the election? | C" - YES/NO
createMultiOutcomeMarket(question, labels, deadline, resolver) - creates N binary markets via the factory, all sharing the same resolver.
Each binary market trades independently on the OrderBook. Resolution is per-leg - each leg is resolved through the resolver like any other binary market. No resolveMultiOutcome function on the adapter.
isResolved(marketId) - returns true when all legs have been resolved.
Resolver modules
Pluggable. Anyone deploys. Core doesn't care. A resolver is any contract that calls factory.resolveMarket(conditionId, payouts).
BaseResolver (abstract base)
Abstract contract providing the boilerplate every resolver needs. Custom resolvers inherit this instead of building from scratch.
FACTORYimmutable - set in constructor_requireResolver(conditionId)- reverts if not the market's resolver_requireResolvable(conditionId)- reverts if before deadline or already resolved_resolveOutcome(conditionId, winnerIndex)- builds payouts and resolves.type(uint256).max= void._notifyInitiated(conditionId)/_notifyFinalized(conditionId)- pending resolution tracking for async resolvers
UMAResolver, KalshiResolver, and PolymarketResolver were built before BaseResolver existed and use their own implementations of these patterns. New custom resolvers should inherit BaseResolver.
UMAResolver (reference implementation)
Connects markets to UMA's Optimistic Oracle V3 using ASSERT_TRUTH2 (UMIP-191).
Flow:
assertOutcome(conditionId, outcome)- asserter posts bond (750 USDC floor or UMA's minimum, whichever is higher). Callsfactory.notifyResolutionInitiated(). UMA starts a 2-hour liveness window.- If no dispute:
settleAssertion()→ UMA callsassertionResolvedCallback()→ callsfactory.resolveMarket()with payouts → callsfactory.notifyResolutionFinalized(). - If disputed: UMA calls
assertionDisputedCallback()→ active assertion cleared → someone can re-assert. (pendingResolutionCountstays incremented during the DVM dispute, preventing premature voiding. It decrements when the DVM resolves viaassertionResolvedCallback.)
assertOutcomeFor(conditionId, outcome, asserter) - lets a third party submit on behalf of another address.
One active assertion per market. Prevents spam. Disputed assertions free the slot for a new one.
VOID: Outcome type(uint256).max triggers equal payouts [1, 1] - pro-rata refund for all holders.
KalshiResolver
Links PredictionLabs markets to Kalshi event tickers via UMA oracle.
linkMarket(conditionId, ticker)- associates a market with a Kalshi tickerassertOutcome(conditionId, outcome)- submits a UMA assertion with a ticker-aware claim (e.g., "Kalshi ticker XYZ resolved YES")- Same liveness/dispute/callback flow as UMAResolver
PolymarketResolver
Links PredictionLabs markets to already-resolved Polymarket conditions.
linkMarket(conditionId, polymarketConditionId)- associates a market with a Polymarket conditionresolve(conditionId)- reads payouts from the shared Gnosis CTF (payoutNumerators/payoutDenominator), forwards them tofactory.resolveMarket()- No assertion, no liveness - instant resolution from an existing source of truth
Deployment order
1. Treasury(recipient)
2. MarketFactory(ctf)
3. OrderBook(ctf, factory, treasury, usdc)
4. NegRiskAdapter(factory)
5. UMAResolver(factory, oov3, usdc) - independent, no circular dependency
6. KalshiResolver(factory, oov3, usdc) - independent
7. PolymarketResolver(factory, ctf) - independentNo circular dependencies. Resolvers are deployed independently and reference the factory. The factory doesn't need to know about resolvers - markets point to them at creation time.
External dependencies
| Dependency | Address (Polygon) | Role |
|---|---|---|
| Gnosis CTF | 0x4D97DCd97eC945f40cF65F87097ACe5EA0476045 | ERC-1155 outcome tokens, position splitting, payout redemption |
| UMA OOv3 | 0x5953f2538F613E05bAED8A5AeFa8e6622467AD3D | Optimistic oracle, assertion/dispute/settlement |
| USDC | 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359 | Settlement currency, bond token |