Prediction Market
Overview
Quick Start
# Create new project from this template
npx labz create prediction-market my-project
# Navigate and install
cd my-project
npm install
# Run tests
npx hardhat testContract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import { FHE, euint64, euint8, ebool, eaddress, externalEuint64, externalEuint8, externalEbool, externalEaddress } from "@fhevm/solidity/lib/FHE.sol";
import { ZamaEthereumConfig } from "@fhevm/solidity/config/ZamaConfig.sol";
/**
* @title PredictionMarket
* @notice Polymarket-style prediction market with encrypted positions
* @dev Uses FHE for private bet amounts - pools decrypted after resolution for payout calc
*/
contract PredictionMarket is ZamaEthereumConfig {
// ============ Errors ============
error MarketNotFound();
error MarketAlreadyResolved();
error MarketNotResolved();
error InvalidOutcome();
error NoBetPlaced();
error AlreadyClaimed();
error PoolsNotDecrypted();
// ============ Events ============
event MarketCreated(uint256 indexed marketId, string question, uint256 deadline);
event BetPlaced(uint256 indexed marketId, address indexed bettor, bool outcome);
event MarketResolved(uint256 indexed marketId, bool outcome);
event PoolsDecrypted(uint256 indexed marketId, uint256 yesPool, uint256 noPool);
event WinningsClaimed(uint256 indexed marketId, address indexed winner);
// ============ Structs ============
struct Market {
string question;
uint256 deadline;
bool resolved;
bool outcome;
euint64 yesPool;
euint64 noPool;
uint256 yesCount;
uint256 noCount;
uint256 decryptedYesPool;
uint256 decryptedNoPool;
bool poolsDecrypted;
}
struct Position {
euint64 yesAmount;
euint64 noAmount;
bool claimed;
}
// ============ State Variables ============
mapping(uint256 => Market) public _markets;
mapping(uint256 => mapping(address => Position)) public _positions;
uint256 public _marketCount;
address public oracle;
uint256 public minBetAmount;
// ============ Modifiers ============
modifier onlyOracle() {
require(msg.sender == oracle, "Only oracle");
_;
}
modifier marketExists(uint256 marketId) {
if (marketId >= _marketCount) revert MarketNotFound();
_;
}
modifier marketOpen(uint256 marketId) {
if (_markets[marketId].resolved) revert MarketAlreadyResolved();
if (block.timestamp > _markets[marketId].deadline) revert MarketAlreadyResolved();
_;
}
// ============ Constructor ============
constructor(address _oracle, uint256 _minBetAmount) {
oracle = _oracle;
minBetAmount = _minBetAmount;
}
// ============ External Functions ============
function createMarket(string calldata question, uint256 deadline) external returns (uint256) {
require(deadline > block.timestamp, "Deadline must be future");
uint256 marketId = _marketCount++;
Market storage market = _markets[marketId];
market.question = question;
market.deadline = deadline;
market.yesPool = FHE.asEuint64(0);
market.noPool = FHE.asEuint64(0);
FHE.allowThis(market.yesPool);
FHE.allowThis(market.noPool);
emit MarketCreated(marketId, question, deadline);
return marketId;
}
function placeBet(
uint256 marketId,
bool outcome,
externalEuint64 encryptedAmount,
bytes calldata inputProof
) external marketExists(marketId) marketOpen(marketId) {
euint64 amount = FHE.fromExternal(encryptedAmount, inputProof);
Position storage pos = _positions[marketId][msg.sender];
if (outcome) {
pos.yesAmount = FHE.add(pos.yesAmount, amount);
_markets[marketId].yesPool = FHE.add(_markets[marketId].yesPool, amount);
_markets[marketId].yesCount++;
} else {
pos.noAmount = FHE.add(pos.noAmount, amount);
_markets[marketId].noPool = FHE.add(_markets[marketId].noPool, amount);
_markets[marketId].noCount++;
}
FHE.allowThis(pos.yesAmount);
FHE.allowThis(pos.noAmount);
FHE.allowThis(_markets[marketId].yesPool);
FHE.allowThis(_markets[marketId].noPool);
FHE.allow(pos.yesAmount, msg.sender);
FHE.allow(pos.noAmount, msg.sender);
emit BetPlaced(marketId, msg.sender, outcome);
}
function resolveMarket(uint256 marketId, bool outcome) external onlyOracle marketExists(marketId) {
Market storage market = _markets[marketId];
if (market.resolved) revert MarketAlreadyResolved();
require(block.timestamp > market.deadline, "Deadline not passed");
market.resolved = true;
market.outcome = outcome;
FHE.allow(market.yesPool, oracle);
FHE.allow(market.noPool, oracle);
emit MarketResolved(marketId, outcome);
}
function setDecryptedPools(uint256 marketId, uint256 yesPool, uint256 noPool) external onlyOracle marketExists(marketId) {
Market storage market = _markets[marketId];
require(market.resolved, "Market not resolved");
require(!market.poolsDecrypted, "Already decrypted");
market.decryptedYesPool = yesPool;
market.decryptedNoPool = noPool;
market.poolsDecrypted = true;
emit PoolsDecrypted(marketId, yesPool, noPool);
}
function claimWinnings(uint256 marketId) external marketExists(marketId) returns (euint64) {
Market storage market = _markets[marketId];
if (!market.resolved) revert MarketNotResolved();
if (!market.poolsDecrypted) revert PoolsNotDecrypted();
Position storage pos = _positions[marketId][msg.sender];
if (pos.claimed) revert AlreadyClaimed();
euint64 userBet;
uint256 winningPoolDecrypted;
uint256 totalPoolDecrypted = market.decryptedYesPool + market.decryptedNoPool;
if (market.outcome) {
userBet = pos.yesAmount;
winningPoolDecrypted = market.decryptedYesPool;
} else {
userBet = pos.noAmount;
winningPoolDecrypted = market.decryptedNoPool;
}
// FHE.div requires plaintext divisor - using decrypted pool value
euint64 numerator = FHE.mul(userBet, uint64(totalPoolDecrypted));
euint64 payout = FHE.div(numerator, uint64(winningPoolDecrypted));
pos.claimed = true;
FHE.allow(payout, msg.sender);
FHE.allowThis(payout);
emit WinningsClaimed(marketId, msg.sender);
return payout;
}
// ============ View Functions ============
function getMarket(uint256 marketId) external view returns (
string memory question,
uint256 deadline,
bool resolved,
bool outcome,
uint256 yesCount,
uint256 noCount,
bool poolsDecrypted
) {
Market storage market = _markets[marketId];
return (market.question, market.deadline, market.resolved, market.outcome, market.yesCount, market.noCount, market.poolsDecrypted);
}
function getMarketCount() external view returns (uint256) {
return _marketCount;
}
function hasClaimed(uint256 marketId, address user) external view returns (bool) {
return _positions[marketId][user].claimed;
}
function getDecryptedPools(uint256 marketId) external view returns (uint256 yesPool, uint256 noPool) {
Market storage market = _markets[marketId];
require(market.poolsDecrypted, "Pools not decrypted");
return (market.decryptedYesPool, market.decryptedNoPool);
}
}
Code Explanation
Market Struct
Place Bet
Claim Winnings
FHE Operations Used
FHE Types Used
Tags
Related Examples
Prerequisites
Next Steps
Last updated
