前言在Web3领域,Polymarket的成功证明了“链上对冲+现实预测”模式的巨大潜力。不同于传统的博弈平台,Polymarket的精髓在于利用乐观预言机(OptimisticOracle)将现实世界的非对称信息转化为链上可结算的资产。本文将从架构设计、核心代码到自动化测试,完整拆
在 Web3 领域,Polymarket 的成功证明了“链上对冲+现实预测”模式的巨大潜力。不同于传统的博弈平台,Polymarket 的精髓在于利用乐观预言机(Optimistic Oracle) 将现实世界的非对称信息转化为链上可结算的资产。本文将从架构设计、核心代码到自动化测试,完整拆解一个去中心化预测市场的技术实现。
传统的预言机(如 Chainlink Price Feeds)擅长处理高频、标准化的数据(如币价)。但对于“2026年比特币是否突破20万美金”这类离散的、需人工核实的事件,UMA 乐观预言机提供了更优的方案:
我们使用 Solidity 0.8.24 和 OpenZeppelin V5 编写了核心逻辑。合约实现了资产托管、双向对冲池和预言机异步结算。
注释:奖励瓜分算法
采用 Pari-mutuel(等额奖池) 机制:胜方根据其投入的份额,等比例瓜分败方的资金池。
$$𝑅𝑒𝑤𝑎𝑟𝑑=\frac{𝑈𝑠𝑒𝑟𝐵𝑒𝑡}{𝑇𝑜𝑡𝑎𝑙𝑊𝑖𝑛𝑛𝑖𝑛𝑔𝑃𝑜𝑜𝑙}×𝑇𝑜𝑡𝑎𝑙𝑃𝑜𝑡$$
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
interface IOptimisticOracleV3 {
function assertTruthWithDefaults(bytes calldata claim, address callbackRecipient) external returns (bytes32);
function getAssertion(bytes32 assertionId) external view returns (bool settled, bool domainResolved, address caller, uint256 expirationTime, bool truthValue);
}
contract SimplePolymarketWithUMA is ReentrancyGuard {
IERC20 public immutable usdc;
IOptimisticOracleV3 public immutable umaOO;
struct Market {
string description;
uint256 totalYes;
uint256 totalNo;
uint8 outcome; // 0: Open, 1: Yes, 2: No
bool resolved;
bytes32 assertionId;
}
uint256 public marketCount;
mapping(uint256 => Market) public markets;
mapping(uint256 => mapping(address => uint256)) public yesBets;
mapping(uint256 => mapping(address => uint256)) public noBets;
constructor(address _usdc, address _umaOO) {
usdc = IERC20(_usdc);
umaOO = IOptimisticOracleV3(_umaOO);
}
function createMarket(string calldata _description) external {
uint256 marketId = marketCount++;
markets[marketId].description = _description;
}
function placeBet(uint256 _marketId, bool _isYes, uint256 _amount) external nonReentrant {
Market storage m = markets[_marketId];
require(!m.resolved, "Market resolved");
require(usdc.transferFrom(msg.sender, address(this), _amount), "Transfer failed");
if (_isYes) {
yesBets[_marketId][msg.sender] += _amount;
m.totalYes += _amount;
} else {
noBets[_marketId][msg.sender] += _amount;
m.totalNo += _amount;
}
}
// 向 UMA 提出断言:结果为 YES (1) 或 NO (2)
function proposeOutcome(uint256 _marketId, uint8 _proposedOutcome) external {
Market storage m = markets[_marketId];
require(m.assertionId == bytes32(0), "Outcome already proposed");
string memory claim = string(abi.encodePacked("Market:", m.description, " Result:", _proposedOutcome == 1 ? "YES" : "NO"));
bytes32 aid = umaOO.assertTruthWithDefaults(bytes(claim), address(this));
m.assertionId = aid;
}
// 挑战期结束后,根据 UMA 判定结果结算
function settleMarket(uint256 _marketId) external {
Market storage m = markets[_marketId];
require(!m.resolved, "Already resolved");
(bool settled, , , , bool truthValue) = umaOO.getAssertion(m.assertionId);
require(settled, "UMA assertion not settled");
// 若 UMA 判定断言为真,则接受提议的结果
// 注意:这里简化逻辑,假设提议结果总是 1 (YES)
m.outcome = truthValue ? 1 : 2;
m.resolved = true;
}
function claimReward(uint256 _marketId) external nonReentrant {
Market storage m = markets[_marketId];
require(m.resolved, "Not resolved");
uint256 reward;
uint256 totalPool = m.totalYes + m.totalNo;
if (m.outcome == 1) {
uint256 userBet = yesBets[_marketId][msg.sender];
require(userBet > 0, "No winning bet or already claimed"); // 增加此行效果更佳
reward = (userBet * totalPool) / m.totalYes;
yesBets[_marketId][msg.sender] = 0;
} else {
uint256 userBet = noBets[_marketId][msg.sender];
reward = (userBet * totalPool) / m.totalNo;
noBets[_marketId][msg.sender] = 0;
}
require(usdc.transfer(msg.sender, reward), "Reward failed");
}
}
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
/**
* @dev 测试网专用 USDT,任意人都能 mint
*/
contract TestUSDT is ERC20 {
uint8 private _decimals;
constructor(
string memory name,
string memory symbol,
uint8 decimals_
) ERC20(name, symbol) {
_decimals = decimals_;
}
function decimals() public view override returns (uint8) {
return _decimals;
}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract MockOptimisticOracleV3 {
struct Assertion {
bool settled;
bool truthValue;
uint256 expirationTime;
}
mapping(bytes32 => Assertion) public assertions;
uint256 public constant LIVENESS = 7200; // 2小时挑战期
function assertTruthWithDefaults(bytes calldata claim, address) external returns (bytes32) {
bytes32 aid = keccak256(abi.encodePacked(claim, block.timestamp));
assertions[aid] = Assertion(false, false, block.timestamp + LIVENESS);
return aid;
}
// 模拟挑战期结束并手动结算
function mockSettle(bytes32 aid, bool _truth) external {
require(block.timestamp >= assertions[aid].expirationTime, "Liveness not met");
assertions[aid].settled = true;
assertions[aid].truthValue = _truth;
}
function getAssertion(bytes32 aid) external view returns (bool, bool, address, uint256, bool) {
Assertion memory a = assertions[aid];
return (a.settled, true, address(0), a.expirationTime, a.truthValue);
}
}
<!---->
import assert from "node:assert/strict";
import { describe, it, beforeEach } from "node:test";
import { network } from "hardhat";
import { type Address, parseUnits, decodeEventLog } from "viem";
describe("Polymarket + UMA 自动化集成测试", function () {
let poly: any, usdc: any, uma: any;
let admin: any, userYes: any, userNo: any;
let vClient: any, pClient: any;
let testClient: any;
beforeEach(async function () {
const { viem } = await (network as any).connect();
vClient = viem;
[admin, userYes, userNo] = await vClient.getWalletClients();
pClient = await vClient.getPublicClient();
testClient = await viem.getTestClient();
// 1. 部署环境
usdc = await vClient.deployContract("TestUSDT", ["USDC", "USDC", 6]);
uma = await vClient.deployContract("MockOptimisticOracleV3");
poly = await vClient.deployContract("SimplePolymarketWithUMA", [usdc.address, uma.address]);
// 2. 准备资金
const amount = parseUnits("1000", 6);
for (const u of [userYes, userNo]) {
await usdc.write.mint([u.account.address, amount], { account: admin.account });
await usdc.write.approve([poly.address, amount], { account: u.account });
}
});
it("完整流程:下注 -> UMA断言 -> 时间推进 -> 结算 -> 领奖", async function () {
const marketId = 0n;
await poly.write.createMarket(["Bitcoin > $100k?"], { account: admin.account });
// 用户下注
await poly.write.placeBet([marketId, true, parseUnits("100", 6)], { account: userYes.account });
await poly.write.placeBet([marketId, false, parseUnits("50", 6)], { account: userNo.account });
// 提出断言 (提议结果为 YES)
await poly.write.proposeOutcome([marketId, 1], { account: admin.account });
const mInfo = await poly.read.markets([marketId]);
const aid = mInfo[5]; // 获取 assertionId
// 模拟时间推进 (跳过 2 小时挑战期)
// await network.provider.send("evm_increaseTime", [7201]);
// await network.provider.send("evm_mine");
await testClient.increaseTime({ seconds: 7201 });
await testClient.mine({ blocks: 1 });
// 模拟 UMA 结算该断言
await uma.write.mockSettle([aid, true], { account: admin.account });
// Polymarket 最终结算
await poly.write.settleMarket([marketId], { account: admin.account });
// 验证领奖:UserYes 投入 100,UserNo 投入 50,总池 150
const balBefore = await usdc.read.balanceOf([userYes.account.address]);
await poly.write.claimReward([marketId], { account: userYes.account });
const balAfter = await usdc.read.balanceOf([userYes.account.address]);
assert.strictEqual(balAfter - balBefore, parseUnits("150", 6), "奖池分配错误");
console.log("✅ 经 UMA 判定后,胜方成功瓜分奖池");
});
it("安全性测试 (重复领取拦截)", async function () {
const marketId = 0n;
await poly.write.createMarket(["安全性攻击测试"], { account: admin.account });
// 1. 下注
await poly.write.placeBet([marketId, true, parseUnits("100", 6)], { account: userYes.account });
await poly.write.proposeOutcome([marketId, 1], { account: admin.account });
// 2. 推进时间
await testClient.increaseTime({ seconds: 7201 });
await testClient.mine({ blocks: 1 });
// 3. 修复后的 aid 获取方式
const mInfoBefore = await poly.read.markets([marketId]);
const aid = mInfoBefore[5]; // 获取 bytes32 类型的 assertionId
// 确保 aid 不是 undefined
assert.ok(aid && aid !== '0x0000000000000000000000000000000000000000000000000000000000000000', "未获取到有效的 Assertion ID");
await uma.write.mockSettle([aid, true], { account: admin.account });
await poly.write.settleMarket([marketId], { account: admin.account });
// 4. 第一次领取
await poly.write.claimReward([marketId], { account: userYes.account });
console.log("✅ 第一次合法领取完成");
// 5. 第二次领取(预期失败)
try {
await poly.write.claimReward([marketId], { account: userYes.account });
assert.fail("不应允许重复领取");
} catch (err: any) {
// Viem 的错误通常在 err.message 或 err.shortMessage 中
assert.ok(err.message.includes("revert"), "应该触发合约 revert");
console.log("✅ 重复领取拦截成功");
}
});
});
// scripts/deploy.js
import { network, artifacts } from "hardhat";
import { parseUnits } from "viem";
async function main() {
// 连接网络
const { viem } = await network.connect({ network: network.name });//指定网络进行链接
// 获取客户端
const [deployer, investor] = await viem.getWalletClients();
const publicClient = await viem.getPublicClient();
const deployerAddress = deployer.account.address;
console.log("部署者的地址:", deployerAddress);
// 部署TestUSDTReceipt合约
const TestUSDTArtifact = await artifacts.readArtifact("TestUSDT");
// 1. 部署合约并获取交易哈希
const TestUSDTHash = await deployer.deployContract({
abi: TestUSDTArtifact.abi,
bytecode: TestUSDTArtifact.bytecode,
args: ["USDC", "USDC", 6],
});
const TestUSDTReceipt = await publicClient.waitForTransactionReceipt({
hash: TestUSDTHash
});
console.log("TestUSDTReceipt合约地址:", TestUSDTReceipt.contractAddress);
// 部署MockOptimisticOracleV3合约
const MockOptimisticOracleV3Artifact = await artifacts.readArtifact("MockOptimisticOracleV3");
// 1. 部署合约并获取交易哈希
const MockOptimisticOracleV3Hash = await deployer.deployContract({
abi: MockOptimisticOracleV3Artifact.abi,
bytecode: MockOptimisticOracleV3Artifact.bytecode,
args: [],
});
const MockOptimisticOracleV3Receipt = await publicClient.waitForTransactionReceipt({
hash: MockOptimisticOracleV3Hash
});
console.log("MockOptimisticOracleV3合约地址:", MockOptimisticOracleV3Receipt.contractAddress);
// SimplePolymarketWithUMA脚本
const SimplePolymarketWithUMAArtifact=await artifacts.readArtifact("SimplePolymarketWithUMA");
const SimplePolymarketWithUMAHash = await deployer.deployContract({
abi: SimplePolymarketWithUMAArtifact.abi,
bytecode: SimplePolymarketWithUMAArtifact.bytecode,
args: [TestUSDTReceipt.contractAddress,MockOptimisticOracleV3Receipt.contractAddress],
});
const SimplePolymarketWithUMAReceipt = await publicClient.waitForTransactionReceipt({
hash: SimplePolymarketWithUMAHash
});
console.log("SimplePolymarketWithUMAReceipt合约地址",SimplePolymarketWithUMAReceipt.contractAddress)
}
main().catch(console.error);
至此,简洁版 Polymarket 核心运行机制相关智能合约已完成开发、测试与部署全流程。期间完成了理论梳理、核心架构设计,并明确了 UMA 乐观预言机的选型依据,整体工作圆满落地。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!