Polymarket CTF Exchange v2

QuantstampCode version: not recorded (see the report)

Quantstamp reviewed Polymarket (Polymarket CTF Exchange v2) on 25 Mar 2026: 5 findings, 0 critical; all marked fixed.

No open critical findings
Open the reportOpen Polymarket’s security record

Report hosted on certificate.quantstamp.com.

Trustblock does not review code.

Findings5

Statuses are the security firm’s own, from its report.

SeverityOpenAcknowledgedFixed
Critical000
High000
Medium001
Low004
MediumTakers Can Be Overcharged or Underfilled in COMPLEMENTARY pathFixed

In the COMPLEMENTARY settlement path, the parameter `takerFillAmount` only updates the taker's `orderStatus.remaining` capacity but does not constrain the actual tokens flowing to or from the taker. The taker's real inflows and outflows are determined entirely by the makers' orders and the `makerFillAmounts[i]` supplied by the operator. Because `takerFillAmount` and `makerFillAmounts[i]` are independent inputs and are not in the same asset units in the COMPLEMENTARY path, there is no on-chain check that aggregate maker-side execution is economically consistent with the declared taker fill; as a result, settlements can violate the taker’s signed constraints without being rejected. The missing enforcement opens two categories of incorrect settlement: 1. **Overspend.** If the off-chain logic submits a small `takerFillAmount` (consuming little order capacity) with disproportionately large `makerFillAmounts`, the taker's actual collateral (or CTF) outflow, computed from the makers' order ratios, can exceed the taker's signed `makerAmount`. Because each such match consumes only a sliver of order capacity, the same taker order can be matched repeatedly with new makers, each time pulling more funds than authorized, until the taker's balance or approval is exhausted. 2. **Underpay.** If the off-chain logic submits a large `takerFillAmount` (consuming most or all order capacity) with insufficient maker fills, the taker's order is fully consumed while the taker receives a fraction of the tokens their ratio guarantees. The `taking` value (the minimum output the taker's ratio entitles them to) is computed by `_performOrderChecks()` but is never checked or enforced in the COMPLEMENTARY branch. Compare this to the non-COMPLEMENTARY path, where `_updateTakingWithSurplus(taking, takerAssetId)` at line 112 enforces the minimum. It is important to note that the MINT/MERGE path is not affected: there the taker transfers `takerFillAmount` to the exchange upfront, and `_updateTakingWithSurplus()` enforces the minimum output. In those paths, even a faulty off-chain submission cannot produce an out-of-bounds settlement.

LowBuilder Volume Can Be MisattributedFixed

The values emitted from the `OrderFilled` event are important for attributing correct builder rewards and fee sharing. The `builder` value itself is signed as part of the order payload and included in the EIP-712 struct hash. To ensure that these rewards are calculated correctly, it is important that the emitted volume value from the `OrderFilled` event match exactly the amounts executed in the trade. In v2, emitted taker volume is not always derived from per-match transfer deltas. In COMPLEMENTARY settlement, `_settleComplementaryTaker()` finalizes taker `OrderFilled` values from `takerFillAmount`. But actual transfers are executed in `_settleComplementaryMaker()` from `makerFillAmounts`. In non-COMPLEMENTARY settlement, `_updateTakingWithSurplus` can replace expected taking with full exchange balance for that asset before taker event emission. The following scenarios can result in incorrect `OrderFilled` volume: 1. Empty maker list in COMPLEMENTARY branch can still emit non-zero taker fill. 2. COMPLEMENTARY taker BUY can emit collateral-side input as `takerAmountFilled` instead of conditional tokens received. 3. COMPLEMENTARY `takerFillAmount = 0` with non-zero maker fills can emit zero taker fill while real transfers occur. 4. Non-COMPLEMENTARY taker fill can include pooled or pre-existing surplus, not only current-match execution. If rewards are paid from raw emitted volume, this can over-credit, under-credit, or misattribute builder performance.

LowOperator Can Route Value Across Non-COMPLEMENTARY Matches and Drain Stranded InventoryFixed

In non-COMPLEMENTARY matching (MINT/MERGE), settlement is pooled at the exchange level instead of isolated per match. In `_matchOrders()`, taker and maker assets move into shared exchange balances. `_settleMakerOrders()` then executes maker matches in batch, and `_settleTakerOrder()` pays the taker from the exchange inventory. During the non-COMPLEMENTARY MINT/MERGE matching flow, `_mint()`/`_merge()` use the `conditionId` supplied to `matchOrders()`, while payout assets are derived from order `tokenId`s. Non-COMPLEMENTARY matches can produce surplus from price crossing: in MINT, surplus exists when taker price + maker price > 1; in MERGE, surplus exists when taker price + maker price < 1. In v1, non-COMPLEMENTARY matches required the maker `tokenId` to be the complement of the taker `tokenId` via `validateComplement()`. In v2, the complement check was removed, allowing mixed maker composition on-chain. The matching logic assumes that the maker `tokenId`s in a match correspond to outcomes of the provided `conditionId`, so that any non-COMPLEMENTARY surplus remains confined to the intended match. Because this relationship is not enforced on-chain in v2, mismatches can occur even without malicious intent (e.g., batching mistakes or matcher bugs). When mixed batches do not align with the intended match composition, settlement can leave stranded inventory in the shared exchange balances. This breaks per-order value isolation because later matches can consume value created by earlier unrelated matches, even when each order still appears valid against its signed limit when checked individually. `_updateTakingWithSurplus()` can increase the taker payout from the match minimum to the exchange’s full balance of that taker asset. `_settleTakerOrder()` only refunds leftover taker `makerAssetId`. These functions together allow surplus and pooled balances to be routed across unrelated matches and across batches. Variants: 1. Accidental / matcher-bug variant (more likely): Same-market mixed maker composition (e.g., batching or matching errors) can strand side-token balances even when `conditionId` is correct. 2. Trust-failure variant (less likely): cross-market mixed batches with operator-provided `conditionId` can mint or merge one market while paying another, also creating stranded inventory. Operator extraction method: 1. After stranded inventory exists, submit a small follow-up order where taker asset equals the stranded asset. 2. `_updateTakingWithSurplus()` uses full exchange balance (not per-match delta), so payout can include that pre-existing stranded inventory. 3. `_settleTakerOrder()` only refunds taker `makerAssetId`, so other residual balances remain available for later extraction.

LowOrders Where `cashValue == 0` Can Be Charged Uncapped FeesFixed

The fee amounts charged on orders in the `CTFExchange` contract are dynamic and may be unknown to the user at the time the order is created. To limit the trust required of the operator, the total fee to be paid by an order is validated to be within some accepted `maxFeeRateBps` in the `validateFeeWithMaxFeeRate()` function. However, in scenarios where the `cashValue` of an order is zero, the `validateFeeWithMaxFeeRate()` function returns before these checks are made, meaning such orders can be charged an uncapped fee. As the `cashValue` is based on values supplied by the operator (`takerFillAmount` or `makerFillAmounts`) and not a value included in the order struct signed by the user, the user cannot reasonably prevent this.

LowOverflow in `calculateTakingAmount()` Can Produce Incorrect Settlement AmountsFixed

The `CalculatorHelper.calculateTakingAmount()` function uses inline assembly to compute `(makingAmount * takerAmount) / makerAmount`. The intermediate product `makingAmount * takerAmount` can silently overflow and wrap to a smaller value when it exceeds `type(uint256).max`. Off-chain validation may restrict the range of order parameters such that overflow does not occur in practice, but there is no on-chain logic that enforces or validates this. The affected `takingAmount` is returned by `_performOrderChecks()` and is used inside both COMPLEMENTARY and non-COMPLEMENTARY paths to determine transfer amounts. The overflow requires `order.makerAmount * order.takerAmount >= 2^256`. Since both amounts are price parameters in the signed order (not a transferred balance), it is not constrained by real token supply. No scenario was identified in which this overflow could be used to systematically extract value from the opposing side of a match, if strict off-chain overflow checks are always applied to order and fill amounts.

Contracts in scope

No deployed address is recorded for this audit.

The report names a repository or files. Open the report to see its scope.

Part of Polymarket’s security record

Polymarket
Audits7 audits by 2 firmsLast audit Oct 2026, by Quantstamp.
Critical findingsNo open critical findingsAcross the 7 reports, 2 critical findings were reported. Each is marked fixed by the firm that reported it.
Open Polymarket’s security record

Security firm

Quantstamp

236 projects reviewed · 332 reports · latest Oct 2026

See Quantstamp’s audits

Something wrong on this page? A missing report, a wrong date, a finding that was fixed since. Tell us and we check it against its source.

Tell us