// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "./interfaces/IOracleHub.sol";
/**
* @title PredictionMarket
* @notice Polymarket-style prediction market on L2
* @dev Uses ERC-1155 for outcome tokens
*/
contract PredictionMarket is ERC1155, AccessControl, ReentrancyGuard {
bytes32 public constant MARKET_CREATOR_ROLE = keccak256("MARKET_CREATOR_ROLE");
bytes32 public constant RESOLVER_ROLE = keccak256("RESOLVER_ROLE");
IERC20 public immutable collateralToken; // USDC
IOracleHub public immutable oracleHub;
// ==================== 数据结构 ====================
enum MarketType {
Binary, // YES/NO
Categorical, // 多选一
Scalar // 数值范围
}
enum MarketStatus {
Active, // 开放交易
Closed, // 停止交易, 等待结算
Resolved, // 已结算
Canceled // 已取消 (退款)
}
struct Market {
bytes32 marketId;
string question; // 问题描述
MarketType marketType;
MarketStatus status;
uint256 endTime; // 市场关闭时间
uint256 resolutionTime; // 预计结算时间
uint256 createdAt;
address creator;
// Outcome tokens
uint256[] outcomeTokenIds; // [YES, NO] or [A, B, C, ...]
string[] outcomeNames; // ["YES", "NO"] or ["Alice", "Bob", ...]
// Resolution
uint256 winningOutcome; // 胜出选项 index
bytes32 oracleCondition; // Oracle 条件 (如 "BTC/USD > 100000")
// Stats
uint256 totalVolume; // 总交易量
uint256 totalLiquidity; // 总流动性
}
struct Position {
address user;
bytes32 marketId;
uint256 outcomeIndex;
uint256 shares; // 持有的 outcome tokens
uint256 avgPrice; // 平均买入价
}
// ==================== 状态变量 ====================
// marketId => Market
mapping(bytes32 => Market) public markets;
bytes32[] public allMarketIds;
// 下一个 token ID
uint256 public nextTokenId = 1;
// tokenId => marketId
mapping(uint256 => bytes32) public tokenToMarket;
// user => marketId => outcomeIndex => shares
mapping(address => mapping(bytes32 => mapping(uint256 => uint256))) public userShares;
// 手续费率 (basis points, 20 = 0.2%)
uint256 public feeRate = 20;
address public feeRecipient;
// ==================== 事件 ====================
event MarketCreated(
bytes32 indexed marketId,
string question,
MarketType marketType,
uint256 endTime,
uint256[] outcomeTokenIds
);
event SharesPurchased(
bytes32 indexed marketId,
address indexed user,
uint256 outcomeIndex,
uint256 shares,
uint256 cost
);
event SharesSold(
bytes32 indexed marketId,
address indexed user,
uint256 outcomeIndex,
uint256 shares,
uint256 payout
);
event MarketResolved(
bytes32 indexed marketId,
uint256 winningOutcome,
uint256 timestamp
);
event WinningsClaimed(
bytes32 indexed marketId,
address indexed user,
uint256 payout
);
// ==================== 构造函数 ====================
constructor(
address _collateralToken,
address _oracleHub,
address _feeRecipient
) ERC1155("https://api.prediction.market/token/{id}.json") {
collateralToken = IERC20(_collateralToken);
oracleHub = IOracleHub(_oracleHub);
feeRecipient = _feeRecipient;
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(MARKET_CREATOR_ROLE, msg.sender);
_grantRole(RESOLVER_ROLE, msg.sender);
}
// ==================== 市场创建 ====================
/**
* @notice 创建二元预测市场
* @param question 问题描述
* @param endTime 市场结束时间
* @param resolutionTime 预计结算时间
* @param oracleCondition Oracle 条件 (如 "BTC/USD>100000@2026-12-31")
*/
function createBinaryMarket(
string calldata question,
uint256 endTime,
uint256 resolutionTime,
bytes32 oracleCondition
) external onlyRole(MARKET_CREATOR_ROLE) returns (bytes32 marketId) {
require(endTime > block.timestamp, "Invalid end time");
require(resolutionTime >= endTime, "Resolution before end");
marketId = keccak256(abi.encodePacked(
question,
endTime,
block.timestamp
));
require(markets[marketId].createdAt == 0, "Market exists");
// 创建 YES/NO token IDs
uint256 yesTokenId = nextTokenId++;
uint256 noTokenId = nextTokenId++;
uint256[] memory outcomeTokenIds = new uint256[](2);
outcomeTokenIds[0] = yesTokenId;
outcomeTokenIds[1] = noTokenId;
string[] memory outcomeNames = new string[](2);
outcomeNames[0] = "YES";
outcomeNames[1] = "NO";
markets[marketId] = Market({
marketId: marketId,
question: question,
marketType: MarketType.Binary,
status: MarketStatus.Active,
endTime: endTime,
resolutionTime: resolutionTime,
createdAt: block.timestamp,
creator: msg.sender,
outcomeTokenIds: outcomeTokenIds,
outcomeNames: outcomeNames,
winningOutcome: 0,
oracleCondition: oracleCondition,
totalVolume: 0,
totalLiquidity: 0
});
allMarketIds.push(marketId);
tokenToMarket[yesTokenId] = marketId;
tokenToMarket[noTokenId] = marketId;
emit MarketCreated(
marketId,
question,
MarketType.Binary,
endTime,
outcomeTokenIds
);
return marketId;
}
/**
* @notice 创建分类市场
* @param question 问题
* @param outcomeNames 选项名称 (e.g., ["Alice", "Bob", "Charlie"])
* @param endTime 结束时间
* @param resolutionTime 结算时间
*/
function createCategoricalMarket(
string calldata question,
string[] calldata outcomeNames,
uint256 endTime,
uint256 resolutionTime
) external onlyRole(MARKET_CREATOR_ROLE) returns (bytes32 marketId) {
require(outcomeNames.length >= 2 && outcomeNames.length <= 10, "Invalid outcomes");
require(endTime > block.timestamp, "Invalid end time");
marketId = keccak256(abi.encodePacked(
question,
endTime,
block.timestamp
));
// 创建 outcome tokens
uint256[] memory outcomeTokenIds = new uint256[](outcomeNames.length);
for (uint256 i = 0; i < outcomeNames.length; i++) {
outcomeTokenIds[i] = nextTokenId++;
tokenToMarket[outcomeTokenIds[i]] = marketId;
}
markets[marketId] = Market({
marketId: marketId,
question: question,
marketType: MarketType.Categorical,
status: MarketStatus.Active,
endTime: endTime,
resolutionTime: resolutionTime,
createdAt: block.timestamp,
creator: msg.sender,
outcomeTokenIds: outcomeTokenIds,
outcomeNames: outcomeNames,
winningOutcome: 0,
oracleCondition: bytes32(0),
totalVolume: 0,
totalLiquidity: 0
});
allMarketIds.push(marketId);
emit MarketCreated(
marketId,
question,
MarketType.Categorical,
endTime,
outcomeTokenIds
);
return marketId;
}
// ==================== 交易功能 ====================
/**
* @notice 买入 shares (铸造 outcome tokens)
* @param marketId 市场 ID
* @param outcomeIndex 选项索引 (0 = YES, 1 = NO)
* @param amount 投入金额 (USDC)
* @param minShares 最少获得 shares (滑点保护)
*/
function buyShares(
bytes32 marketId,
uint256 outcomeIndex,
uint256 amount,
uint256 minShares
) external nonReentrant returns (uint256 shares) {
Market storage market = markets[marketId];
require(market.status == MarketStatus.Active, "Market not active");
require(block.timestamp < market.endTime, "Market ended");
require(outcomeIndex < market.outcomeTokenIds.length, "Invalid outcome");
// 收取手续费
uint256 fee = (amount * feeRate) / 10000;
uint256 netAmount = amount - fee;
// 转移 USDC
collateralToken.transferFrom(msg.sender, address(this), amount);
if (fee > 0) {
collateralToken.transfer(feeRecipient, fee);
}
// 计算 shares (简化: 1 USDC = 1 share, 实际应该用 AMM 定价)
// TODO: 集成 AMM 或订单簿定价
shares = netAmount;
require(shares >= minShares, "Slippage exceeded");
// 铸造 outcome tokens
uint256 tokenId = market.outcomeTokenIds[outcomeIndex];
_mint(msg.sender, tokenId, shares, "");
// 更新用户持仓
userShares[msg.sender][marketId][outcomeIndex] += shares;
// 更新市场统计
market.totalVolume += amount;
emit SharesPurchased(marketId, msg.sender, outcomeIndex, shares, amount);
return shares;
}
/**
* @notice 卖出 shares (销毁 outcome tokens)
* @param marketId 市场 ID
* @param outcomeIndex 选项索引
* @param shares 卖出数量
* @param minPayout 最少获得 USDC (滑点保护)
*/
function sellShares(
bytes32 marketId,
uint256 outcomeIndex,
uint256 shares,
uint256 minPayout
) external nonReentrant returns (uint256 payout) {
Market storage market = markets[marketId];
require(market.status == MarketStatus.Active, "Market not active");
require(block.timestamp < market.endTime, "Market ended");
uint256 tokenId = market.outcomeTokenIds[outcomeIndex];
// 销毁 outcome tokens
_burn(msg.sender, tokenId, shares);
// 计算赔付 (简化: 1 share = 1 USDC, 实际应该用 AMM)
// TODO: 集成 AMM 定价
payout = shares;
uint256 fee = (payout * feeRate) / 10000;
uint256 netPayout = payout - fee;
require(netPayout >= minPayout, "Slippage exceeded");
// 转账
collateralToken.transfer(msg.sender, netPayout);
if (fee > 0) {
collateralToken.transfer(feeRecipient, fee);
}
// 更新持仓
userShares[msg.sender][marketId][outcomeIndex] -= shares;
emit SharesSold(marketId, msg.sender, outcomeIndex, shares, netPayout);
return netPayout;
}
// ==================== 结算功能 ====================
/**
* @notice 自动结算 (基于 Oracle)
* @dev 仅适用于价格类市场
*/
function resolveMarketWithOracle(bytes32 marketId) external {
Market storage market = markets[marketId];
require(market.status == MarketStatus.Active || market.status == MarketStatus.Closed, "Invalid status");
require(block.timestamp >= market.resolutionTime, "Too early");
require(market.oracleCondition != bytes32(0), "No oracle condition");
// 解析 Oracle 条件: "BTC/USD>100000@timestamp"
(string memory symbol, uint256 targetPrice, bool isGreaterThan) =
_parseOracleCondition(market.oracleCondition);
// 从 Oracle Hub 读取价格
(uint256 actualPrice, ) = oracleHub.getLatestPrice(symbol);
// 判断结果
bool condition = isGreaterThan
? actualPrice >= targetPrice
: actualPrice < targetPrice;
uint256 winningOutcome = condition ? 0 : 1; // 0 = YES, 1 = NO
_resolveMarket(marketId, winningOutcome);
}
/**
* @notice 人工结算 (需要 RESOLVER 权限)
* @dev 适用于主观事件或 Oracle 失败
*/
function resolveMarketManually(bytes32 marketId, uint256 winningOutcome)
external
onlyRole(RESOLVER_ROLE)
{
Market storage market = markets[marketId];
require(market.status == MarketStatus.Active || market.status == MarketStatus.Closed, "Invalid status");
require(block.timestamp >= market.resolutionTime, "Too early");
require(winningOutcome < market.outcomeTokenIds.length, "Invalid outcome");
_resolveMarket(marketId, winningOutcome);
}
function _resolveMarket(bytes32 marketId, uint256 winningOutcome) private {
Market storage market = markets[marketId];
market.status = MarketStatus.Resolved;
market.winningOutcome = winningOutcome;
emit MarketResolved(marketId, winningOutcome, block.timestamp);
}
/**
* @notice 领取赢家奖金
* @param marketId 市场 ID
*/
function claimWinnings(bytes32 marketId) external nonReentrant {
Market storage market = markets[marketId];
require(market.status == MarketStatus.Resolved, "Not resolved");
uint256 winningTokenId = market.outcomeTokenIds[market.winningOutcome];
uint256 shares = balanceOf(msg.sender, winningTokenId);
require(shares > 0, "No winning shares");
// 销毁 winning tokens
_burn(msg.sender, winningTokenId, shares);
// 赔付: 1 share = 1 USDC
uint256 payout = shares;
collateralToken.transfer(msg.sender, payout);
emit WinningsClaimed(marketId, msg.sender, payout);
}
/**
* @notice 取消市场 (退款)
* @dev 仅限紧急情况
*/
function cancelMarket(bytes32 marketId)
external
onlyRole(DEFAULT_ADMIN_ROLE)
{
Market storage market = markets[marketId];
require(market.status != MarketStatus.Resolved, "Already resolved");
market.status = MarketStatus.Canceled;
// 用户可以销毁 shares 获得退款 (1:1)
// TODO: 实现退款逻辑
}
// ==================== 查询功能 ====================
function getMarket(bytes32 marketId) external view returns (Market memory) {
return markets[marketId];
}
function getAllMarkets() external view returns (bytes32[] memory) {
return allMarketIds;
}
function getUserPosition(address user, bytes32 marketId, uint256 outcomeIndex)
external
view
returns (uint256 shares)
{
Market storage market = markets[marketId];
uint256 tokenId = market.outcomeTokenIds[outcomeIndex];
return balanceOf(user, tokenId);
}
// ==================== 内部函数 ====================
function _parseOracleCondition(bytes32 condition)
private
pure
returns (string memory symbol, uint256 targetPrice, bool isGreaterThan)
{
// 简化实现, 实际应该更健壮
// "BTC/USD>100000" 编码为 bytes32
// TODO: 实现完整的解析逻辑
symbol = "BTC/USD";
targetPrice = 100000 * 1e18;
isGreaterThan = true;
return (symbol, targetPrice, isGreaterThan);
}
// ==================== 管理功能 ====================
function setFeeRate(uint256 newFeeRate) external onlyRole(DEFAULT_ADMIN_ROLE) {
require(newFeeRate <= 500, "Fee too high"); // Max 5%
feeRate = newFeeRate;
}
function setFeeRecipient(address newRecipient) external onlyRole(DEFAULT_ADMIN_ROLE) {
feeRecipient = newRecipient;
}
}