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 callingmatchOrders 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 calledincrementNonce() (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:
- The attacker places orders on both sides of a market (YES and NO)
- One side gets matched by the CLOB
- Before the operator’s
matchOrderstransaction lands on-chain, the attacker callsincrementNonce()on the CTF Exchange - The
matchOrderstransaction reverts because the attacker’s order nonce no longer matches their on-chain nonce - Since
matchOrdersis atomic (no try/catch per order), one invalid order kills the entire batch — every other order in that transaction also fails - The counterparties who were matched against the attacker get nothing. Their trades evaporate.
- Polymarket’s off-chain system removes all affected orders from the book
Two attack variants
Our research identified two distinct methods that cause settlement failures:Variant 1: Nonce flip
The attacker callsincrementNonce() 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’smatchOrders 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 everymatchOrders transaction from polynode’s settlement stream and checking each receipt for revert status.
| Metric | Value |
|---|---|
| Timestamp | 2026-04-09 ~06:15 UTC |
| Collection | 10 rounds x 30 seconds |
| Total matchOrders TXs | 9,294 |
| Succeeded | 9,263 |
| Reverted | 31 |
| Overall failure rate | 0.334% |
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
| Round | TXs | Reverted | Rate |
|---|---|---|---|
| 1 | 1,091 | 2 | 0.183% |
| 2 | 792 | 2 | 0.253% |
| 3 | 1,313 | 2 | 0.152% |
| 4 | 1,684 | 7 | 0.416% |
| 5 | 683 | 7 | 1.025% |
| 6 | 557 | 4 | 0.718% |
| 7 | 513 | 2 | 0.390% |
| 8 | 541 | 3 | 0.555% |
| 9 | 1,252 | 2 | 0.160% |
| 10 | 868 | 0 | 0.000% |
Market category breakdown
| Category | Reverts | Percentage |
|---|---|---|
| Short-term crypto (BTC/ETH 5–15 min) | 29 | 93.5% |
| Other crypto | 1 | 3.2% |
| Other (politics) | 1 | 3.2% |
Revert cause analysis
We traced every reverted transaction usingtrace_transaction to decode the exact revert reason.
| Revert reason | Count | Variant |
|---|---|---|
ERC20: transfer amount exceeds balance | 20 | Balance drain |
SafeMath: subtraction overflow | 11 | Balance drain |
| Total | 31 |
Attacker identification
We scanned 1,800 consecutive blocks (~1 hour) for all calls toincrementNonce() (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)
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 at0xfc64b6660ed1... - Similar ~5 minute cadence
- Has a Polymarket account and profile page
- Attribution to specific reverts pending (proxy routing makes block-level correlation less precise)
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 theincrementNonce() call log:
| Attribution | Count | Notes |
|---|---|---|
| Confirmed nonce flip | 7 | incrementNonce() call in same block or 1–3 blocks before revert. All from Attacker 1. |
| Unattributed | 24 | No nearby incrementNonce() call. Likely balance drain variant or nonce flip from undetected proxy call. |
Sample reverted transactions
How polynode detects ghost fills
polynode detects settlements from the mempool 3–5 seconds before on-chain confirmation. When amatchOrders transaction is detected, a pending settlement event is emitted. When the block confirms:
- If the receipt shows
status: 0x1(success): astatus_updateevent is emitted withconfirmed_fillscontaining exact on-chain execution data - If the receipt shows
status: 0x0(reverted): nostatus_updateis emitted. The pending settlement was a ghost fill.
status_update:
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 uniquetx_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.
