30 min read

> "The blockchain does one thing: it replaces third-party trust with mathematical proof."

Chapter 34: Blockchain Fundamentals for Prediction Markets

"The blockchain does one thing: it replaces third-party trust with mathematical proof." — Adam Draper

Throughout the earlier parts of this book, we explored prediction markets primarily through the lens of centralized platforms---exchanges operated by companies that match orders, hold funds, and settle contracts. While centralized prediction markets have powered decades of successful forecasting, they carry inherent limitations: regulatory constraints restrict who can participate, operator insolvency threatens deposited funds, and geographic boundaries fragment global liquidity. Blockchain technology offers a fundamentally different architecture for prediction markets, one where the rules of the market are encoded in immutable code, settlement happens automatically, and anyone with an internet connection can participate.

This chapter provides the technical foundation you need to understand---and build upon---blockchain-based prediction markets. We will start from first principles: what a blockchain is, how Ethereum works, and what smart contracts do. From there, we will connect Python to the blockchain using web3.py, explore token standards that underpin market tokens, examine Layer 2 scaling solutions that make on-chain prediction markets practical, and learn how to read real on-chain prediction market data. By the end of this chapter, you will have the conceptual framework and practical skills to interact programmatically with any blockchain-based prediction market.


34.1 Why Blockchain for Prediction Markets?

Before diving into the technical details, let us establish why blockchain technology matters for prediction markets. This is not technology for technology's sake---blockchain solves specific, concrete problems that have plagued prediction markets for decades.

34.1.1 Censorship Resistance

Traditional prediction markets operate at the pleasure of governments and payment processors. Intrade, once the world's most prominent prediction market, was shut down by the U.S. Commodity Futures Trading Commission (CFTC) in 2012. Even platforms operating in favorable jurisdictions face constant pressure from regulators who view event contracts as gambling.

A smart contract deployed on Ethereum or another public blockchain cannot be "shut down" by any single entity. The contract exists on thousands of nodes across the globe. As long as the underlying blockchain operates, the prediction market contract continues to function. This does not mean blockchain prediction markets are beyond the reach of law---operators of front-end interfaces can be targeted, and on-ramps to fiat currency remain regulated---but the core market mechanism becomes extraordinarily resilient.

34.1.2 Global Access

Centralized prediction markets require accounts, identity verification (KYC), and often restrict users by jurisdiction. PredictIt caps positions at $850; Kalshi requires U.S. residency for many markets. These restrictions fragment liquidity and limit the information aggregation that makes prediction markets valuable.

Blockchain-based prediction markets are permissionless by default. Anyone with a cryptocurrency wallet can trade. This global access pool increases liquidity, incorporates information from a wider set of participants, and brings prediction markets closer to the theoretical ideal of efficient information aggregation.

34.1.3 Trustless Settlement

In a centralized prediction market, the operator decides when and how contracts settle. Users must trust that the operator will honor the outcome. History shows this trust is sometimes misplaced---platforms have disputed settlements, delayed payouts, or simply disappeared.

Smart contracts execute settlement automatically based on pre-defined rules and oracle-provided data. When the outcome is reported, the contract distributes funds according to the encoded logic. No human intervention is required, and no operator can override the result (assuming the contract is properly designed and the oracle is reliable).

34.1.4 Transparency

Every trade, every market creation, and every settlement on a blockchain-based prediction market is publicly verifiable. Anyone can audit the contract code, verify the outstanding positions, and confirm that settlement was correct. This transparency builds trust and enables external analysis.

34.1.5 Composability

Perhaps the most underappreciated advantage of on-chain prediction markets is composability---the ability of different protocols to interact with each other. A prediction market token (representing a position in a market) is just another token on the blockchain. It can be:

  • Used as collateral in lending protocols
  • Traded on decentralized exchanges
  • Bundled into structured products
  • Integrated into automated trading strategies

This composability creates an ecosystem where prediction market positions become building blocks for more complex financial instruments.

34.1.6 The Value Proposition, Summarized

Feature Centralized PM Blockchain PM
Censorship resistance Low High
Global access Restricted Permissionless
Settlement trust Operator-dependent Code-enforced
Transparency Limited Full
Composability None Native
Speed Fast Varies (L1 vs L2)
Cost per trade Low Varies (gas fees)
Regulatory clarity Clearer Ambiguous
User experience Familiar Learning curve

Blockchain prediction markets are not strictly superior to centralized alternatives---they involve trade-offs in speed, cost, and user experience. But for many use cases, the benefits of trustlessness, global access, and composability outweigh the costs. Understanding the underlying technology lets you make informed decisions about which architecture best serves your needs.


34.2 Blockchain Fundamentals

34.2.1 What Is a Blockchain?

A blockchain is a distributed, append-only data structure maintained by a network of nodes that follow a consensus protocol. Let us unpack each element.

Distributed: The blockchain is replicated across thousands of computers (nodes) worldwide. No single node is authoritative; the network collectively maintains the canonical state.

Append-only: New data is added in sequential blocks. Once a block is added to the chain, it is extremely difficult to modify retroactively. This property is called immutability.

Consensus protocol: Nodes must agree on which blocks to add and in what order. The consensus mechanism is the algorithm that achieves this agreement despite the presence of faulty or malicious nodes.

34.2.2 Blocks, Chains, and Hashing

A block is a data structure containing:

  1. A header with metadata (timestamp, block number, reference to the previous block)
  2. A set of transactions (the actual data being recorded)
  3. A hash of the block's contents

The critical innovation is the chain structure. Each block header contains the hash of the previous block, creating a cryptographic link between consecutive blocks:

Block N-1                    Block N                      Block N+1
+-----------------+         +-----------------+          +-----------------+
| Header          |         | Header          |          | Header          |
|   prev_hash: ...| <------ |   prev_hash: H(N-1) <---- |   prev_hash: H(N)|
|   timestamp     |         |   timestamp     |          |   timestamp     |
|   nonce         |         |   nonce         |          |   nonce         |
+-----------------+         +-----------------+          +-----------------+
| Transactions    |         | Transactions    |          | Transactions    |
|   tx1, tx2, ... |         |   tx1, tx2, ... |          |   tx1, tx2, ... |
+-----------------+         +-----------------+          +-----------------+

A cryptographic hash function $H$ takes input of any size and produces a fixed-size output (e.g., 256 bits for SHA-256). It has three essential properties:

  1. Deterministic: The same input always produces the same output.
  2. Collision-resistant: It is computationally infeasible to find two different inputs with the same hash.
  3. Avalanche effect: A tiny change in the input produces a drastically different output.

Because each block includes the hash of the previous block, modifying any historical block would change its hash, which would invalidate the next block's prev_hash reference, cascading through every subsequent block. Retroactively altering data requires recomputing every block from the modified one to the chain tip---and doing so faster than the rest of the network adds new blocks. This is what makes blockchains tamper-evident.

34.2.3 Merkle Trees

Transactions within a block are organized into a Merkle tree (binary hash tree). Each leaf node is the hash of a transaction; each internal node is the hash of its two children. The root of the tree (the Merkle root) is stored in the block header.

$$\text{MerkleRoot} = H\bigl(H(H(tx_1) \| H(tx_2)) \| H(H(tx_3) \| H(tx_4))\bigr)$$

Merkle trees enable efficient verification: to prove that a transaction is included in a block, you only need to provide $O(\log n)$ hashes (the Merkle proof), not the entire block. This is critical for light clients that cannot store the full blockchain.

34.2.4 Consensus Mechanisms

The consensus mechanism determines how the network agrees on the next valid block. The two dominant approaches are:

Proof of Work (PoW): Miners compete to solve a computationally expensive puzzle. The first miner to find a valid solution (a nonce that makes the block hash satisfy a difficulty target) gets to propose the next block and earns a reward. Bitcoin uses PoW. The security assumption is that no single entity controls more than 50% of the network's computational power.

The puzzle requires finding nonce $n$ such that:

$$H(\text{block\_header} \| n) < \text{target}$$

where the target is inversely proportional to the difficulty.

Proof of Stake (PoS): Validators stake (lock up) cryptocurrency as collateral. The protocol selects validators to propose and attest to blocks based on their stake. Validators who act honestly earn rewards; those who misbehave lose their stake (this is called slashing). Ethereum transitioned from PoW to PoS in September 2022 ("The Merge").

Property Proof of Work Proof of Stake
Security basis Computational power Economic stake
Energy consumption Very high Low
Hardware requirement Specialized (ASICs) Standard servers
Finality Probabilistic Faster, often economic
Decentralization Mining pool concentration Stake concentration

34.2.5 Finality

Finality refers to the point at which a transaction is considered irreversible. In PoW systems like Bitcoin, finality is probabilistic: the more blocks added after your transaction, the harder it becomes to reverse. The convention is to wait for 6 confirmations (~60 minutes on Bitcoin).

Ethereum's PoS provides stronger finality guarantees. Blocks are finalized in "epochs" (roughly 6.4 minutes), after which reverting them would require destroying at least one-third of all staked ETH---an enormous economic cost.

For prediction market applications, finality matters because settlement and payout transactions must be irreversible. Building on a chain with strong finality guarantees reduces the risk of settlement disputes.

34.2.6 The Decentralization Spectrum

Not all blockchains are equally decentralized. The spectrum ranges from:

  • Fully centralized: A single entity controls all nodes (essentially a traditional database with blockchain branding).
  • Consortium/permissioned: A known set of entities operate nodes (e.g., Hyperledger).
  • Public/permissionless: Anyone can run a node and participate in consensus (e.g., Bitcoin, Ethereum).

For prediction markets, the degree of decentralization directly impacts censorship resistance and trust assumptions. A "blockchain prediction market" running on a permissioned chain controlled by a single company offers few advantages over a traditional database.


34.3 Ethereum and the EVM

34.3.1 Why Ethereum?

While Bitcoin introduced the blockchain concept, it was designed primarily for transferring value. Ethereum, proposed by Vitalik Buterin in 2013 and launched in 2015, extended the blockchain paradigm with a Turing-complete programming environment. This means Ethereum can execute arbitrary programs---including the complex logic required for prediction markets.

Nearly all major decentralized prediction markets (Augur, Polymarket, Gnosis/Omen) are built on Ethereum or Ethereum-compatible chains. Understanding Ethereum is therefore essential for anyone working with blockchain prediction markets.

34.3.2 Ethereum Architecture

Ethereum maintains a world state---a mapping from addresses to account states. Every transaction modifies this world state, and the state root (a hash of the entire state) is stored in each block header.

The Ethereum state can be thought of as a key-value store:

$$\text{State}: \text{Address} \rightarrow \text{AccountState}$$

where each AccountState contains:

  • Nonce: Number of transactions sent (for EOAs) or contracts created (for contract accounts)
  • Balance: Amount of Ether (ETH) held
  • Storage root: Hash of the account's storage trie (for contracts)
  • Code hash: Hash of the account's code (empty for EOAs)

34.3.3 Account Types

Ethereum has two types of accounts:

Externally Owned Accounts (EOAs): Controlled by private keys. These are "user accounts." An EOA can send transactions (transfers or contract calls) by signing them with its private key. EOAs have no code.

Contract Accounts: Controlled by their code. A contract account has associated bytecode (the compiled smart contract) and storage. It cannot initiate transactions on its own---it can only execute code in response to a transaction from an EOA or a call from another contract.

EOA (User Account)                Contract Account
+------------------+              +------------------+
| Address: 0xABC...|              | Address: 0xDEF...|
| Nonce: 5         |              | Nonce: 1         |
| Balance: 2.5 ETH |              | Balance: 100 ETH |
| Code: (empty)    |              | Code: (bytecode) |
| Storage: (empty) |              | Storage: (state)  |
+------------------+              +------------------+

34.3.4 Transactions

A transaction is a signed message from an EOA that modifies the world state. Every transaction contains:

Field Description
from Sender address (EOA)
to Recipient address (EOA or contract; empty for contract creation)
value Amount of ETH to transfer (in wei; 1 ETH = $10^{18}$ wei)
data Input data (function call for contracts, empty for simple transfers)
nonce Sender's transaction count (prevents replay attacks)
gasLimit Maximum gas units the transaction can consume
maxFeePerGas Maximum total fee per gas unit (post-EIP-1559)
maxPriorityFeePerGas Maximum tip per gas unit for the validator
signature ECDSA signature proving authorization

34.3.5 Gas and Gas Limits

Gas is the unit of computational effort on Ethereum. Every operation in the EVM has a fixed gas cost:

Operation Gas Cost
Addition (ADD) 3
Multiplication (MUL) 5
Storage write (SSTORE, new) 20,000
Storage write (SSTORE, update) 5,000
ETH transfer 21,000 (base)
Contract creation 32,000+

The gas price determines how much you pay per unit of gas. Since EIP-1559 (August 2021), Ethereum uses a two-component fee model:

$$\text{Transaction Fee} = \text{gas\_used} \times (\text{base\_fee} + \text{priority\_fee})$$

  • Base fee: Algorithmically determined by network congestion. This portion is burned (destroyed).
  • Priority fee (tip): Goes to the validator. Higher tips incentivize faster inclusion.

The gas limit is the maximum gas a transaction can consume. If execution exceeds the gas limit, the transaction reverts but the gas is still consumed (you pay for the failed attempt). This prevents infinite loops and denial-of-service attacks.

For prediction market transactions: - A simple token transfer might cost ~65,000 gas - Placing a bet on a prediction market might cost 100,000--300,000 gas - Creating a new market might cost 500,000--2,000,000 gas - Settling a market might cost 200,000--1,000,000 gas

At a base fee of 20 gwei ($1 \text{ gwei} = 10^{-9} \text{ ETH}$) and ETH at \$3,000, a 200,000-gas transaction costs approximately:

$$200{,}000 \times 20 \times 10^{-9} \times 3{,}000 = \$12.00$$

This illustrates why gas costs are a major concern for on-chain prediction markets, and why Layer 2 solutions (Section 34.7) are so important.

34.3.6 The EVM Execution Model

The Ethereum Virtual Machine (EVM) is the runtime environment for smart contracts. It is a stack-based virtual machine with the following characteristics:

  • Stack: Up to 1,024 elements, each 256 bits (32 bytes) wide
  • Memory: Byte-addressable, volatile (cleared between calls)
  • Storage: Word-addressable (256-bit keys and values), persistent (stored on-chain)
  • Calldata: Read-only input data for the current transaction
  • Returndata: Output from the most recent external call

When a transaction calls a contract function, the EVM:

  1. Loads the contract's bytecode
  2. Initializes a new execution context (stack, memory, gas counter)
  3. Executes opcodes sequentially
  4. Deducts gas for each operation
  5. Either completes successfully (committing state changes) or reverts (rolling back all changes)

The EVM is deterministic: given the same input state and transaction, every node will produce the same output. This determinism is what enables consensus---all nodes can independently verify that a block's state transitions are correct.

34.3.7 State Management

Ethereum's state is stored in a Modified Merkle Patricia Trie, a data structure that combines the properties of Merkle trees (efficient proofs) and Patricia tries (efficient key-value lookup). This structure enables:

  • Efficient verification that a particular account has a particular balance
  • Light clients that can verify state without downloading the full database
  • Historical state queries (with archive nodes)

For prediction market builders, understanding state management is important because every piece of data stored on-chain (market parameters, positions, balances) costs gas to write and update. Efficient state design directly impacts the cost of operating a prediction market protocol.


34.4 Smart Contracts Basics

34.4.1 What Are Smart Contracts?

A smart contract is a program that runs on the blockchain. Once deployed, its code is immutable (unless designed with upgrade patterns) and its execution is deterministic and verifiable. Smart contracts can:

  • Hold and transfer cryptocurrency
  • Maintain state (e.g., market parameters, user balances)
  • Enforce rules (e.g., market settlement logic)
  • Interact with other contracts (composability)

For prediction markets, smart contracts serve as the "exchange"---they define the rules of trading, hold collateral, and execute settlement.

34.4.2 Solidity Overview

Solidity is the most widely used language for Ethereum smart contracts. It is statically typed, supports inheritance, and compiles to EVM bytecode. Here is a minimal example:

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

contract SimpleBet {
    // State variables
    address public oracle;
    string public question;
    bool public resolved;
    bool public outcome;

    mapping(address => uint256) public yesShares;
    mapping(address => uint256) public noShares;

    uint256 public totalYes;
    uint256 public totalNo;

    // Events
    event BetPlaced(address indexed bettor, bool position, uint256 amount);
    event MarketResolved(bool outcome);
    event Payout(address indexed recipient, uint256 amount);

    // Modifier
    modifier onlyOracle() {
        require(msg.sender == oracle, "Only oracle can call this");
        _;
    }

    modifier notResolved() {
        require(!resolved, "Market already resolved");
        _;
    }

    // Constructor
    constructor(string memory _question, address _oracle) {
        question = _question;
        oracle = _oracle;
    }

    // Buy YES shares
    function buyYes() external payable notResolved {
        require(msg.value > 0, "Must send ETH");
        yesShares[msg.sender] += msg.value;
        totalYes += msg.value;
        emit BetPlaced(msg.sender, true, msg.value);
    }

    // Buy NO shares
    function buyNo() external payable notResolved {
        require(msg.value > 0, "Must send ETH");
        noShares[msg.sender] += msg.value;
        totalNo += msg.value;
        emit BetPlaced(msg.sender, false, msg.value);
    }

    // Resolve the market
    function resolve(bool _outcome) external onlyOracle notResolved {
        resolved = true;
        outcome = _outcome;
        emit MarketResolved(_outcome);
    }

    // Claim winnings
    function claim() external {
        require(resolved, "Market not resolved");

        uint256 payout = 0;
        uint256 totalPool = totalYes + totalNo;

        if (outcome) {
            // YES wins
            uint256 shares = yesShares[msg.sender];
            require(shares > 0, "No winning shares");
            payout = (shares * totalPool) / totalYes;
            yesShares[msg.sender] = 0;
        } else {
            // NO wins
            uint256 shares = noShares[msg.sender];
            require(shares > 0, "No winning shares");
            payout = (shares * totalPool) / totalNo;
            noShares[msg.sender] = 0;
        }

        emit Payout(msg.sender, payout);
        payable(msg.sender).transfer(payout);
    }
}

Let us examine the key elements of this contract.

34.4.3 Contract Structure

State Variables are stored on-chain in the contract's storage. Each state variable occupies one or more 256-bit storage slots. In our example, oracle, question, resolved, outcome, totalYes, totalNo, and the two mappings are all state variables.

address public oracle;           // 20 bytes, stored in slot 0
string public question;          // Dynamic, stored starting at slot 1
bool public resolved;            // 1 byte, packed into slot 2
bool public outcome;             // 1 byte, packed into slot 2
mapping(address => uint256) public yesShares;  // Slot 3 (base)

Functions define the contract's interface. They can be: - external: Callable only from outside the contract - public: Callable from anywhere - internal: Callable only within the contract and derived contracts - private: Callable only within the contract

Functions that read state but do not modify it are marked view; functions that neither read nor modify state are marked pure. These do not cost gas when called externally (they run locally on the node).

Events are logs emitted during execution that are stored in the transaction receipt (not in contract storage). Events are cheap to emit and are the primary way off-chain applications track what happened on-chain. In our example, BetPlaced, MarketResolved, and Payout are events.

Modifiers are reusable conditions that can be applied to functions. The onlyOracle modifier ensures that only the designated oracle address can call the resolve function. The _; syntax indicates where the modified function's body is inserted.

The Constructor runs once during deployment and cannot be called again. It initializes the contract's state.

34.4.4 Deploying Contracts

Deploying a contract involves:

  1. Compiling the Solidity source to bytecode
  2. Creating a transaction with an empty to field and the bytecode as data
  3. Signing and sending the transaction
  4. The EVM executes the constructor and stores the resulting bytecode at a new address

The contract address is deterministic, computed from the deployer's address and nonce:

$$\text{address} = \text{keccak256}(\text{RLP}(\text{sender}, \text{nonce}))[12:]$$

34.4.5 The ABI (Application Binary Interface)

The ABI defines how to encode function calls and decode return values when interacting with a contract. It is a JSON specification that describes:

  • Function names, input parameters, and output parameters
  • Event names and parameters
  • Error definitions

When you call buyYes() with a value, the actual transaction data is the ABI-encoded function selector (first 4 bytes of the keccak256 hash of the function signature) followed by the encoded parameters:

$$\text{data} = \text{keccak256}(\texttt{"buyYes()"})[:4] = \texttt{0x...}$$

Python's web3.py handles ABI encoding automatically, but understanding what happens under the hood is valuable for debugging and security analysis.

34.4.6 Contract Interactions

Contracts can call other contracts, creating complex interaction chains. In the prediction market ecosystem:

  • A market factory contract creates individual market contracts
  • Market contracts call token contracts to transfer outcome tokens
  • Oracle contracts provide resolution data to market contracts
  • Router contracts aggregate liquidity across multiple markets

This composability is what enables sophisticated prediction market protocols like Polymarket and Augur.


34.5 Connecting Python to Blockchain with web3.py

34.5.1 Installation

web3.py is the standard Python library for interacting with Ethereum-compatible blockchains:

pip install web3

For additional functionality:

pip install web3[tester]  # Includes a local test blockchain
pip install eth-account    # Account management
pip install eth-abi        # ABI encoding/decoding

34.5.2 Provider Setup

web3.py connects to the blockchain through a provider---a service that runs an Ethereum node and exposes an API. There are three types:

HTTP Provider (most common for applications):

from web3 import Web3

# Connect to Ethereum mainnet via Infura
w3 = Web3(Web3.HTTPProvider('https://mainnet.infura.io/v3/YOUR_PROJECT_ID'))

# Connect to Ethereum mainnet via Alchemy
w3 = Web3(Web3.HTTPProvider('https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY'))

# Verify connection
print(f"Connected: {w3.is_connected()}")
print(f"Chain ID: {w3.eth.chain_id}")
print(f"Latest block: {w3.eth.block_number}")

WebSocket Provider (for real-time subscriptions):

w3 = Web3(Web3.WebsocketProvider('wss://mainnet.infura.io/ws/v3/YOUR_PROJECT_ID'))

IPC Provider (for local nodes):

w3 = Web3(Web3.IPCProvider('/path/to/geth.ipc'))

For development, you can use a local test blockchain:

from web3 import Web3, EthereumTesterProvider

w3 = Web3(EthereumTesterProvider())
print(f"Test accounts: {w3.eth.accounts}")

Obtaining API Keys: Both Infura and Alchemy offer free tiers sufficient for development and moderate usage. Sign up at infura.io or alchemy.com, create a project, and copy the API key.

34.5.3 Reading Blockchain Data

With a connection established, you can read blockchain data without paying gas (read operations are free):

from web3 import Web3

w3 = Web3(Web3.HTTPProvider('https://mainnet.infura.io/v3/YOUR_PROJECT_ID'))

# Get the latest block number
block_number = w3.eth.block_number
print(f"Latest block: {block_number}")

# Get a specific block
block = w3.eth.get_block(block_number)
print(f"Block hash: {block['hash'].hex()}")
print(f"Timestamp: {block['timestamp']}")
print(f"Transactions: {len(block['transactions'])}")
print(f"Gas used: {block['gasUsed']}")

# Get an account balance
address = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'  # vitalik.eth
balance_wei = w3.eth.get_balance(address)
balance_eth = w3.from_wei(balance_wei, 'ether')
print(f"Balance: {balance_eth} ETH")

# Get a transaction
tx_hash = '0x...'  # Any transaction hash
tx = w3.eth.get_transaction(tx_hash)
print(f"From: {tx['from']}")
print(f"To: {tx['to']}")
print(f"Value: {w3.from_wei(tx['value'], 'ether')} ETH")

# Get a transaction receipt (includes logs/events)
receipt = w3.eth.get_transaction_receipt(tx_hash)
print(f"Status: {'Success' if receipt['status'] == 1 else 'Failed'}")
print(f"Gas used: {receipt['gasUsed']}")
print(f"Logs: {len(receipt['logs'])}")

34.5.4 Sending Transactions

Sending transactions requires a private key and costs gas:

from web3 import Web3
from eth_account import Account

w3 = Web3(Web3.HTTPProvider('https://sepolia.infura.io/v3/YOUR_PROJECT_ID'))

# NEVER hardcode private keys in production!
# Use environment variables or a secure key management system
private_key = '0x...'
account = Account.from_key(private_key)

# Build a transaction
tx = {
    'from': account.address,
    'to': '0xRecipientAddress...',
    'value': w3.to_wei(0.01, 'ether'),
    'nonce': w3.eth.get_transaction_count(account.address),
    'gas': 21000,
    'maxFeePerGas': w3.to_wei(50, 'gwei'),
    'maxPriorityFeePerGas': w3.to_wei(2, 'gwei'),
    'chainId': 11155111,  # Sepolia testnet
}

# Sign the transaction
signed_tx = w3.eth.account.sign_transaction(tx, private_key)

# Send the signed transaction
tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction)
print(f"Transaction sent: {tx_hash.hex()}")

# Wait for confirmation
receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
print(f"Confirmed in block: {receipt['blockNumber']}")

34.5.5 Interacting with Smart Contracts

To interact with a deployed contract, you need its address and ABI:

from web3 import Web3
import json

w3 = Web3(Web3.HTTPProvider('https://mainnet.infura.io/v3/YOUR_PROJECT_ID'))

# Contract address and ABI
contract_address = '0x...'
contract_abi = json.loads('[...]')  # ABI JSON array

# Create a contract object
contract = w3.eth.contract(address=contract_address, abi=contract_abi)

# Call a read-only function (free, no gas)
result = contract.functions.totalYes().call()
print(f"Total YES: {result}")

# Get the question
question = contract.functions.question().call()
print(f"Question: {question}")

# Build a transaction to call a state-changing function
tx = contract.functions.buyYes().build_transaction({
    'from': account.address,
    'value': w3.to_wei(0.1, 'ether'),
    'nonce': w3.eth.get_transaction_count(account.address),
    'gas': 200000,
    'maxFeePerGas': w3.to_wei(50, 'gwei'),
    'maxPriorityFeePerGas': w3.to_wei(2, 'gwei'),
})

# Sign and send
signed_tx = w3.eth.account.sign_transaction(tx, private_key)
tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction)
receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
print(f"Bet placed! Gas used: {receipt['gasUsed']}")

34.5.6 Reading Events

Events are crucial for tracking prediction market activity:

# Get past events
bet_filter = contract.events.BetPlaced.create_filter(
    from_block=18000000,
    to_block='latest'
)
events = bet_filter.get_all_entries()

for event in events:
    print(f"Bettor: {event['args']['bettor']}")
    print(f"Position: {'YES' if event['args']['position'] else 'NO'}")
    print(f"Amount: {w3.from_wei(event['args']['amount'], 'ether')} ETH")
    print(f"Block: {event['blockNumber']}")
    print("---")

# Alternative: get_logs (more flexible)
logs = w3.eth.get_logs({
    'fromBlock': 18000000,
    'toBlock': 'latest',
    'address': contract_address,
    'topics': [w3.keccak(text='BetPlaced(address,bool,uint256)').hex()]
})

34.6 Token Standards: ERC-20 and Beyond

34.6.1 ERC-20: The Fungible Token Standard

ERC-20 (Ethereum Request for Comments 20) is the standard interface for fungible tokens---tokens where every unit is identical and interchangeable. Most prediction market platforms use ERC-20 tokens to represent outcome shares.

The ERC-20 interface specifies these functions:

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address to, uint256 amount) external returns (bool);
    function allowance(address owner, address spender) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address from, address to, uint256 amount) external returns (bool);

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

Key concepts:

  • balanceOf(address): Returns how many tokens a given address holds
  • transfer(to, amount): Directly sends tokens from the caller to another address
  • approve(spender, amount): Authorizes another address (typically a contract) to spend tokens on your behalf
  • transferFrom(from, to, amount): Moves tokens from one address to another, using the allowance mechanism

The approve/transferFrom pattern is fundamental to how prediction markets work. When you place a bet, you first approve the market contract to spend your collateral tokens, then the market contract calls transferFrom to pull the collateral from your wallet.

34.6.2 Interacting with ERC-20 Tokens in Python

from web3 import Web3

w3 = Web3(Web3.HTTPProvider('https://mainnet.infura.io/v3/YOUR_PROJECT_ID'))

# Standard ERC-20 ABI (minimal)
ERC20_ABI = [
    {
        "constant": True,
        "inputs": [{"name": "_owner", "type": "address"}],
        "name": "balanceOf",
        "outputs": [{"name": "balance", "type": "uint256"}],
        "type": "function"
    },
    {
        "constant": True,
        "inputs": [],
        "name": "decimals",
        "outputs": [{"name": "", "type": "uint8"}],
        "type": "function"
    },
    {
        "constant": True,
        "inputs": [],
        "name": "symbol",
        "outputs": [{"name": "", "type": "string"}],
        "type": "function"
    },
    {
        "constant": True,
        "inputs": [],
        "name": "totalSupply",
        "outputs": [{"name": "", "type": "uint256"}],
        "type": "function"
    },
    {
        "constant": False,
        "inputs": [
            {"name": "_spender", "type": "address"},
            {"name": "_value", "type": "uint256"}
        ],
        "name": "approve",
        "outputs": [{"name": "", "type": "bool"}],
        "type": "function"
    },
    {
        "constant": False,
        "inputs": [
            {"name": "_to", "type": "address"},
            {"name": "_value", "type": "uint256"}
        ],
        "name": "transfer",
        "outputs": [{"name": "", "type": "bool"}],
        "type": "function"
    }
]

# USDC on Ethereum mainnet
usdc_address = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'
usdc = w3.eth.contract(address=usdc_address, abi=ERC20_ABI)

# Read token info
symbol = usdc.functions.symbol().call()
decimals = usdc.functions.decimals().call()
total_supply = usdc.functions.totalSupply().call()

print(f"Token: {symbol}")
print(f"Decimals: {decimals}")
print(f"Total Supply: {total_supply / 10**decimals:,.2f}")

# Check a balance
address = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'
balance = usdc.functions.balanceOf(address).call()
print(f"Balance: {balance / 10**decimals:,.2f} {symbol}")

34.6.3 ERC-721: Non-Fungible Tokens

ERC-721 defines the standard for non-fungible tokens (NFTs)---tokens where each unit is unique. While less common in prediction markets, NFTs can represent:

  • Unique market positions (e.g., a specific bet with specific terms)
  • Governance rights over specific markets
  • Achievement badges for accurate forecasters

34.6.4 ERC-1155: Multi-Token Standard

ERC-1155 combines fungible and non-fungible tokens in a single contract. This is particularly relevant for prediction markets because a single contract can manage multiple outcome tokens:

  • Token ID 0: YES shares for Market A
  • Token ID 1: NO shares for Market A
  • Token ID 2: YES shares for Market B
  • Token ID 3: NO shares for Market B

Polymarket uses a Conditional Token Framework (CTF) based on ERC-1155, where each market outcome is represented by a distinct token ID within the same contract.

34.6.5 Prediction Market Tokens

In a typical blockchain prediction market:

  1. Collateral token: The base currency (e.g., USDC) used to fund positions
  2. Outcome tokens: ERC-20 or ERC-1155 tokens representing shares in each outcome

The relationship between collateral and outcome tokens is governed by the market maker contract:

$$\text{Collateral} \xrightarrow{\text{split}} \text{YES tokens} + \text{NO tokens}$$

$$\text{YES tokens} + \text{NO tokens} \xrightarrow{\text{merge}} \text{Collateral}$$

For a binary market, 1 unit of collateral can always be split into 1 YES token and 1 NO token, and vice versa. After resolution:

  • If outcome is YES: each YES token redeems for 1 unit of collateral
  • If outcome is NO: each NO token redeems for 1 unit of collateral
  • Losing tokens have zero redemption value

This split/merge mechanism ensures that YES price + NO price $\approx$ 1 (minus fees), maintaining the prediction market invariant.


34.7 Layer 2 Solutions

34.7.1 The Scalability Problem

Ethereum mainnet (Layer 1, or L1) processes approximately 15-30 transactions per second. During peak demand, gas prices can spike to hundreds of gwei, making simple transactions cost $50-100 or more. For prediction markets, where a single trade might need to be economically viable at \$1 or less, L1 gas costs are prohibitive.

Layer 2 (L2) solutions process transactions off the main Ethereum chain while inheriting its security guarantees. They achieve this through various mechanisms:

34.7.2 Types of Layer 2 Solutions

Optimistic Rollups (Optimism, Arbitrum, Base): - Assume transactions are valid by default - Allow a challenge period (typically 7 days) where anyone can submit a fraud proof - If fraud is detected, the invalid transaction is reverted and the malicious party is penalized - Lower cost, higher throughput, but withdrawals to L1 have a delay

ZK-Rollups (zkSync, StarkNet, Polygon zkEVM): - Generate cryptographic validity proofs for each batch of transactions - Proofs are verified on L1, providing immediate finality - More computationally expensive to generate proofs, but withdrawals are faster

Sidechains (Polygon PoS): - Independent blockchains with their own consensus mechanism - Connected to Ethereum via bridges - Lower security guarantees than rollups (security depends on the sidechain's validators, not Ethereum's) - Very low fees and fast transactions

34.7.3 Comparison for Prediction Markets

Property Ethereum L1 Optimistic Rollup ZK-Rollup Sidechain (Polygon)
TPS ~15-30 ~2,000-4,000 ~2,000-10,000 ~7,000
Gas cost (swap) $5-50 | $0.10-1.00 $0.05-0.50 | $0.001-0.01
Finality ~12 min ~minutes (L2), 7 days (L1) ~minutes (L2+L1) ~2 seconds
Security Ethereum consensus Ethereum + fraud proofs Ethereum + validity proofs Own validators
Withdrawal to L1 N/A ~7 days ~hours ~30 min (PoS bridge)

34.7.4 Polymarket on Polygon

Polymarket, the largest decentralized prediction market by volume, operates on the Polygon PoS sidechain. This choice reflects the trade-off between security and usability:

  • Low fees: Trading costs fractions of a cent, enabling small bets
  • Fast confirmations: 2-second block times provide near-instant trade confirmation
  • USDC collateral: Uses the Polygon-bridged version of USDC
  • Acceptable security: For prediction market sizes (typically <$100M per market), Polygon's security is sufficient

34.7.5 Connecting to Layer 2 with Python

Connecting to an L2 from Python is identical to connecting to L1---you just use a different RPC endpoint:

from web3 import Web3

# Polygon PoS
polygon_w3 = Web3(Web3.HTTPProvider('https://polygon-rpc.com'))
print(f"Polygon chain ID: {polygon_w3.eth.chain_id}")  # 137

# Arbitrum One
arbitrum_w3 = Web3(Web3.HTTPProvider('https://arb1.arbitrum.io/rpc'))
print(f"Arbitrum chain ID: {arbitrum_w3.eth.chain_id}")  # 42161

# Optimism
optimism_w3 = Web3(Web3.HTTPProvider('https://mainnet.optimism.io'))
print(f"Optimism chain ID: {optimism_w3.eth.chain_id}")  # 10

# Base
base_w3 = Web3(Web3.HTTPProvider('https://mainnet.base.org'))
print(f"Base chain ID: {base_w3.eth.chain_id}")  # 8453

# Interacting with contracts on L2 works exactly the same
contract = polygon_w3.eth.contract(
    address='0xContractOnPolygon...',
    abi=contract_abi
)
result = contract.functions.someFunction().call()

34.7.6 Bridging Assets

To use an L2, you need to transfer assets from L1 (or from a centralized exchange that supports direct L2 withdrawals). Bridging involves:

  1. Locking assets on L1 in a bridge contract
  2. Minting equivalent assets on L2
  3. (For withdrawals) Burning assets on L2 and unlocking on L1

Most users interact with bridges through web interfaces (e.g., bridge.polygon.technology, bridge.arbitrum.io), but programmatic bridging is possible via the bridge contracts.


34.8 Wallets and Key Management

34.8.1 Cryptographic Keys and Addresses

Ethereum uses Elliptic Curve Cryptography (ECC) with the secp256k1 curve---the same curve used by Bitcoin.

Private key: A random 256-bit integer $k$ in the range $[1, n-1]$ where $n$ is the order of the curve's generator point.

Public key: The point $K = k \cdot G$ on the elliptic curve, where $G$ is the generator point. The public key is 64 bytes (uncompressed) or 33 bytes (compressed).

Address: The last 20 bytes of the Keccak-256 hash of the public key:

$$\text{address} = \text{keccak256}(K)[12:32]$$

The address is typically displayed with a 0x prefix and an optional EIP-55 checksum (mixed-case encoding that allows detecting typos):

0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045  (checksummed)
0xd8da6bf26964af9d7eed9e03e53415d37aa96045  (lowercase, no checksum)

34.8.2 Wallet Types

Software wallets (MetaMask, Rainbow): - Private keys encrypted and stored on the user's device - Convenient for daily use - Vulnerable to malware and phishing

Hardware wallets (Ledger, Trezor): - Private keys stored on a dedicated security chip - Transaction signing happens on the device - Highly resistant to remote attacks

Smart contract wallets (Safe, Argent): - Wallet logic implemented as a smart contract - Support multisig (requires multiple signatures to authorize transactions) - Can implement recovery mechanisms, spending limits, etc.

Programmatic wallets (for bots and applications): - Private keys managed in code - Require careful security practices (see below)

34.8.3 Programmatic Wallet Management in Python

from eth_account import Account
import secrets
import os

# Generate a new account
private_key = '0x' + secrets.token_hex(32)
account = Account.from_key(private_key)
print(f"Address: {account.address}")
print(f"Private key: {private_key}")  # Store securely!

# Load from environment variable (recommended)
private_key = os.environ.get('ETH_PRIVATE_KEY')
if private_key:
    account = Account.from_key(private_key)
    print(f"Loaded account: {account.address}")

# Generate from mnemonic (BIP-39)
Account.enable_unaudited_hdwallet_features()
account, mnemonic = Account.create_with_mnemonic()
print(f"Mnemonic: {mnemonic}")
print(f"Address: {account.address}")

# Derive from mnemonic
account = Account.from_mnemonic(
    mnemonic,
    account_path="m/44'/60'/0'/0/0"  # Standard Ethereum derivation path
)

# Sign a message
from eth_account.messages import encode_defunct
message = encode_defunct(text="I agree to the terms of service")
signed = account.sign_message(message)
print(f"Signature: {signed.signature.hex()}")

# Verify a signature
recovered_address = Account.recover_message(message, signature=signed.signature)
assert recovered_address == account.address

34.8.4 Security Best Practices

  1. Never hardcode private keys in source code or commit them to version control
  2. Use environment variables or dedicated secrets management (AWS Secrets Manager, HashiCorp Vault)
  3. Minimize funds in hot wallets (programmatic wallets); keep reserves in cold storage
  4. Use separate accounts for different purposes (trading, testing, deployment)
  5. Implement spending limits in your code---a bug should not drain your entire wallet
  6. Monitor your accounts for unexpected transactions
  7. Test on testnets first before deploying to mainnet
import os
from web3 import Web3

class SecureWallet:
    """Wallet wrapper with basic security practices."""

    def __init__(self, env_var: str = 'ETH_PRIVATE_KEY'):
        key = os.environ.get(env_var)
        if not key:
            raise ValueError(f"Environment variable {env_var} not set")
        self._account = Account.from_key(key)
        self._max_tx_value = Web3.to_wei(1, 'ether')  # Safety limit

    @property
    def address(self) -> str:
        return self._account.address

    def sign_transaction(self, tx: dict) -> bytes:
        if tx.get('value', 0) > self._max_tx_value:
            raise ValueError(
                f"Transaction value {tx['value']} exceeds limit "
                f"{self._max_tx_value}"
            )
        return self._account.sign_transaction(tx)

34.9 Reading On-Chain Prediction Market Data

34.9.1 Querying Smart Contracts

The most direct way to read prediction market data is by calling the contract's view functions:

from web3 import Web3
import json

# Connect to Polygon (where Polymarket operates)
w3 = Web3(Web3.HTTPProvider('https://polygon-rpc.com'))

# Conditional Token Framework (CTF) contract on Polygon
CTF_ADDRESS = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045'

# Minimal ABI for reading condition data
CTF_ABI = [
    {
        "inputs": [
            {"name": "conditionId", "type": "bytes32"}
        ],
        "name": "getOutcomeSlotCount",
        "outputs": [
            {"name": "", "type": "uint256"}
        ],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [
            {"name": "collectionId", "type": "bytes32"},
            {"name": "conditionId", "type": "bytes32"},
            {"name": "indexSet", "type": "uint256"}
        ],
        "name": "getCollectionId",
        "outputs": [
            {"name": "", "type": "bytes32"}
        ],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [
            {"name": "account", "type": "address"},
            {"name": "id", "type": "uint256"}
        ],
        "name": "balanceOf",
        "outputs": [
            {"name": "", "type": "uint256"}
        ],
        "stateMutability": "view",
        "type": "function"
    }
]

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

# Query outcome slot count for a condition
condition_id = '0x...'  # Condition ID from Polymarket
outcome_count = ctf.functions.getOutcomeSlotCount(condition_id).call()
print(f"Number of outcomes: {outcome_count}")

34.9.2 Reading Event Logs

Event logs are the primary data source for tracking prediction market activity over time:

from web3 import Web3

w3 = Web3(Web3.HTTPProvider('https://polygon-rpc.com'))

# Query Transfer events for a specific token
# This shows trading activity
transfer_topic = w3.keccak(text='Transfer(address,address,uint256)')

logs = w3.eth.get_logs({
    'fromBlock': w3.eth.block_number - 10000,  # Last ~10,000 blocks
    'toBlock': 'latest',
    'address': '0xTokenAddress...',
    'topics': [transfer_topic.hex()]
})

print(f"Found {len(logs)} transfer events")

for log in logs[:5]:
    # Decode the log data
    from_addr = '0x' + log['topics'][1].hex()[-40:]
    to_addr = '0x' + log['topics'][2].hex()[-40:]
    value = int(log['data'].hex(), 16)
    print(f"From: {from_addr} -> To: {to_addr}, Value: {value}")

34.9.3 Historical Data with Pagination

Blockchain nodes typically limit the range of blocks you can query at once. For historical data, you need to paginate:

def get_events_paginated(w3, contract, event_name, from_block, to_block,
                         batch_size=2000):
    """Fetch events with automatic pagination."""
    all_events = []
    current_block = from_block

    while current_block <= to_block:
        end_block = min(current_block + batch_size - 1, to_block)

        try:
            event_filter = getattr(contract.events, event_name).create_filter(
                from_block=current_block,
                to_block=end_block
            )
            events = event_filter.get_all_entries()
            all_events.extend(events)
            print(f"Blocks {current_block}-{end_block}: {len(events)} events")
        except Exception as e:
            # If batch is too large, reduce size
            if batch_size > 100:
                batch_size = batch_size // 2
                continue
            else:
                raise e

        current_block = end_block + 1

    return all_events

34.9.4 Using The Graph for Indexed Data

The Graph is a decentralized indexing protocol that indexes blockchain data and exposes it via GraphQL APIs. Many prediction market protocols maintain subgraphs that provide pre-indexed, queryable data.

import requests

# Query a subgraph (example: a prediction market subgraph)
SUBGRAPH_URL = "https://api.thegraph.com/subgraphs/name/example/prediction-market"

query = """
{
    markets(first: 10, orderBy: volume, orderDirection: desc) {
        id
        question
        outcomes
        totalVolume
        createdAt
        resolved
        winningOutcome
    }
}
"""

response = requests.post(SUBGRAPH_URL, json={'query': query})
data = response.json()

for market in data['data']['markets']:
    print(f"Market: {market['question']}")
    print(f"Volume: {market['totalVolume']}")
    print(f"Resolved: {market['resolved']}")
    print("---")

34.9.5 Building a Comprehensive Data Pipeline

For serious analysis, combine multiple data sources:

import time
from web3 import Web3
from datetime import datetime
import json

class OnChainDataReader:
    """Read and organize on-chain prediction market data."""

    def __init__(self, rpc_url: str, contract_address: str, abi: list):
        self.w3 = Web3(Web3.HTTPProvider(rpc_url))
        self.contract = self.w3.eth.contract(
            address=contract_address, abi=abi
        )

    def get_market_state(self, market_id: bytes) -> dict:
        """Get current state of a prediction market."""
        return {
            'outcome_count': self.contract.functions.getOutcomeSlotCount(
                market_id
            ).call(),
            'block_number': self.w3.eth.block_number,
            'timestamp': datetime.now().isoformat()
        }

    def get_recent_trades(self, from_block: int, to_block: int = None) -> list:
        """Get recent trading activity."""
        if to_block is None:
            to_block = self.w3.eth.block_number

        events = get_events_paginated(
            self.w3, self.contract, 'Transfer',
            from_block, to_block
        )

        trades = []
        for event in events:
            block = self.w3.eth.get_block(event['blockNumber'])
            trades.append({
                'block': event['blockNumber'],
                'timestamp': datetime.fromtimestamp(
                    block['timestamp']
                ).isoformat(),
                'tx_hash': event['transactionHash'].hex(),
                'from': event['args'].get('from', ''),
                'to': event['args'].get('to', ''),
                'value': event['args'].get('value', 0)
            })

        return trades

    def get_token_holder_distribution(self, token_id: int,
                                       addresses: list) -> dict:
        """Get token distribution across known addresses."""
        distribution = {}
        for addr in addresses:
            balance = self.contract.functions.balanceOf(addr, token_id).call()
            if balance > 0:
                distribution[addr] = balance
        return distribution

34.10 Transaction Lifecycle

34.10.1 From Signing to Confirmation

Understanding the full lifecycle of a transaction is essential for building reliable prediction market bots and applications.

Step 1: Construction Build the transaction with all required fields (from, to, value, data, gas, nonce, chain ID).

Step 2: Signing Sign the transaction with the sender's private key using ECDSA. The signature $(v, r, s)$ proves that the private key holder authorized the transaction.

Step 3: Broadcasting Send the signed transaction to a node, which validates it and forwards it to other nodes. The transaction enters the mempool (memory pool)---a holding area for unconfirmed transactions.

Step 4: Inclusion A validator (PoS) selects transactions from the mempool to include in the next block. Transactions with higher priority fees are typically selected first.

Step 5: Execution The EVM executes the transaction, modifying the world state. If the execution fails (e.g., reverts), the state changes are rolled back but gas is still consumed.

Step 6: Confirmation The block containing the transaction is added to the chain. Additional blocks built on top provide increasing confidence in finality.

User Signs Tx    Node Validates    Mempool    Validator Selects    EVM Executes    Block Added
    |                 |               |              |                  |               |
    v                 v               v              v                  v               v
[Build Tx] --> [Broadcast] --> [Wait in Pool] --> [Include] --> [Execute] --> [Confirm]
    |                                                              |
    |                                                              v
    |                                                    [State Changed]
    |                                                         or
    |                                                    [Reverted + Gas Consumed]

34.10.2 Gas Price Strategies

Choosing the right gas price involves balancing speed and cost:

from web3 import Web3

w3 = Web3(Web3.HTTPProvider('https://mainnet.infura.io/v3/YOUR_PROJECT_ID'))

class GasStrategy:
    """Gas price estimation strategies."""

    def __init__(self, w3: Web3):
        self.w3 = w3

    def get_base_fee(self) -> int:
        """Get current base fee from the latest block."""
        block = self.w3.eth.get_block('latest')
        return block.get('baseFeePerGas', 0)

    def slow(self) -> dict:
        """Economical: wait for low-congestion periods."""
        base_fee = self.get_base_fee()
        return {
            'maxFeePerGas': int(base_fee * 1.1),
            'maxPriorityFeePerGas': self.w3.to_wei(1, 'gwei'),
        }

    def standard(self) -> dict:
        """Standard: included in the next few blocks."""
        base_fee = self.get_base_fee()
        return {
            'maxFeePerGas': int(base_fee * 1.5),
            'maxPriorityFeePerGas': self.w3.to_wei(2, 'gwei'),
        }

    def fast(self) -> dict:
        """Fast: high priority, next block likely."""
        base_fee = self.get_base_fee()
        return {
            'maxFeePerGas': int(base_fee * 2),
            'maxPriorityFeePerGas': self.w3.to_wei(5, 'gwei'),
        }

    def urgent(self) -> dict:
        """Urgent: time-sensitive (e.g., arbitrage)."""
        base_fee = self.get_base_fee()
        return {
            'maxFeePerGas': int(base_fee * 3),
            'maxPriorityFeePerGas': self.w3.to_wei(20, 'gwei'),
        }

34.10.3 Transaction Monitoring and Retry

import time
from web3 import Web3
from web3.exceptions import TransactionNotFound

class TransactionManager:
    """Manage transaction lifecycle with monitoring and retry."""

    def __init__(self, w3: Web3, account, max_retries: int = 3):
        self.w3 = w3
        self.account = account
        self.max_retries = max_retries

    def send_transaction(self, tx: dict,
                         timeout: int = 120) -> dict:
        """Send a transaction with monitoring and retry logic."""

        for attempt in range(self.max_retries):
            try:
                # Ensure nonce is current
                tx['nonce'] = self.w3.eth.get_transaction_count(
                    self.account.address
                )

                # Sign and send
                signed = self.w3.eth.account.sign_transaction(
                    tx, self.account.key
                )
                tx_hash = self.w3.eth.send_raw_transaction(
                    signed.raw_transaction
                )

                print(f"Attempt {attempt + 1}: Sent {tx_hash.hex()}")

                # Wait for receipt
                receipt = self.w3.eth.wait_for_transaction_receipt(
                    tx_hash, timeout=timeout
                )

                if receipt['status'] == 1:
                    print(f"Confirmed in block {receipt['blockNumber']}")
                    return {
                        'success': True,
                        'tx_hash': tx_hash.hex(),
                        'receipt': receipt,
                        'gas_used': receipt['gasUsed']
                    }
                else:
                    print(f"Transaction reverted! Gas used: "
                          f"{receipt['gasUsed']}")
                    return {
                        'success': False,
                        'tx_hash': tx_hash.hex(),
                        'receipt': receipt,
                        'error': 'Transaction reverted'
                    }

            except Exception as e:
                print(f"Attempt {attempt + 1} failed: {e}")
                if attempt < self.max_retries - 1:
                    # Increase gas price for retry
                    if 'maxFeePerGas' in tx:
                        tx['maxFeePerGas'] = int(
                            tx['maxFeePerGas'] * 1.3
                        )
                        tx['maxPriorityFeePerGas'] = int(
                            tx['maxPriorityFeePerGas'] * 1.3
                        )
                    time.sleep(5 * (attempt + 1))

        return {'success': False, 'error': 'Max retries exceeded'}

    def speed_up_transaction(self, original_tx_hash: str,
                              gas_multiplier: float = 1.5) -> str:
        """Speed up a pending transaction by resubmitting with higher gas."""
        original_tx = self.w3.eth.get_transaction(original_tx_hash)

        new_tx = {
            'from': original_tx['from'],
            'to': original_tx['to'],
            'value': original_tx['value'],
            'data': original_tx['input'],
            'nonce': original_tx['nonce'],  # Same nonce replaces original
            'gas': original_tx['gas'],
            'maxFeePerGas': int(
                original_tx.get('maxFeePerGas', 0) * gas_multiplier
            ),
            'maxPriorityFeePerGas': int(
                original_tx.get('maxPriorityFeePerGas', 0) * gas_multiplier
            ),
            'chainId': original_tx.get('chainId', 1)
        }

        signed = self.w3.eth.account.sign_transaction(
            new_tx, self.account.key
        )
        new_hash = self.w3.eth.send_raw_transaction(
            signed.raw_transaction
        )
        print(f"Speed-up tx sent: {new_hash.hex()}")
        return new_hash.hex()

    def cancel_transaction(self, original_nonce: int) -> str:
        """Cancel a pending transaction by sending 0 ETH to self
        with the same nonce and higher gas."""
        cancel_tx = {
            'from': self.account.address,
            'to': self.account.address,
            'value': 0,
            'nonce': original_nonce,
            'gas': 21000,
            'maxFeePerGas': self.w3.to_wei(100, 'gwei'),
            'maxPriorityFeePerGas': self.w3.to_wei(50, 'gwei'),
            'chainId': self.w3.eth.chain_id
        }

        signed = self.w3.eth.account.sign_transaction(
            cancel_tx, self.account.key
        )
        cancel_hash = self.w3.eth.send_raw_transaction(
            signed.raw_transaction
        )
        print(f"Cancel tx sent: {cancel_hash.hex()}")
        return cancel_hash.hex()

34.10.4 Handling Reverts

When a transaction reverts, it means the smart contract's execution encountered an error condition. Common causes in prediction markets:

  • Insufficient token approval
  • Insufficient balance
  • Market already resolved
  • Slippage exceeds tolerance
  • Gas limit too low

You can simulate a transaction before sending to check for reverts:

def simulate_transaction(w3, tx):
    """Simulate a transaction to check for reverts."""
    try:
        # eth_call simulates without broadcasting
        result = w3.eth.call(tx)
        return {'success': True, 'result': result}
    except Exception as e:
        error_msg = str(e)
        # Parse revert reason if available
        if 'revert' in error_msg.lower():
            return {'success': False, 'reason': error_msg}
        return {'success': False, 'reason': f'Unknown error: {error_msg}'}

34.11 Security Considerations

34.11.1 Smart Contract Risks

Smart contracts are immutable once deployed---bugs cannot be patched (unless the contract implements an upgrade pattern). The most notorious vulnerabilities include:

Reentrancy: An attacker contract calls back into the vulnerable contract before the first call completes, potentially draining funds. The 2016 DAO hack exploited this vulnerability, leading to a \$60 million loss and the Ethereum/Ethereum Classic chain split.

// VULNERABLE - DO NOT USE
function withdraw() external {
    uint256 balance = balances[msg.sender];
    // External call BEFORE state update - reentrancy risk!
    (bool success, ) = msg.sender.call{value: balance}("");
    require(success);
    balances[msg.sender] = 0;  // Too late - attacker already re-entered
}

// SECURE - Checks-Effects-Interactions pattern
function withdraw() external {
    uint256 balance = balances[msg.sender];
    balances[msg.sender] = 0;  // State update BEFORE external call
    (bool success, ) = msg.sender.call{value: balance}("");
    require(success);
}

Integer Overflow/Underflow: In Solidity versions before 0.8.0, arithmetic operations could silently overflow or underflow. A uint256 set to 0 and decremented would wrap to $2^{256} - 1$. Solidity 0.8+ includes built-in overflow checks.

Front-Running: Transactions in the mempool are visible before inclusion. An attacker can see a profitable prediction market trade, submit their own transaction with higher gas to be included first, and profit at the original trader's expense. This is a form of Miner Extractable Value (MEV).

Mitigation strategies for front-running: - Commit-reveal schemes: Users commit to a hashed action, then reveal after commitment - Private mempools: Services like Flashbots allow transactions to bypass the public mempool - Slippage protection: Set maximum acceptable price impact

Oracle Manipulation: Prediction markets rely on oracles to report outcomes. If the oracle can be manipulated, the market can be settled fraudulently. This is perhaps the most critical security challenge for decentralized prediction markets.

Oracle security approaches: - Decentralized oracle networks (Chainlink, UMA): Multiple independent reporters with economic incentives for honesty - Optimistic oracles (UMA): Outcomes are assumed correct unless challenged within a dispute window - Prediction market-based oracles (Augur): Use the prediction market itself as the oracle, with dispute resolution via token voting

Access Control Errors: Functions that should be restricted (e.g., market resolution) may be accidentally left callable by anyone.

34.11.2 User Security

Key Management: The single most common cause of cryptocurrency loss is poor key management. Private keys that are lost cannot be recovered; keys that are stolen allow irreversible theft.

Best practices: - Use hardware wallets for significant holdings - Never share private keys or seed phrases - Be skeptical of all requests for signatures---review what you are signing - Use separate wallets for different risk levels

Phishing: Malicious websites that mimic legitimate prediction market interfaces can trick users into signing transactions that drain their wallets. Always verify contract addresses against official sources.

Approval Risks: The ERC-20 approve function grants a contract permission to spend your tokens. Granting unlimited approval (common for convenience) means a compromised contract can take all your tokens. Consider using exact approval amounts or revoking unused approvals.

# Check and revoke token approvals
def check_approval(w3, token_contract, owner, spender):
    """Check how much a spender can take from the owner."""
    allowance = token_contract.functions.allowance(owner, spender).call()
    return allowance

def revoke_approval(w3, token_contract, spender, account, private_key):
    """Revoke a token approval by setting allowance to 0."""
    tx = token_contract.functions.approve(spender, 0).build_transaction({
        'from': account,
        'nonce': w3.eth.get_transaction_count(account),
        'gas': 60000,
        'maxFeePerGas': w3.to_wei(50, 'gwei'),
        'maxPriorityFeePerGas': w3.to_wei(2, 'gwei'),
    })
    signed = w3.eth.account.sign_transaction(tx, private_key)
    tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
    return w3.eth.wait_for_transaction_receipt(tx_hash)

34.11.3 Protocol Security

Audits: Reputable prediction market protocols undergo security audits by specialized firms (Trail of Bits, OpenZeppelin, Consensys Diligence). Audit reports are typically published and should be reviewed before using a protocol.

Bug Bounties: Protocols offer financial rewards for responsibly disclosed vulnerabilities. Immunefi is the leading platform for DeFi bug bounties, with some programs offering millions of dollars for critical findings.

Formal Verification: The highest level of assurance---mathematically proving that the contract code matches its specification. This is expensive and time-consuming but provides the strongest guarantees.

Upgradability: Some contracts are designed to be upgradeable via proxy patterns. While this allows bug fixes, it also means the contract behavior can change, requiring trust in the upgrade authority. Verify whether a prediction market's contracts are upgradeable and who controls the upgrade process.

34.11.4 Security Checklist for Prediction Market Users

  1. Verify contract addresses against official documentation
  2. Read audit reports before depositing significant funds
  3. Start small and increase exposure as you gain confidence
  4. Use hardware wallets for large positions
  5. Monitor your approvals and revoke unused ones
  6. Be wary of new, unaudited protocols regardless of promised returns
  7. Understand the oracle mechanism and its failure modes
  8. Check for upgrade keys and multisig configurations
  9. Consider insurance (Nexus Mutual, InsurAce) for large positions
  10. Keep private keys offline and backed up securely

34.12 Chapter Summary

This chapter provided a comprehensive introduction to blockchain technology as it applies to prediction markets. We covered:

  1. Why blockchain matters for prediction markets: Censorship resistance, global access, trustless settlement, transparency, and composability offer compelling advantages over centralized alternatives.

  2. Blockchain fundamentals: Blocks, chains, cryptographic hashing, Merkle trees, and consensus mechanisms (PoW vs. PoS) form the foundation. Immutability and finality are the key properties that enable trustless prediction markets.

  3. Ethereum and the EVM: Ethereum extends blockchain with programmable smart contracts. The account model (EOA vs. contract), transaction structure, gas economics, and the EVM execution model are essential knowledge for anyone building on Ethereum.

  4. Smart contracts: Solidity contracts define the rules of on-chain prediction markets. State variables, functions, events, and modifiers compose the building blocks. The ABI bridges the gap between high-level contract interfaces and low-level bytecode.

  5. web3.py: Python's primary library for blockchain interaction enables reading blockchain data, sending transactions, and interacting with smart contracts. Provider setup, event reading, and contract interaction patterns are the core skills.

  6. Token standards: ERC-20 (fungible), ERC-721 (non-fungible), and ERC-1155 (multi-token) standards underpin prediction market tokens. The split/merge mechanism connects collateral to outcome tokens.

  7. Layer 2 solutions: Polygon, Arbitrum, Optimism, and Base reduce transaction costs and increase throughput, making on-chain prediction markets economically viable. Connecting to L2s from Python uses the same web3.py patterns with different RPC endpoints.

  8. Wallets and key management: Private key security is paramount. Programmatic wallets require careful handling of keys through environment variables and spending limits.

  9. On-chain data reading: Contract queries, event logs, paginated fetching, and The Graph provide multiple approaches to extracting prediction market data from the blockchain.

  10. Transaction lifecycle: From signing through mempool to confirmation, understanding the full lifecycle enables building reliable applications with proper gas strategies, monitoring, and retry logic.

  11. Security: Smart contract risks (reentrancy, overflow, front-running, oracle manipulation), user security (key management, phishing), and protocol security (audits, bug bounties) are critical considerations.


What's Next

With the blockchain fundamentals established, Chapter 35 will explore decentralized prediction market protocols in depth. We will examine the architectures of Augur, Polymarket, and Gnosis/Omen; understand their market making mechanisms (LMSR on-chain, order books, AMMs); explore the oracle problem and decentralized resolution; and build Python tools for interacting with these protocols. The knowledge from this chapter---smart contracts, web3.py, token standards, and L2 networks---forms the foundation for everything that follows.

In Chapter 36, we will build a complete on-chain trading bot that monitors markets, identifies opportunities, and executes trades programmatically. The transaction management and security patterns from this chapter will be essential for that implementation.


References

  1. Buterin, V. (2014). A Next-Generation Smart Contract and Decentralized Application Platform. Ethereum Whitepaper.
  2. Wood, G. (2014). Ethereum: A Secure Decentralised Generalised Transaction Ledger. Ethereum Yellow Paper.
  3. Szabo, N. (1994). Smart Contracts. Unpublished manuscript.
  4. Nakamoto, S. (2008). Bitcoin: A Peer-to-Peer Electronic Cash System.
  5. OpenZeppelin. Contracts Documentation. https://docs.openzeppelin.com/contracts/
  6. web3.py Documentation. https://web3py.readthedocs.io/
  7. Ethereum Foundation. Ethereum Development Documentation. https://ethereum.org/developers/
  8. Peterson, J., Krug, J., Zoltu, M., Williams, A. K., & Alexander, S. (2019). Augur: a Decentralized Oracle and Prediction Market Platform. arXiv:1501.01042v2.
  9. Auer, R. (2019). Beyond the doomsday economics of proof-of-work in cryptocurrencies. BIS Working Papers, No. 765.
  10. Daian, P., Goldfeder, S., Kell, T., Li, Y., Zhao, X., Bentov, I., Breidenbach, L., & Juels, A. (2020). Flash Boys 2.0: Frontrunning in Decentralized Exchanges, Miner Extractable Value, and Consensus Instability. IEEE S&P 2020.