返回博客

预测市场背后的条件合约

·0xPM
Web3预测市场CTF
系列预测市场1 / 1
  1. 1预测市场背后的条件合约
  2. 2预测市场的定价机制:从 LMSR 到订单簿即将更新
  3. 3预言机与结算:预测市场如何裁定结果即将更新
  4. 查看系列 →

Polymarket 上的每一笔预测交易,背后都依赖同一套智能合约标准——Gnosis 开发的 Conditional Tokens Framework(CTF)。它将"对未来事件的判断"转化为可以链上持有、交易和赎回的代币,优雅地解决了预测市场的核心问题:如何让用户安全地下注,同时保证结果兑付的去信任性。

本文从工作流程和核心数据结构两个维度,系统讲解 CTF 的设计原理。


一、整体工作流程

CTF 的运转分为四个阶段:创建条件 → 铸造代币 → 自由交易 → 结果兑付。

第一步:定义条件(Condition)

一切从一个问题开始。市场创建者调用 prepareCondition,在合约中登记这个问题:

bytes32 conditionId = CTF.prepareCondition( oracle, // 谁来报告结果(如 UMA 预言机) questionId, // 问题的唯一标识 outcomeSlotCount // 可能的结果数量(二元市场 = 2) );

以"特朗普会赢得 2024 大选吗?"为例,outcomeSlotCount = 2,代表 Yes 和 No 两个结果。合约根据这三个参数确定性地计算出 conditionId,作为这个市场在链上的唯一身份。

第二步:分割抵押品(Split Collateral)

用户存入 USDC,合约将其"一拆为二",铸造出等量的条件代币:

CTF.splitPosition( collateralToken, // USDC 地址 parentCollectionId, // 0x0(无父条件) conditionId, // 市场 ID partition, // [1, 2] 对应 Yes 和 No amount // 存入金额 );

存入 100 USDC,得到:

  • 100 个 Yes 代币(tokenId = positionId_Yes)
  • 100 个 No 代币(tokenId = positionId_No)

这两种代币都是 ERC1155 标准,可以在 DEX 或订单簿上自由交易。

第三步:交易

条件代币的价格反映了市场对结果概率的判断。Alice 认为 Yes 概率高,可以将手中的 No 代币以市场价卖出:

Alice 持有:100 Yes 代币 + 100 No 代币 在 Polymarket 以 0.35 USDC/份 卖出 100 个 No 代币 → 收回 35 USDC 现金,保留 100 个 Yes 代币

如果 Yes 最终获胜,Alice 以净成本 65 USDC 持有价值 100 USDC 的 Yes 代币,净赚 35 USDC。

第四步:结果报告与赎回

预言机报告结果后,获胜方代币持有人可按比例赎回抵押品:

// UMA 预言机报告:Yes 获胜 CTF.reportPayouts(questionId, [1, 0]); // [1, 0] = Yes 赢, No 清零 // 持有人赎回 CTF.redeemPositions(collateralToken, parentCollectionId, conditionId, indexSets);

赎回规则简洁直接:

持有代币结果 Yes 获胜结果 No 获胜
100 Yes 代币赎回 100 USDC赎回 0 USDC
100 No 代币赎回 0 USDC赎回 100 USDC

若出现 [0.5, 0.5] 的平局/争议结果,双方各赎回 50 USDC。


二、四个核心 ID 详解

理解 CTF 绕不开四个 ID。它们存在严格的层级派生关系,混淆任何一个都会导致合约调用失败。

questionId(问题标识) ↓ prepareCondition(oracle, questionId, outcomeSlotCount) conditionId(市场标识) ↓ + indexSet(位掩码) collectionId(结果标识) ↓ + collateralToken(抵押品地址) positionId(代币标识)

2.1 questionId——问题标识

由市场创建者自定义生成,用于区分不同问题。通常做法是对问题文本加时间戳做哈希:

const questionId = ethers.utils.keccak256( ethers.utils.defaultAbiCoder.encode( ['string', 'uint256'], ["Will Trump win 2024 election?", Date.now()] ) ); // → 0x111aaa...

questionId 是整个链条的起点,完全由创建者控制,可以是任意 bytes32 值。不同平台也可以共享同一个 questionId 来引用同一个问题(但 conditionId 可能因预言机不同而不同)。

2.2 conditionId——市场标识

由 CTF 合约在 prepareCondition 调用时自动计算,唯一标识一个市场:

conditionId = keccak256(abi.encodePacked( oracle, // 预言机地址 questionId, // 问题 ID outcomeSlotCount // 结果数量 ));

为什么要把 oracle 和 outcomeSlotCount 也纳入计算?

// 同一个问题,不同预言机 → 不同市场 conditionId_1 = hash(UMA_ORACLE, questionId, 2) conditionId_2 = hash(CHAINLINK_ORACLE, questionId, 2) // 同一个问题,不同结果数 → 不同市场 conditionId_3 = hash(UMA_ORACLE, questionId, 2) // Yes/No conditionId_4 = hash(UMA_ORACLE, questionId, 3) // Win/Draw/Lose

这种设计避免了不同配置的市场之间产生 ID 冲突。conditionId 是铸造和管理代币的核心依据。

2.3 collectionId——结果标识

表示某个(或某些)结果组合的标识符。它通过 conditionId 加上 位掩码(indexSet) 计算得出:

collectionId = keccak256(abi.encodePacked( conditionId, indexSet // 位掩码,表示哪些结果被纳入 ));

indexSet 的位掩码逻辑:

二元市场(Yes/No): indexSet = 0b01 → outcome 0(Yes) indexSet = 0b10 → outcome 1(No) 三元市场(Win/Draw/Lose): indexSet = 0b001 → Win indexSet = 0b010 → Draw indexSet = 0b100 → Lose indexSet = 0b011 → Win 或 Draw(组合) indexSet = 0b111 → 全部结果

collectionId 的意义在于支持复合条件代币——可以创建"条件 A 的某结果 且 条件 B 的某结果"的组合代币:

// 条件 A:特朗普赢? 条件 B:共和党控参议院? // 可以创建 4 种组合: collectionId_1 = hash(A_Yes + B_Yes) // 两者都发生 collectionId_2 = hash(A_Yes + B_No) collectionId_3 = hash(A_No + B_Yes) collectionId_4 = hash(A_No + B_No) // 两者都不发生

2.4 positionId——代币标识

用户钱包中实际持有的 ERC1155 代币的 tokenId,由抵押品地址加 collectionId 计算:

positionId = keccak256(abi.encodePacked( collateralToken, // 抵押品地址(如 USDC) collectionId ));

为什么要包含抵押品? 同一个"Yes"结果,用 USDC 和用 DAI 抵押得到的是两种不同的代币,不可直接互换:

positionId_USDC_Yes = hash(USDC_ADDRESS, collectionId_Yes) // = 0xAAA... positionId_DAI_Yes = hash(DAI_ADDRESS, collectionId_Yes) // = 0xBBB... // 两者 positionId 不同,是两种独立的代币

对比一览

ID 类型由谁生成计算输入主要用途
questionId创建者自定义标识问题
conditionIdCTF 合约oracle + questionId + outcomes标识市场
collectionIdCTF 合约conditionId + indexSet标识某个结果
positionIdCTF 合约collateralToken + collectionIdERC1155 代币 ID

三、完整示例:从创建到赎回

以"拜登会连任吗?"市场为例,演示完整的 ID 派生链路:

// ① 创建者生成 questionId const questionId = keccak256("biden_reelection_2024" + timestamp); // → questionId = 0x111aaa... // ② 调用 prepareCondition,CTF 计算 conditionId await CTF.prepareCondition(UMA_ORACLE, questionId, 2); // conditionId = keccak256(pack(0x999eee..., 0x111aaa..., 2)) = 0x222bbb... // ③ CTF 内部计算 collectionId collectionId_Yes = keccak256(pack(0x222bbb..., 0b01)) = 0x333ccc... collectionId_No = keccak256(pack(0x222bbb..., 0b10)) = 0x444ddd... // ④ 计算 positionId(用户持有的实际代币 ID) positionId_Yes = keccak256(pack(USDC_ADDRESS, 0x333ccc...)) = 0x555eee... positionId_No = keccak256(pack(USDC_ADDRESS, 0x444ddd...)) = 0x666fff... // ⑤ Alice 存入 100 USDC,铸造条件代币 await CTF.splitPosition(USDC_ADDRESS, bytes32(0), conditionId, [0b01, 0b10], 100e6); // Alice 获得:100 个 tokenId=0x555eee(Yes)+ 100 个 tokenId=0x666fff(No) // ⑥ Alice 看好 Yes,卖出 No 代币(在 Polymarket 以 0.35 USDC 成交) // 获得 35 USDC 现金 // ⑦ 结果公布:Yes 获胜 await CTF.reportPayouts(questionId, [1, 0]); // ⑧ Alice 赎回 await CTF.redeemPositions(USDC_ADDRESS, bytes32(0), conditionId, [0b01]); // 获得 100 USDC,加上卖 No 的 35 USDC,总计 135 USDC,净利润 35 USDC

四、进阶:合并头寸与资本效率

CTF 还支持一个实用操作:合并(Merge)。如果用户同时持有 Yes 和 No 代币,可以无需等待结果直接兑回抵押品:

mergePositions(100_Yes_tokens, 100_No_tokens); // → 立即获得 100 USDC,无需等待裁决

这带来了套利空间:当 Yes + No 代币的市场总价格偏离 1 USDC 时,套利者可以通过分割/合并操作无风险获利,同时推动价格回归均衡。


五、CTF 的设计哲学

CTF 能够支撑 Polymarket 这样体量的预测市场,源于几个关键设计决策:

  • 原子性:分割与合并是单笔交易完成的原子操作,不存在中间状态风险
  • 可组合性:同一抵押品可以参与无限数量的条件市场,且支持跨条件组合
  • 标准化:ERC1155 代币标准保证了条件代币与现有 DEX、钱包生态的天然兼容
  • 资本效率:用户可以铸造代币后卖出不看好的一侧,实际有效成本远低于面值

四个 ID 的层级设计,本质上是将"哪个问题 → 哪个市场 → 哪个结果 → 哪种代币"的语义逐层编码进链上标识符,既保证了确定性,又支持了复合条件的扩展性。这是 DeFi 基础设施设计中少有的兼具表达力与工程严谨性的范本。

评论

加载中…