Skip to content
On this page

L3-6: Prediction Market

1. Problem

Prediction markets allow participants to trade on future events (e.g., "who will win the 2024 election"), with prices reflecting the market's collective judgment of an event's probability. On-chain implementation faces three core challenges: (1) how to create financial instruments representing "yes" and "no" outcomes; (2) how to ensure market creators and participants cannot manipulate the final result; (3) how to ensure fair payout distribution to winning positions.

Unlike large-scale prediction markets like Polymarket, this challenge builds a simplified version — fixed-price purchase of YES/NO share pairs, with results submitted by anyone (rather than a decentralized oracle). This introduces a trust assumption but preserves the core prediction market mechanics of binary outcomes and share redemption.

2. Rationale

Polymarket's success (billions in monthly trading volume) has validated prediction markets' product-market fit in Web3. Blockchain enables permissionless market creation, transparent pricing, and trustless settlement. In traditional prediction markets, legal and regional restrictions are major obstacles — Polymarket circumvents some of these through cryptocurrency.

For developers, prediction markets are an excellent entry point for understanding the concept of "conditional tokens." The Conditional Token Framework (CTF) encapsulates each possible outcome in a prediction market as a tradable ERC-1155 token. This simplified implementation focuses on the most essential binary market mechanics: buying shares, resolving markets, and redeeming payouts.

3. Solution

PredictionMarket.sol implements an ETH-based binary prediction market.

Create Market: createMarket() accepts a question description and duration. The market struct contains: question, endTime, resolved (whether settled), isYes (outcome), and totalYesShares/totalNoShares (total share amounts).

Buy Shares: buyShares() allows users to simultaneously purchase both YES and NO shares. Simplified pricing model: each share pair (1 YES + 1 NO) = SHARE_PRICE = 0.01 ETH. Users pay (yesAmount + noAmount) * SHARE_PRICE ETH, with any overpayment refunded. This is essentially a "buy the entire market exposure" model — users hold shares in both directions and can sell the unwanted direction on the market.

Resolve Market: resolveMarket() is called by anyone (in a testing/educational setting). It sets the isYes flag and resolved = true. In production, settlement should be performed by a decentralized oracle (such as UMA's Optimistic Oracle or Chainlink Functions).

Redeem Payout: redeem() allows holders of winning shares to withdraw payouts proportionally. Total prize pool = total purchase cost of all shares (totalYes + totalNo) * SHARE_PRICE. If YES wins, YES share holders receive: (myYesShares * totalPool) / totalYesShares.

Reference source file: src/level3/PredictionMarket.sol

4. Pitfalls Encountered

  • Resolver trustworthiness: anyone can call resolveMarket(); without access control, a malicious actor can submit an incorrect result
  • Liquidity issues with share pair pricing: the current model forces users to buy both YES and NO shares simultaneously, preventing separate purchase of a single direction and limiting price discovery
  • Share state after redemption: redeem() does not burn shares after payout, nor does it reduce totalYesShares/totalNoShares, potentially causing confusion in subsequent redemption calculations
  • Prize pool disconnected from contract balance: the prize pool is calculated as totalShares * SHARE_PRICE, but the actual contract balance may be inconsistent due to errors or direct transfers
  • Lack of fees: the market creator receives no economic incentive (e.g., trading fees), lacking sustainability

5. Root Causes of Pitfalls

Resolver trustworthiness is the core problem of decentralized prediction markets — the "oracle problem." In PredictionMarket.sol, resolveMarket() has no access control and can be called by anyone. This is because permission management was omitted in the educational design (in test-driven development, anyone should be able to settle for testing convenience). But in production, this leads to severe manipulation risk.

A deeper issue is that the true outcome of many events is subjective or requires off-chain verification. For example, "Bitcoin reaches $200K in 2025" — when exactly does it count as reaching? Based on which price data source? These all require oracles to bridge. UMA's Optimistic Oracle solves this through economic incentives (challenge mechanism) — resolvers must stake tokens, and if the result is successfully challenged, the stake is slashed.

The share pair pricing model also deserves discussion. The current forced YES+NO purchase model is known as a "simplified binary CLOB." In Polymarket's actual implementation, it uses CLOB (Central Limit Order Book) + CTF (Conditional Token Framework), allowing users to buy and sell YES or NO shares independently. Prices are discovered naturally through the order book's bid and ask. YES share prices float between $0 and $1, reflecting the market's judgment of the event probability.

6. How to Resolve the Pitfalls

Oracle integration: in production, use UMA Optimistic Oracle or Chainlink Functions to resolve markets:

solidity
function resolveMarket(uint256 _marketId) external {
    // Get outcome from UMA oracle
    bool outcome = umaOracle.getOutcome(market.question);
    // Or use Chainlink Functions to request off-chain data
    // ... rest of logic
}

Separate share purchase: modify buyShares() into standalone buyYes() and buyNo():

solidity
function buyYes(uint256 _marketId, uint256 _amount) external payable {
    uint256 cost = _amount * currentYesPrice; // requires AMM pricing model
    positions[_marketId][msg.sender].yesShares += _amount;
    market.totalYesShares += _amount;
}

Price discovery can be achieved through an AMM x*y=k curve or CLOB order book matching.

Economic incentives: add a fee share for the market creator:

solidity
uint256 public constant CREATOR_FEE_BPS = 100; // 1%
// Deduct fee in buyShares and transfer to marketCreator

Balance consistency: in redeem(), deduct from the contract's actual balance rather than using the formula-calculated totalPool:

solidity
uint256 payout = (winningShares * address(this).balance) / totalWinningShares;

7. Technical Highlights

TechniqueDescription
Binary MarketYES/NO, two mutually exclusive outcomes
Share Pair Pricing1 YES + 1 NO = 0.01 ETH (simplified model)
Redemption AlgorithmProportional distribution: (myShares * totalPool) / winningShares
State MachineCreate → Trading → Resolved → Redeemed
Reentrancy ProtectionbuyShares and redeem use nonReentrant
Overpayment RefundExcess payment auto-refunded, improving UX
Oracle DependencySettlement result requires external data input

Built with AiAda