AMM Design for Prediction Markets: Advanced Pricing Mechanisms

Build sophisticated automated market makers for prediction markets. Learn LMSR, CPMM adaptations, and liquidity-efficient pricing mechanisms.

Kevan Shah · · 15 min read

Automated Market Makers (AMMs) are the liquidity engines of prediction markets. Unlike traditional exchanges that rely on market makers, AMMs algorithmically price trades and provide continuous liquidity. The choice of AMM mechanism profoundly impacts trader experience, capital efficiency, and market accuracy.

This deep dive explores advanced AMM designs specifically optimized for prediction markets, from classic LMSR to modern hybrid approaches.

The Prediction Market AMM Challenge

Prediction markets have unique requirements:

  1. Binary outcomes: Prices must stay within [0, 1] range (probabilities)
  2. Time decay: Value changes as resolution approaches
  3. Resolution uncertainty: Markets can resolve early or be invalidated
  4. Information asymmetry: Informed traders move prices

Logarithmic Market Scoring Rule (LMSR)

Robin Hanson's LMSR remains the gold standard for prediction market AMMs.

Mathematical Foundation

C(q) = b * ln(Σ e^(qi/b))

Where:
- C(q): Cost function
- b: Liquidity parameter
- qi: Quantity of shares in outcome i

Implementation

contract LMSR {
    uint256 public b; // Liquidity parameter
    uint256 public outcomeCount;
    uint256[] public quantities;
    
    constructor(uint256 _b, uint256 _outcomeCount) {
        b = _b;
        outcomeCount = _outcomeCount;
        quantities = new uint256[](_outcomeCount);
    }
    
    // Cost function: b * ln(sum(e^(q_i/b)))
    function cost(uint256[] memory q) public view returns (uint256) {
        uint256 sum = 0;
        for (uint i = 0; i < outcomeCount; i++) {
            sum += exp(q[i] / b);
        }
        return b * ln(sum);
    }
    
    // Price for outcome i: e^(q_i/b) / sum(e^(q_j/b))
    function price(uint256 outcomeIndex) public view returns (uint256) {
        uint256 numerator = exp(quantities[outcomeIndex] / b);
        uint256 denominator = 0;
        
        for (uint i = 0; i < outcomeCount; i++) {
            denominator += exp(quantities[i] / b);
        }
        
        return (numerator * 1e18) / denominator;
    }
    
    // Cost to buy shares
    function calcBuyCost(
        uint256 outcomeIndex,
        uint256 shareAmount
    ) external view returns (uint256) {
        uint256[] memory newQuantities = new uint256[](outcomeCount);
        
        for (uint i = 0; i < outcomeCount; i++) {
            newQuantities[i] = quantities[i];
        }
        
        newQuantities[outcomeIndex] += shareAmount;
        
        return cost(newQuantities) - cost(quantities);
    }
    
    function buy(uint256 outcomeIndex, uint256 shareAmount) external {
        uint256 costAmount = calcBuyCost(outcomeIndex, shareAmount);
        
        collateral.transferFrom(msg.sender, address(this), costAmount);
        quantities[outcomeIndex] += shareAmount;
        
        _mintShares(msg.sender, outcomeIndex, shareAmount);
    }
}

Liquidity Parameter (b)

The b parameter controls market depth:

  • Small b: High slippage, less liquidity needed, more price sensitive
  • Large b: Low slippage, more liquidity needed, less price sensitive
function adjustLiquidity(uint256 newB) external {
    require(msg.sender == governance, "Not governance");
    
    // When increasing b, need to add collateral
    if (newB > b) {
        uint256 collateralNeeded = calcAdditionalCollateral(newB);
        collateral.transferFrom(msg.sender, address(this), collateralNeeded);
    }
    
    b = newB;
}

Advantages of LMSR:

  • Bounded loss for market maker (b * ln(n))
  • Natural probability interpretation
  • Resistant to manipulation
  • No liquidity fragmentation

Disadvantages:

  • Requires setting b parameter
  • Gas intensive (exponential calculations)
  • Maximum loss occurs if one outcome is certain

Constant Product Market Maker (CPMM) Adaptation

Uniswap's CPMM can be adapted for binary prediction markets.

Two-Outcome CPMM

contract CPMMBinary {
    uint256 public yesReserves;
    uint256 public noReserves;
    uint256 public constantK;
    
    function initialize(uint256 initialLiquidity) external {
        yesReserves = initialLiquidity;
        noReserves = initialLiquidity;
        constantK = initialLiquidity * initialLiquidity;
    }
    
    // Price of YES = NO / (YES + NO)
    function yesPrice() public view returns (uint256) {
        return (noReserves * 1e18) / (yesReserves + noReserves);
    }
    
    // Buy YES shares
    function buyYes(uint256 collateralAmount) external {
        // Transfer collateral
        collateral.transferFrom(msg.sender, address(this), collateralAmount);
        
        // Calculate YES shares to mint
        uint256 newYesReserves = yesReserves + collateralAmount;
        uint256 newNoReserves = constantK / newYesReserves;
        uint256 noSharesSold = noReserves - newNoReserves;
        
        // User gets YES shares + NO shares sold
        uint256 yesShares = collateralAmount + noSharesSold;
        
        yesReserves = newYesReserves;
        noReserves = newNoReserves;
        
        _mintYesShares(msg.sender, yesShares);
    }
    
    // Sell YES shares
    function sellYes(uint256 yesShares) external {
        // Calculate collateral to return
        uint256 newYesReserves = yesReserves - yesShares;
        uint256 newNoReserves = constantK / newYesReserves;
        uint256 noSharesBought = newNoReserves - noReserves;
        
        uint256 collateralReturn = yesShares - noSharesBought;
        
        yesReserves = newYesReserves;
        noReserves = newNoReserves;
        
        _burnYesShares(msg.sender, yesShares);
        collateral.transfer(msg.sender, collateralReturn);
    }
}

Comparison: LMSR vs CPMM

FeatureLMSRCPMM
Loss boundYes (b*ln(n))No (unbounded)
Price range[0,1] naturallyMust normalize
Capital efficiencyModerateHigh
Gas costHighLower
SlippagePredictableVariable
Best forLow volumeHigh volume

Liquidity-Sensitive LMSR (LS-LMSR)

Improves capital efficiency by adjusting b based on trading volume.

contract LSLMSR {
    uint256 public baseB;
    uint256 public volume;
    uint256 public volumeAdjustmentRate;
    
    function getEffectiveB() public view returns (uint256) {
        // b increases with volume
        uint256 adjustment = (volume * volumeAdjustmentRate) / 1e18;
        return baseB + adjustment;
    }
    
    function buy(uint256 outcomeIndex, uint256 shares) external {
        uint256 b = getEffectiveB();
        uint256 cost = calcBuyCost(outcomeIndex, shares, b);
        
        collateral.transferFrom(msg.sender, address(this), cost);
        
        // Update volume
        volume += cost;
        
        quantities[outcomeIndex] += shares;
        _mintShares(msg.sender, outcomeIndex, shares);
    }
}

Benefits:

  • Lower initial capital requirement
  • Market depth increases with activity
  • Rewards early liquidity providers

Order Book Hybrid

Combine AMM liquidity with limit orders for price improvement.

contract HybridAMMOB {
    // AMM component
    LMSR public amm;
    
    // Order book component
    struct Order {
        address trader;
        uint256 outcome;
        uint256 price;
        uint256 amount;
        bool isBuy;
    }
    
    mapping(uint256 => Order[]) public buyOrders;
    mapping(uint256 => Order[]) public sellOrders;
    
    function trade(uint256 outcome, uint256 amount, uint256 maxPrice) external {
        uint256 remaining = amount;
        
        // First, match with order book
        remaining = _matchWithOrderBook(outcome, remaining, maxPrice);
        
        // Then use AMM for remainder
        if (remaining > 0) {
            amm.buy(outcome, remaining);
        }
    }
    
    function _matchWithOrderBook(
        uint256 outcome,
        uint256 amount,
        uint256 maxPrice
    ) internal returns (uint256 remaining) {
        Order[] storage orders = buyOrders[outcome];
        remaining = amount;
        
        for (uint i = 0; i < orders.length && remaining > 0; i++) {
            if (orders[i].price >= maxPrice) {
                uint256 matchAmount = min(remaining, orders[i].amount);
                
                // Execute match
                _executeMatch(orders[i], matchAmount);
                
                remaining -= matchAmount;
                orders[i].amount -= matchAmount;
            }
        }
        
        return remaining;
    }
}

Automated Liquidity Management

Dynamic Liquidity Allocation

contract DynamicLiquidity {
    struct LiquidityProvider {
        uint256 deposit;
        uint256 entryTimestamp;
        uint256 entryPrice;
    }
    
    mapping(address => LiquidityProvider) public providers;
    uint256 public totalLiquidity;
    
    function addLiquidity(uint256 amount) external {
        collateral.transferFrom(msg.sender, address(this), amount);
        
        providers[msg.sender] = LiquidityProvider({
            deposit: amount,
            entryTimestamp: block.timestamp,
            entryPrice: getCurrentPrice()
        });
        
        totalLiquidity += amount;
        _updateAMMParameters();
    }
    
    function _updateAMMParameters() internal {
        // Adjust b based on total liquidity
        uint256 newB = calculateOptimalB(totalLiquidity);
        amm.adjustLiquidity(newB);
    }
}

Impermanent Loss Protection

contract ILProtection {
    uint256 public ilProtectionBps = 500; // 5% max IL
    
    function removeLiquidity() external returns (uint256 amount) {
        uint256 initialDeposit = lpDeposits[msg.sender];
        uint256 currentValue = calcLPValue(msg.sender);
        
        // Calculate IL
        uint256 impermanentLoss = initialDeposit > currentValue 
            ? initialDeposit - currentValue 
            : 0;
        
        uint256 maxIL = (initialDeposit * ilProtectionBps) / 10000;
        
        if (impermanentLoss > maxIL) {
            // Compensate from insurance fund
            uint256 compensation = impermanentLoss - maxIL;
            insuranceFund.transfer(msg.sender, compensation);
        }
        
        amount = currentValue;
        _removeLiquidity(msg.sender, amount);
    }
}

Advanced Pricing Features

Dynamic Fees Based on Certainty

function calculateFee(uint256 price) public view returns (uint256) {
    // Lower fees when price is near 0 or 1 (high certainty)
    // Higher fees when price is near 0.5 (uncertainty)
    
    uint256 distanceFromMidpoint = abs(int256(price) - 0.5e18);
    uint256 certainty = 1e18 - (2 * distanceFromMidpoint);
    
    // Base fee 1%, reduces to 0.1% at extremes
    uint256 baseFee = 100; // 1%
    uint256 minFee = 10;   // 0.1%
    
    return minFee + ((baseFee - minFee) * certainty) / 1e18;
}

Time-Weighted Pricing

function timeAdjustedPrice(uint256 basePrice) public view returns (uint256) {
    uint256 timeToResolution = market.resolutionTime - block.timestamp;
    uint256 totalDuration = market.resolutionTime - market.creationTime;
    
    // As resolution approaches, price becomes more "sticky"
    uint256 timeWeight = timeToResolution / totalDuration;
    
    // Pull price toward 0.5 as time runs out (if no new info)
    uint256 decayedPrice = 0.5e18 + ((basePrice - 0.5e18) * timeWeight);
    
    return decayedPrice;
}

Multi-Outcome Markets

contract MultiOutcomeAMM {
    uint256 public outcomeCount;
    uint256[] public quantities;
    uint256 public b;
    
    function buyShares(
        uint256[] memory outcomeAmounts
    ) external returns (uint256 cost) {
        require(outcomeAmounts.length == outcomeCount, "Invalid length");
        
        uint256[] memory newQuantities = new uint256[](outcomeCount);
        for (uint i = 0; i < outcomeCount; i++) {
            newQuantities[i] = quantities[i] + outcomeAmounts[i];
        }
        
        cost = calcCost(newQuantities) - calcCost(quantities);
        
        collateral.transferFrom(msg.sender, address(this), cost);
        quantities = newQuantities;
        
        for (uint i = 0; i < outcomeCount; i++) {
            if (outcomeAmounts[i] > 0) {
                _mintShares(msg.sender, i, outcomeAmounts[i]);
            }
        }
    }
    
    // Dutch auction for multi-outcome
    function dutchAuctionBuy(
        uint256 outcome,
        uint256 shares,
        uint256 maxPrice
    ) external {
        uint256 currentPrice = price(outcome);
        require(currentPrice <= maxPrice, "Price too high");
        
        // Execute trade
        buyShares(outcome, shares);
    }
}

Gas Optimization

Approximation Techniques

// Use approximations instead of precise calculations
function fastExp(uint256 x) internal pure returns (uint256) {
    // Taylor series approximation for e^x
    // e^x ≈ 1 + x + x^2/2 + x^3/6 + x^4/24
    
    uint256 x2 = (x * x) / 1e18;
    uint256 x3 = (x2 * x) / 1e18;
    uint256 x4 = (x3 * x) / 1e18;
    
    return 1e18 + x + x2/2 + x3/6 + x4/24;
}

function fastLn(uint256 x) internal pure returns (uint256) {
    // Approximation for ln(x)
    // ln(x) ≈ 2 * [(x-1)/(x+1) + (x-1)^3/3(x+1)^3 + ...]
    
    // Simplified: use lookup tables for common values
    if (x == 1e18) return 0;
    if (x == 2e18) return 693147e12; // ln(2) * 1e18
    // ... more common values
}

Batch Operations

function batchBuy(
    uint256[] memory outcomes,
    uint256[] memory amounts
) external {
    require(outcomes.length == amounts.length, "Length mismatch");
    
    uint256 totalCost = 0;
    uint256[] memory newQuantities = quantities;
    
    for (uint i = 0; i < outcomes.length; i++) {
        newQuantities[outcomes[i]] += amounts[i];
    }
    
    totalCost = calcCost(newQuantities) - calcCost(quantities);
    
    collateral.transferFrom(msg.sender, address(this), totalCost);
    quantities = newQuantities;
    
    // Mint all shares
    for (uint i = 0; i < outcomes.length; i++) {
        _mintShares(msg.sender, outcomes[i], amounts[i]);
    }
}

Conclusion

AMM design is critical infrastructure for prediction markets. The optimal choice depends on specific use cases:

Choose LMSR when:

  • Lower trading volume expected
  • Mathematical precision is paramount
  • Gas costs are acceptable
  • You need bounded loss guarantees

Choose CPMM when:

  • High volume expected
  • Gas efficiency matters
  • Capital efficiency is priority
  • You have sufficient liquidity

Key Design Principles:

  1. Match liquidity to expected volume: Too much wastes capital, too little causes high slippage
  2. Optimize for your users: Retail traders need low slippage, sophisticated traders need efficiency
  3. Plan for scale: Design AMMs that can grow with your platform
  4. Monitor and adapt: Adjust parameters based on market data
  5. Protect liquidity providers: IL protection attracts long-term liquidity

The most successful prediction markets will feature AMMs that provide tight spreads, deep liquidity, and fair prices—enabling traders to express their predictions efficiently while ensuring the market accurately aggregates collective intelligence.