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:
| Function | Purpose |
|---|---|
_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
| Value | Meaning | Payouts |
|---|---|---|
0 | YES wins | [1, 0] |
1 | NO wins | [0, 1] |
type(uint256).max | VOID - 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:
- Clears the active assertion slot (so a new assertion can be submitted)
- Escalates to UMA's decentralized voter network (DVM)
- DVM votes on the correct outcome
- 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 livenessnegRiskAdapter.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)