Oracle Design for Prediction Markets: Reliable Resolution Systems
Design robust oracle systems for prediction market resolution. Learn about multi-source verification, dispute mechanisms, and maintaining market integrity.
The oracle problem is particularly acute in prediction markets—blockchains cannot natively access off-chain information. When a market asks "Will it rain tomorrow?" or "Who will win the election?", the blockchain needs a trustworthy mechanism to bring that external data on-chain. Oracle design can make or break a prediction market protocol.
This comprehensive guide explores oracle architectures, resolution mechanisms, and dispute systems for building reliable prediction market infrastructure.
The Oracle Challenge in Prediction Markets
Why Oracles Matter
In prediction markets, oracles determine payouts. An incorrect resolution destroys user trust and protocol integrity. Unlike price oracles that update continuously, prediction market oracles resolve once—making accuracy paramount.
Key Requirements:
- Accuracy: Correct outcome determination
- Timeliness: Resolution when outcome is known
- Manipulation resistance: Difficult to corrupt
- Transparency: Verifiable resolution process
- Finality: Clear dispute resolution
Centralized Oracle Models
Single Trusted Resolver
Simplest approach—designated entity resolves markets:
contract CentralizedOracle {
address public resolver;
function resolveMarket(
uint256 marketId,
uint256 outcome
) external {
require(msg.sender == resolver, "Not resolver");
require(!markets[marketId].resolved, "Already resolved");
markets[marketId].resolved = true;
markets[marketId].outcome = outcome;
emit MarketResolved(marketId, outcome, msg.sender);
}
}
Pros:
- Fast resolution
- Low cost
- Simple implementation
Cons:
- Single point of failure
- Trust assumptions
- Censorship risk
- Collusion potential
When to Use:
- Prototype/testing
- Internal company markets
- Low-stakes predictions
Multi-Sig Resolver
Require multiple parties to agree:
contract MultiSigOracle {
address[] public resolvers;
uint256 public requiredConfirmations;
mapping(uint256 => mapping(uint256 => mapping(address => bool))) public confirmations;
mapping(uint256 => mapping(uint256 => uint256)) public confirmationCount;
function confirmResolution(
uint256 marketId,
uint256 outcome
) external {
require(isResolver[msg.sender], "Not resolver");
require(!confirmations[marketId][outcome][msg.sender], "Already confirmed");
confirmations[marketId][outcome][msg.sender] = true;
confirmationCount[marketId][outcome]++;
emit ConfirmationSubmitted(marketId, outcome, msg.sender);
// If enough confirmations, resolve
if (confirmationCount[marketId][outcome] >= requiredConfirmations) {
_resolveMarket(marketId, outcome);
}
}
}
Pros:
- Distributed trust
- Redundancy
- Harder to corrupt
Cons:
- Coordination overhead
- Potential for disagreement
- Slower resolution
When to Use:
- Moderate-stakes markets
- Trusted consortiums
- Proprietary data
Decentralized Oracle Networks
UMA Optimistic Oracle
UMA's optimistic oracle assumes proposals are correct unless disputed:
import "@uma/core/contracts/oracle/interfaces/OptimisticOracleV2Interface.sol";
contract UMAOracleIntegration {
OptimisticOracleV2Interface public oracle;
IERC20 public collateral;
bytes32 public constant IDENTIFIER = "YES_OR_NO_QUERY";
uint256 public constant REWARD = 0; // No reward for proposers
uint256 public constant BOND = 500e18; // 500 tokens to propose
uint256 public constant LIVENESS = 7200; // 2 hours dispute window
function requestResolution(
uint256 marketId,
string memory question,
uint256 resolutionTime
) external {
bytes memory ancillaryData = abi.encodePacked(question);
oracle.requestPrice(
IDENTIFIER,
resolutionTime,
ancillaryData,
collateral,
REWARD
);
oracle.setCustomLiveness(IDENTIFIER, resolutionTime, ancillaryData, LIVENESS);
oracle.setBond(IDENTIFIER, resolutionTime, ancillaryData, BOND);
}
function settle(uint256 marketId) external {
Market storage market = markets[marketId];
bytes memory ancillaryData = abi.encodePacked(market.question);
int256 price = oracle.settleAndGetPrice(
IDENTIFIER,
market.resolutionTime,
ancillaryData
);
// Price of 1 = YES, 0 = NO, 0.5 = 50/50 (ambiguous)
if (price == 1e18) {
market.outcome = 1; // YES
} else if (price == 0) {
market.outcome = 0; // NO
} else {
market.invalid = true; // Ambiguous
}
market.resolved = true;
}
}
Key Mechanism:
- Proposer posts bond and suggests outcome
- 2-hour dispute window opens
- If disputed, goes to UMA's Data Verification Mechanism (DVM)
- UMA token holders vote on outcome
- Winner receives bond, loser loses it
Pros:
- Economically secure
- Scalable to any question
- Human judgment for edge cases
Cons:
- 2-hour delay minimum
- Dispute resolution takes days
- Costs for proposers/disputers
Chainlink Oracle Integration
For markets with clear price feeds:
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
contract ChainlinkOraclePM {
AggregatorV3Interface public priceFeed;
struct PriceMarket {
string asset;
uint256 targetPrice;
uint256 expiration;
bool priceAbove; // Will price be above target?
}
function resolvePriceMarket(uint256 marketId) external {
PriceMarket storage market = priceMarkets[marketId];
require(block.timestamp >= market.expiration, "Not expired");
(, int256 price, , , ) = priceFeed.latestRoundData();
uint256 currentPrice = uint256(price);
bool outcome = market.priceAbove
? currentPrice >= market.targetPrice
: currentPrice <= market.targetPrice;
markets[marketId].resolved = true;
markets[marketId].outcome = outcome ? 1 : 0;
}
}
Reality.eth (formerly Reality Keys)
Crowd-sourced oracle with escalation:
interface IRealityETH {
function askQuestion(
uint256 template_id,
string memory question,
address arbitrator,
uint32 timeout,
uint32 opening_ts,
uint256 nonce
) external payable returns (bytes32);
function submitAnswer(
bytes32 question_id,
bytes32 answer,
uint256 max_previous
) external payable;
function finalize(bytes32 question_id) external;
}
contract RealityETHIntegration {
IRealityETH public reality;
mapping(uint256 => bytes32) public marketQuestions;
function createMarketWithReality(
string memory question,
uint256 resolutionTime
) external payable returns (uint256 marketId) {
// Create market
marketId = _createMarket(question, resolutionTime);
// Post question to Reality.eth
bytes32 questionId = reality.askQuestion{value: msg.value}(
0, // Template ID for yes/no
question,
address(this), // Arbitrator
86400, // 1 day timeout
uint32(resolutionTime),
0 // Nonce
);
marketQuestions[marketId] = questionId;
}
function resolveFromReality(uint256 marketId) external {
bytes32 questionId = marketQuestions[marketId];
reality.finalize(questionId);
bytes32 answer = reality.getFinalAnswer(questionId);
if (answer == bytes32(uint256(1))) {
markets[marketId].outcome = 1; // YES
} else if (answer == bytes32(uint256(0))) {
markets[marketId].outcome = 0; // NO
} else {
markets[marketId].invalid = true; // Invalid
}
markets[marketId].resolved = true;
}
}
Optimistic Resolution with Dispute Windows
Basic Optimistic Oracle
contract OptimisticResolution {
uint256 public constant DISPUTE_PERIOD = 2 days;
uint256 public constant BOND_AMOUNT = 1000e18;
struct ResolutionProposal {
address proposer;
uint256 outcome;
uint256 timestamp;
uint256 bond;
bool disputed;
}
mapping(uint256 => ResolutionProposal) public proposals;
function proposeResolution(
uint256 marketId,
uint256 outcome
) external payable {
require(msg.value >= BOND_AMOUNT, "Insufficient bond");
require(block.timestamp >= markets[marketId].resolutionTime, "Too early");
require(proposals[marketId].timestamp == 0, "Already proposed");
proposals[marketId] = ResolutionProposal({
proposer: msg.sender,
outcome: outcome,
timestamp: block.timestamp,
bond: msg.value,
disputed: false
});
emit ResolutionProposed(marketId, outcome, msg.sender);
}
function disputeResolution(uint256 marketId) external payable {
ResolutionProposal storage proposal = proposals[marketId];
require(proposal.timestamp > 0, "No proposal");
require(!proposal.disputed, "Already disputed");
require(
block.timestamp < proposal.timestamp + DISPUTE_PERIOD,
"Dispute window closed"
);
require(msg.value >= BOND_AMOUNT, "Insufficient bond");
proposal.disputed = true;
// Escalate to dispute resolution mechanism
_escalateDispute(marketId, msg.sender);
emit ResolutionDisputed(marketId, msg.sender);
}
function finalizeResolution(uint256 marketId) external {
ResolutionProposal storage proposal = proposals[marketId];
require(proposal.timestamp > 0, "No proposal");
require(!proposal.disputed, "Under dispute");
require(
block.timestamp >= proposal.timestamp + DISPUTE_PERIOD,
"Dispute period active"
);
markets[marketId].resolved = true;
markets[marketId].outcome = proposal.outcome;
// Return bond to proposer
payable(proposal.proposer).transfer(proposal.bond);
emit MarketResolved(marketId, proposal.outcome);
}
}
Multi-Source Verification
Aggregated Oracle System
contract AggregatedOracle {
struct OracleSource {
address oracle;
uint256 weight;
bool active;
}
OracleSource[] public sources;
uint256 public minAgreements;
function resolveWithAggregation(uint256 marketId) external {
uint256[] memory outcomes = new uint256[](sources.length);
uint256 outcomeCount = 0;
for (uint i = 0; i < sources.length; i++) {
if (!sources[i].active) continue;
try IOracle(sources[i].oracle).getOutcome(marketId) returns (uint256 outcome) {
outcomes[outcomeCount] = outcome;
outcomeCount++;
} catch {
// Oracle failed, continue
}
}
require(outcomeCount >= minAgreements, "Insufficient responses");
// Find consensus
uint256 consensusOutcome = _findConsensus(outcomes, outcomeCount);
markets[marketId].resolved = true;
markets[marketId].outcome = consensusOutcome;
}
function _findConsensus(
uint256[] memory outcomes,
uint256 count
) internal pure returns (uint256) {
// Simple majority voting
mapping(uint256 => uint256) frequency;
for (uint i = 0; i < count; i++) {
frequency[outcomes[i]]++;
}
uint256 maxFreq = 0;
uint256 consensus = 0;
for (uint i = 0; i < count; i++) {
if (frequency[outcomes[i]] > maxFreq) {
maxFreq = frequency[outcomes[i]];
consensus = outcomes[i];
}
}
return consensus;
}
}
Resolution Source Design
Defining Resolution Sources
struct ResolutionSource {
string name; // "CNN", "Bloomberg", "Chainlink"
string url; // https://www.cnn.com/elections
string specificSource; // Exact page or API endpoint
uint256 sourceType; // 0=News, 1=API, 2=On-chain, 3=Multiple
bool requiresAuth;
}
Source Hierarchy
struct SourceHierarchy {
ResolutionSource primary;
ResolutionSource[] backups;
uint256 backupThreshold; // Use backup if primary delays beyond this
}
function getEffectiveSource(uint256 marketId) external view returns (ResolutionSource memory) {
SourceHierarchy memory hierarchy = sourceHierarchies[marketId];
if (block.timestamp > markets[marketId].resolutionTime + hierarchy.backupThreshold) {
// Check backups
for (uint i = 0; i < hierarchy.backups.length; i++) {
if (_isSourceAvailable(hierarchy.backups[i])) {
return hierarchy.backups[i];
}
}
}
return hierarchy.primary;
}
Dispute Resolution Systems
Kleros Integration
Kleros provides decentralized court systems:
interface IKlerosArbitrator {
function createDispute(
uint256 _choices,
bytes calldata _extraData
) external payable returns (uint256 disputeID);
function appeal(
uint256 _disputeID,
bytes calldata _extraData
) external payable;
function arbitratorExtraData() external view returns (bytes calldata);
}
contract KlerosDisputeResolution {
IKlerosArbitrator public arbitrator;
uint256 public constant META_EVIDENCE_ID = 0;
mapping(uint256 => uint256) public marketDisputes;
function raiseDispute(
uint256 marketId,
uint256 proposedOutcome
) external payable {
require(!markets[marketId].resolved, "Already resolved");
uint256 disputeId = arbitrator.createDispute{value: msg.value}(
2, // Binary: YES or NO
arbitrator.arbitratorExtraData()
);
marketDisputes[marketId] = disputeId;
emit DisputeCreated(marketId, disputeId, proposedOutcome);
}
function rule(uint256 _disputeID, uint256 _ruling) external {
require(msg.sender == address(arbitrator), "Only arbitrator");
uint256 marketId = disputeToMarket[_disputeID];
markets[marketId].resolved = true;
markets[marketId].outcome = _ruling - 1; // Kleros ruling is 1-indexed
emit DisputeResolved(marketId, _ruling);
}
}
Bond Escalation Games
contract BondEscalation {
uint256 public constant BASE_BOND = 1000e18;
uint256 public constant BOND_MULTIPLIER = 2;
uint256 public constant MAX_ROUNDS = 5;
struct DisputeRound {
address challenger;
uint256 bond;
uint256 outcome;
}
mapping(uint256 => DisputeRound[]) public disputeRounds;
function challengeResolution(
uint256 marketId,
uint256 alternativeOutcome
) external payable {
DisputeRound[] storage rounds = disputeRounds[marketId];
require(rounds.length < MAX_ROUNDS, "Max rounds reached");
uint256 requiredBond = rounds.length == 0
? BASE_BOND
: rounds[rounds.length - 1].bond * BOND_MULTIPLIER;
require(msg.value >= requiredBond, "Insufficient bond");
rounds.push(DisputeRound({
challenger: msg.sender,
bond: msg.value,
outcome: alternativeOutcome
}));
emit ChallengeSubmitted(marketId, rounds.length, alternativeOutcome, msg.value);
}
function resolveDispute(uint256 marketId) external {
DisputeRound[] storage rounds = disputeRounds[marketId];
require(rounds.length > 0, "No disputes");
// If no one challenges the last round for 2 days, it wins
// Otherwise, continue escalation or go to arbitration
}
}
Handling Edge Cases
Ambiguous Outcomes
function handleAmbiguousOutcome(uint256 marketId) external {
Market storage market = markets[marketId];
// Determine if question was resolved ambiguously
bool isAmbiguous = _checkIfAmbiguous(marketId);
if (isAmbiguous) {
market.invalid = true;
market.resolved = true;
// Return all funds to traders
_refundAllTraders(marketId);
emit MarketInvalidated(marketId, "Ambiguous outcome");
}
}
Delayed Outcomes
function handleDelayedResolution(uint256 marketId) external {
Market storage market = markets[marketId];
require(block.timestamp > market.resolutionTime + MAX_DELAY, "Not delayed enough");
require(!market.resolved, "Already resolved");
// Option 1: Extend resolution time
market.resolutionTime += EXTENSION_PERIOD;
// Option 2: Invalidate and refund
// market.invalid = true;
// _refundAllTraders(marketId);
emit ResolutionDelayed(marketId, market.resolutionTime);
}
Multiple Correct Answers
For categorical markets where multiple outcomes could be correct:
function resolveWithMultipleOutcomes(
uint256 marketId,
uint256[] memory winningOutcomes,
uint256[] memory proportions
) external {
require(winningOutcomes.length == proportions.length, "Length mismatch");
require(_isAuthorizedResolver(msg.sender), "Not authorized");
uint256 totalProportion = 0;
for (uint i = 0; i < proportions.length; i++) {
totalProportion += proportions[i];
}
require(totalProportion == 1e18, "Proportions must sum to 100%");
markets[marketId].resolved = true;
markets[marketId].multipleOutcomes = true;
markets[marketId].winningOutcomes = winningOutcomes;
markets[marketId].outcomeProportions = proportions;
}
Best Practices for Oracle Design
1. Source Transparency
Always specify exactly how resolution will occur:
struct ResolutionDetails {
string question;
string detailedCriteria; // Specific conditions
string primarySource; // Exact source
string backupSource;
uint256 resolutionTime;
uint256 disputeWindow;
}
2. Multiple Verification Layers
Don't rely on single sources:
- Primary source (e.g., official website)
- Secondary sources (e.g., news outlets)
- Community verification
- Dispute mechanisms
3. Time Buffers
Add buffer time for resolution:
uint256 public constant RESOLUTION_BUFFER = 1 days;
function canResolve(uint256 marketId) public view returns (bool) {
return block.timestamp >= markets[marketId].resolutionTime + RESOLUTION_BUFFER;
}
4. Fallback Mechanisms
If primary oracle fails:
function emergencyResolution(
uint256 marketId,
uint256 outcome,
bytes calldata justification
) external {
require(msg.sender == emergencyResolver, "Not authorized");
require(block.timestamp > markets[marketId].resolutionTime + 7 days, "Not emergency");
require(!markets[marketId].resolved, "Already resolved");
markets[marketId].resolved = true;
markets[marketId].outcome = outcome;
markets[marketId].emergencyResolution = true;
emit EmergencyResolution(marketId, outcome, justification);
}
5. Clear Dispute Process
Document exactly how disputes work:
- Who can dispute?
- What bond is required?
- How long is the dispute window?
- What happens after a dispute?
Conclusion
Oracle design is the critical infrastructure layer for prediction markets. The best oracle systems balance:
- Speed vs. Security: Faster resolution increases risk; slower resolution reduces utility
- Cost vs. Reliability: Cheap oracles may be unreliable; expensive oracles limit participation
- Decentralization vs. Efficiency: Fully decentralized systems are slower; centralized systems have trust risks
Successful prediction market platforms typically use:
- Optimistic oracles for most markets (fast, cheap, with dispute backup)
- Multiple verification sources for high-value markets
- Clear escalation paths when disputes occur
- Transparent resolution criteria defined at market creation
- Fair bond economics to discourage frivolous disputes
The oracle is the final authority in prediction markets. Users must trust that outcomes will be determined fairly and accurately. Building that trust through robust oracle design is essential for any prediction market platform seeking long-term success.