ethPM

Deterministic Prediction Markets Protocol

V1.0 // Technical Documentation

00 // Protocol Overview

ethPM enables deterministic, on-chain prediction markets that resolve based on arbitrary smart contract state. Markets can bet on any verifiable on-chain data: TVL, token prices, trading activity, governance votes—without human resolvers.

Core Principles

Deterministic

Resolution via staticcall to any on-chain data source. No oracle dependency or human intervention.

Composable

ERC6909 shares integrate with any DeFi primitive. Modular hook system for custom fee logic.

Immutable

Core contracts deployed without upgrade patterns. Markets persist independently of protocol governance.

Contract Addresses

Contract Address Network
PAMM.sol 0x000000000044bfe6c2BBFeD8862973E0612f07C0 Ethereum
ZAMM.sol 0x000000000000040470635EB91b7CE4D132D616eD Ethereum
PMHookRouter.sol 0x0000000000BADa259Cb860c12ccD9500d9496B3e Ethereum
PMHookQuoter.sol 0x0000000000f0bf4ea3a43560324376e62fe390bc Ethereum
Resolver.sol 0x00000000002205020E387b6a378c05639047BcFB Ethereum
MasterRouter.sol 0x000000000055CdB14b66f37B96a571108FFEeA5C Ethereum
GasPM.sol 0x0000000000ee3d4294438093EaA34308f47Bc0b4 Ethereum
PMFeeHook.sol Deployed per market instance Ethereum

01 // System Architecture

The ethPM stack consists of five interconnected layers, each responsible for a specific domain of functionality.

System Architecture Overview
ETHPM PROTOCOL STACK LAYER 5 USER INTERFACE Frontend / IPFS / PMHookQuoter.sol LAYER 4 ORDER ROUTING PMHookRouter Vault OTC + AMM MasterRouter Limit Orders LAYER 3 FEE POLICY PMFeeHook.sol Bootstrap Decay • Skew Protection • Price Impact Limits LAYER 2 LIQUIDITY zAMM Full Range XYK • TWAP Oracle • LP Hooks LAYER 1 CORE VAULT PAMM.sol ERC6909 YES/NO Shares Resolver.sol Deterministic Oracle View-only quotes Best execution Dynamic fees Price discovery Collateral

Data Flow

User interactions flow through the stack as follows:

  1. Trade Request: User submits collateral with desired position (YES/NO)
  2. Routing: PMHookRouter evaluates Vault OTC vs AMM pricing
  3. Fee Calculation: PMFeeHook computes dynamic fee based on market state
  4. Execution: zAMM executes swap or PAMM mints new shares
  5. Settlement: User receives conditional tokens

02 // PAMM.sol

PAMM (Prediction AMM) is the core collateral vault that mints and manages ERC6909 conditional tokens (YES/NO shares). Each market is fully collateralized—1 share = 1 wei of collateral.

Key Insight

PAMM is hyperoptimized for zAMM integration. The ERC6909 singleton design enables operator approvals for single-tx share operations and reduces gas costs through warm storage access patterns.

Token ID Derivation

Market ID Generation
description string resolver address collateral address keccak256 "PMARKET:YES" marketId uint256 keccak256 noId "PMARKET:NO" + marketId

Market Lifecycle

State Machine
OPEN trading CLOSED awaiting RESOLVED final block.timestamp >= close resolve() by resolver closeMarket() if canClose ALLOWED ACTIONS OPEN: split() merge() buyYes() buyNo() sellYes() sellNo() CLOSED: merge() RESOLVED: claim()

Core Functions

Market Creation

function createMarket(string calldata description, address resolver, address collateral, uint64 close, bool canClose) external returns (uint256 marketId, uint256 noId)

Creates a new prediction market. The resolver address has exclusive authority to call resolve() and optionally closeMarket().

Split (Collateral → Shares)

function split(uint256 marketId, uint256 collateralIn, address to) external payable returns (uint256 shares, uint256 used)

Locks collateral and mints equal amounts of YES and NO shares. For ETH markets, send ETH with the call.

Merge (Shares → Collateral)

function merge(uint256 marketId, uint256 shares, address to) external returns (uint256 merged, uint256 collateralOut)

Burns equal amounts of YES and NO shares to unlock collateral. Allowed until market resolution.

Claim (After Resolution)

function claim(uint256 marketId, address to) external returns (uint256 shares, uint256 payout)

Burns winning shares for collateral minus resolver fee (0-10% configurable).

Storage Layout

Slot Field Type Description
Market.0 resolver + resolved + outcome + canClose + close packed 31 bytes Market metadata
Market.1 collateral address Collateral token (0x0 = ETH)
Market.2 collateralLocked uint256 Total collateral in market

03 // PMFeeHook.sol

PMFeeHook is a policy layer that sits between zAMM and PAMM, implementing dynamic swap fees and market protection mechanisms. It uses EIP-1153 transient storage for gas optimization.

Fee Components

Dynamic Fee Calculation
TOTAL FEE = min(SUM, feeCap) BOOTSTRAP max → min decay 0.75% → 0.10% over 2 days SKEW imbalance penalty 0% → 0.80% at 90/10 skew ASYMMETRIC directional fee 0% → 0.20% linear scaling VOLATILITY price variance optional 10 snapshots + + + FEE CAP: 3% feeCapBps = 300 CLOSE WINDOW MODES 0: HALT 1: FIXED FEE 2: MIN FEE 3: DYNAMIC

Configuration Parameters

Field Type Default Description
minFeeBps uint16 10 Steady-state fee (0.10%)
maxFeeBps uint16 75 Bootstrap starting fee (0.75%)
maxSkewFeeBps uint16 80 Max skew penalty (0.80%)
feeCapBps uint16 300 Absolute fee ceiling (3%)
skewRefBps uint16 4000 Skew threshold (90/10 = 40%)
bootstrapWindow uint32 172800 Decay duration (2 days)
closeWindow uint16 3600 Pre-close window (1 hour)
maxPriceImpactBps uint16 1200 Max single-trade impact (12%)

Flag Bits

flags (uint16):
  bit 0: FLAG_SKEW          (0x01) - Enable skew fee
  bit 1: FLAG_BOOTSTRAP     (0x02) - Enable bootstrap decay
  bit 2-3: closeWindowMode  (0x0C) - 0=halt, 1=fixed, 2=min, 3=dynamic
  bit 4: FLAG_ASYMMETRIC    (0x10) - Enable directional fee
  bit 5: FLAG_PRICE_IMPACT  (0x20) - Enable impact check
  bit 6: FLAG_VOLATILITY    (0x40) - Enable volatility fee

extraFlags (uint16):
  bit 0-1: skewCurve        - 0=linear, 1=quadratic, 2=cubic, 3=quartic
  bit 2-3: decayMode        - 0=linear, 1=cubic, 2=sqrt, 3=ease-in

Default flags: 0x37 (skew + bootstrap + closeMode=1 + asymmetric + priceImpact)
Default extraFlags: 0x01 (quadratic skew, linear decay)

04 // PMHookRouter.sol

PMHookRouter provides smart order routing with integrated vault market-making. It creates a "waterfall" effect for best-execution across three venues: Vault OTC, AMM, and Mint fallback.

Order Routing Flow

Buy Order Execution Waterfall
COLLATERAL IN buyWithBootstrap() QUOTE COMPARISON Vault OTC vs AMM VAULT OTC TWAP-based pricing 1% base + 4% imbalance 30% max per fill zAMM SWAP split + swap opposite hook enforces impact binary search cap MINT FALLBACK if conditions met SOURCE TAGS "otc" = vault "amm" = swap "mint" = split

Key Parameters

Constant Value Description
MIN_ABSOLUTE_SPREAD_BPS 20 0.2% minimum spread
MAX_SPREAD_BPS 500 5% maximum spread cap
LP_FEE_SPLIT_BPS_BALANCED 9000 90% to LPs when balanced
LP_FEE_SPLIT_BPS_IMBALANCED 7000 70% to LPs when imbalanced
BOOTSTRAP_WINDOW 14400 4h mint path disabled before close
MIN_TWAP_UPDATE_INTERVAL 1800 30 minute TWAP window

05 // PMHookQuoter.sol

View-only quoter contract providing consolidated market data and execution quotes. Separated from routers to keep them under bytecode limit while enabling efficient frontend queries.

Fully On-Chain Market Infrastructure
FULLY ON-CHAIN MARKET INFRASTRUCTURE IPFS/dApp frontends can run entirely on-chain with PMHookRouter + Quoter IPFS FRONTEND Static HTML/JS No backend needed Censorship resistant Fully decentralized dAPP FRONTEND React/Next.js RPC calls only No indexer required All data on-chain eth_call (view functions) ON-CHAIN DATA LAYER PMHookRouter (State) MARKET STATE canonicalPoolId[marketId] bootstrapVaults[marketId] rebalanceCollateralBudget[id] USER POSITIONS vaultPositions[id][user] .yesVaultShares .noVaultShares .yesRewardDebt .noRewardDebt TWAP ORACLE twapObservations[marketId] PMHookQuoter (View) QUOTE FUNCTIONS quoteBuy(marketId, buyYes, coll) quoteSell(marketId, sellYes, shares) → returns expected output MARKET INFO getMarketInfo(marketId) getVaultDepth(marketId) getTWAPPrice(marketId) USER POSITION getUserPosition(marketId, user) getPendingFees(marketId, user) getShareBalances(marketId, user) NO OFF-CHAIN DEPENDENCIES FULLY DETERMINISTIC EXECUTION

Market View Functions

Function Returns Purpose
getMarketSummary() AMM reserves, fees, vault data, TWAP, timing Single-call market overview for dapps
getUserFullPosition() Share balances, vault LP, pending rewards Complete user position in one call
getLiquidityBreakdown() Vault OTC, AMM, pool depths Liquidity across all venues
getTWAPPrice() pYes in basis points (0-10000) Time-weighted average price

Quote Functions

PMHookRouter Quotes

Simulate vault OTC + AMM execution waterfall.

quoteBootstrapBuy(marketId, buyYes, collateralIn, minOut)
→ (totalShares, usesVault, source, vaultLP)

quoteSellWithBootstrap(marketId, sellYes, sharesIn)
→ (collateralOut, source)
MasterRouter Quotes

Simulate pool sweep + PMHookRouter fallback.

quoteBuyWithSweep(marketId, buyYes, collateralIn, maxPrice)
→ (totalShares, poolShares, levels, pmShares, pmSource)

quoteSellWithSweep(marketId, sellYes, sharesIn, minPrice)
→ (totalCollateral, poolColl, levels, pmColl, pmSource)

Orderbook View Functions

Function Returns Purpose
getActiveLevels() Ask/bid prices and depths Orderbook UI display
getUserActivePositions() All positions on a market side User's limit orders
getUserAllLimitOrders() YES and NO positions combined Complete user order view
getUserPositionsBatch() Positions at specific prices Efficient batch queries

Source Identifiers

Quote functions return a source indicating the execution path:

Source Meaning
"otc" Filled via vault OTC
"amm" Filled via AMM swap
"mint" Filled via vault mint (collateral → shares)
"mult" Multiple venues used
Design Note

PMHookQuoter exists as a separate contract because PMHookRouter and MasterRouter are near the 24KB bytecode limit. Moving view functions to a dedicated quoter keeps routers deployable while providing comprehensive query capabilities.

06 // Resolver.sol

The deterministic resolver enables markets that resolve based on arbitrary on-chain reads. No human intervention required—resolution is computed via staticcall at close time.

Resolver Architecture
RESOLVER.SOL — DETERMINISTIC ORACLE On-chain oracle for PAMM markets via arbitrary staticcall reads Resolver.sol Creates markets + Resolves via staticcall conditions marketId → Condition Op enum LT GT LTE GTE EQ NEQ PAMM 0x0000...07C0 Market creation + Resolution createMarket() resolve() closeMarket() TARGET A Any contract with view/pure functions or EOA for balance TARGET B Optional (for ratio) value = A × 1e18 / B staticcall staticcall CREATION createNumericMarket() createRatioMarket() *AndSeed() *SeedAndBuy() RESOLUTION resolveMarket() RESOLUTION RULES condition TRUE → YES wins condition FALSE → NO wins (after close) canClose = true → early resolution OK DETERMINISTIC RESOLUTION FLOW 1. resolveMarket(marketId) called by anyone 2. value = staticcall(target, callData) → decode as uint256 3. outcome = compare(value, op, threshold) → YES or NO

Resolution Modes

Scalar Mode

Single staticcall decoded as uint256. Compare result against threshold using operator.

target.staticcall(data) → uint256 value
value [op] threshold → YES/NO
Ratio Mode

Two staticcalls with result computed as A * 1e18 / B. Compare ratio against threshold.

targetA.staticcall(dataA) → A
targetB.staticcall(dataB) → B
(A * 1e18 / B) [op] threshold

Comparison Operators

Op Symbol Description
0<Less than
1>Greater than
2Less or equal
3Greater or equal
4==Equal
5Not equal
Comparison Operators (Op enum)
COMPARISON OPERATORS (Op enum) OP SYMBOL MEANING EXAMPLE 0 (LT) < Less than price < 1000 1 (GT) > Greater than tvl > 1000000 2 (LTE) Less or equal supply ≤ cap 3 (GTE) Greater or equal balance ≥ 100e18 4 (EQ) == Equal isActive == 1 (true) 5 (NEQ) Not equal status != 0 BOOLEAN SUPPORT Functions returning bool work natively (ABI encodes as uint256: false=0, true=1) Use Op.EQ with threshold=1 for "is true", threshold=0 for "is false"
Data Sources & Special Cases
DATA SOURCES & SPECIAL CASES _readUint(target, callData) if callData.length == 0: return target.balance // ETH balance else: return staticcall → decode uint256 CONTRACT READS • token.totalSupply() • oracle.latestAnswer() • pool.getReserves() • governor.proposalCount() • nft.balanceOf(address) ETH BALANCE Pass empty callData ("") target = address to check Example conditions: • wallet.balance > 100 ETH BOOLEAN FUNCTIONS ABI encodes bool as uint256 false → 0, true → 1 Example conditions: • isActive() == 1 (true) SPECIAL CASES & EDGE HANDLING RATIO: B == 0 → value = uint256.max STATICCALL FAILS → revert TargetCallFailed() DATA < 32 BYTES → revert TargetCallFailed()
Important

The target contract must be immutable or the data being read must be manipulation-resistant. Markets on mutable contracts can be exploited by changing state before resolution.

07 // MasterRouter.sol

MasterRouter provides pooled limit orders for prediction markets. ASK pools sell shares at fixed prices; BID pools buy shares at fixed prices.

MasterRouter System Overview
MASTERROUTER SYSTEM OVERVIEW USER MasterRouter.sol Pooled Orderbook + Vault Integration ASK Pools BID Pools Price Bitmap PMHookRouter 0x0000...496B3e Vault OTC + AMM routing PAMM 0x0000...07C0 ERC6909 YES/NO Shares buyWithBootstrap() sellWithBootstrap() split() transfer() transferFrom() CONSTRUCTOR setOperator(PMHookRouter, true) KEY FUNCTIONS buyWithSweep() sellWithSweep() mintAndPool() mintAndVault() createBidPool() multicall() CONSTANTS ETH = address(0) BPS_DENOM = 10000 ACC = 1e18 Bitmap: 40 x uint256

Order Book Architecture

  • ASK Pools: Sell shares at fixed price, filled lowest-first by buyWithSweep()
  • BID Pools: Buy shares at fixed price, filled highest-first by sellWithSweep()
  • Price Bitmap: 40 uint256s per market/side for O(1) best bid/ask discovery
  • Accumulator LP: Prevents late-joiner exploitation of pending fills
Pooled Orderbook Architecture
POOLED ORDERBOOK ARCHITECTURE ASK POOLS (Selling Shares) Pool Struct totalShares: uint256 totalScaled: uint256 (LP units) accCollPerScaled: uint256 collateralEarned: uint256 UserPosition scaled: uint256 (LP units owned) collDebt: uint256 (reward debt) BID POOLS (Buying Shares) BidPool Struct totalCollateral: uint256 totalScaled: uint256 (LP units) accSharesPerScaled: uint256 sharesAcquired: uint256 BidPosition scaled: uint256 (LP units owned) sharesDebt: uint256 (reward debt) poolId = keccak256(marketId, isYes, priceInBps) Unique ID per market/side/price bidPoolId = keccak256("BID", marketId, buyYes, price) Prefixed to avoid collision with ASK PRICE BITMAP O(1) Best Bid/Ask Discovery mapping(bytes32 => uint256[40]) priceBitmap key = keccak256(marketId, isYes, isAsk) ... [0] [1] [2] [38] [39] bucket = price >> 8 | bit = price & 0xff | Valid range: 1-9999 bps

Pooled Orderbook Design

Unlike traditional orderbooks with individual orders, MasterRouter uses pooled limit orders where multiple LPs can deposit into the same price level. This enables gas-efficient fills and fair pro-rata distribution.

Accumulator Pattern

The accumulator LP pattern (similar to MasterChef) tracks rewards per unit of liquidity. When fills occur, accCollPerScaled (ASK) or accSharesPerScaled (BID) increases. Each user's pending rewards are calculated as:

pending = (position.scaled * accPerScaled) - position.debt

Price Bitmap

The bitmap enables O(1) discovery of the best available price. Each bit represents a price level (1-9999 bps). Finding the lowest ASK or highest BID requires scanning at most 40 uint256 words using bit manipulation.

Accumulator LP Model
ACCUMULATOR LP MODEL Prevents Late Joiner Theft via Reward Debt Pattern pending = (scaled × accPerScaled / ACC) − debt ACC = 1e18 (precision scalar) ASK POOL (Shares → Collateral) _deposit() mint LP units set initial debt _fill() remove shares accCollPerScaled += _claim() calc pending coll update debt _withdraw() burn LP units return shares Pool: totalShares, totalScaled, accCollPerScaled User: scaled (LP units), collDebt (reward debt) Collateral distributes pro-rata to LP holders BID POOL (Collateral → Shares) _depositToBidPool() mint LP units set initial debt _fillBidPool() remove collateral accSharesPerScaled += _claimBidShares() calc pending shares update debt _withdrawFromBidPool() burn LP units return collateral BidPool: totalCollateral, totalScaled, accSharesPerScaled User: scaled (LP units), sharesDebt (reward debt) Shares distribute pro-rata to LP holders

Sweep Operations

The sweep functions (buyWithSweep, sellWithSweep) traverse the price bitmap to fill orders at the best available prices, then fall back to PMHookRouter for any remaining amount.

sellWithSweep() Execution Flow
sellWithSweep() EXECUTION FLOW SHARES IN sharesIn, minPriceBps, minCollateralOut STEP 1: SWEEP BID POOLS Scan bitmap high → low (best price first) 7500 BEST 7000 depth 6500 depth ... minPrice _fillBidPool() accSharesPerScaled += shares/LP remaining? STEP 2: PMHOOKROUTER FALLBACK Route remainder through Vault OTC → AMM PM_HOOK_ROUTER.sellWithBootstrap(remainingShares, ...) COLLATERAL OUT + SOURCES totalCollateralOut, poolCollateralOut, sources[] SOURCE TAGS "BIDPOOL" = bids "otc" = vault "amm" = zAMM SLIPPAGE CHECK totalCollateralOut >= minCollateralOut or revert

Mint Operations

MasterRouter provides three mint variants that split collateral into YES/NO shares, keep one side for directional exposure, and route the other side differently based on strategy.

Mint Operations Comparison
MINT OPERATIONS Split collateral → Keep one side, route other side PAMM.split() collateral → YES + NO shares mintAndPool() 1. Keep one side 2. Pool other at price 3. Get LP position → ASK pool + kept shares mintAndVault() 1. Keep one side 2. Deposit other to vault 3. Get vault LP shares → Vault shares + kept shares mintAndSellOther() 1. Keep one side 2. Sweep bid pools 3. Route via PMHookRouter → Collateral + kept shares USE CASE Maker: set limit order Earn spread when filled Keep directional exposure USE CASE LP: provide liquidity Earn fees from OTC fills Keep directional exposure USE CASE Taker: instant exposure Recover cost via selling Better than pure buy OUTPUT COMPARISON mintAndPool: LP units mintAndVault: Vault shares mintAndSellOther: Collateral (passive income) (passive income) (immediate)

08 // GasPM.sol

GasPM is a self-contained "infinite market" example built on top of ethPM. It demonstrates a specialized resolver for gas price predictions with built-in oracle infrastructure.

GasPM Architecture
GASPM: INFINITE GAS PRICE PREDICTION MARKETS ORACLE LAYER STATE TRACKING cumulativeBaseFee // TWAP source lastBaseFee // Current reading maxBaseFee // All-time high minBaseFee // All-time low PERMISSIONLESS UPDATE update() // Anyone can call Optional ETH rewards for updaters block.basefee → Native EVM opcode (EIP-1559) MARKET TYPES DIRECTIONAL Above/below threshold RANGE Between lower/upper bounds BREAKOUT New all-time high PEAK New windowed high TROUGH New windowed low VOLATILITY Max-min spread exceeds X STABILITY Max-min spread under X SPOT Exact value (±tolerance) COMPARISON TWAP vs starting TWAP + Window variants track metrics since market creation view functions create markets ETHPM INTEGRATION Resolver.sol GasPM address as target getTwap() / getMax() / etc. Deterministic resolution PAMM.sol YES/NO share minting Collateral custody Resolution & redemption

Market Types

Type Description
DirectionalAbove/below threshold
RangeBetween bounds
BreakoutNew all-time high
Peak/TroughNew windowed high/low
VolatilityVariance-based
SpotExact value (±tolerance)
ComparisonA vs B

09 // Deployment

Prerequisites

  • Solidity ^0.8.30 (PAMM), ^0.8.31 (PMFeeHook, PMHookRouter)
  • EIP-1153 support (Cancun/Dencun hardfork) for transient storage
  • Existing zAMM deployment

Deployment Order

  1. PAMM — Core vault (immutable)
  2. PMFeeHook — Fee policy (owner-configurable)
  3. PMHookRouter — Sets PAMM operator, registered as REGISTRAR
  4. Resolver — Per-market deployment

Gas Estimates

Operation Gas (approx)
bootstrapMarket()~450,000
buyWithBootstrap() [OTC]~200,000
buyWithBootstrap() [AMM]~280,000
buyWithBootstrap() [Mint]~320,000
depositToVault()~120,000
rebalanceBootstrapVault()~350,000
claim()~80,000