预测市场背后的条件合约
系列预测市场1 / 1
- 1预测市场背后的条件合约
- 2预测市场的定价机制:从 LMSR 到订单簿即将更新
- 3预言机与结算:预测市场如何裁定结果即将更新
- 查看系列 →
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 | 创建者 | 自定义 | 标识问题 |
conditionId | CTF 合约 | oracle + questionId + outcomes | 标识市场 |
collectionId | CTF 合约 | conditionId + indexSet | 标识某个结果 |
positionId | CTF 合约 | collateralToken + collectionId | ERC1155 代币 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 基础设施设计中少有的兼具表达力与工程严谨性的范本。
评论
加载中…