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.
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:
-
Smart Contract Exploit
- Immediate pause
- Fund recovery procedures
- User communication
- Post-mortem analysis
-
Oracle Manipulation
- Circuit breaker activation
- Oracle fallback
- Market invalidation decision
- Compensation procedures
-
Liquidity Crisis
- Emergency liquidity injection
- Fee adjustments
- Trading limitations
- Communication strategy
-
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:
- Validate rigorously: Prevent invalid markets from launching
- Monitor continuously: Detect anomalies in real-time
- Respond quickly: Have clear emergency procedures
- Protect users: Provide insurance and compensation mechanisms
- 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.