深度拆解 Web3 预测市场:基于 Solidity 0.8.24 与 UMA 乐观预言机的核心实现

  • 木西
  • 发布于 2小时前
  • 阅读 22

前言在Web3领域,Polymarket的成功证明了“链上对冲+现实预测”模式的巨大潜力。不同于传统的博弈平台,Polymarket的精髓在于利用乐观预言机(OptimisticOracle)将现实世界的非对称信息转化为链上可结算的资产。本文将从架构设计、核心代码到自动化测试,完整拆

前言

在 Web3 领域,Polymarket 的成功证明了“链上对冲+现实预测”模式的巨大潜力。不同于传统的博弈平台,Polymarket 的精髓在于利用乐观预言机(Optimistic Oracle) 将现实世界的非对称信息转化为链上可结算的资产。本文将从架构设计、核心代码到自动化测试,完整拆解一个去中心化预测市场的技术实现。

一、 核心架构:为何选择 UMA?

传统的预言机(如 Chainlink Price Feeds)擅长处理高频、标准化的数据(如币价)。但对于“2026年比特币是否突破20万美金”这类离散的、需人工核实的事件,UMA 乐观预言机提供了更优的方案:

  1. 断言机制(Assertion) :任何人都可以提交一个结果断言,并缴纳保证金。
  2. 挑战期(Liveness Period) :如果在特定时间内(如 2 小时)没人挑战,系统默认该断言为真。
  3. 博弈均衡:通过经济激励确保提交者不敢造假,因为挑战者可以推翻错误断言并赢得其保证金。

二、 核心智能合约实现

我们使用 Solidity 0.8.24 和 OpenZeppelin V5 编写了核心逻辑。合约实现了资产托管、双向对冲池和预言机异步结算。

注释:奖励瓜分算法

采用 Pari-mutuel(等额奖池)  机制:胜方根据其投入的份额,等比例瓜分败方的资金池。

$$𝑅𝑒𝑤𝑎𝑟𝑑=\frac{𝑈𝑠𝑒𝑟𝐵𝑒𝑡}{𝑇𝑜𝑡𝑎𝑙𝑊𝑖𝑛𝑛𝑖𝑛𝑔𝑃𝑜𝑜𝑙}×𝑇𝑜𝑡𝑎𝑙𝑃𝑜𝑡$$

1. SimplePolymarketWithUMA合约

// 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");
    }
}

2. TestUSDT代币

//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);
    }
}

3. MockOptimisticOracleV3合约

// 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);
    }
}

四、 自动化测试:Viem + Hardhat

测试用例:

  • ✅ 经 UMA 判定后,胜方成功瓜分奖池
  • Polymarket + UMA 自动化集成测试
    • ✔ 完整流程:下注 -> UMA断言 -> 时间推进 -> 结算 -> 领奖
  • ✅ 第一次合法领取完成
  • ✅ 重复领取拦截成功
    • ✔ 安全性测试 (重复领取拦截)

<!---->

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 乐观预言机的选型依据,整体工作圆满落地。

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
木西
木西
0x5D5C...2dD7
江湖只有他的大名,没有他的介绍。