去中心化预测市场实战开发:Solidity+Viem 从合约设计到工程化落地
前言
在 Web3 生态迈向 2026 年的新阶段,去中心化预测市场(Decentralized Prediction Market,DPM)早已突破单纯的博弈工具属性,成为融合群体智慧价格发现、去中心化金融对冲、现实世界事件价值映射的核心 DeFi 组件。与中心化预测平台相比,基于智能合约的 DPM 凭借链上透明、无需信任、资产自管的特性,成为 Web3 落地现实应用的重要载体。
本文将从底层核心逻辑出发,拆解去中心化预测市场的设计精髓,通过Solidity实现可落地的智能合约架构,并结合 Web3 开发新标椎Viem完成工程化测试,同时给出从基础版本到生产级应用的优化方向与技术栈选型,让开发者能够快速上手并搭建属于自己的去中心化预测市场。
一、核心定位与底层逻辑
1. 市场定位
2026 年的 DPM 已突破博弈属性,成为融合群体智慧价格发现、去中心化金融对冲、现实世界事件价值映射的核心 DeFi 组件,核心优势是链上透明、无需信任、资产自管。
2. 底层核心法则:抵押品守恒(1=1+1)
所有设计围绕 “1 单位抵押品 = 1 份 Yes 代币 + 1 份 No 代币” 展开,分三个核心环节:
| 环节 | 核心动作 | 价值逻辑 |
|---|---|---|
| 资产对冲 | 用户存入抵押品,合约 1:1 铸造 Yes/No 结果代币 | 抵押品等价于 Yes+No 代币组合,单一代币成为投注头寸 |
| 概率定价 | Yes/No 代币二级市场自由交易 | 代币价格直接反映市场对事件结果的概率预期(如 Yes=0.7ETH→70% 发生概率) |
| 最终结算 | 预言机提交结果,胜出代币 = 1 单位抵押品,失败代币归零 | 抵押品总量不变,用户销毁胜出代币赎回抵押品 |
二、技术实现(Solidity+Viem)
智能合约
IOutcomeToken接口合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
interface IOutcomeToken is IERC20 {
function mint(address to, uint256 amount) external;
function burn(address from, uint256 amount) external;
}
OutcomeToken(结果代币)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "./IOutcomeToken.sol";
contract OutcomeToken is ERC20, IOutcomeToken {
address public immutable market;
error OnlyMarketAllowed();
constructor(
string memory name,
string memory symbol,
address _market
) ERC20(name, symbol) {
market = _market;
}
modifier onlyMarket() {
if (msg.sender != market) revert OnlyMarketAllowed();
_;
}
function mint(address to, uint256 amount) external onlyMarket {
_mint(to, amount);
}
function burn(address from, uint256 amount) external onlyMarket {
_burn(from, amount);
}
}
MockV3Aggregator(预言机合约):在本地开发和测试环境中部署模拟合约,而在正式生产环境的项目中,则使用 Chainlink 提供的 MockV3Aggregator 合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract MockV3Aggregator4 {
int256 private _price;
constructor(int256 initialPrice) { _price = initialPrice; }
function updatePrice(int256 newPrice) external { _price = newPrice; }
function latestRoundData() external view returns (uint80, int256 price, uint256, uint256, uint80) {
return (0, _price, 0, 0, 0);
}
}
PredictionMarket(预测市场合约)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";
import "./OutcomeToken.sol";
contract PredictionMarket is Ownable, ReentrancyGuard {
IOutcomeToken public immutable yesToken;
IOutcomeToken public immutable noToken;
AggregatorV3Interface public immutable priceFeed;
uint256 public immutable targetPrice;
bool public resolved;
bool public winningOutcome; // true = Yes, false = No
event MarketMinted(address indexed user, uint256 amount);
event MarketResolved(bool winningOutcome, int256 finalPrice);
event Redeemed(address indexed user, uint256 amount);
error AlreadyResolved();
error NotResolved();
error InsufficientBalance();
constructor(
address _priceFeed,
uint256 _targetPrice
) Ownable(msg.sender) {
priceFeed = AggregatorV3Interface(_priceFeed);
targetPrice = _targetPrice;
// 实例化 Outcome 代币
yesToken = new OutcomeToken("Predict YES", "YES", address(this));
noToken = new OutcomeToken("Predict NO", "NO", address(this));
}
/**
* @notice 存入 ETH 铸造 1:1 的 Yes 和 No 头寸
*/
function mintPositions() external payable nonReentrant {
if (resolved) revert AlreadyResolved();
if (msg.value == 0) revert InsufficientBalance();
yesToken.mint(msg.sender, msg.value);
noToken.mint(msg.sender, msg.value);
emit MarketMinted(msg.sender, msg.value);
}
/**
* @notice 调用 Chainlink 获取价格并结算市场
*/
function resolveMarket() external onlyOwner {
if (resolved) revert AlreadyResolved();
(, int256 price, , , ) = priceFeed.latestRoundData();
// 判定逻辑:当前价 > 目标价则 Yes 赢
winningOutcome = uint256(price) > targetPrice;
resolved = true;
emit MarketResolved(winningOutcome, price);
}
/**
* @notice 结算后,胜方销毁代币取回 1:1 的 ETH
*/
function redeem(uint256 amount) external nonReentrant {
if (!resolved) revert NotResolved();
if (winningOutcome) {
yesToken.burn(msg.sender, amount);
} else {
noToken.burn(msg.sender, amount);
}
(bool success, ) = payable(msg.sender).call{value: amount}("");
require(success, "Transfer failed");
emit Redeemed(msg.sender, amount);
}
receive() external payable {}
}
测试脚本
测试用例:
- Minting (铸造头寸)
- Resolution & Redemption (结算与兑付)
- 当价格超过目标时,YES 持有者应能兑现
- 未结算前不应允许兑现
import assert from "node:assert/strict";
import { describe, it, beforeEach } from "node:test";
import { parseEther, formatEther } from 'viem';
import { network } from "hardhat";
describe("PredictionMarket", function () {
let market: any, mockOracle: any;
let yesToken: any, noToken: any;
let publicClient: any;
let owner: any, user1: any;
let deployerAddress: string;
const TARGET_PRICE = 3000n * 10n**8n; // 假设目标价 3000 (8位小数)
const INITIAL_PRICE = 2500n * 10n**8n; // 初始价 2500
beforeEach(async function () {
const { viem } = await (network as any).connect();
publicClient = await viem.getPublicClient();
[owner, user1] = await viem.getWalletClients();
deployerAddress = owner.account.address;
// 1. 部署 Mock 预言机 (初始价格 2500)
mockOracle = await viem.deployContract("MockV3Aggregator4", [INITIAL_PRICE]);
// 2. 部署预测市场主合约
market = await viem.deployContract("PredictionMarket", [
mockOracle.address,
TARGET_PRICE
]);
// 3. 获取生成的 YES/NO 代币合约实例
const yesAddr = await market.read.yesToken();
const noAddr = await market.read.noToken();
yesToken = await viem.getContractAt("OutcomeToken", yesAddr);
noToken = await viem.getContractAt("OutcomeToken", noAddr);
console.log(`市场部署成功: ${market.address}`);
});
describe("Minting (铸造头寸)", function () {
it("应该允许用户存入 ETH 并获得 1:1 的 Yes/No 代币", async function () {
const mintAmount = parseEther("1");
// 执行铸造
const hash = await market.write.mintPositions({
value: mintAmount,
account: user1.account
});
await publicClient.waitForTransactionReceipt({ hash });
// 检查余额
const userYesBalance = await yesToken.read.balanceOf([user1.account.address]);
const userNoBalance = await noToken.read.balanceOf([user1.account.address]);
assert.equal(userYesBalance, mintAmount, "YES 代币数量不匹配");
assert.equal(userNoBalance, mintAmount, "NO 代币数量不匹配");
console.log(`用户1 成功铸造: ${formatEther(userYesBalance)} YES & NO`);
});
});
describe("Resolution & Redemption (结算与兑付)", function () {
beforeEach(async function () {
// 预先为 user1 铸造 2 ETH 的头寸
await market.write.mintPositions({
value: parseEther("2"),
account: user1.account
});
});
it("当价格超过目标时,YES 持有者应能兑现", async function () {
// 1. 模拟价格上涨至 3500 (超过目标 3000)
const newPrice = 3500n * 10n**8n;
await mockOracle.write.updatePrice([newPrice]);
// 2. 所有者结算市场
await market.write.resolveMarket();
const winningOutcome = await market.read.winningOutcome();
assert.equal(winningOutcome, true, "应该是 YES 赢");
// 3. 用户兑现 YES 代币
const redeemAmount = parseEther("2");
const balanceBefore = await publicClient.getBalance({ address: user1.account.address });
const hash = await market.write.redeem([redeemAmount], {
account: user1.account
});
await publicClient.waitForTransactionReceipt({ hash });
// 4. 检查结果
const yesBalanceAfter = await yesToken.read.balanceOf([user1.account.address]);
const balanceAfter = await publicClient.getBalance({ address: user1.account.address });
assert.equal(yesBalanceAfter, 0n, "兑现后代币应销毁");
assert.ok(balanceAfter > balanceBefore, "用户余额应增加 (忽略 Gas)");
console.log("✅ YES 胜出,用户成功兑回 ETH");
});
it("未结算前不应允许兑现", async function () {
await assert.rejects(
market.write.redeem([parseEther("1")], { account: user1.account }),
/NotResolved/,
"未结算时不应允许 redeem"
);
});
});
});
部署脚本
// scripts/deploy.js
import { network, artifacts } from "hardhat";
async function main() {
// 连接网络
const { viem } = await network.connect({ network: network.name });//指定网络进行链接
// 获取客户端
const [deployer] = await viem.getWalletClients();
const publicClient = await viem.getPublicClient();
const deployerAddress = deployer.account.address;
console.log("部署者的地址:", deployerAddress);
// 加载合约
const MockV3AggregatorArtifact = await artifacts.readArtifact("MockV3Aggregator4");
const PredictionMarketArtifact = await artifacts.readArtifact("PredictionMarket");
const TARGET_PRICE = 3000n * 10n**8n; // 假设目标价 3000 (8位小数)
const INITIAL_PRICE = 2500n * 10n**8n; // 初始价 2500
// 部署(构造函数参数:recipient, initialOwner)
const MockV3AggregatorHash = await deployer.deployContract({
abi: MockV3AggregatorArtifact.abi,//获取abi
bytecode: MockV3AggregatorArtifact.bytecode,//硬编码
args: [INITIAL_PRICE],//部署者地址,初始所有者地址
});
const MockV3AggregatorReceipt = await publicClient.waitForTransactionReceipt({ hash: MockV3AggregatorHash });
console.log("预言机合约地址:", MockV3AggregatorReceipt.contractAddress);
//
const PredictionMarketHash = await deployer.deployContract({
abi: PredictionMarketArtifact.abi,//获取abi
bytecode: PredictionMarketArtifact.bytecode,//硬编码
args: [MockV3AggregatorReceipt.contractAddress,TARGET_PRICE],//部署者地址,初始所有者地址
});
// 等待确认并打印地址
const PredictionMarketReceipt = await publicClient.waitForTransactionReceipt({ hash: PredictionMarketHash });
console.log("预测市场合约地址:", PredictionMarketReceipt.contractAddress);
}
main().catch(console.error);
三、生态价值与未来展望
在 2026 年的 Web3 生态中,去中心化预测市场不仅是 DeFi 的核心组件,更是连接链上与链下世界的重要桥梁,其生态价值早已超越单纯的预测功能
1. 核心生态价值
| 价值方向 | 具体应用场景 |
|---|---|
| 群体智慧价格发现 | 金融对冲、企业决策、政策制定的概率数据支撑 |
| DeFi 生态补充 | 开发事件保险、对冲工具、合成资产等创新产品 |
| DAO 治理工具 | 为 DAO 提案提供社区预期参考,提升治理科学性 |
| RWA 价值映射 | 实现现实事件(大宗商品 / 体育赛事 / 宏观数据)的链上价值映射 |
2. 未来演进方向
- 技术层面:结合 ZKP、乐观预言机、跨链技术,提升去中心化程度、降低参与门槛;
- 待解决问题:合规性、流动性、用户教育;
- 核心目标:实现 “群体智慧的价值化”,成为 Web3 落地现实应用的核心载体。
总结
-
底层逻辑:DPM 的核心是 “1=1+1 抵押品守恒法则”,贯穿铸造、定价、结算全流程;
-
技术实现:合约采用模块化设计,结合 OpenZeppelin 保障安全,Viem 替代 Ethers 实现高效测试 / 部署;
-
生态价值:DPM 的核心价值是挖掘群体智慧、推动链上链下融合,而非单纯的博弈功能;
-
拓展性:本文代码为基础框架,可基于此拓展 AMM(提升流动性)、ERC1155(多结果支持)等生产级功能。