PredictionLabs

Resolution

How markets are resolved through UMA, Kalshi, Polymarket, and custom resolvers.

Each market has a resolver chosen at creation time. The resolver determines how the market gets resolved. The factory doesn't know or care which resolver a market uses - it just needs the resolver to call factory.resolveMarket(conditionId, payouts) when it's time.

Resolver flows

UMAResolver (reference implementation)

Uses UMA's Optimistic Oracle. Anyone can assert an outcome, anyone can dispute it.

1. Deadline passes → market is resolvable
2. Asserter calls assertOutcome(conditionId, outcome) → posts bond (750 USDC minimum)
3. 2-hour liveness window starts
4. No dispute? → settleAssertion() → resolver calls factory.resolveMarket() → winners can redeem
5. Disputed? → UMA voters decide → loser forfeits bond
// Approve UMAResolver for bond (check umaResolver.bondAmount() for current minimum)
usdc.approve(address(umaResolver), umaResolver.bondAmount());

// Assert that YES (outcome 0) won
umaResolver.assertOutcome(conditionId, 0);

// After 2hr liveness, anyone can settle
umaResolver.settleAssertion(assertionId);

Bond: 750 USDC minimum (or UMA's current minimum, whichever is higher). Refunded if the assertion is correct. Forfeited if disputed and wrong.

One assertion at a time. If an assertion is already active, you must wait for it to settle or be disputed before submitting a new one.

KalshiResolver

Links markets to Kalshi event tickers. Resolves from Kalshi outcomes.

1. linkMarket(conditionId, ticker) → links on-chain market to Kalshi ticker
2. assertOutcome(conditionId, outcome) → posts bond, starts liveness
3. settle → resolver calls factory.resolveMarket()

PolymarketResolver

Links markets to Polymarket conditions. Resolves by reading Polymarket payouts.

1. linkMarket(conditionId, polyConditionId) → links on-chain market to Polymarket condition
2. resolve(conditionId) → reads Polymarket payouts → calls factory.resolveMarket()

Custom Resolvers (BaseResolver)

BaseResolver is an abstract contract that handles the boilerplate every resolver needs. Inherit it and implement your resolution logic.

import {BaseResolver} from "./BaseResolver.sol";

contract MyResolver is BaseResolver {
    constructor(address factory) BaseResolver(factory) {}

    function resolve(bytes32 conditionId, uint256 winner) external {
        _requireResolver(conditionId);
        _requireResolvable(conditionId);
        _resolveOutcome(conditionId, winner);
    }
}

Helpers provided:

FunctionPurpose
_requireResolver(conditionId)Reverts if this contract isn't the market's resolver
_requireResolvable(conditionId)Reverts if before deadline or already resolved
_resolveOutcome(conditionId, winnerIndex)Builds payouts and calls factory.resolveMarket(). Pass type(uint256).max to void. Does not check resolver identity or resolvability - call _requireResolver and _requireResolvable first.
_notifyInitiated(conditionId)Tells factory async resolution started (prevents premature voiding)
_notifyFinalized(conditionId)Tells factory async resolution completed

For resolvers that mirror external data (like PolymarketResolver), call FACTORY.resolveMarket(conditionId, payouts) directly with custom payout arrays.

Outcome values

ValueMeaningPayouts
0YES wins[1, 0]
1NO wins[0, 1]
type(uint256).maxVOID - market is invalid[1, 1] - equal split, everyone gets pro-rata refund

Resolution lifecycle

When a resolver initiates resolution, it calls factory.notifyResolutionInitiated(conditionId) to increment the pending resolution count. When resolution completes (or a dispute clears), it calls factory.notifyResolutionFinalized(conditionId) to decrement. This tracking prevents premature voiding.

The resolver finalizes by calling factory.resolveMarket(conditionId, payouts), which reports payouts to the CTF and marks the market as resolved.

Disputes (UMA)

If someone disagrees with an assertion, they dispute it through UMA directly. The dispute:

  1. Clears the active assertion slot (so a new assertion can be submitted)
  2. Escalates to UMA's decentralized voter network (DVM)
  3. DVM votes on the correct outcome
  4. Winner gets the loser's bond

During a dispute, the pendingResolutionCount stays incremented. This prevents premature voiding.

Redeeming winnings

After resolution, winners redeem through the CTF directly (not through our contracts).

The indexSet is a bitmask: 1 << outcomeIndex. For binary markets, YES = 1 (binary 01), NO = 2 (binary 10).

// Build the index set for your winning position
// YES = index 0 → indexSet = 1 << 0 = 1 (binary: 01)
// NO = index 1 → indexSet = 1 << 1 = 2 (binary: 10)
uint256[] memory indexSets = new uint256[](1);
indexSets[0] = 1;  // redeem YES tokens

ctf.redeemPositions(
    address(usdc),     // collateral token
    bytes32(0),        // parent collection (root)
    conditionId,       // which market
    indexSets          // which outcome tokens to redeem
);

Each winning token redeems for $1.00 USDC. VOID markets pay out equally - each token (YES and NO) redeems for $0.50.

Multi-outcome resolution

For NegRiskAdapter markets, each binary leg is resolved independently through its resolver. There is no resolveMultiOutcome function.

// Resolve each leg independently through the resolver
umaResolver.assertOutcome(trumpConditionId, 0);   // assert Trump YES
umaResolver.assertOutcome(harrisConditionId, 1);   // assert Harris NO
umaResolver.assertOutcome(desantisConditionId, 1); // assert DeSantis NO
// ... settle each after liveness

negRiskAdapter.isResolved(marketId) returns true when all legs are resolved.

Voiding markets

forceVoid lives on MarketFactory, not on any resolver.

Standard void

If a market is stuck unresolved 90 days past deadline with no pending resolutions, anyone can void it:

factory.forceVoid(conditionId);

Equal payouts [1, 1] - everyone gets their money back proportionally.

Emergency void

If a resolver is broken and callbacks never fire, the emergency path kicks in at 180 days past deadline:

// Works even with pending resolutions after 180 days
factory.forceVoid(conditionId);

This resets the pending resolution count and voids the market. Last resort - covers the scenario where the resolver is completely non-functional.

Timeline

Market created ──────────── Deadline ──── 2hr liveness ──── Resolved
                               │              │                │
                          Can assert     Can settle      Can redeem

                          + 90 days ──── Can void (if no resolutions pending)
                          + 180 days ─── Can void (even with resolutions pending)

On this page