
背景2026 年 2 月 19 日推特用户itslirrato披露 Polymarket 存在一个已知的漏洞攻击者可以利用 CTF Exchange 合约上的incrementNonce()函数将已经在链下订单簿上撮合的订单取消导致被撮合的对手挂单在链上合约因撮合失败而被消耗。攻击者可以在清空了挂单后的订单簿中作为唯一的做市商进行挂单定价从而完成获利。这种攻击手法也被称为Ghost Fills幽灵订单。一笔订单的完整生命周期如下图所示用户将订单信息发送到 CLOB 中CLOB 根据用户提交的订单类型是否为市价单Marketable进行操作市价单直接将其与订单簿中的挂单进行配对并提交到链上执行非市价单添加到订单簿上进行挂单。在官方文档中用户提交的订单在提交到 CLOB 进行匹配之后就无法取消了。骗你的CLOB 匹配了以后也能够取消。正是利用了这个漏洞攻击者能够完成 Ghost Fills 操作。2. 攻击流程攻击流程涉及 CLOB 订单簿和链上合约两部分初始状态 Maker A 在 CLOB 上挂着 1000 股 YES 0.55 的卖单用 nonce_A 签名 这个订单在前端盘口显示所有人都能吃 T0 攻击者发送一个市价买单 1000 股 YES market T0.1 CLOB 撮合引擎撮合到 Maker A 的卖单 → 把 Maker A 的卖单从盘口移除标记为 filled → 通知 Maker A 的客户端你的单成交了 → 通知攻击者你的单成交了 T0.5 Operator 准备 matchOrders(攻击者订单, [Maker A 订单]) 上链 T0.8 ⚡ 攻击者调用 incrementNonce() → 攻击者自己的 nonce T1.0 Operator 的 matchOrders() 上链 → 校验攻击者订单的 nonce → 失败 → 整笔撮合 revert → Maker A 的卖单也跟着没被结算 T1.0 之后 Maker A 的订单签名【技术上仍然有效】他的 nonce 没变 但他的订单已经【从 CLOB 盘口被移除】 Maker A 的客户端以为成交了可能已经更新内部状态、对冲、移仓 盘口上那 1000 股 YES 0.55 的卖单【消失了】3. 详细攻击流程首先通过 CLOB Client 的 create_market_order() 创建市价单随后订单簿撮合器会根据 amount 的数量撮合对应数量的挂单。from py_clob_client.client import ClobClient from py_clob_client.clob_types import MarketOrderArgs, OrderType from py_clob_client.order_builder.constants import BUY HOST https://clob.polymarket.com CHAIN_ID 137 PRIVATE_KEY your-private-key FUNDER your-funder-address client ClobClient( HOST, # The CLOB API endpoint keyPRIVATE_KEY, # Your wallets private key chain_idCHAIN_ID, # Polygon chain ID (137) signature_type1, # 1 for email/Magic wallet signatures funderFUNDER # Address that holds your funds ) client.set_api_creds(client.create_or_derive_api_creds()) mo MarketOrderArgs(token_idtoken-id, amount25.0, sideBUY, order_typeOrderType.FOK) # Get a token ID: https://docs.polymarket.com/developers/gamma-markets-api/get-markets signed client.create_market_order(mo) resp client.post_order(signed, OrderType.FOK) print(resp)随后经过撮合的订单列表会被 Operator 打包发送到CTFExchange.matchOrders()函数进行链上交易。经过下面的调用链条最终进行nonce检查。matchOrders -- _matchOrders -- _fillMakerOrders -- _fillMakerOrder -- _performOrderChecks -- _validateOrder -- isValidNonce而攻击者正是在 CLOB 撮合订单后Operator 执行matchOrders()函数前的这个时间段内调用incrementNonce()函数使得自己的 nonces 值增加 1 。在合约调用isValidNonce()函数检查时由于 nonces 已经更新无法成功执行交易回滚。攻击者就是通过这种方式来无效化自己的市价单导致“幽灵订单”的情况出现。4. 漏洞修复过程在 2026 年 4 月 28 日 Polymarket 升级了 V2 版本其中存在一些更新项用以缓解 Ghost Fills 带来的影响。4.1 V2 更新内容下面是和 Ghost Fills 相关的更新内容订单结构简化移除nonce、feeRateBps和taker添加timestamp、metadata和builder。链上取消操作已替换为操作员控制的pauseUser/unpauseUser。移除incrementNonce()和cancelOrder()两个可以批量无效化订单的函数。在更新完以后移除了nonce机制以及cancelOrder()函数用户无法通过 CTFExchange 合约批量取消已经被 CLOB 配对的订单。如果用户需要取消订单只能通过调用 CLOB 的接口从 CLOB 中取消py-clob-client-v2/examples/orders/cancel_orders.py at main · Polymarket/py-clob-client-v2 · GitHub 也就是说已经通过 CLOB 配对提交到链上的订单无法在合约层面进行取消。4.2 V2 遗留风险Ghost Fills 产生的前提条件主要有三个CLOB 撮合与链上订单交易是串行的且两者之间存在时间差。只要 CLOB 完成撮合后就默认交易已经完成移除已经撮合的挂单。链上订单交易可以被用户的其他链上行为进行阻拦表现为链上订单交易 revert。V2 版本的更新都是围绕着第三点进行的通过取消 nonce 机制以及 cancelOrder() 函数来禁止用户抢在CLOB 撮合与链上订单交易执行的时间差内取消订单。但是第三点也提到了阻拦链上订单交易最终呈现出来的效果是让链上订单交易 revert。即使官方在 Polymarket 合约层面进行了限制用户依然可以通过其他手法来阻止订单交易的执行余额转移把地址持有的 pUSD/CTF 转走使得订单在成交时因余额不足而 revert授权取消将对 CTFExchangeV2 的 pUSD/CTF 授权取消在转移代币时因授权不足而 revert此次 V2 的更新只是局部地解决了链上订单取消的问题。从攻击平面看ghost fills 仍然有完整的可利用通道。4.3 Deposit Wallets 更新紧接着在 2026 年 5 月 4 日Polymarket 紧急上线 Deposit Wallet 功能本次更新专门用于解决 ghost fills 问题。Deposit Wallets Addresshttps://polygonscan.com/address/0x00000000000Fb5C9ADea0298D729A0CB3823Cc07#code本次更新在 V2 的基础上引入了一个“Deposit Wallets”的概念背后是一套链下中继器 链上代理合约 ERC-1271 签名包装的混合系统。Deposit Wallet 是一个每个用户独立部署的 ERC-1967 代理合约。它替代了 V1/V2 里 maker 直接用 EOA 钱包持有 pUSD 和 CTF token 的模式。用户需要创建一个唯一的 Deposit Wallet 来存储在 Polymarket 中使用的资金。引入 Deposit Wallet 后用户的操作分为 CLOB 下单与钱包操作两种类型分别对应不同的签名与执行方式CLOB 下单signatureType 3 (POLY_1271)Step 1: 用 Owner 或 Session Signer 私钥准备签名 Step 2: 构造 V2 订单maker signer Deposit WalletsignatureType 3 Step 3: 用 ERC-7739 包装签订单内层签 CTFExchangeV2 域的订单哈希外层嵌套 Deposit Wallet 的 EIP-712 domain Step 4: 提交到 CLOBPOST /order Step 5: CTFExchangeV2 撮合后直接调 Deposit Wallet.isValidSignature 结算钱包操作EIP-712 签名 Wallet BatchStep 1: 用 Owner EOA 准备签名 Step 2: 构造 Batchwallet、nonce、deadline、calls Step 3: 用标准 EIP-712 签 Batch Step 4: 提交到 Polymarket RelayerPOST /submit Step 5: Operator 调 Factory.proxy 转发 Step 6: Factory 调 DepositWallet.execute 执行 Step 7: 必要时同步 CLOB 缓存/balance-allowance/update通过钱包操作的流程可以看出由于资金不再存放在用户的 EOA 中而是在 Deposit Wallet 中。这使得用户在进行代币的 transfer 或 approve 操作时需要经过“签 Batch → Polymarket Relayer → Operator → Factory → Wallet”这个流程。此时所有钱包操作都需要经过由官方控制的 Relayer 和 Operator 的检查使得项目方可以在多个环节对 ghost fills 等各个操作进行检测与拦截但是具体是如何实现的官方并没有给出具体的说明如果是为了避免攻击者根据检测规则更新攻击手法也可以理解。5. 总结经过了 V2 Deposit Wallet 两次大更新后Ghost Fills 的问题被彻底解决了吗很遗憾并没有。尽管新用户采用的是 V2 版本但是现存的 V1 版本老用户依然暴露在 Ghost Fills 的风险之中。Operator 层的拦截取决于 Polymarket 风控代码的具体实现不能确认能够 100% 地拦截掉所有的 Ghost Fills 操作。时间差问题依旧存在目前 CLOB 撮合与链上行为存在时间差且为非原子性的攻击者理论上还是可以通过这个特征来干扰正常的交易操作。可以遇见地在未来 Polymarket 和 Ghost Fills 等其他问题还会不断地纠缠下去可谓魔高一尺道高一丈。但是在预测市场这个新兴领域需要这种纠缠才能不断地发展成一个成熟的安全的领域。