Prediction Market Risk Management: Protecting Users and Protocol

Implement comprehensive risk management for prediction markets. Learn about exposure limits, circuit breakers, and protecting against market manipulation.

Kevan Shah · · 14 min read

Prediction markets face unique risks that can devastate users and destroy protocol trust. From oracle manipulation to cascading liquidations, from invalid markets to information asymmetry exploits, risk management must be woven into every layer of the protocol.

This guide examines comprehensive risk management strategies for prediction market platforms.

Risk Categories in Prediction Markets

1. Market Risk

  • Invalid or ambiguous markets
  • Oracle failure
  • Resolution delays
  • Unexpected outcomes

2. Operational Risk

  • Smart contract bugs
  • Oracle downtime
  • Liquidity crises
  • MEV exploitation

3. Credit Risk

  • Counterparty default
  • Insufficient collateral
  • Bad debt accumulation

4. Regulatory Risk

  • Changing legal frameworks
  • Jurisdiction restrictions
  • Compliance failures

5. Behavioral Risk

  • Market manipulation
  • Insider trading
  • Pump and dump schemes

Market Validation Framework

Pre-Launch Market Screening

contract MarketValidator {
    struct ValidationCriteria {
        uint256 minQuestionLength;
        uint256 minDescriptionLength;
        uint256 maxResolutionTime;
        uint256 minResolutionTime;
        bool requiresResolutionSource;
        uint256 maxCategories;
    }
    
    ValidationCriteria public criteria;
    
    function validateMarket(
        string memory question,
        string memory description,
        uint256 resolutionTime,
        string memory resolutionSource
    ) public view returns (bool isValid, string memory reason) {
        // Check question length
        if (bytes(question).length < criteria.minQuestionLength) {
            return (false, "Question too short");
        }
        
        // Check description
        if (bytes(description).length < criteria.minDescriptionLength) {
            return (false, "Description insufficient");
        }
        
        // Check resolution timing
        uint256 timeUntilResolution = resolutionTime - block.timestamp;
        if (timeUntilResolution < criteria.minResolutionTime) {
            return (false, "Resolution too soon");
        }
        if (timeUntilResolution > criteria.maxResolutionTime) {
            return (false, "Resolution too far in future");
        }
        
        // Check resolution source
        if (criteria.requiresResolutionSource && bytes(resolutionSource).length == 0) {
            return (false, "Resolution source required");
        }
        
        // Check for ambiguous language
        if (containsAmbiguousTerms(question)) {
            return (false, "Contains ambiguous terms");
        }
        
        return (true, "");
    }
    
    function containsAmbiguousTerms(string memory question) internal pure returns (bool) {
        // Check for ambiguous terms like "soon", "large", "significant"
        string memory lowerQuestion = toLower(question);
        
        string[10] memory ambiguousTerms = [
            "soon", "quickly", "large", "significant", 
            "substantial", "major", "important", "soon",
            "near future", "in the coming weeks"
        ];
        
        for (uint i = 0; i < ambiguousTerms.length; i++) {
            if (contains(lowerQuestion, ambiguousTerms[i])) {
                return true;
            }
        }
        
        return false;
    }
}

Invalid Market Insurance

contract InvalidMarketProtection {
    uint256 public constant INSURANCE_FEE_BPS = 100; // 1%
    
    function buySharesWithInsurance(
        uint256 marketId,
        uint256 amount,
        bool isYes
    ) external {
        uint256 insuranceFee = (amount * INSURANCE_FEE_BPS) / 10000;
        uint256 tradeAmount = amount - insuranceFee;
        
        // Buy shares
        _buyShares(marketId, tradeAmount, isYes);
        
        // Record insurance
        insuredTrades[msg.sender][marketId] = InsuredTrade({
            amount: tradeAmount,
            timestamp: block.timestamp
        });
        
        // Send fee to insurance fund
        insuranceFund.deposit(insuranceFee);
    }
    
    function claimInvalidMarketRefund(uint256 marketId) external {
        require(markets[marketId].invalid, "Market not invalid");
        
        InsuredTrade memory trade = insuredTrades[msg.sender][marketId];
        require(trade.amount > 0, "No insured trade");
        
        // Refund from insurance fund
        insuranceFund.withdraw(msg.sender, trade.amount);
        
        delete insuredTrades[msg.sender][marketId];
    }
}

Exposure Limits and Position Sizing

Individual Market Limits

contract ExposureLimits {
    mapping(uint256 => uint256) public maxMarketExposure;
    mapping(uint256 => uint256) public currentMarketExposure;
    
    mapping(address => mapping(uint256 => uint256)) public userExposure;
    mapping(address => uint256) public userTotalExposure;
    uint256 public constant MAX_USER_EXPOSURE = 1000000e18; // $1M
    
    function setMarketLimit(uint256 marketId, uint256 limit) external {
        require(msg.sender == riskManager, "Not authorized");
        maxMarketExposure[marketId] = limit;
    }
    
    modifier withinLimits(uint256 marketId, uint256 amount) {
        require(
            currentMarketExposure[marketId] + amount <= maxMarketExposure[marketId],
            "Exceeds market limit"
        );
        
        require(
            userTotalExposure[msg.sender] + amount <= MAX_USER_EXPOSURE,
            "Exceeds user limit"
        );
        _;
    }
    
    function updateExposure(
        address user,
        uint256 marketId,
        int256 amount
    ) internal {
        if (amount > 0) {
            currentMarketExposure[marketId] += uint256(amount);
            userExposure[user][marketId] += uint256(amount);
            userTotalExposure[user] += uint256(amount);
        } else {
            currentMarketExposure[marketId] -= uint256(-amount);
            userExposure[user][marketId] -= uint256(-amount);
            userTotalExposure[user] -= uint256(-amount);
        }
    }
}

Concentration Limits

contract ConcentrationLimits {
    uint256 public constant MAX_SINGLE_MARKET_PCT = 5000; // 50%
    uint256 public constant MAX_CATEGORY_PCT = 7000; // 70%
    
    function checkConcentrationLimits(
        address user,
        uint256 newMarketId,
        uint256 newAmount
    ) public view returns (bool) {
        uint256 totalExposure = getTotalExposure(user);
        
        // Check single market concentration
        uint256 newMarketExposure = getUserExposure(user, newMarketId) + newAmount;
        if ((newMarketExposure * 10000) / totalExposure > MAX_SINGLE_MARKET_PCT) {
            return false;
        }
        
        // Check category concentration
        string memory category = markets[newMarketId].category;
        uint256 categoryExposure = getCategoryExposure(user, category) + newAmount;
        if ((categoryExposure * 10000) / totalExposure > MAX_CATEGORY_PCT) {
            return false;
        }
        
        return true;
    }
}

Circuit Breakers and Emergency Controls

Price Movement Circuit Breakers

contract CircuitBreakers {
    uint256 public constant PRICE_CHANGE_THRESHOLD = 2000; // 20%
    uint256 public constant CIRCUIT_BREAKER_DURATION = 1 hours;
    
    mapping(uint256 => uint256) public lastPrice;
    mapping(uint256 => uint256) public circuitBreakerEnd;
    mapping(uint256 => bool) public isCircuitBreakerActive;
    
    function checkCircuitBreaker(uint256 marketId) internal {
        uint256 currentPrice = amm.price(marketId);
        uint256 previousPrice = lastPrice[marketId];
        
        if (previousPrice > 0) {
            uint256 priceChange = currentPrice > previousPrice
                ? ((currentPrice - previousPrice) * 10000) / previousPrice
                : ((previousPrice - currentPrice) * 10000) / previousPrice;
            
            if (priceChange > PRICE_CHANGE_THRESHOLD) {
                activateCircuitBreaker(marketId);
            }
        }
        
        lastPrice[marketId] = currentPrice;
    }
    
    function activateCircuitBreaker(uint256 marketId) internal {
        isCircuitBreakerActive[marketId] = true;
        circuitBreakerEnd[marketId] = block.timestamp + CIRCUIT_BREAKER_DURATION;
        
        emit CircuitBreakerActivated(marketId, block.timestamp);
    }
    
    function checkCircuitBreakerStatus(uint256 marketId) public {
        if (isCircuitBreakerActive[marketId] && block.timestamp >= circuitBreakerEnd[marketId]) {
            isCircuitBreakerActive[marketId] = false;
            emit CircuitBreakerDeactivated(marketId);
        }
    }
    
    modifier notCircuitBroken(uint256 marketId) {
        checkCircuitBreakerStatus(marketId);
        require(!isCircuitBreakerActive[marketId], "Circuit breaker active");
        _;
    }
}

Volume Spike Detection

contract VolumeAnomalyDetection {
    struct VolumeMetrics {
        uint256 hourlyVolume;
        uint256 lastUpdateHour;
        uint256 averageHourlyVolume;
        uint256 volumeHistory; // Sum of last 24 hours
    }
    
    mapping(uint256 => VolumeMetrics) public marketVolume;
    uint256 public constant VOLUME_SPIKE_THRESHOLD = 500; // 5x average
    
    function recordVolume(uint256 marketId, uint256 amount) internal {
        VolumeMetrics storage metrics = marketVolume[marketId];
        uint256 currentHour = block.timestamp / 3600;
        
        if (currentHour > metrics.lastUpdateHour) {
            // New hour - update averages
            if (metrics.lastUpdateHour > 0) {
                metrics.volumeHistory = (metrics.volumeHistory * 23 + metrics.hourlyVolume) / 24;
                metrics.averageHourlyVolume = metrics.volumeHistory / 24;
            }
            
            metrics.hourlyVolume = amount;
            metrics.lastUpdateHour = currentHour;
        } else {
            metrics.hourlyVolume += amount;
        }
        
        // Check for anomaly
        if (metrics.averageHourlyVolume > 0) {
            uint256 spikeRatio = (metrics.hourlyVolume * 100) / metrics.averageHourlyVolume;
            if (spikeRatio > VOLUME_SPIKE_THRESHOLD) {
                emit VolumeAnomaly(marketId, metrics.hourlyVolume, metrics.averageHourlyVolume);
                // Can trigger additional verification or circuit breaker
            }
        }
    }
}

Emergency Pause Functionality

contract EmergencyControls {
    address public guardian;
    address public governance;
    bool public globalPause;
    mapping(uint256 => bool) public marketPaused;
    
    modifier onlyGuardian() {
        require(msg.sender == guardian, "Not guardian");
        _;
    }
    
    modifier onlyGovernance() {
        require(msg.sender == governance, "Not governance");
        _;
    }
    
    modifier whenNotPaused(uint256 marketId) {
        require(!globalPause, "Global pause active");
        require(!marketPaused[marketId], "Market paused");
        _;
    }
    
    function emergencyPauseMarket(uint256 marketId) external onlyGuardian {
        marketPaused[marketId] = true;
        emit MarketPaused(marketId, msg.sender, block.timestamp);
    }
    
    function emergencyGlobalPause() external onlyGuardian {
        globalPause = true;
        emit GlobalPause(msg.sender, block.timestamp);
    }
    
    function unpauseMarket(uint256 marketId) external onlyGovernance {
        marketPaused[marketId] = false;
        emit MarketUnpaused(marketId);
    }
    
    function unpauseGlobal() external onlyGovernance {
        globalPause = false;
        emit GlobalUnpause();
    }
}

Oracle Risk Management

Multi-Oracle Fallback

contract OracleRedundancy {
    address[] public primaryOracles;
    address[] public backupOracles;
    uint256 public requiredConfirmations;
    
    mapping(uint256 => mapping(address => uint256)) public oracleResponses;
    mapping(uint256 => mapping(uint256 => uint256)) public outcomeConfirmations;
    
    function submitResolution(
        uint256 marketId,
        uint256 outcome
    ) external onlyOracle {
        require(!markets[marketId].resolved, "Already resolved");
        
        oracleResponses[marketId][msg.sender] = outcome;
        outcomeConfirmations[marketId][outcome]++;
        
        emit OracleSubmission(marketId, msg.sender, outcome);
        
        // Check if we have enough confirmations
        if (outcomeConfirmations[marketId][outcome] >= requiredConfirmations) {
            _resolveMarket(marketId, outcome);
        }
    }
    
    function forceResolutionWithBackup(uint256 marketId) external {
        require(block.timestamp > markets[marketId].resolutionTime + 7 days, "Not delayed enough");
        require(!markets[marketId].resolved, "Already resolved");
        
        // Use backup oracle
        uint256 outcome = IOracle(backupOracles[0]).getOutcome(marketId);
        _resolveMarket(marketId, outcome);
        
        emit ForcedResolution(marketId, outcome, "Backup oracle used");
    }
}

Oracle Staleness Detection

contract OracleHealthMonitor {
    uint256 public constant MAX_STALENESS = 1 hours;
    
    struct OracleStatus {
        uint256 lastUpdate;
        uint256 lastPrice;
        bool isHealthy;
    }
    
    mapping(address => OracleStatus) public oracleStatuses;
    
    function updateOracleStatus(address oracle, uint256 price) external {
        OracleStatus storage status = oracleStatuses[oracle];
        
        status.lastUpdate = block.timestamp;
        status.lastPrice = price;
        
        // Check health
        status.isHealthy = (block.timestamp - status.lastUpdate) <= MAX_STALENESS;
        
        emit OracleStatusUpdated(oracle, price, status.isHealthy);
    }
    
    function isOracleHealthy(address oracle) public view returns (bool) {
        OracleStatus memory status = oracleStatuses[oracle];
        return (block.timestamp - status.lastUpdate) <= MAX_STALENESS;
    }
}

Anti-Manipulation Measures

Insider Trading Prevention

contract InsiderTradingProtection {
    mapping(address => bool) public restrictedUsers;
    mapping(uint256 => address[]) public marketCreators;
    
    uint256 public constant RESTRICTION_PERIOD = 7 days;
    mapping(address => uint256) public restrictionEndTime;
    
    function restrictCreator(uint256 marketId) external {
        address creator = markets[marketId].creator;
        
        // Restrict creator from trading on their own market
        restrictedUsers[creator] = true;
        restrictionEndTime[creator] = markets[marketId].resolutionTime + RESTRICTION_PERIOD;
        
        emit UserRestricted(creator, marketId, restrictionEndTime[creator]);
    }
    
    modifier notRestricted() {
        if (restrictedUsers[msg.sender]) {
            require(block.timestamp > restrictionEndTime[msg.sender], "User restricted");
            restrictedUsers[msg.sender] = false; // Clear restriction
        }
        _;
    }
}

Wash Trading Detection

contract WashTradingMonitor {
    struct TradePattern {
        uint256 lastTradeTime;
        uint256 tradeCount;
        uint256 volume24h;
        address lastCounterparty;
    }
    
    mapping(address => TradePattern) public userPatterns;
    uint256 public constant MAX_TRADES_PER_HOUR = 100;
    uint256 public constant SUSPICIOUS_VOLUME_THRESHOLD = 1000000e18; // $1M
    
    function recordTrade(address user, address counterparty, uint256 volume) internal {
        TradePattern storage pattern = userPatterns[user];
        
        // Reset if hour passed
        if (block.timestamp > pattern.lastTradeTime + 1 hours) {
            pattern.tradeCount = 0;
            pattern.volume24h = 0;
        }
        
        pattern.tradeCount++;
        pattern.volume24h += volume;
        pattern.lastTradeTime = block.timestamp;
        pattern.lastCounterparty = counterparty;
        
        // Check for suspicious patterns
        if (pattern.tradeCount > MAX_TRADES_PER_HOUR) {
            emit SuspiciousActivity(user, "Excessive trading frequency");
        }
        
        if (pattern.volume24h > SUSPICIOUS_VOLUME_THRESHOLD) {
            emit SuspiciousActivity(user, "High volume");
        }
        
        // Check for self-trading
        if (counterparty == user) {
            emit SuspiciousActivity(user, "Self-trading detected");
        }
    }
}

Liquidity Risk Management

Liquidity Crunch Protection

contract LiquidityProtection {
    uint256 public constant MIN_LIQUIDITY_THRESHOLD = 10000e18; // $10k
    uint256 public constant MAX_SLIPPAGE_THRESHOLD = 1000; // 10%
    
    function checkLiquidityHealth(uint256 marketId) public view returns (bool) {
        uint256 liquidity = amm.getTotalLiquidity(marketId);
        
        if (liquidity < MIN_LIQUIDITY_THRESHOLD) {
            return false;
        }
        
        // Check slippage for standard trade
        uint256 testAmount = 1000e18; // $1000
        uint256 slippage = amm.calculateSlippage(marketId, testAmount);
        
        if (slippage > MAX_SLIPPAGE_THRESHOLD) {
            return false;
        }
        
        return true;
    }
    
    function emergencyLiquidityInjection(uint256 marketId) external onlyGuardian {
        require(!checkLiquidityHealth(marketId), "Liquidity healthy");
        
        uint256 injectionAmount = calculateInjectionAmount(marketId);
        
        // Use protocol treasury
        treasury.withdraw(injectionAmount);
        amm.addLiquidity(marketId, injectionAmount);
        
        emit EmergencyLiquidityInjected(marketId, injectionAmount);
    }
}

Insurance and Backstop Funds

Protocol Insurance Fund

contract ProtocolInsurance {
    uint256 public totalFunds;
    uint256 public maxCoveragePerMarket;
    
    mapping(uint256 => uint256) public marketCoverage;
    
    function deposit(uint256 amount) external {
        collateral.transferFrom(msg.sender, address(this), amount);
        totalFunds += amount;
    }
    
    function claimInsurance(
        uint256 marketId,
        uint256 amount,
        string memory reason
    ) external onlyGovernance {
        require(amount <= maxCoveragePerMarket, "Exceeds max coverage");
        require(amount <= totalFunds, "Insufficient funds");
        
        totalFunds -= amount;
        marketCoverage[marketId] += amount;
        
        // Distribute to affected users
        distributeCompensation(marketId, amount);
        
        emit InsuranceClaim(marketId, amount, reason);
    }
    
    function distributeCompensation(uint256 marketId, uint256 totalAmount) internal {
        address[] memory affectedUsers = getAffectedUsers(marketId);
        uint256 totalLoss = calculateTotalLoss(marketId);
        
        for (uint i = 0; i < affectedUsers.length; i++) {
            address user = affectedUsers[i];
            uint256 userLoss = calculateUserLoss(user, marketId);
            uint256 compensation = (userLoss * totalAmount) / totalLoss;
            
            collateral.transfer(user, compensation);
        }
    }
}

Monitoring and Alerting

Real-Time Risk Monitoring

interface RiskMetrics {
  marketId: string;
  totalExposure: number;
  largestPosition: number;
  liquidityDepth: number;
  priceVolatility: number;
  tradeVelocity: number;
  oracleHealth: boolean;
  circuitBreakerStatus: boolean;
}

class RiskMonitor {
  async checkRiskLevels(): Promise<void> {
    const markets = await this.getActiveMarkets();
    
    for (const market of markets) {
      const metrics = await this.calculateRiskMetrics(market);
      
      // Check thresholds
      if (metrics.totalExposure > MAX_MARKET_EXPOSURE) {
        await this.alert(`High exposure in market ${market.id}: ${metrics.totalExposure}`);
      }
      
      if (metrics.priceVolatility > VOLATILITY_THRESHOLD) {
        await this.triggerCircuitBreaker(market.id);
      }
      
      if (!metrics.oracleHealth) {
        await this.alert(`Oracle unhealthy for market ${market.id}`);
        await this.switchToBackupOracle(market.id);
      }
    }
  }
  
  async calculateRiskMetrics(market: Market): Promise<RiskMetrics> {
    return {
      marketId: market.id,
      totalExposure: await this.getTotalExposure(market.id),
      largestPosition: await this.getLargestPosition(market.id),
      liquidityDepth: await this.getLiquidityDepth(market.id),
      priceVolatility: await this.calculateVolatility(market.id, 24),
      tradeVelocity: await this.getTradeVelocity(market.id),
      oracleHealth: await this.checkOracleHealth(market.id),
      circuitBreakerStatus: await this.getCircuitBreakerStatus(market.id)
    };
  }
}

Incident Response Planning

Emergency Response Procedures

Document clear procedures for:

  1. Smart Contract Exploit

    • Immediate pause
    • Fund recovery procedures
    • User communication
    • Post-mortem analysis
  2. Oracle Manipulation

    • Circuit breaker activation
    • Oracle fallback
    • Market invalidation decision
    • Compensation procedures
  3. Liquidity Crisis

    • Emergency liquidity injection
    • Fee adjustments
    • Trading limitations
    • Communication strategy
  4. Market Invalidation

    • Invalidation criteria
    • Refund procedures
    • Insurance claims
    • Prevention measures
contract IncidentResponse {
    enum IncidentType {
        SmartContractExploit,
        OracleManipulation,
        LiquidityCrisis,
        MarketInvalidation,
        RegulatoryAction
    }
    
    struct Incident {
        IncidentType incidentType;
        uint256 timestamp;
        string description;
        bool resolved;
        bytes resolutionActions;
    }
    
    mapping(uint256 => Incident) public incidents;
    uint256 public incidentCount;
    
    function reportIncident(
        IncidentType incidentType,
        string memory description
    ) external onlyAuthorized {
        uint256 incidentId = incidentCount++;
        
        incidents[incidentId] = Incident({
            incidentType: incidentType,
            timestamp: block.timestamp,
            description: description,
            resolved: false,
            resolutionActions: ""
        });
        
        // Trigger automated response
        _triggerAutomatedResponse(incidentType);
        
        emit IncidentReported(incidentId, incidentType, description);
    }
    
    function _triggerAutomatedResponse(IncidentType incidentType) internal {
        if (incidentType == IncidentType.SmartContractExploit) {
            emergencyGlobalPause();
        } else if (incidentType == IncidentType.OracleManipulation) {
            activateAllCircuitBreakers();
        }
        // ... additional responses
    }
}

Best Practices Summary

Prevention:

  • Rigorous market validation
  • Exposure limits and position sizing
  • Multi-layered oracle systems
  • Real-time monitoring

Detection:

  • Anomaly detection algorithms
  • Circuit breakers on price/volume
  • Oracle health monitoring
  • User behavior analysis

Response:

  • Emergency pause capabilities
  • Clear incident procedures
  • Insurance and backstop funds
  • Communication protocols

Recovery:

  • Compensation mechanisms
  • Market invalidation procedures
  • Liquidity restoration
  • Post-incident reviews

Conclusion

Risk management in prediction markets requires a multi-layered approach combining prevention, detection, response, and recovery. The protocols that survive long-term will be those that:

  1. Validate rigorously: Prevent invalid markets from launching
  2. Monitor continuously: Detect anomalies in real-time
  3. Respond quickly: Have clear emergency procedures
  4. Protect users: Provide insurance and compensation mechanisms
  5. Learn constantly: Update risk models based on incidents

Remember: in prediction markets, trust is everything. A single major incident can destroy user confidence permanently. Invest heavily in risk management—it's not overhead, it's the foundation of sustainable protocol growth.