Skip to main content
polynode monitors every Polymarket settlement in real time, including ones that fail. This page documents our ongoing research into settlement failures caused by the incrementNonce() exploit on Polymarket’s V1 CTF Exchange. All data on this page is from Polygon mainnet. Every transaction hash is real and verifiable.

Background

Polymarket uses a hybrid architecture: orders are matched off-chain by the CLOB (Central Limit Order Book), then settled on-chain by an operator calling matchOrders on the CTF Exchange contract. There is a 2-3 second window between when the operator builds the settlement transaction and when it lands in a block. This window is the attack surface.

The exploit

The CTF Exchange contract has a public function called incrementNonce() (selector 0x627cdcb9). Anyone can call it on their own address for less than $0.10 in gas. When called, it increments the caller’s on-chain nonce, which instantly invalidates every outstanding order they have on the exchange. The attack:
  1. The attacker places orders on both sides of a market (YES and NO)
  2. One side gets matched by the CLOB
  3. Before the operator’s matchOrders transaction lands on-chain, the attacker calls incrementNonce() on the CTF Exchange
  4. The matchOrders transaction reverts because the attacker’s order nonce no longer matches their on-chain nonce
  5. Since matchOrders is atomic (no try/catch per order), one invalid order kills the entire batch — every other order in that transaction also fails
  6. The counterparties who were matched against the attacker get nothing. Their trades evaporate.
  7. Polymarket’s off-chain system removes all affected orders from the book
The attacker keeps their winning side and loses nothing on their losing side. The cost is ~$0.10 per cycle.

Two attack variants

Our research identified two distinct methods that cause settlement failures:

Variant 1: Nonce flip

The attacker calls incrementNonce() directly on the CTF Exchange contract. This is the canonical exploit described in public disclosures. The on-chain call is permanent and auditable — every instance can be traced to a specific wallet and block.

Variant 2: Balance drain

The attacker transfers their USDC out of their wallet between order matching and on-chain settlement. When the operator’s matchOrders transaction executes, the token transfer fails with "ERC20: transfer amount exceeds balance". The effect is identical — the entire atomic batch reverts, killing all orders in the transaction. This variant does not leave an incrementNonce() trace. It manifests as a standard ERC-20 balance error. Polymarket’s CLOB validates balances at order placement time, so a balance error at settlement time indicates the funds were moved deliberately in the ~2 second window between matching and settlement. Both variants produce the same outcome: ghost fills. A trade appears to match off-chain but never settles on-chain.

Live audit: April 9, 2026

Settlement failure rate

We ran 10 consecutive 30-second monitoring sessions collecting every matchOrders transaction from polynode’s settlement stream and checking each receipt for revert status.
MetricValue
Timestamp2026-04-09 ~06:15 UTC
Collection10 rounds x 30 seconds
Total matchOrders TXs9,294
Succeeded9,263
Reverted31
Overall failure rate0.334%
A separate scan of 1,000 consecutive blocks (~33 minutes, blocks 85296314–85297314) found 66,638 total matchOrders transactions with 100 reverts (0.150% failure rate). The rate fluctuates between 0% and 1% depending on whether short-term crypto markets are actively resolving.

Per-round breakdown

RoundTXsRevertedRate
11,09120.183%
279220.253%
31,31320.152%
41,68470.416%
568371.025%
655740.718%
751320.390%
854130.555%
91,25220.160%
1086800.000%

Market category breakdown

CategoryRevertsPercentage
Short-term crypto (BTC/ETH 5–15 min)2993.5%
Other crypto13.2%
Other (politics)13.2%
93.5% of all settlement failures occur in short-term crypto markets. These are the 5-minute and 15-minute BTC/ETH “up or down” markets where the outcome becomes clear in the final seconds, giving the attacker a window to invalidate their losing orders before settlement.

Revert cause analysis

We traced every reverted transaction using trace_transaction to decode the exact revert reason.
Revert reasonCountVariant
ERC20: transfer amount exceeds balance20Balance drain
SafeMath: subtraction overflow11Balance drain
Total31
All 31 reverts in this sample produced balance-related errors at the ERC-20 transfer level, consistent with both the nonce flip (which cascades into balance errors when the atomic batch unwinds) and the balance drain variant.

Attacker identification

We scanned 1,800 consecutive blocks (~1 hour) for all calls to incrementNonce() (selector 0x627cdcb9) on the CTF Exchange contract (0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E). 21 incrementNonce() calls found in 1 hour. Two wallets responsible for all of them.

Attacker 1: 0xe5f5f9bf130f4e0947ab9e79b2ee0ab3f33885f5

  • 12 calls in 1 hour
  • Calls incrementNonce() directly on the CTF Exchange (no proxy)
  • Regular ~150 block cadence (~5 minutes), matching BTC/ETH short-term market resolution cycles
  • No Polymarket profile found for this address
  • Directly caused 7 of 31 reverted settlements in our audit (confirmed via same-block correlation)
Nonce flip blocks: 85296283, 85296433, 85296583, 85296733, 85296883, 85297033, 85297183, 85297333, 85297483, 85297633, 85297783, 85297933 At block 85297633, this wallet called incrementNonce() in the same block as 3 reverted BTC Up/Down settlements, and 4 more reverted in the next 1–3 blocks as the operator’s queued transactions hit the invalidated nonce.

Attacker 2: 0xfd29d745edaf724bae8ade4f7b3a3465eed3b905

  • 7 calls in 1 hour
  • Calls incrementNonce() through a Safe proxy wallet at 0xfc64b6660ed1...
  • Similar ~5 minute cadence
  • Has a Polymarket account and profile page
  • Attribution to specific reverts pending (proxy routing makes block-level correlation less precise)
Nonce flip blocks (via proxy): 85296604, 85296825, 85297016, 85297484, 85297770, 85297921, 85297985

Additional callers

Two wallets (0x2df3626c..., 0xa2b2a759...) made single incrementNonce() calls during the scan period. These may be legitimate order cancellations rather than exploit activity.

Attribution: nonce flip to settlement failure

For the 31 reverted settlements in our audit, we cross-referenced each revert block against the incrementNonce() call log:
AttributionCountNotes
Confirmed nonce flip7incrementNonce() call in same block or 1–3 blocks before revert. All from Attacker 1.
Unattributed24No nearby incrementNonce() call. Likely balance drain variant or nonce flip from undetected proxy call.
The 7 confirmed nonce-flip reverts all occurred in BTC Up/Down 2:10–2:15AM ET markets within blocks 85297633–85297636. The 24 unattributed reverts are concentrated in ETH Up/Down markets and revert with balance errors, consistent with the balance drain variant.

Sample reverted transactions

BTC Up/Down 2:10-2:15AM — Nonce flip confirmed (Attacker 1, block 85297633):
  0x6694bb41b21ec924d29e8126659db6be34bfa057a9a32f7abbc2fc773855a031
  0x727488a9527e0b140e2907e51385d3ef3c1df07bdfa54e2de5bda867b8daeb8a
  0x5e20b0d3d6bb59dfa981c75b2e0900c1a9474d5127f4859c88b9a0cd5469ddda

ETH Up/Down 2:15-2:30AM — Balance drain (unattributed):
  0x1ba2ed39c77e767ae2b000f521e14e618187746520c4385092f083d4871d158d
  0x56aec1b48fc1c6e0d7c87e7533d66b4d00537979ab9fb0ce0c87e9a6f6b6f5e4
  0xf595c73f55dd2008f5a83855fd44b277ca4cf3468f8e19e155d1cdec584445c6

How polynode detects ghost fills

polynode detects settlements from the mempool 3–5 seconds before on-chain confirmation. When a matchOrders transaction is detected, a pending settlement event is emitted. When the block confirms:
  • If the receipt shows status: 0x1 (success): a status_update event is emitted with confirmed_fills containing exact on-chain execution data
  • If the receipt shows status: 0x0 (reverted): no status_update is emitted. The pending settlement was a ghost fill.
To detect ghost fills, track pending settlements that never receive a status_update:
const pending = new Map();

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);

  if (msg.type === "settlement" && msg.data.status === "pending") {
    pending.set(msg.data.tx_hash, {
      ...msg.data,
      detected_at: Date.now(),
    });
  }

  if (msg.type === "status_update") {
    // Confirmed on-chain — not a ghost fill
    pending.delete(msg.data.tx_hash);
  }
};

// Check for ghost fills every 5 seconds
setInterval(() => {
  const now = Date.now();
  for (const [txHash, data] of pending) {
    if (now - data.detected_at > 15000) {
      console.log("Ghost fill:", txHash, data.market_title);
      pending.delete(txHash);
    }
  }
}, 5000);
For verified trade data, use the confirmed_fills field on status_update events. See the Trade Tracking Guide for details on using pending settlements vs confirmed fills.

Methodology

All data was collected using polynode’s live settlement stream and Polygon RPC. Settlement failure rate: Subscribe to settlements via WebSocket, collect unique tx_hash values for a fixed period, wait for block confirmations, batch-query receipts, check receipt.status. Revert cause analysis: Use trace_transaction on reverted TXs to decode the revert reason string from the deepest failing call. Attacker identification: Scan all transactions in the block range for calls to incrementNonce() (selector 0x627cdcb9) targeting the CTF Exchange contract (0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E) or the NegRisk CTF Exchange. Both direct calls and proxy calls (where the selector appears inside execTransaction calldata) are detected. Attribution: Cross-reference incrementNonce() call blocks against reverted settlement blocks. A revert is “confirmed” if a nonce flip occurred in the same block or 1–3 blocks prior. No mock data is used. All transaction hashes, block numbers, and wallet addresses are from Polygon mainnet.