阅读视图

发现新文章,点击刷新页面。

去中心化预测市场实战开发: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 {}
}

测试脚本

测试用例

  1. Minting (铸造头寸)
  2. Resolution & Redemption (结算与兑付)
  3. 当价格超过目标时,YES 持有者应能兑现
  4. 未结算前不应允许兑现
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(多结果支持)等生产级功能。

❌