28 min read

> "The real innovation of blockchain prediction markets is not decentralization for its own sake --- it is the creation of composable, programmable outcome tokens that can serve as primitives in a broader financial ecosystem."

Chapter 35: Smart Contract Market Mechanisms

"The real innovation of blockchain prediction markets is not decentralization for its own sake --- it is the creation of composable, programmable outcome tokens that can serve as primitives in a broader financial ecosystem." --- Gnosis Whitepaper, 2017

Prediction markets existed long before blockchains, but smart contracts introduced something genuinely new: the ability to encode market rules, token economics, and resolution mechanisms into immutable, transparent, and composable code. In this chapter, we move beyond the conceptual blockchain foundations of Chapter 34 and into the concrete mechanisms that power on-chain prediction markets. We will dissect the Conditional Token Framework (CTF) developed by Gnosis, examine Augur's pioneering oracle and dispute resolution system, trace the architecture of Polymarket, and build a simple on-chain prediction market from scratch.

This chapter is dense and technical. It assumes familiarity with Ethereum basics (accounts, transactions, gas), the ERC-20 and ERC-1155 token standards, and the Python web3.py library. If you have not yet worked through Chapter 34, do so before proceeding.


35.1 On-Chain Market Mechanism Overview

A prediction market operating on a blockchain must solve the same fundamental problems as any prediction market --- pricing, trading, and settlement --- but it must do so within the constraints of a decentralized, adversarial computing environment. The key components of any on-chain prediction market are:

35.1.1 The Five Stages of an On-Chain Prediction Market

Stage 1: Market Creation. A market creator defines a question ("Will event X occur by date Y?"), the set of mutually exclusive and exhaustive outcomes, and the resolution source (oracle). This information is encoded in a smart contract or in a registry contract that tracks all markets.

Stage 2: Token Minting. Users deposit collateral (typically a stablecoin like USDC or DAI) into the market contract. In return, they receive a complete set of outcome tokens --- one token for each possible outcome. If the market has two outcomes (Yes/No), depositing 1 USDC yields 1 Yes token and 1 No token. This operation is called splitting.

Stage 3: Trading. Users trade outcome tokens on secondary markets --- either automated market makers (AMMs) or order books. The prices of outcome tokens reflect the market's collective probability estimate for each outcome. A Yes token trading at 0.70 USDC implies a 70% probability estimate.

Stage 4: Resolution. An oracle reports the actual outcome. This can be a centralized oracle (a trusted entity reports the result), a decentralized oracle (token holders vote on the outcome), or an optimistic oracle (a result is proposed and accepted unless disputed).

Stage 5: Redemption. Holders of winning outcome tokens redeem them for the collateral. Each winning token is redeemable for 1 unit of collateral (minus any fees). Losing tokens become worthless.

35.1.2 The Fundamental Invariant

The core invariant of any well-designed on-chain prediction market is:

$$\text{Total Collateral Deposited} = \sum_{i=1}^{n} \text{Supply of Outcome Token}_i$$

Since every split operation creates exactly one token of each outcome type from one unit of collateral, and every merge operation destroys exactly one token of each type to return one unit of collateral, the system is always fully collateralized. This is the on-chain equivalent of a traditional exchange's clearing guarantee --- but enforced by code rather than by trust.

35.1.3 Why Smart Contracts Matter

Smart contracts provide several properties that are difficult or impossible to achieve with traditional infrastructure:

Property Traditional Market Smart Contract Market
Collateral custody Held by operator Held by contract (self-custody)
Settlement T+2 or longer Immediate upon resolution
Market rules Enforced by operator Enforced by code
Transparency Operator-dependent Fully auditable on-chain
Composability None Outcome tokens can be used in DeFi
Censorship resistance Operator can censor Permissionless (with caveats)
Counterparty risk Operator default risk Smart contract risk

The trade-offs are real: smart contracts introduce gas costs, latency (block times), and smart contract risk (bugs in the code). But for applications where trust minimization, global access, and composability matter, on-chain markets offer genuine advantages.

35.1.4 Architecture Patterns

Three main architecture patterns have emerged for on-chain prediction markets:

Pattern 1: Monolithic Contract. A single smart contract handles market creation, trading (via an AMM), and resolution. Augur v1 followed this approach. It is simple but inflexible.

Pattern 2: Modular Framework. Separate contracts handle token logic (CTF), trading (AMM or order book), and resolution (oracle). Gnosis and Polymarket follow this approach. It is more complex but enables composition and upgrades.

Pattern 3: Hybrid Off-Chain/On-Chain. Trading happens off-chain (order matching), but settlement, token custody, and resolution happen on-chain. Polymarket's current architecture and dYdX follow this approach. It offers the best user experience but introduces some centralization in the matching layer.

Pattern 2: Modular Framework (Gnosis/Polymarket Style)

+------------------+     +-------------------+     +----------------+
|   Oracle         |     | Conditional Token |     | Exchange       |
|   Contract       |<--->| Framework (CTF)   |<--->| (AMM or CLOB)  |
|                  |     |                   |     |                |
| - reportOutcome  |     | - splitPosition   |     | - placeOrder   |
| - disputeResult  |     | - mergePositions  |     | - fillOrder    |
| - finalizeResult |     | - redeemPositions |     | - cancelOrder  |
+------------------+     +-------------------+     +----------------+
                               ^
                               |
                          +-----------+
                          | Collateral|
                          | (ERC-20)  |
                          +-----------+

35.2 The Conditional Token Framework (Gnosis CTF)

The Conditional Token Framework, developed by Gnosis, is the most widely adopted standard for on-chain prediction market tokens. It is used by Polymarket, Omen, and numerous other platforms. Understanding CTF is essential for working with modern on-chain prediction markets.

35.2.1 Core Concepts

The CTF defines four key primitives:

Condition. A condition represents a question with a fixed number of mutually exclusive outcomes. It is identified by a conditionId computed as:

$$\texttt{conditionId} = \text{keccak256}(\texttt{oracle}, \texttt{questionId}, \texttt{outcomeSlotCount})$$

where oracle is the address authorized to report the outcome, questionId is an arbitrary 32-byte identifier for the question, and outcomeSlotCount is the number of possible outcomes.

Outcome Slot. Each condition has outcomeSlotCount outcome slots, indexed from 0 to $n-1$. For a binary market (Yes/No), there are two outcome slots: slot 0 (No) and slot 1 (Yes).

Index Set. An index set is a bitmask that specifies a subset of outcome slots. For a binary market: - Index set 0b01 = 1 represents outcome slot 0 (No) - Index set 0b10 = 2 represents outcome slot 1 (Yes) - Index set 0b11 = 3 represents both slots (the "full" index set)

For a market with three outcomes (A, B, C): - Index set 0b001 = 1 represents outcome A - Index set 0b010 = 2 represents outcome B - Index set 0b100 = 4 represents outcome C - Index set 0b011 = 3 represents A or B (a "merged" position) - Index set 0b111 = 7 represents all outcomes (full index set)

Position. A position is defined by a collateral token, a set of conditions, and for each condition an index set. Positions are identified by a positionId and are represented as ERC-1155 tokens.

$$\texttt{positionId} = \text{keccak256}(\texttt{collateralToken}, \texttt{collectionId})$$

where collectionId encodes the condition and index set information.

35.2.2 The Split Operation

Splitting is the process of converting collateral into outcome tokens. Given a condition with $n$ outcomes, a user can split collateral into a partition --- a set of disjoint index sets that cover all outcomes.

For a binary market, the simplest partition is {1, 2} --- splitting into separate Yes and No tokens. The user deposits $x$ units of collateral and receives $x$ units of each outcome token in the partition.

The mathematical guarantee is:

$$\text{collateral}(x) \xrightarrow{\text{split}} \sum_{j \in \text{partition}} \text{token}_j(x)$$

And conversely:

$$\sum_{j \in \text{partition}} \text{token}_j(x) \xrightarrow{\text{merge}} \text{collateral}(x)$$

Here is the Solidity interface for splitting:

function splitPosition(
    IERC20 collateralToken,
    bytes32 parentCollectionId,
    bytes32 conditionId,
    uint[] calldata partition,
    uint amount
) external;

The parentCollectionId parameter enables nested conditions --- splitting outcome tokens from one condition into sub-positions contingent on another condition. When splitting from raw collateral, parentCollectionId is bytes32(0).

35.2.3 Merging and Redemption

Merging is the reverse of splitting. A user who holds equal amounts of all outcome tokens in a partition can merge them back into collateral. This creates an arbitrage opportunity: if the sum of outcome token prices exceeds 1 (the cost of a complete set), a trader can buy all tokens and merge for a profit.

Redemption occurs after a condition is resolved. The oracle reports a payout vector $[p_0, p_1, \ldots, p_{n-1}]$ where $\sum p_i = 1$ (or more precisely, $\sum p_i = \text{outcomeSlotCount}$ in the CTF's fixed-point representation). Token holders redeem their tokens for collateral proportional to the payout.

For a binary market resolved to "Yes": - Payout vector: $[0, 1]$ - Each Yes token redeems for 1 unit of collateral - Each No token redeems for 0

For a scalar market (e.g., "What will the temperature be?") resolved at 75 on a 0-100 scale: - Payout vector: $[0.25, 0.75]$ - Each "Low" token redeems for 0.25 - Each "High" token redeems for 0.75

35.2.4 Collection IDs and Position IDs

The CTF uses a layered hashing scheme to compute position IDs:

Collection ID for a single condition: $$\texttt{collectionId} = \text{keccak256}(\texttt{conditionId}, \texttt{indexSet})$$

Collection ID for multiple conditions (AND logic): $$\texttt{collectionId} = \texttt{collectionId}_1 \oplus \texttt{collectionId}_2 \oplus \ldots$$

where $\oplus$ is the XOR operation. This elegant construction allows positions contingent on multiple conditions to be computed without storing explicit condition trees.

Position ID: $$\texttt{positionId} = \text{keccak256}(\texttt{collateralToken}, \texttt{collectionId})$$

The position ID serves as the ERC-1155 token ID. Any user holding a balance of this token ID in the CTF contract holds that position.

35.2.5 Python Interaction with CTF Contracts

Here is how to interact with the CTF from Python using web3.py:

from web3 import Web3
import json

# Connect to an Ethereum node (or Polygon/Gnosis Chain)
w3 = Web3(Web3.HTTPProvider("https://polygon-rpc.com"))

# CTF contract address on Polygon (Polymarket's deployment)
CTF_ADDRESS = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045"

# Load the CTF ABI (simplified - key functions only)
CTF_ABI = json.loads('''[
    {
        "inputs": [
            {"name": "oracle", "type": "address"},
            {"name": "questionId", "type": "bytes32"},
            {"name": "outcomeSlotCount", "type": "uint256"}
        ],
        "name": "prepareCondition",
        "outputs": [],
        "type": "function"
    },
    {
        "inputs": [
            {"name": "collateralToken", "type": "address"},
            {"name": "parentCollectionId", "type": "bytes32"},
            {"name": "conditionId", "type": "bytes32"},
            {"name": "partition", "type": "uint256[]"},
            {"name": "amount", "type": "uint256"}
        ],
        "name": "splitPosition",
        "outputs": [],
        "type": "function"
    },
    {
        "inputs": [
            {"name": "collateralToken", "type": "address"},
            {"name": "parentCollectionId", "type": "bytes32"},
            {"name": "conditionId", "type": "bytes32"},
            {"name": "partition", "type": "uint256[]"},
            {"name": "amount", "type": "uint256"}
        ],
        "name": "mergePositions",
        "outputs": [],
        "type": "function"
    },
    {
        "inputs": [
            {"name": "conditionId", "type": "bytes32"}
        ],
        "name": "getOutcomeSlotCount",
        "outputs": [{"name": "", "type": "uint256"}],
        "type": "function"
    },
    {
        "inputs": [
            {"name": "account", "type": "address"},
            {"name": "id", "type": "uint256"}
        ],
        "name": "balanceOf",
        "outputs": [{"name": "", "type": "uint256"}],
        "type": "function"
    }
]''')

ctf = w3.eth.contract(address=CTF_ADDRESS, abi=CTF_ABI)

# Compute a conditionId
def compute_condition_id(oracle: str, question_id: bytes, outcome_slot_count: int) -> bytes:
    """Compute the conditionId as keccak256(oracle, questionId, outcomeSlotCount)."""
    return Web3.solidity_keccak(
        ['address', 'bytes32', 'uint256'],
        [oracle, question_id, outcome_slot_count]
    )

# Compute a collectionId
def compute_collection_id(condition_id: bytes, index_set: int) -> bytes:
    """Compute collectionId = keccak256(conditionId, indexSet)."""
    return Web3.solidity_keccak(
        ['bytes32', 'uint256'],
        [condition_id, index_set]
    )

# Compute a positionId
def compute_position_id(collateral_token: str, collection_id: bytes) -> int:
    """Compute positionId = uint256(keccak256(collateralToken, collectionId))."""
    pos_hash = Web3.solidity_keccak(
        ['address', 'bytes32'],
        [collateral_token, collection_id]
    )
    return int.from_bytes(pos_hash, 'big')

# Example: compute position IDs for a binary market
oracle = "0x6A9D222616C90FcA5754cd1333cFD9b7fb6a4F74"
question_id = b'\x00' * 31 + b'\x01'  # simplified
outcome_count = 2
collateral = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"  # USDC on Polygon

condition_id = compute_condition_id(oracle, question_id, outcome_count)
print(f"Condition ID: 0x{condition_id.hex()}")

# Yes token (index set = 0b10 = 2) and No token (index set = 0b01 = 1)
for name, index_set in [("No", 1), ("Yes", 2)]:
    collection_id = compute_collection_id(condition_id, index_set)
    position_id = compute_position_id(collateral, collection_id)
    print(f"{name} token position ID: {position_id}")

35.2.6 Partition Mathematics

A partition of a condition with $n$ outcome slots is a set of index sets $\{I_1, I_2, \ldots, I_k\}$ such that:

  1. Disjointness: $I_i \cap I_j = \emptyset$ for all $i \neq j$
  2. Coverage: $I_1 \cup I_2 \cup \ldots \cup I_k = \{0, 1, \ldots, n-1\}$

In bitmask form, the conditions become: 1. $I_i \,\&\, I_j = 0$ for all $i \neq j$ 2. $I_1 \,|\, I_2 \,|\, \ldots \,|\, I_k = 2^n - 1$

The full index set is $2^n - 1$ (all bits set). A partition into individual outcomes uses singleton index sets: $\{1, 2, 4, \ldots, 2^{n-1}\}$.

A non-trivial partition groups outcomes. For a market with outcomes {A, B, C, D}: - Partition {0b0011, 0b1100} = {3, 12} groups (A or B) vs (C or D) - This creates two tokens: one that pays off if A or B wins, another if C or D wins

This flexibility enables rich market structures on a single condition.


35.3 Augur's Market Protocol

Augur was the first major decentralized prediction market on Ethereum, launching its v1 in July 2018 and v2 in 2020. While it has largely been superseded by newer platforms in terms of trading volume, Augur's design introduced many concepts that remain foundational.

35.3.1 Augur v1 Architecture

Augur v1 was a monolithic system deployed entirely on Ethereum mainnet. Its key components:

Universe. The top-level contract holding all markets. Augur supported multiple universes to handle irreconcilable disputes (forking).

Market. Each market had a question, a set of outcomes, a designated reporter, a resolution deadline, and fee parameters. Markets could be binary (Yes/No), categorical (multiple choices), or scalar (numeric range).

Share Tokens. Augur used ERC-20 tokens (one per outcome) rather than the ERC-1155 approach of CTF. Each share represented a claim on the market's collateral pool.

REP Token (Reputation). Augur's governance and oracle token. REP holders reported outcomes, participated in disputes, and earned fees. REP was an ERC-20 token with an initial supply of 11 million tokens.

Reporting and Dispute Resolution. This was Augur's most innovative feature, a multi-round escalation game:

1. Designated Reporter reports outcome (24-hour window)
        |
        v
2. If no dispute: outcome finalized after 7 days
        |
        v [if disputed]
3. Dispute Round 1: Challengers stake REP on alternative outcome
   Required stake: 2x previous round
        |
        v [if disputed again]
4. Dispute Round 2: Required stake: 2x previous round
        |
        ... (escalating rounds)
        |
        v [if stake exceeds fork threshold]
5. FORK: Universe splits into child universes, one per outcome.
   REP holders migrate to the universe they believe is correct.
   This is the "nuclear option" - designed to be so costly that
   it deters lying at earlier stages.

35.3.2 The Reporting Game Theory

Augur's dispute resolution is a Schelling game with escalating stakes. The key economic insight is:

$$\text{Cost to lie at round } r = 2^r \times \text{base\_stake}$$

The cost to sustain a false outcome grows exponentially. If a false outcome survives $k$ rounds of dispute, the attacker must have staked:

$$\text{Total attacker stake} = \sum_{i=0}^{k} 2^i \times \text{base\_stake} = (2^{k+1} - 1) \times \text{base\_stake}$$

If the attacker is ultimately defeated, they lose all staked REP. The fork mechanism serves as the ultimate backstop: if the dispute reaches the fork threshold (approximately 2.5% of all REP), the entire universe forks, and the market is resolved by the migration patterns of REP holders.

The expected cost of a successful attack on a market with value $V$:

$$\text{Expected Attack Cost} \geq \min\left(\frac{\text{REP Market Cap}}{2}, V\right)$$

This means Augur is secure as long as the REP market cap is at least $2V$ for any single market. This was a significant limitation: high-value markets required a correspondingly high REP market cap to be secure.

35.3.3 Augur v2 Improvements

Augur v2 introduced several improvements:

DAI as Collateral. Markets used DAI (a decentralized stablecoin) rather than ETH, reducing the correlation between collateral value and outcome.

0x Integration. Off-chain order matching via the 0x protocol reduced gas costs for trading.

Invalid Outcome. Added an explicit "Invalid" outcome to handle ambiguous or unanswerable questions, addressing a significant v1 pain point.

Augur Turbo. A faster variant using Chainlink oracles for common event types (sports, crypto prices), sacrificing decentralization for speed and cost.

35.3.4 Lessons from Augur's History

Augur's experience provides valuable lessons:

  1. Gas costs are critical. Augur v1 markets cost $30-100+ in gas to create and $5-20+ to trade during Ethereum congestion. This priced out most retail users.

  2. Liquidity is king. Despite technical innovation, Augur never achieved sufficient liquidity. Most markets had wide spreads and thin order books.

  3. UX matters more than decentralization. Users consistently chose centralized alternatives with better UX over Augur's more decentralized approach.

  4. Oracle design is the hardest problem. Augur's dispute resolution was elegant in theory but slow in practice (markets could take weeks to resolve) and confusing for average users.

  5. Regulatory arbitrage is temporary. Augur was designed to be uncensorable, but this also meant it could host ethically questionable markets (assassination markets were a constant concern). The social and regulatory implications of fully permissionless markets remain unresolved.

# Analyzing Augur historical data
# This demonstrates reading Augur v2 market data from Ethereum

from web3 import Web3
from typing import Dict, List, Optional
from dataclasses import dataclass
from datetime import datetime

@dataclass
class AugurMarket:
    """Represents an Augur market's on-chain state."""
    address: str
    question: str
    outcomes: List[str]
    end_time: datetime
    num_ticks: int
    market_type: str  # 'binary', 'categorical', 'scalar'
    creator: str
    designated_reporter: str
    is_finalized: bool
    winning_payout: Optional[List[int]]

class AugurAnalyzer:
    """Analyze Augur market data from Ethereum."""

    # Augur v2 Universe contract on Ethereum mainnet
    UNIVERSE_ADDRESS = "0x0A1e4D0B5c71B955c0a5993023fc48bA6E380496"

    def __init__(self, web3_provider: str):
        self.w3 = Web3(Web3.HTTPProvider(web3_provider))

    def get_market_count(self) -> int:
        """Get the total number of markets created in the universe."""
        # In practice, you would query the MarketCreated events
        market_created_topic = self.w3.keccak(
            text="MarketCreated(address,uint256,string,address,address,"
                 "address,bytes32[],uint256,int256[],uint8,uint256)"
        )
        logs = self.w3.eth.get_logs({
            'fromBlock': 10_000_000,  # Augur v2 deployment block (approx)
            'toBlock': 'latest',
            'topics': [market_created_topic.hex()]
        })
        return len(logs)

    def analyze_dispute_resolution(
        self, market_address: str
    ) -> Dict[str, any]:
        """Analyze the dispute resolution history of a market."""
        dispute_topic = self.w3.keccak(
            text="DisputeCrowdsourcerCompleted(address,address,uint256[],"
                 "uint256,uint256,bool)"
        )
        logs = self.w3.eth.get_logs({
            'address': market_address,
            'fromBlock': 0,
            'toBlock': 'latest',
            'topics': [dispute_topic.hex()]
        })
        return {
            'dispute_rounds': len(logs),
            'was_disputed': len(logs) > 0,
            'logs': logs
        }

35.3.5 Augur's Fee Structure

Augur implemented a dynamic fee system:

  • Creator Fee: Set by the market creator (0-50% of settlement). Higher fees incentivized market creation but discouraged trading.
  • Reporting Fee: A dynamic fee that funded the dispute resolution system. It adjusted based on the ratio of open interest to REP market cap.

The reporting fee target maintained the security invariant:

$$\text{Reporting Fee} = f\left(\frac{\text{Total Open Interest}}{\text{REP Market Cap}}\right)$$

If open interest grew too large relative to REP value, the fee increased, discouraging new positions until the ratio normalized.


35.4 Polymarket's Architecture

Polymarket is currently the most successful on-chain prediction market by trading volume. Understanding its architecture is essential for anyone working in this space.

35.4.1 The Stack

Polymarket operates on Polygon (an Ethereum Layer 2/sidechain) and uses the following components:

  1. Gnosis Conditional Token Framework (CTF): Token minting, splitting, merging, and redemption
  2. CTF Exchange (CLOB): A central limit order book for trading outcome tokens
  3. UMA Optimistic Oracle: Resolution of market outcomes
  4. USDC: Collateral token (USDC on Polygon)
  5. Neg Risk Adapter: A wrapper that enables efficient trading of negatively correlated binary markets (e.g., "Who will win the election?" with multiple candidates)

35.4.2 The Neg Risk Adapter

One of Polymarket's most important innovations is the Neg Risk Adapter, which handles multi-outcome markets using binary CTF conditions.

Consider the question "Who will win the 2024 US Presidential Election?" with candidates A, B, C. Rather than creating a single three-outcome CTF condition, Polymarket creates three separate binary conditions:

  • "Will A win?" (Yes/No)
  • "Will B win?" (Yes/No)
  • "Will C win?" (Yes/No)

The Neg Risk Adapter enforces the constraint that exactly one candidate wins. It wraps the individual binary positions and ensures:

$$\sum_{i} p(\text{candidate}_i \text{ wins}) \leq 1$$

This design choice has practical benefits: - Each binary market has its own order book, improving liquidity - Traders can express views on individual candidates without taking positions on all - The math is simpler (binary tokens rather than multi-outcome) - It reuses the well-tested binary CTF infrastructure

35.4.3 Order Types and Matching

Polymarket's CLOB (Central Limit Order Book) supports:

  • Limit Orders: Specify price and quantity. Resting orders earn maker fees.
  • Market Orders: Execute immediately at the best available price. Pay taker fees.
  • Good-Till-Cancel (GTC): Orders remain active until filled or cancelled.
  • Fill-or-Kill (FOK): Orders must be fully filled immediately or are cancelled.

Orders are signed off-chain (EIP-712 typed data) and matched by Polymarket's operator. Settlement happens on-chain through the CTF Exchange contract. This hybrid model provides:

  • Off-chain speed: Order placement and cancellation are fast (sub-second)
  • On-chain settlement: Trades settle on Polygon (~2 second block time)
  • Self-custody: Tokens remain in the user's wallet until a trade is matched

35.4.4 The Trade Lifecycle

Let us trace a complete trade through the Polymarket system:

1. Alice wants to buy 100 Yes tokens at $0.65

   Alice signs an EIP-712 order:
   {
     maker: Alice's address,
     tokenId: <Yes token position ID>,
     makerAmount: 65 USDC (what Alice pays),
     takerAmount: 100 Yes tokens (what Alice receives),
     side: BUY,
     expiration: <timestamp>,
     nonce: <unique nonce>,
     signature: <Alice's signature>
   }

2. The order is submitted to Polymarket's off-chain order book

3. Bob has 100 Yes tokens and wants to sell at $0.65

   Bob signs a matching order (or Alice's order matches Bob's resting order)

4. Polymarket's operator matches the orders and submits a transaction
   to the CTF Exchange contract:

   exchange.matchOrders(aliceOrder, bobOrder, fillAmount)

5. The CTF Exchange contract:
   a. Verifies both signatures
   b. Transfers 65 USDC from Alice to Bob
   c. Transfers 100 Yes tokens from Bob to Alice
   d. Emits OrderFilled events

6. Both Alice and Bob now have updated balances:
   - Alice: -65 USDC, +100 Yes tokens
   - Bob: +65 USDC, -100 Yes tokens

35.4.5 UMA Optimistic Oracle

Polymarket uses UMA's Optimistic Oracle for market resolution. The process:

  1. Proposal: Anyone can propose an outcome by posting a bond (in UMA tokens).
  2. Challenge Period: There is a 2-hour dispute window (configurable). If no one disputes, the proposed outcome is accepted.
  3. Dispute: If someone disputes, the question goes to UMA's Data Verification Mechanism (DVM), where UMA token holders vote on the correct outcome.
  4. Resolution: The CTF condition is resolved with the final outcome, enabling redemption.

The optimistic oracle is much faster than Augur's dispute resolution (2 hours vs. potentially weeks) but relies on the assumption that most proposals are honest and that the UMA DVM is a credible backstop.

# Polymarket trade lifecycle simulation
from dataclasses import dataclass, field
from typing import Optional
import time
import hashlib

@dataclass
class PolymarketOrder:
    """Represents a Polymarket order."""
    maker: str
    token_id: int
    maker_amount: int      # in USDC base units (6 decimals)
    taker_amount: int      # in outcome token units (6 decimals)
    side: str              # 'BUY' or 'SELL'
    expiration: int
    nonce: int
    signature: Optional[bytes] = None

    @property
    def price(self) -> float:
        """Implied price per outcome token."""
        if self.side == 'BUY':
            return self.maker_amount / self.taker_amount
        else:
            return self.taker_amount / self.maker_amount

    def is_expired(self) -> bool:
        return int(time.time()) > self.expiration


class OrderBook:
    """Simplified Polymarket-style order book."""

    def __init__(self, token_id: int):
        self.token_id = token_id
        self.bids: list = []  # Buy orders, sorted by price descending
        self.asks: list = []  # Sell orders, sorted by price ascending

    def add_order(self, order: PolymarketOrder):
        """Add an order to the book, attempting to match first."""
        if order.side == 'BUY':
            matches = self._match_buy(order)
            if order.taker_amount > 0:  # Remaining unfilled
                self.bids.append(order)
                self.bids.sort(key=lambda o: o.price, reverse=True)
            return matches
        else:
            matches = self._match_sell(order)
            if order.maker_amount > 0:  # Remaining unfilled
                self.asks.append(order)
                self.asks.sort(key=lambda o: o.price)
            return matches

    def _match_buy(self, buy_order: PolymarketOrder) -> list:
        """Match a buy order against resting sell orders."""
        matches = []
        while self.asks and buy_order.taker_amount > 0:
            best_ask = self.asks[0]
            if buy_order.price < best_ask.price:
                break  # No match possible

            fill_amount = min(buy_order.taker_amount, best_ask.maker_amount)
            fill_cost = int(fill_amount * best_ask.price)

            matches.append({
                'buyer': buy_order.maker,
                'seller': best_ask.maker,
                'amount': fill_amount,
                'price': best_ask.price,
                'cost': fill_cost
            })

            buy_order.taker_amount -= fill_amount
            buy_order.maker_amount -= fill_cost
            best_ask.maker_amount -= fill_amount
            best_ask.taker_amount -= fill_cost

            if best_ask.maker_amount <= 0:
                self.asks.pop(0)

        return matches

    def _match_sell(self, sell_order: PolymarketOrder) -> list:
        """Match a sell order against resting buy orders."""
        matches = []
        while self.bids and sell_order.maker_amount > 0:
            best_bid = self.bids[0]
            if sell_order.price > best_bid.price:
                break

            fill_amount = min(sell_order.maker_amount, best_bid.taker_amount)
            fill_cost = int(fill_amount * best_bid.price)

            matches.append({
                'buyer': best_bid.maker,
                'seller': sell_order.maker,
                'amount': fill_amount,
                'price': best_bid.price,
                'cost': fill_cost
            })

            sell_order.maker_amount -= fill_amount
            best_bid.taker_amount -= fill_amount
            best_bid.maker_amount -= fill_cost

            if best_bid.taker_amount <= 0:
                self.bids.pop(0)

        return matches

    @property
    def best_bid(self) -> Optional[float]:
        return self.bids[0].price if self.bids else None

    @property
    def best_ask(self) -> Optional[float]:
        return self.asks[0].price if self.asks else None

    @property
    def spread(self) -> Optional[float]:
        if self.best_bid and self.best_ask:
            return self.best_ask - self.best_bid
        return None

35.5 On-Chain Order Books vs AMMs

On-chain prediction markets have experimented with two primary trading mechanisms: Automated Market Makers (AMMs) and Central Limit Order Books (CLOBs). Each has distinct trade-offs.

35.5.1 AMMs for Prediction Markets

AMMs for prediction markets use a scoring rule (typically LMSR --- the Logarithmic Market Scoring Rule, covered in Chapter 8) embedded in a smart contract. The AMM holds a reserve of outcome tokens and prices them according to a fixed formula.

LMSR Cost Function:

$$C(\mathbf{q}) = b \ln\left(\sum_{i=1}^{n} e^{q_i / b}\right)$$

where $\mathbf{q} = (q_1, \ldots, q_n)$ is the vector of outstanding shares for each outcome and $b$ is the liquidity parameter.

The price of outcome $i$ is:

$$p_i = \frac{e^{q_i / b}}{\sum_{j=1}^{n} e^{q_j / b}}$$

Advantages of AMMs: - Always available liquidity (no empty order books) - Simple user experience (just buy or sell, no limit orders) - No need for market makers - Gas-efficient for simple trades

Disadvantages of AMMs: - Impermanent loss for liquidity providers - Price impact scales with trade size (slippage) - The liquidity parameter $b$ must be set in advance - Cannot express complex order types - MEV (Maximal Extractable Value) exposure: arbitrageurs can front-run trades

35.5.2 CLOBs for Prediction Markets

A Central Limit Order Book matches buyers and sellers at specified prices. Polymarket's approach is a hybrid: order matching happens off-chain, but settlement is on-chain.

Advantages of CLOBs: - Zero price impact for small orders (within the spread) - Professional market makers can provide tight spreads - Complex order types (limit, stop, FOK, etc.) - Better price discovery for high-volume markets

Disadvantages of CLOBs: - Requires active market makers for liquidity - On-chain CLOBs are extremely gas-intensive - Off-chain matching introduces centralization - Empty order books provide no liquidity

35.5.3 Gas Cost Comparison

The gas costs differ dramatically between the approaches:

Operation On-Chain AMM On-Chain CLOB Hybrid CLOB
Place order N/A 150,000-300,000 gas Free (off-chain)
Cancel order N/A 50,000-100,000 gas Free (off-chain)
Execute trade 100,000-200,000 gas 200,000-400,000 gas 150,000-250,000 gas
Add liquidity 150,000-300,000 gas N/A N/A

On Ethereum mainnet at 30 gwei gas price and $3,000 ETH: - AMM trade: $9-18 - On-chain CLOB trade: $18-36 - Hybrid CLOB trade: $13.50-22.50

On Polygon at 30 gwei and $0.50 MATIC: - All operations: < $0.01

This explains why Polymarket chose Polygon: the gas costs on Ethereum mainnet made prediction markets impractical for most users.

35.5.4 MEV Considerations

Maximal Extractable Value (MEV) is a critical consideration for on-chain markets. MEV refers to the profit that can be extracted by reordering, inserting, or censoring transactions within a block.

For prediction markets, MEV manifests in several ways:

  1. Front-running: A searcher observes a large buy order in the mempool and places their own buy order first, profiting from the price impact.

  2. Sandwich attacks: A searcher places a buy before and a sell after a victim's trade, profiting from the artificially inflated price.

  3. Oracle front-running: If the oracle resolution transaction is visible in the mempool, a searcher can buy winning tokens before the resolution is confirmed.

  4. Arbitrage: Cross-market arbitrage between an on-chain AMM and an off-chain CLOB or between different AMMs.

# MEV simulation for a prediction market AMM
import numpy as np
from typing import Tuple

class LMSRMarketMaker:
    """LMSR-based AMM for a binary prediction market."""

    def __init__(self, b: float, initial_probability: float = 0.5):
        """
        Args:
            b: Liquidity parameter (higher = more liquidity, less price impact)
            initial_probability: Starting probability for outcome 1
        """
        self.b = b
        # Initialize quantities so that p1 = initial_probability
        self.q = np.array([0.0, 0.0])
        self.q[1] = b * np.log(initial_probability / (1 - initial_probability))

    def cost(self) -> float:
        """Current cost function value."""
        return self.b * np.log(np.sum(np.exp(self.q / self.b)))

    def prices(self) -> np.ndarray:
        """Current prices for each outcome."""
        exp_q = np.exp(self.q / self.b)
        return exp_q / np.sum(exp_q)

    def buy(self, outcome: int, amount: float) -> float:
        """
        Buy `amount` shares of `outcome`.
        Returns the cost in collateral.
        """
        old_cost = self.cost()
        self.q[outcome] += amount
        new_cost = self.cost()
        return new_cost - old_cost

    def sell(self, outcome: int, amount: float) -> float:
        """
        Sell `amount` shares of `outcome`.
        Returns the proceeds in collateral.
        """
        old_cost = self.cost()
        self.q[outcome] -= amount
        new_cost = self.cost()
        return old_cost - new_cost  # Proceeds are the reduction in cost

    def simulate_sandwich_attack(
        self, victim_outcome: int, victim_amount: float
    ) -> dict:
        """Simulate a sandwich attack on a victim's trade."""
        initial_prices = self.prices().copy()

        # Step 1: Attacker front-runs with a buy
        attacker_amount = victim_amount * 0.5  # Attacker buys half the victim's amount
        attacker_cost = self.buy(victim_outcome, attacker_amount)
        prices_after_frontrun = self.prices().copy()

        # Step 2: Victim's trade executes at worse price
        victim_cost = self.buy(victim_outcome, victim_amount)
        prices_after_victim = self.prices().copy()

        # Step 3: Attacker sells (back-runs)
        attacker_proceeds = self.sell(victim_outcome, attacker_amount)

        # Calculate MEV
        attacker_profit = attacker_proceeds - attacker_cost
        victim_overpay = victim_cost - self._hypothetical_cost(
            victim_outcome, victim_amount, initial_prices
        )

        return {
            'initial_prices': initial_prices,
            'prices_after_frontrun': prices_after_frontrun,
            'prices_after_victim': prices_after_victim,
            'attacker_cost': attacker_cost,
            'attacker_proceeds': attacker_proceeds,
            'attacker_profit': attacker_profit,
            'victim_cost': victim_cost,
            'victim_overpay': victim_overpay
        }

    def _hypothetical_cost(
        self, outcome: int, amount: float, prices: np.ndarray
    ) -> float:
        """Estimate what the victim would have paid without front-running."""
        # Simple linear approximation
        avg_price = prices[outcome]
        return avg_price * amount


# Example: MEV analysis
amm = LMSRMarketMaker(b=1000, initial_probability=0.5)
print(f"Initial prices: {amm.prices()}")

result = amm.simulate_sandwich_attack(victim_outcome=1, victim_amount=100)
print(f"Attacker profit: ${result['attacker_profit']:.4f}")
print(f"Victim overpay: ${result['victim_overpay']:.4f}")

35.5.5 Hybrid Approaches

The most successful approaches combine off-chain and on-chain components:

Polymarket Model: - Order matching: Off-chain (centralized operator) - Token custody: On-chain (user wallets) - Settlement: On-chain (CTF Exchange contract) - Resolution: On-chain (UMA oracle)

dYdX Model (for perpetuals): - Order matching: Off-chain (Starkware L2) - Margin: On-chain (Ethereum L1) - Settlement: L2 with L1 finality

These hybrid models sacrifice some decentralization for usability. The key design question is: which components must be decentralized for the market to function as intended, and which can be centralized without unacceptable trust assumptions?


35.6 Token Economics of Prediction Markets

Outcome tokens in prediction markets are financial instruments with interesting economic properties. This section explores their pricing, composition, and use in broader DeFi strategies.

35.6.1 Outcome Tokens as ERC-1155

The CTF implements outcome tokens as ERC-1155 multi-tokens. Unlike ERC-20 (one contract per token), ERC-1155 allows a single contract to manage an unlimited number of token types, each identified by a tokenId (the position ID).

Benefits of ERC-1155 for prediction markets: - Gas efficiency: Batch transfers of multiple token types in a single transaction - Single approval: One approval for the CTF contract covers all token types - Metadata: Token metadata (market question, outcome description) can be stored off-chain and referenced by URI

35.6.2 Token Pricing Theory

The price of an outcome token reflects the market's probability estimate plus a risk premium:

$$P_i = \mathbb{E}[\text{payout}_i] + \text{risk premium}_i$$

For a binary market: $$P_{\text{Yes}} = \Pr(\text{Yes}) \cdot 1 + (1 - \Pr(\text{Yes})) \cdot 0 + \text{risk premium}$$ $$P_{\text{Yes}} = \Pr(\text{Yes}) + \text{risk premium}$$

The risk premium can be positive (demand for the specific outcome token exceeds supply) or negative (excess supply). In efficient markets, risk premiums should be small.

The Complete Set Constraint:

$$\sum_{i=1}^{n} P_i = 1 + \text{spread}$$

where the spread is the market maker's profit. If the sum exceeds 1, arbitrageurs can buy a complete set (all outcomes) for $\sum P_i$, which is guaranteed to pay out 1, locking in a risk-free profit of $\sum P_i - 1$. If the sum is less than 1, arbitrageurs can sell a complete set (merge tokens into collateral) for a profit.

35.6.3 Portfolio Analysis

A portfolio of outcome tokens across multiple markets creates exposure to various outcomes. Consider a trader holding positions in three binary markets:

$$\text{Portfolio Value} = \sum_{m=1}^{M} \sum_{i=1}^{n_m} h_{m,i} \cdot P_{m,i}$$

where $h_{m,i}$ is the holding of outcome $i$ in market $m$ and $P_{m,i}$ is the current price.

The portfolio's P&L at resolution is:

$$\text{P\&L} = \sum_{m=1}^{M} \sum_{i=1}^{n_m} h_{m,i} \cdot (\text{payout}_{m,i} - P_{m,i}^{\text{entry}})$$

# Portfolio analysis for outcome tokens
from dataclasses import dataclass
from typing import List, Dict
import numpy as np

@dataclass
class Position:
    """A position in a prediction market."""
    market_id: str
    market_question: str
    outcome: str
    quantity: float
    entry_price: float
    current_price: float

    @property
    def market_value(self) -> float:
        return self.quantity * self.current_price

    @property
    def cost_basis(self) -> float:
        return self.quantity * self.entry_price

    @property
    def unrealized_pnl(self) -> float:
        return self.market_value - self.cost_basis

    @property
    def unrealized_pnl_pct(self) -> float:
        if self.cost_basis == 0:
            return 0.0
        return self.unrealized_pnl / self.cost_basis * 100


class PredictionMarketPortfolio:
    """Manage and analyze a portfolio of prediction market positions."""

    def __init__(self):
        self.positions: List[Position] = []

    def add_position(self, position: Position):
        self.positions.append(position)

    @property
    def total_value(self) -> float:
        return sum(p.market_value for p in self.positions)

    @property
    def total_cost(self) -> float:
        return sum(p.cost_basis for p in self.positions)

    @property
    def total_pnl(self) -> float:
        return self.total_value - self.total_cost

    def scenario_analysis(
        self, outcomes: Dict[str, str]
    ) -> float:
        """
        Compute portfolio value given a set of market outcomes.

        Args:
            outcomes: {market_id: winning_outcome}

        Returns:
            Total portfolio redemption value
        """
        total = 0.0
        for p in self.positions:
            if p.market_id in outcomes:
                if outcomes[p.market_id] == p.outcome:
                    total += p.quantity  # Winning token redeems at 1.0
                # else: 0 (losing token is worthless)
            else:
                total += p.market_value  # Unresolved markets at current price
        return total

    def correlation_exposure(self) -> Dict[str, float]:
        """Identify exposure by market."""
        exposure = {}
        for p in self.positions:
            key = p.market_id
            if key not in exposure:
                exposure[key] = 0.0
            exposure[key] += p.market_value
        return exposure

    def max_loss(self) -> float:
        """
        Compute maximum possible loss (all positions lose).
        """
        return -self.total_cost

    def max_gain(self) -> float:
        """
        Compute maximum possible gain (all positions win).
        """
        return sum(p.quantity for p in self.positions) - self.total_cost

    def summary(self) -> str:
        lines = [
            "Portfolio Summary",
            "=" * 60,
            f"{'Market':<30} {'Outcome':<8} {'Qty':>8} "
            f"{'Entry':>7} {'Curr':>7} {'P&L':>10}",
            "-" * 60
        ]
        for p in self.positions:
            lines.append(
                f"{p.market_question[:30]:<30} {p.outcome:<8} "
                f"{p.quantity:>8.1f} {p.entry_price:>7.3f} "
                f"{p.current_price:>7.3f} {p.unrealized_pnl:>10.2f}"
            )
        lines.append("-" * 60)
        lines.append(
            f"{'TOTAL':<30} {'':>8} {'':>8} {'':>7} "
            f"{'':>7} {self.total_pnl:>10.2f}"
        )
        lines.append(f"Max Loss: {self.max_loss():.2f}")
        lines.append(f"Max Gain: {self.max_gain():.2f}")
        return "\n".join(lines)


# Example usage
portfolio = PredictionMarketPortfolio()
portfolio.add_position(Position(
    market_id="m1", market_question="Will BTC > $100k by Dec 2025?",
    outcome="Yes", quantity=1000, entry_price=0.45, current_price=0.62
))
portfolio.add_position(Position(
    market_id="m2", market_question="Will Fed cut rates in Q1?",
    outcome="No", quantity=500, entry_price=0.30, current_price=0.35
))
portfolio.add_position(Position(
    market_id="m3", market_question="Will AI bill pass Congress?",
    outcome="Yes", quantity=200, entry_price=0.70, current_price=0.55
))

print(portfolio.summary())

# Scenario analysis: m1=Yes, m2=No, m3=No
outcome_scenario = {"m1": "Yes", "m2": "No", "m3": "No"}
redemption_value = portfolio.scenario_analysis(outcome_scenario)
print(f"\nScenario payout: {redemption_value:.2f}")
print(f"Scenario P&L: {redemption_value - portfolio.total_cost:.2f}")

35.6.4 Yield Strategies with Outcome Tokens

Outcome tokens can be integrated into DeFi yield strategies:

Strategy 1: Complete Set Arbitrage If $\sum P_i > 1 + \epsilon$ (where $\epsilon$ accounts for gas and fees), buy all outcome tokens and merge them into collateral for a risk-free profit.

$$\text{Profit} = 1 - \sum_{i} P_i - \text{gas} - \text{fees}$$

Strategy 2: Lending Winning Tokens If a market is heavily favored (e.g., Yes at 0.95), lend Yes tokens on a lending protocol. The borrower pays interest, and if the market resolves Yes, you redeem the tokens. Risk: the market resolves No, and your tokens become worthless.

Strategy 3: Collateralized Positions Use outcome tokens as collateral for loans on DeFi protocols. A Yes token trading at 0.90 might be accepted as collateral at a 50% LTV (loan-to-value), allowing you to borrow 0.45 USDC against each token.

Strategy 4: Calendar Spreads If two markets cover the same question at different time horizons, trade the spread between them.

35.6.5 Token Valuation Edge Cases

Several edge cases affect token valuation:

Time Value. Outcome tokens that will resolve soon are worth more (in risk-adjusted terms) than those resolving far in the future, because capital is locked for a shorter period.

Resolution Risk. The risk that the oracle reports incorrectly or that the market is invalidated. This creates a discount on all outcome tokens.

Liquidity Premium. Illiquid tokens trade at a discount because exiting the position is difficult.

The effective expected return of holding an outcome token is:

$$E[R] = \frac{\Pr(\text{win}) \cdot 1 + \Pr(\text{lose}) \cdot 0}{P_{\text{entry}}} - 1 = \frac{\Pr(\text{win})}{P_{\text{entry}}} - 1$$

If you believe $\Pr(\text{win}) = 0.80$ and the token price is $P = 0.70$:

$$E[R] = \frac{0.80}{0.70} - 1 = 14.3\%$$

But this return is earned over the life of the market. If the market resolves in 30 days, the annualized return is:

$$E[R_{\text{annual}}] = \left(1 + 0.143\right)^{365/30} - 1 \approx 400\%$$

This explains why prediction markets can offer attractive yields --- but the risk is substantial: if the event does not occur, the entire position is lost.


35.7 Smart Contract Security for Prediction Markets

Smart contract security is paramount for prediction markets. Bugs can result in loss of all collateral, incorrect resolution, or market manipulation. This section covers the most common vulnerabilities and testing strategies.

35.7.1 Common Vulnerabilities

Reentrancy. A reentrancy attack occurs when a malicious contract calls back into the prediction market contract before the first invocation is complete. In prediction markets, this could occur during: - Token redemption (the attacker redeems the same tokens multiple times) - Collateral withdrawal - Fee distribution

The classic defense is the checks-effects-interactions pattern:

// VULNERABLE
function redeem(uint256 amount) external {
    uint256 payout = calculatePayout(amount);
    collateral.transfer(msg.sender, payout);  // External call first!
    balances[msg.sender] -= amount;            // State update after!
}

// SAFE
function redeem(uint256 amount) external {
    uint256 payout = calculatePayout(amount);
    balances[msg.sender] -= amount;            // State update first!
    collateral.transfer(msg.sender, payout);   // External call after!
}

Oracle Manipulation. If the oracle can be manipulated, an attacker can force incorrect resolution. Attack vectors include: - Bribing the oracle - Flash loan attacks on price oracle-dependent markets - Timing attacks (reporting the outcome just before a price change)

Front-Running. As discussed in Section 35.5.4, front-running is particularly dangerous for prediction markets with on-chain order books or AMMs.

Integer Overflow/Underflow. While Solidity 0.8+ includes built-in overflow checks, older contracts or those using unchecked blocks are vulnerable. In prediction markets, overflow bugs could allow: - Minting unlimited outcome tokens - Redeeming more collateral than deposited - Bypassing fee calculations

Access Control. Improper access control on oracle functions could allow anyone to resolve a market. Common mistakes: - Missing onlyOracle modifiers - Hardcoded addresses that cannot be updated - Insufficient checks on the condition state machine (e.g., resolving an already-resolved market)

35.7.2 Audit Patterns

Professional audits of prediction market contracts typically check:

  1. Collateral Invariant: After every operation, does total collateral equal total outstanding tokens?
  2. Resolution Correctness: Does the payout vector sum to the correct value? Can a condition be resolved twice?
  3. Access Control: Who can call each function? Are there privileged roles?
  4. Token Accounting: Are ERC-1155 balances correctly updated on split, merge, and redeem?
  5. Edge Cases: Zero amounts, empty partitions, self-transfers, reentrancy vectors.
  6. Upgrade Safety: If the contract is upgradeable, are storage layouts compatible?

35.7.3 Python Security Testing

We can use Python to write security tests for prediction market contracts:

"""
Security testing framework for prediction market smart contracts.
Uses web3.py and pytest to verify contract invariants.
"""

from web3 import Web3
from eth_tester import EthereumTester
from typing import List, Tuple
import pytest

class PredictionMarketSecurityTester:
    """Test security properties of a prediction market contract."""

    def __init__(self, contract_abi: list, contract_bytecode: str):
        """
        Initialize with a local test blockchain.
        """
        self.tester = EthereumTester()
        self.w3 = Web3(Web3.EthereumTesterProvider(self.tester))
        self.accounts = self.tester.get_accounts()

        # Deploy the contract
        Contract = self.w3.eth.contract(
            abi=contract_abi, bytecode=contract_bytecode
        )
        tx_hash = Contract.constructor().transact(
            {'from': self.accounts[0]}
        )
        receipt = self.w3.eth.get_transaction_receipt(tx_hash)
        self.contract = self.w3.eth.contract(
            address=receipt['contractAddress'], abi=contract_abi
        )

    def test_collateral_invariant(
        self, collateral_token: str, condition_id: bytes
    ) -> bool:
        """
        Verify: total collateral == sum of all outcome token supplies.
        """
        # Get collateral balance of the CTF contract
        collateral_contract = self.w3.eth.contract(
            address=collateral_token,
            abi=[{
                "inputs": [{"name": "account", "type": "address"}],
                "name": "balanceOf",
                "outputs": [{"name": "", "type": "uint256"}],
                "type": "function"
            }]
        )
        collateral_balance = collateral_contract.functions.balanceOf(
            self.contract.address
        ).call()

        # Get total supply of each outcome token
        outcome_count = self.contract.functions.getOutcomeSlotCount(
            condition_id
        ).call()
        total_token_supply = 0
        for i in range(outcome_count):
            index_set = 1 << i
            # Compute position ID and get total supply
            # (simplified - actual computation involves collection IDs)
            total_token_supply += self._get_token_supply(
                condition_id, index_set
            )

        return collateral_balance >= total_token_supply

    def test_double_resolution(self, condition_id: bytes) -> bool:
        """
        Test that a condition cannot be resolved twice.
        Should revert on second resolution attempt.
        """
        payout = [0, 1]  # Binary market, Yes wins

        # First resolution should succeed
        try:
            self.contract.functions.reportPayouts(
                condition_id, payout
            ).transact({'from': self.accounts[0]})
        except Exception:
            return False  # First resolution failed unexpectedly

        # Second resolution should fail
        try:
            self.contract.functions.reportPayouts(
                condition_id, payout
            ).transact({'from': self.accounts[0]})
            return False  # Second resolution succeeded - BUG!
        except Exception:
            return True  # Correctly reverted

    def test_zero_amount_operations(self, condition_id: bytes) -> bool:
        """
        Test that zero-amount splits/merges/redeems are handled correctly.
        """
        try:
            # Attempt to split 0 collateral
            self.contract.functions.splitPosition(
                "0x" + "0" * 40,  # collateral token
                b'\x00' * 32,     # parent collection ID
                condition_id,
                [1, 2],           # partition
                0                 # zero amount
            ).transact({'from': self.accounts[0]})
            # If it succeeds, verify no tokens were minted
            return True
        except Exception:
            # Reverting on zero amount is also acceptable behavior
            return True

    def _get_token_supply(
        self, condition_id: bytes, index_set: int
    ) -> int:
        """Get total supply of a specific outcome token."""
        # Implementation depends on contract interface
        # Simplified placeholder
        return 0

    def run_all_tests(self, condition_id: bytes, collateral: str) -> dict:
        """Run all security tests and return results."""
        results = {}
        results['collateral_invariant'] = self.test_collateral_invariant(
            collateral, condition_id
        )
        results['double_resolution'] = self.test_double_resolution(
            condition_id
        )
        results['zero_amount'] = self.test_zero_amount_operations(
            condition_id
        )
        return results

35.7.4 Formal Verification

For high-stakes prediction market contracts, formal verification can mathematically prove that certain properties always hold. Key properties to verify:

  1. Conservation of Value: $\forall \text{state transitions}: \text{collateral}_{\text{before}} = \text{collateral}_{\text{after}}$ (excluding fee operations)
  2. Outcome Completeness: $\forall \text{resolved conditions}: \sum \text{payouts}_i = 1$
  3. No Token Creation from Nothing: $\forall \text{users}: \text{tokens}_{\text{after}} \leq \text{tokens}_{\text{before}} + \text{tokens}_{\text{split}}$

Tools like Certora, Mythril, and Echidna can be used for formal verification and fuzzing of Solidity contracts. While beyond the scope of this Python-focused chapter, awareness of these tools is important for anyone deploying prediction market contracts.


35.8 Building a Simple On-Chain Market

In this section, we will design, implement, and interact with a minimal prediction market smart contract. The goal is pedagogical: we want to understand every component, not build a production system.

35.8.1 Design

Our minimal prediction market has the following features: - Binary outcomes (Yes/No) - ETH as collateral (for simplicity) - A single trusted oracle (the contract deployer) - Split, merge, and redeem operations - No trading --- users trade outcome tokens externally

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

/**
 * @title SimplePredictionMarket
 * @notice A minimal binary prediction market for educational purposes.
 */
contract SimplePredictionMarket {
    address public oracle;
    string public question;
    uint256 public resolutionTime;

    bool public isResolved;
    uint8 public winningOutcome;  // 0 = No, 1 = Yes

    mapping(address => uint256) public yesTokens;
    mapping(address => uint256) public noTokens;
    uint256 public totalSupply;

    event Split(address indexed user, uint256 amount);
    event Merged(address indexed user, uint256 amount);
    event Redeemed(address indexed user, uint256 amount, uint8 outcome);
    event Resolved(uint8 winningOutcome);

    modifier onlyOracle() {
        require(msg.sender == oracle, "Only oracle");
        _;
    }

    modifier notResolved() {
        require(!isResolved, "Already resolved");
        _;
    }

    modifier onlyResolved() {
        require(isResolved, "Not yet resolved");
        _;
    }

    constructor(string memory _question, uint256 _resolutionTime) {
        oracle = msg.sender;
        question = _question;
        resolutionTime = _resolutionTime;
    }

    /**
     * @notice Deposit ETH and receive equal amounts of Yes and No tokens.
     */
    function split() external payable notResolved {
        require(msg.value > 0, "Must deposit ETH");
        yesTokens[msg.sender] += msg.value;
        noTokens[msg.sender] += msg.value;
        totalSupply += msg.value;
        emit Split(msg.sender, msg.value);
    }

    /**
     * @notice Return equal amounts of Yes and No tokens to receive ETH back.
     */
    function merge(uint256 amount) external notResolved {
        require(yesTokens[msg.sender] >= amount, "Insufficient Yes tokens");
        require(noTokens[msg.sender] >= amount, "Insufficient No tokens");

        yesTokens[msg.sender] -= amount;
        noTokens[msg.sender] -= amount;
        totalSupply -= amount;

        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "ETH transfer failed");

        emit Merged(msg.sender, amount);
    }

    /**
     * @notice Oracle resolves the market.
     */
    function resolve(uint8 _winningOutcome) external onlyOracle notResolved {
        require(block.timestamp >= resolutionTime, "Too early");
        require(_winningOutcome <= 1, "Invalid outcome");

        isResolved = true;
        winningOutcome = _winningOutcome;

        emit Resolved(_winningOutcome);
    }

    /**
     * @notice Redeem winning tokens for ETH.
     */
    function redeem() external onlyResolved {
        uint256 amount;
        if (winningOutcome == 1) {
            amount = yesTokens[msg.sender];
            yesTokens[msg.sender] = 0;
        } else {
            amount = noTokens[msg.sender];
            noTokens[msg.sender] = 0;
        }

        require(amount > 0, "Nothing to redeem");

        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "ETH transfer failed");

        emit Redeemed(msg.sender, amount, winningOutcome);
    }
}

35.8.2 Deploying to a Local Testnet

We can deploy and test this contract using Python:

"""
Deploy and interact with SimplePredictionMarket on a local test blockchain.
"""

from web3 import Web3
from solcx import compile_standard, install_solc
import json
import time

# Install Solidity compiler
install_solc("0.8.19")

# Contract source
SOLIDITY_SOURCE = """
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract SimplePredictionMarket {
    address public oracle;
    string public question;
    uint256 public resolutionTime;
    bool public isResolved;
    uint8 public winningOutcome;
    mapping(address => uint256) public yesTokens;
    mapping(address => uint256) public noTokens;
    uint256 public totalSupply;

    event Split(address indexed user, uint256 amount);
    event Merged(address indexed user, uint256 amount);
    event Redeemed(address indexed user, uint256 amount, uint8 outcome);
    event Resolved(uint8 winningOutcome);

    modifier onlyOracle() { require(msg.sender == oracle, "Only oracle"); _; }
    modifier notResolved() { require(!isResolved, "Already resolved"); _; }
    modifier onlyResolved() { require(isResolved, "Not yet resolved"); _; }

    constructor(string memory _question, uint256 _resolutionTime) {
        oracle = msg.sender;
        question = _question;
        resolutionTime = _resolutionTime;
    }

    function split() external payable notResolved {
        require(msg.value > 0, "Must deposit ETH");
        yesTokens[msg.sender] += msg.value;
        noTokens[msg.sender] += msg.value;
        totalSupply += msg.value;
        emit Split(msg.sender, msg.value);
    }

    function merge(uint256 amount) external notResolved {
        require(yesTokens[msg.sender] >= amount, "Insufficient Yes");
        require(noTokens[msg.sender] >= amount, "Insufficient No");
        yesTokens[msg.sender] -= amount;
        noTokens[msg.sender] -= amount;
        totalSupply -= amount;
        (bool s, ) = msg.sender.call{value: amount}("");
        require(s, "Transfer failed");
        emit Merged(msg.sender, amount);
    }

    function resolve(uint8 _outcome) external onlyOracle notResolved {
        require(block.timestamp >= resolutionTime, "Too early");
        require(_outcome <= 1, "Invalid");
        isResolved = true;
        winningOutcome = _outcome;
        emit Resolved(_outcome);
    }

    function redeem() external onlyResolved {
        uint256 amount;
        if (winningOutcome == 1) {
            amount = yesTokens[msg.sender];
            yesTokens[msg.sender] = 0;
        } else {
            amount = noTokens[msg.sender];
            noTokens[msg.sender] = 0;
        }
        require(amount > 0, "Nothing");
        (bool s, ) = msg.sender.call{value: amount}("");
        require(s, "Transfer failed");
        emit Redeemed(msg.sender, amount, _outcome);
    }
}
"""

def compile_contract():
    """Compile the Solidity contract."""
    compiled = compile_standard({
        "language": "Solidity",
        "sources": {
            "SimplePredictionMarket.sol": {"content": SOLIDITY_SOURCE}
        },
        "settings": {
            "outputSelection": {
                "*": {"*": ["abi", "evm.bytecode"]}
            }
        }
    }, solc_version="0.8.19")

    contract_data = compiled["contracts"]["SimplePredictionMarket.sol"][
        "SimplePredictionMarket"
    ]
    return contract_data["abi"], contract_data["evm"]["bytecode"]["object"]


def deploy_and_test():
    """Deploy the contract and run through the full lifecycle."""
    # Connect to local test blockchain (e.g., Ganache)
    w3 = Web3(Web3.HTTPProvider("http://127.0.0.1:8545"))

    # Check connection
    if not w3.is_connected():
        print("ERROR: Could not connect to local blockchain.")
        print("Start Ganache or another local node first.")
        return

    accounts = w3.eth.accounts
    oracle = accounts[0]
    alice = accounts[1]
    bob = accounts[2]

    # Compile
    abi, bytecode = compile_contract()

    # Deploy
    Contract = w3.eth.contract(abi=abi, bytecode=bytecode)
    resolution_time = int(time.time()) + 60  # Resolves in 60 seconds

    tx_hash = Contract.constructor(
        "Will ETH exceed $5000 by end of 2025?",
        resolution_time
    ).transact({'from': oracle, 'gas': 3000000})

    receipt = w3.eth.get_transaction_receipt(tx_hash)
    market = w3.eth.contract(
        address=receipt['contractAddress'], abi=abi
    )
    print(f"Market deployed at: {market.address}")
    print(f"Question: {market.functions.question().call()}")

    # Alice splits 1 ETH into Yes/No tokens
    tx = market.functions.split().transact({
        'from': alice,
        'value': w3.to_wei(1, 'ether'),
        'gas': 200000
    })
    w3.eth.wait_for_transaction_receipt(tx)

    alice_yes = market.functions.yesTokens(alice).call()
    alice_no = market.functions.noTokens(alice).call()
    print(f"Alice: {w3.from_wei(alice_yes, 'ether')} Yes, "
          f"{w3.from_wei(alice_no, 'ether')} No")

    # Bob splits 2 ETH
    tx = market.functions.split().transact({
        'from': bob,
        'value': w3.to_wei(2, 'ether'),
        'gas': 200000
    })
    w3.eth.wait_for_transaction_receipt(tx)

    total = market.functions.totalSupply().call()
    print(f"Total supply: {w3.from_wei(total, 'ether')} ETH")

    # Wait for resolution time
    print("Waiting for resolution time...")
    time.sleep(65)

    # Oracle resolves: Yes wins
    tx = market.functions.resolve(1).transact({
        'from': oracle, 'gas': 100000
    })
    w3.eth.wait_for_transaction_receipt(tx)
    print(f"Resolved: outcome = {market.functions.winningOutcome().call()}")

    # Alice redeems
    balance_before = w3.eth.get_balance(alice)
    tx = market.functions.redeem().transact({
        'from': alice, 'gas': 100000
    })
    w3.eth.wait_for_transaction_receipt(tx)
    balance_after = w3.eth.get_balance(alice)
    profit = balance_after - balance_before
    print(f"Alice redeemed: ~{w3.from_wei(profit, 'ether'):.4f} ETH")

    # Bob redeems
    balance_before = w3.eth.get_balance(bob)
    tx = market.functions.redeem().transact({
        'from': bob, 'gas': 100000
    })
    w3.eth.wait_for_transaction_receipt(tx)
    balance_after = w3.eth.get_balance(bob)
    profit = balance_after - balance_before
    print(f"Bob redeemed: ~{w3.from_wei(profit, 'ether'):.4f} ETH")


if __name__ == "__main__":
    deploy_and_test()

35.8.3 Testing the Contract

A production contract would require extensive testing:

"""
Test suite for SimplePredictionMarket.
"""

import pytest
from web3 import Web3

class TestSimplePredictionMarket:
    """Test cases for the SimplePredictionMarket contract."""

    def test_split_creates_equal_tokens(self, market, alice, w3):
        """Splitting should create equal Yes and No tokens."""
        amount = w3.to_wei(1, 'ether')
        market.functions.split().transact({
            'from': alice, 'value': amount
        })
        assert market.functions.yesTokens(alice).call() == amount
        assert market.functions.noTokens(alice).call() == amount

    def test_merge_returns_collateral(self, market, alice, w3):
        """Merging should return collateral."""
        amount = w3.to_wei(1, 'ether')
        market.functions.split().transact({
            'from': alice, 'value': amount
        })

        balance_before = w3.eth.get_balance(alice)
        market.functions.merge(amount).transact({'from': alice})
        balance_after = w3.eth.get_balance(alice)

        # Balance should increase (minus gas costs)
        assert balance_after > balance_before - w3.to_wei(0.01, 'ether')

    def test_cannot_resolve_before_time(self, market, oracle):
        """Resolution should fail before the resolution time."""
        with pytest.raises(Exception):
            market.functions.resolve(1).transact({'from': oracle})

    def test_only_oracle_can_resolve(self, market, alice):
        """Non-oracle accounts cannot resolve the market."""
        with pytest.raises(Exception):
            market.functions.resolve(1).transact({'from': alice})

    def test_cannot_resolve_twice(self, market, oracle, w3):
        """Market cannot be resolved twice."""
        # Fast-forward time (in test blockchain)
        # First resolution
        market.functions.resolve(1).transact({'from': oracle})

        # Second resolution should fail
        with pytest.raises(Exception):
            market.functions.resolve(0).transact({'from': oracle})

    def test_only_winning_tokens_redeem(self, market, alice, oracle, w3):
        """Only winning outcome tokens should be redeemable."""
        amount = w3.to_wei(1, 'ether')
        market.functions.split().transact({
            'from': alice, 'value': amount
        })

        # Resolve as Yes (outcome 1)
        market.functions.resolve(1).transact({'from': oracle})

        # Redeem
        market.functions.redeem().transact({'from': alice})

        # Yes tokens should be 0 (redeemed)
        assert market.functions.yesTokens(alice).call() == 0
        # No tokens should still exist (worthless)
        assert market.functions.noTokens(alice).call() == amount

    def test_collateral_invariant(self, market, alice, bob, w3):
        """Contract balance should always equal total supply."""
        # Alice splits 1 ETH
        market.functions.split().transact({
            'from': alice, 'value': w3.to_wei(1, 'ether')
        })
        # Bob splits 2 ETH
        market.functions.split().transact({
            'from': bob, 'value': w3.to_wei(2, 'ether')
        })

        contract_balance = w3.eth.get_balance(market.address)
        total_supply = market.functions.totalSupply().call()

        assert contract_balance == total_supply

35.9 Advanced Market Patterns

Beyond simple binary markets, smart contracts enable sophisticated market structures that would be impractical to implement in traditional systems.

35.9.1 Combinatorial Markets

A combinatorial market allows trading on combinations of outcomes across multiple conditions. For example, given two conditions: - Condition A: "Who will win the election?" (Candidate X, Candidate Y) - Condition B: "Will GDP growth exceed 3%?" (Yes, No)

A combinatorial market enables positions like "Candidate X wins AND GDP exceeds 3%."

In the CTF, this is achieved through nested splitting. Start with collateral and split on Condition A, then split each resulting token on Condition B:

1 USDC
  |
  split on A: {X, Y}
  |
  +-- 1 Token(A=X)
  |     |
  |     split on B: {Yes, No}
  |     |
  |     +-- 1 Token(A=X, B=Yes)
  |     +-- 1 Token(A=X, B=No)
  |
  +-- 1 Token(A=Y)
        |
        split on B: {Yes, No}
        |
        +-- 1 Token(A=Y, B=Yes)
        +-- 1 Token(A=Y, B=No)

This creates four outcome tokens from 1 USDC, covering all combinations. The collection IDs are computed using XOR:

$$\texttt{collectionId}(A=X, B=\text{Yes}) = \texttt{collectionId}(A=X) \oplus \texttt{collectionId}(B=\text{Yes})$$

35.9.2 Conditional-on-Conditional Tokens

The CTF supports "conditional-on-conditional" positions, where a position is contingent on the outcome of another condition. This enables:

  • Conditional bets: "If X wins the election, will policy Y be implemented?"
  • Hedging: A position that pays off only if both X wins AND the market crashes, providing insurance for a specific scenario.

The parentCollectionId parameter in splitPosition enables this nesting. Instead of splitting from raw collateral (parentCollectionId = 0), you split from an existing position.

# Computing nested position IDs
def compute_nested_position(
    collateral: str,
    condition_a_id: bytes,
    index_set_a: int,
    condition_b_id: bytes,
    index_set_b: int
) -> int:
    """
    Compute the position ID for a nested conditional position:
    condition_a[index_set_a] AND condition_b[index_set_b]
    """
    # Collection ID for condition A
    collection_a = Web3.solidity_keccak(
        ['bytes32', 'uint256'], [condition_a_id, index_set_a]
    )

    # Collection ID for condition B
    collection_b = Web3.solidity_keccak(
        ['bytes32', 'uint256'], [condition_b_id, index_set_b]
    )

    # Combined collection ID (XOR)
    combined = bytes(a ^ b for a, b in zip(collection_a, collection_b))

    # Position ID
    pos_hash = Web3.solidity_keccak(
        ['address', 'bytes32'], [collateral, combined]
    )
    return int.from_bytes(pos_hash, 'big')

35.9.3 Market Factories

A market factory is a smart contract that deploys new market contracts according to a template. This pattern is used by Polymarket and Omen to create markets efficiently:

// Simplified Market Factory
contract MarketFactory {
    address public ctf;
    address public oracle;

    event MarketCreated(
        bytes32 indexed conditionId,
        bytes32 questionId,
        uint256 outcomeSlotCount
    );

    constructor(address _ctf, address _oracle) {
        ctf = _ctf;
        oracle = _oracle;
    }

    function createMarket(
        bytes32 questionId,
        uint256 outcomeSlotCount
    ) external returns (bytes32) {
        // Prepare the condition in the CTF
        IConditionalTokens(ctf).prepareCondition(
            oracle,
            questionId,
            outcomeSlotCount
        );

        bytes32 conditionId = keccak256(
            abi.encodePacked(oracle, questionId, outcomeSlotCount)
        );

        emit MarketCreated(conditionId, questionId, outcomeSlotCount);
        return conditionId;
    }
}

35.9.4 Parameterized Markets

Parameterized markets use a template with variable parameters to create families of related markets. For example:

  • Time-series markets: "Will BTC be above $X on date $Y?" for various X and Y
  • Strike-price markets: Similar to options, "Will ETH be above $3000/$4000/$5000?"
  • Conditional markets: "If event A occurs, will event B occur within Z days?"
# Parameterized market factory
from typing import List, Tuple
from datetime import datetime, timedelta

class ParameterizedMarketFactory:
    """Create families of related prediction markets."""

    def __init__(self, ctf_contract, oracle_address: str):
        self.ctf = ctf_contract
        self.oracle = oracle_address

    def create_price_ladder(
        self,
        asset: str,
        strikes: List[float],
        expiry: datetime
    ) -> List[dict]:
        """
        Create a ladder of binary markets at different price levels.

        Example: BTC price ladder at [80k, 90k, 100k, 110k, 120k]
        """
        markets = []
        for strike in strikes:
            question = (
                f"Will {asset} be above ${strike:,.0f} "
                f"on {expiry.strftime('%Y-%m-%d')}?"
            )
            question_id = Web3.solidity_keccak(
                ['string'], [question]
            )

            condition_id = Web3.solidity_keccak(
                ['address', 'bytes32', 'uint256'],
                [self.oracle, question_id, 2]
            )

            markets.append({
                'question': question,
                'question_id': question_id.hex(),
                'condition_id': condition_id.hex(),
                'strike': strike,
                'expiry': expiry.isoformat(),
                'outcomes': ['No', 'Yes']
            })

        return markets

    def create_time_series(
        self,
        question_template: str,
        start_date: datetime,
        end_date: datetime,
        interval_days: int
    ) -> List[dict]:
        """
        Create a time series of markets with the same question
        at different dates.
        """
        markets = []
        current = start_date
        while current <= end_date:
            question = question_template.format(
                date=current.strftime('%Y-%m-%d')
            )
            question_id = Web3.solidity_keccak(
                ['string'], [question]
            )
            condition_id = Web3.solidity_keccak(
                ['address', 'bytes32', 'uint256'],
                [self.oracle, question_id, 2]
            )

            markets.append({
                'question': question,
                'question_id': question_id.hex(),
                'condition_id': condition_id.hex(),
                'resolution_date': current.isoformat()
            })
            current += timedelta(days=interval_days)

        return markets

35.9.5 Automated Resolution Markets

Some market types can be resolved automatically using on-chain data:

  • Price markets: Resolved using Chainlink or other on-chain price feeds
  • Block-based markets: "Will Ethereum block 20,000,000 have more than 200 transactions?"
  • Protocol metrics: "Will Uniswap v3 TVL exceed $5B on date X?"

These markets eliminate the oracle problem entirely for certain question types, making them particularly suited for on-chain deployment.


35.10 Interacting with Deployed Markets from Python

This section provides practical Python code for interacting with live prediction markets on Polygon.

35.10.1 Reading Market Positions

"""
Read and analyze positions from Polymarket's CTF on Polygon.
"""

from web3 import Web3
from typing import Dict, List, Optional
import json
import requests

class PolymarketReader:
    """Read data from Polymarket's on-chain contracts."""

    CTF_ADDRESS = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045"
    USDC_ADDRESS = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"
    NEG_RISK_ADAPTER = "0xC5d563A36AE78145C45a50134d48A1215220f80a"

    # Minimal ABIs for reading
    CTF_ABI = json.loads('''[
        {
            "inputs": [
                {"name": "account", "type": "address"},
                {"name": "id", "type": "uint256"}
            ],
            "name": "balanceOf",
            "outputs": [{"name": "", "type": "uint256"}],
            "stateMutability": "view",
            "type": "function"
        },
        {
            "inputs": [
                {"name": "accounts", "type": "address[]"},
                {"name": "ids", "type": "uint256[]"}
            ],
            "name": "balanceOfBatch",
            "outputs": [{"name": "", "type": "uint256[]"}],
            "stateMutability": "view",
            "type": "function"
        },
        {
            "inputs": [{"name": "conditionId", "type": "bytes32"}],
            "name": "getOutcomeSlotCount",
            "outputs": [{"name": "", "type": "uint256"}],
            "stateMutability": "view",
            "type": "function"
        },
        {
            "inputs": [{"name": "conditionId", "type": "bytes32"}],
            "name": "payoutDenominator",
            "outputs": [{"name": "", "type": "uint256"}],
            "stateMutability": "view",
            "type": "function"
        }
    ]''')

    def __init__(self, rpc_url: str = "https://polygon-rpc.com"):
        self.w3 = Web3(Web3.HTTPProvider(rpc_url))
        self.ctf = self.w3.eth.contract(
            address=self.CTF_ADDRESS, abi=self.CTF_ABI
        )

    def get_position_balance(
        self, account: str, position_id: int
    ) -> int:
        """Get the balance of a specific position for an account."""
        return self.ctf.functions.balanceOf(account, position_id).call()

    def get_multiple_balances(
        self, account: str, position_ids: List[int]
    ) -> List[int]:
        """Get balances for multiple positions in a single call."""
        accounts = [account] * len(position_ids)
        return self.ctf.functions.balanceOfBatch(
            accounts, position_ids
        ).call()

    def is_condition_resolved(self, condition_id: bytes) -> bool:
        """Check if a condition has been resolved."""
        payout_denominator = self.ctf.functions.payoutDenominator(
            condition_id
        ).call()
        return payout_denominator > 0

    def get_market_from_api(self, condition_id: str) -> Optional[dict]:
        """
        Fetch market details from Polymarket's API.
        Note: This uses Polymarket's centralized API, not on-chain data.
        """
        try:
            response = requests.get(
                f"https://clob.polymarket.com/markets/{condition_id}"
            )
            if response.status_code == 200:
                return response.json()
        except requests.RequestException:
            pass
        return None


class PositionTracker:
    """Track and analyze prediction market positions over time."""

    def __init__(self, reader: PolymarketReader):
        self.reader = reader
        self.snapshots: List[Dict] = []

    def take_snapshot(
        self, account: str, positions: Dict[str, int]
    ) -> Dict:
        """
        Take a snapshot of current position values.

        Args:
            account: Wallet address
            positions: {label: position_id}
        """
        import time

        snapshot = {
            'timestamp': time.time(),
            'account': account,
            'positions': {}
        }

        position_ids = list(positions.values())
        balances = self.reader.get_multiple_balances(account, position_ids)

        for (label, pid), balance in zip(positions.items(), balances):
            snapshot['positions'][label] = {
                'position_id': pid,
                'balance': balance,
                'balance_usdc': balance / 1e6  # USDC has 6 decimals
            }

        self.snapshots.append(snapshot)
        return snapshot

    def calculate_pnl(
        self, entry_prices: Dict[str, float]
    ) -> Dict[str, float]:
        """
        Calculate P&L for each position given entry prices.

        Args:
            entry_prices: {label: entry_price}
        """
        if not self.snapshots:
            return {}

        latest = self.snapshots[-1]
        pnl = {}
        for label, data in latest['positions'].items():
            balance = data['balance_usdc']
            if label in entry_prices:
                cost_basis = balance * entry_prices[label]
                # Current value depends on market price
                # (would need order book data for current price)
                pnl[label] = {
                    'balance': balance,
                    'cost_basis': cost_basis,
                    'entry_price': entry_prices[label]
                }
        return pnl

35.10.2 Monitoring Market Activity

"""
Monitor prediction market activity by watching on-chain events.
"""

from web3 import Web3
from typing import Callable, Optional
import json
import time

class MarketMonitor:
    """Monitor CTF events on Polygon."""

    # Key event signatures
    EVENTS = {
        'ConditionPreparation': Web3.keccak(
            text='ConditionPreparation(bytes32,address,bytes32,uint256)'
        ),
        'ConditionResolution': Web3.keccak(
            text='ConditionResolution(bytes32,address,bytes32,uint256,uint256[])'
        ),
        'PositionSplit': Web3.keccak(
            text='PositionSplit(address,address,bytes32,bytes32,uint256[],uint256)'
        ),
        'PositionsMerge': Web3.keccak(
            text='PositionsMerge(address,address,bytes32,bytes32,uint256[],uint256)'
        ),
        'PayoutRedemption': Web3.keccak(
            text='PayoutRedemption(address,address,bytes32,bytes32,uint256[],uint256)'
        ),
    }

    def __init__(
        self,
        rpc_url: str,
        ctf_address: str,
        callback: Optional[Callable] = None
    ):
        self.w3 = Web3(Web3.HTTPProvider(rpc_url))
        self.ctf_address = ctf_address
        self.callback = callback or self._default_callback

    def _default_callback(self, event_type: str, log: dict):
        """Default event handler: print event details."""
        print(f"[{event_type}] Block: {log['blockNumber']}, "
              f"Tx: {log['transactionHash'].hex()}")

    def scan_blocks(
        self,
        from_block: int,
        to_block: int,
        event_type: Optional[str] = None
    ) -> list:
        """
        Scan a range of blocks for CTF events.
        """
        topics = []
        if event_type and event_type in self.EVENTS:
            topics = [self.EVENTS[event_type].hex()]

        all_logs = []
        # Process in chunks of 1000 blocks (RPC limits)
        chunk_size = 1000
        for start in range(from_block, to_block, chunk_size):
            end = min(start + chunk_size - 1, to_block)
            try:
                logs = self.w3.eth.get_logs({
                    'address': self.ctf_address,
                    'fromBlock': start,
                    'toBlock': end,
                    'topics': topics if topics else None
                })
                all_logs.extend(logs)
            except Exception as e:
                print(f"Error scanning blocks {start}-{end}: {e}")

        return all_logs

    def watch_live(self, poll_interval: int = 5):
        """
        Watch for new events in real-time by polling.
        """
        latest_block = self.w3.eth.block_number
        print(f"Starting live watch from block {latest_block}")

        while True:
            try:
                current_block = self.w3.eth.block_number
                if current_block > latest_block:
                    logs = self.scan_blocks(latest_block + 1, current_block)
                    for log in logs:
                        event_type = self._identify_event(log)
                        self.callback(event_type, log)
                    latest_block = current_block
            except Exception as e:
                print(f"Error in live watch: {e}")

            time.sleep(poll_interval)

    def _identify_event(self, log: dict) -> str:
        """Identify the event type from log topics."""
        if not log.get('topics'):
            return 'Unknown'
        topic0 = log['topics'][0]
        for name, sig in self.EVENTS.items():
            if topic0 == sig:
                return name
        return 'Unknown'

    def get_split_volume(
        self, from_block: int, to_block: int
    ) -> dict:
        """
        Calculate total split volume in a block range.
        """
        logs = self.scan_blocks(from_block, to_block, 'PositionSplit')
        total_splits = len(logs)
        # Decode amounts from log data
        total_amount = 0
        for log in logs:
            try:
                # Amount is the last 32 bytes of the log data
                data = log['data']
                if isinstance(data, str):
                    data = bytes.fromhex(data[2:])
                # Amount is at the end of the data field
                amount = int.from_bytes(data[-32:], 'big')
                total_amount += amount
            except Exception:
                pass

        return {
            'block_range': (from_block, to_block),
            'total_splits': total_splits,
            'total_amount_raw': total_amount,
            'total_amount_usdc': total_amount / 1e6
        }

35.10.3 Automated Trading Integration

"""
Automated trading integration for Polymarket.
WARNING: This is educational code. Do not use in production without
thorough testing, security review, and risk management.
"""

from web3 import Web3
from eth_account import Account
from eth_account.messages import encode_typed_data
from typing import Optional, Dict
import json
import time
import requests

class PolymarketTrader:
    """
    Interface for automated trading on Polymarket.

    This class handles:
    - Order signing (EIP-712)
    - Order submission to the CLOB API
    - Position management
    """

    CLOB_API = "https://clob.polymarket.com"
    CHAIN_ID = 137  # Polygon mainnet

    # CTF Exchange contract address
    EXCHANGE_ADDRESS = "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E"

    def __init__(
        self,
        private_key: str,
        rpc_url: str = "https://polygon-rpc.com"
    ):
        self.w3 = Web3(Web3.HTTPProvider(rpc_url))
        self.account = Account.from_key(private_key)
        self.address = self.account.address

    def create_order(
        self,
        token_id: int,
        side: str,       # 'BUY' or 'SELL'
        price: float,    # 0.01 to 0.99
        size: float,     # Number of outcome tokens
        expiration: Optional[int] = None
    ) -> Dict:
        """
        Create and sign a Polymarket order.

        NOTE: This is a simplified version. The actual Polymarket
        order signing uses a specific EIP-712 domain and types.
        """
        if expiration is None:
            expiration = int(time.time()) + 86400  # 24 hours

        nonce = int(time.time() * 1000)

        # Calculate amounts based on side
        if side == 'BUY':
            maker_amount = int(price * size * 1e6)  # USDC (6 decimals)
            taker_amount = int(size * 1e6)           # Outcome tokens
        else:
            maker_amount = int(size * 1e6)           # Outcome tokens
            taker_amount = int(price * size * 1e6)   # USDC

        order = {
            'maker': self.address,
            'taker': '0x' + '0' * 40,  # Open order (any taker)
            'tokenId': str(token_id),
            'makerAmount': str(maker_amount),
            'takerAmount': str(taker_amount),
            'side': side,
            'expiration': str(expiration),
            'nonce': str(nonce),
            'feeRateBps': '0',
            'signatureType': '2',  # EIP-712
        }

        # Sign the order using EIP-712
        # (Simplified - actual implementation uses Polymarket's domain)
        domain = {
            'name': 'Polymarket CTF Exchange',
            'version': '1',
            'chainId': self.CHAIN_ID,
            'verifyingContract': self.EXCHANGE_ADDRESS
        }

        types = {
            'Order': [
                {'name': 'maker', 'type': 'address'},
                {'name': 'taker', 'type': 'address'},
                {'name': 'tokenId', 'type': 'uint256'},
                {'name': 'makerAmount', 'type': 'uint256'},
                {'name': 'takerAmount', 'type': 'uint256'},
                {'name': 'expiration', 'type': 'uint256'},
                {'name': 'nonce', 'type': 'uint256'},
                {'name': 'feeRateBps', 'type': 'uint256'},
                {'name': 'signatureType', 'type': 'uint8'},
            ]
        }

        message = {
            'maker': self.address,
            'taker': '0x' + '0' * 40,
            'tokenId': token_id,
            'makerAmount': maker_amount,
            'takerAmount': taker_amount,
            'expiration': expiration,
            'nonce': nonce,
            'feeRateBps': 0,
            'signatureType': 2,
        }

        # Note: In production, use the actual EIP-712 signing
        # This is a placeholder for the signature
        order['signature'] = '0x' + '00' * 65  # Placeholder

        return order

    def submit_order(self, order: Dict) -> Optional[Dict]:
        """
        Submit a signed order to Polymarket's CLOB API.
        """
        try:
            response = requests.post(
                f"{self.CLOB_API}/order",
                json=order,
                headers={'Content-Type': 'application/json'}
            )
            if response.status_code == 200:
                return response.json()
            else:
                print(f"Order submission failed: {response.text}")
                return None
        except requests.RequestException as e:
            print(f"API error: {e}")
            return None

    def get_orderbook(self, token_id: int) -> Optional[Dict]:
        """Fetch the current order book for a token."""
        try:
            response = requests.get(
                f"{self.CLOB_API}/book",
                params={'token_id': str(token_id)}
            )
            if response.status_code == 200:
                return response.json()
        except requests.RequestException:
            pass
        return None

    def get_midpoint_price(self, token_id: int) -> Optional[float]:
        """Get the midpoint price from the order book."""
        book = self.get_orderbook(token_id)
        if book and book.get('bids') and book.get('asks'):
            best_bid = float(book['bids'][0]['price'])
            best_ask = float(book['asks'][0]['price'])
            return (best_bid + best_ask) / 2
        return None


# Usage example (DO NOT run with real funds without proper testing)
def example_usage():
    """Demonstrate the trading interface."""
    # WARNING: Never hardcode private keys. Use environment variables.
    import os
    private_key = os.environ.get('POLYMARKET_PRIVATE_KEY', '')

    if not private_key:
        print("Set POLYMARKET_PRIVATE_KEY environment variable")
        return

    trader = PolymarketTrader(private_key)

    # Example: Buy 100 Yes tokens at $0.55
    token_id = 12345  # Replace with actual token ID
    order = trader.create_order(
        token_id=token_id,
        side='BUY',
        price=0.55,
        size=100
    )

    print(f"Order created:")
    print(f"  Maker: {order['maker']}")
    print(f"  Side: {order['side']}")
    print(f"  Price: $0.55")
    print(f"  Size: 100 tokens")
    print(f"  Cost: ${0.55 * 100:.2f}")

    # In production, you would submit the order:
    # result = trader.submit_order(order)

35.10.4 Calculating P&L from On-Chain Data

"""
Calculate P&L from on-chain transaction history.
"""

from web3 import Web3
from typing import List, Dict
from dataclasses import dataclass, field
from datetime import datetime

@dataclass
class Trade:
    """Represents a single trade."""
    timestamp: datetime
    token_id: int
    side: str  # 'BUY' or 'SELL'
    quantity: float
    price: float
    tx_hash: str
    gas_cost_usd: float = 0.0

    @property
    def notional(self) -> float:
        return self.quantity * self.price


class PnLCalculator:
    """Calculate P&L from a series of trades."""

    def __init__(self):
        self.trades: List[Trade] = []
        self.positions: Dict[int, dict] = {}  # token_id -> position info

    def add_trade(self, trade: Trade):
        """Record a trade and update position."""
        self.trades.append(trade)

        if trade.token_id not in self.positions:
            self.positions[trade.token_id] = {
                'quantity': 0.0,
                'cost_basis': 0.0,
                'avg_entry_price': 0.0,
                'realized_pnl': 0.0,
                'total_fees': 0.0
            }

        pos = self.positions[trade.token_id]

        if trade.side == 'BUY':
            # Update average entry price
            total_cost = (
                pos['quantity'] * pos['avg_entry_price']
                + trade.quantity * trade.price
            )
            pos['quantity'] += trade.quantity
            if pos['quantity'] > 0:
                pos['avg_entry_price'] = total_cost / pos['quantity']
            pos['cost_basis'] += trade.notional
        else:  # SELL
            if pos['quantity'] > 0:
                # Realized P&L = (sell_price - avg_entry_price) * quantity
                realized = (
                    (trade.price - pos['avg_entry_price']) * trade.quantity
                )
                pos['realized_pnl'] += realized
                pos['quantity'] -= trade.quantity
                pos['cost_basis'] -= pos['avg_entry_price'] * trade.quantity

        pos['total_fees'] += trade.gas_cost_usd

    def record_redemption(
        self, token_id: int, payout_per_token: float
    ):
        """Record a redemption event (market resolution)."""
        if token_id in self.positions:
            pos = self.positions[token_id]
            redemption_value = pos['quantity'] * payout_per_token
            realized = redemption_value - pos['cost_basis']
            pos['realized_pnl'] += realized
            pos['quantity'] = 0
            pos['cost_basis'] = 0

    def get_unrealized_pnl(
        self, token_id: int, current_price: float
    ) -> float:
        """Calculate unrealized P&L for a position."""
        if token_id not in self.positions:
            return 0.0
        pos = self.positions[token_id]
        current_value = pos['quantity'] * current_price
        return current_value - pos['cost_basis']

    def summary(self, current_prices: Dict[int, float] = None) -> str:
        """Generate a P&L summary."""
        if current_prices is None:
            current_prices = {}

        lines = [
            "P&L Summary",
            "=" * 70,
            f"{'Token ID':<20} {'Qty':>8} {'Avg Entry':>10} "
            f"{'Realized':>12} {'Unrealized':>12} {'Fees':>8}",
            "-" * 70
        ]

        total_realized = 0.0
        total_unrealized = 0.0
        total_fees = 0.0

        for token_id, pos in self.positions.items():
            current = current_prices.get(token_id, pos['avg_entry_price'])
            unrealized = self.get_unrealized_pnl(token_id, current)

            lines.append(
                f"{token_id:<20} {pos['quantity']:>8.1f} "
                f"${pos['avg_entry_price']:>9.4f} "
                f"${pos['realized_pnl']:>11.2f} "
                f"${unrealized:>11.2f} "
                f"${pos['total_fees']:>7.2f}"
            )

            total_realized += pos['realized_pnl']
            total_unrealized += unrealized
            total_fees += pos['total_fees']

        lines.append("-" * 70)
        lines.append(
            f"{'TOTAL':<20} {'':>8} {'':>10} "
            f"${total_realized:>11.2f} "
            f"${total_unrealized:>11.2f} "
            f"${total_fees:>7.2f}"
        )
        lines.append(
            f"Net P&L (after fees): "
            f"${total_realized + total_unrealized - total_fees:.2f}"
        )

        return "\n".join(lines)


# Example
calc = PnLCalculator()

# Simulate a series of trades
calc.add_trade(Trade(
    timestamp=datetime(2025, 1, 15), token_id=1001,
    side='BUY', quantity=100, price=0.45,
    tx_hash='0xabc...', gas_cost_usd=0.01
))
calc.add_trade(Trade(
    timestamp=datetime(2025, 1, 20), token_id=1001,
    side='BUY', quantity=50, price=0.50,
    tx_hash='0xdef...', gas_cost_usd=0.01
))
calc.add_trade(Trade(
    timestamp=datetime(2025, 2, 1), token_id=1001,
    side='SELL', quantity=75, price=0.65,
    tx_hash='0xghi...', gas_cost_usd=0.01
))

print(calc.summary(current_prices={1001: 0.70}))

35.11 Chapter Summary

This chapter provided an exhaustive tour of smart contract mechanisms for prediction markets. We covered:

  1. On-Chain Market Architecture. The five stages (creation, minting, trading, resolution, redemption) and three architecture patterns (monolithic, modular, hybrid).

  2. Conditional Token Framework. Gnosis CTF's core primitives (conditions, outcome slots, index sets, positions), the split/merge/redeem operations, and the mathematics of partitions and position IDs.

  3. Augur's Protocol. Market creation, REP-based reporting, escalating dispute resolution, the fork mechanism, and lessons from Augur's operational history.

  4. Polymarket's Architecture. The Neg Risk Adapter, CLOB with off-chain matching, UMA Optimistic Oracle, and the complete trade lifecycle.

  5. Trading Mechanism Trade-offs. AMMs vs. CLOBs, gas costs, MEV exposure, and hybrid approaches.

  6. Token Economics. ERC-1155 outcome tokens, pricing theory, portfolio analysis, yield strategies, and valuation edge cases.

  7. Security. Reentrancy, oracle manipulation, front-running, access control, audit patterns, and Python-based security testing.

  8. Building Markets. A complete walkthrough of a minimal on-chain prediction market: design, Solidity implementation, deployment, and Python interaction.

  9. Advanced Patterns. Combinatorial markets, conditional-on-conditional tokens, market factories, and parameterized markets.

  10. Python Integration. Reading positions, monitoring events, automated trading, and P&L calculation.

The key takeaway is that smart contract prediction markets are not just decentralized versions of traditional markets. They are a new primitive --- composable outcome tokens that can interact with the broader DeFi ecosystem. Understanding the mechanics at this level is essential for building, analyzing, or trading on these platforms.


What's Next

In Chapter 36, we will explore Decentralized Oracle Systems in depth. We will examine how oracles bridge the gap between real-world events and on-chain contracts, compare different oracle designs (Chainlink, UMA, Augur's REP-based system), and analyze the game theory of truthful reporting. The oracle problem is the fundamental bottleneck for on-chain prediction markets, and understanding it is critical for evaluating the trustworthiness of any decentralized market.


References

  1. Gnosis. "Conditional Token Framework Documentation." docs.gnosis.io/conditionaltokens, 2019.
  2. Peterson, J., Krug, J., Zoltu, M., Williams, A., & Alexander, S. "Augur: A Decentralized Oracle and Prediction Market Platform." Forecast Foundation, 2018.
  3. UMA Protocol. "Optimistic Oracle Documentation." docs.umaproject.org, 2021.
  4. Polymarket. "Technical Documentation." docs.polymarket.com, 2023.
  5. Buterin, V. "An Introduction to Futarchy." blog.ethereum.org, 2014.
  6. Hanson, R. "Logarithmic Market Scoring Rules for Modular Combinatorial Information Aggregation." Journal of Prediction Markets, 2007.
  7. Daian, P., et al. "Flash Boys 2.0: Frontrunning in Decentralized Exchanges, Miner Extractable Value, and Consensus Instability." IEEE S&P, 2020.
  8. OpenZeppelin. "ERC-1155 Multi Token Standard." docs.openzeppelin.com, 2021.