As of 30 Oct 2026, Polymarket has 7 audit reports by 2 security firms on Trustblock; the latest is by Quantstamp, dated 5 Oct 2026. Those reports list no open critical findings.
Record last updated . It holds audits and findings, contracts and incidents up to Oct 2026. Trustblock does not review code.
Newest first
| Audit | Security firm | Date | Scope | Critical findings | Report |
|---|---|---|---|---|---|
| Polymarket v2 | Security firm: Quantstamp | Date: | Scope: Not recorded (see the report) | No open critical findings | Open · certificate.quantstamp.com |
| Polymarket CTF Exchange v2 | Security firm: Quantstamp | Date: | Scope: Not recorded (see the report) | No open critical findings | Open · certificate.quantstamp.com |
| UMA Sports Oracle | Security firm: ChainSecurity | Date: | Scope: Files named in the report | No open critical findings | Open · chainsecurity.com |
| NegRiskAdapter | Security firm: ChainSecurity | Date: | Scope: Files named in the report | No open critical findings | Open · chainsecurity.com |
| Conditional Tokens | Security firm: ChainSecurity | Date: | Scope: Not recorded (see the report) | No open critical findings | Open · chainsecurity.com |
| Proxy Wallet Factories | Security firm: ChainSecurity | Date: | Scope: Not recorded (see the report) | No open critical findings | Open · chainsecurity.com |
| Exchange | Security firm: ChainSecurity | Date: | Scope: Not recorded (see the report) | 2 critical findings, fixed | Open · chainsecurity.com |
Statuses are the security firm’s own, from each report.
| Severity | Open | Acknowledged | Fixed |
|---|---|---|---|
| Critical | 0 | 0 | 2 |
| High | 0 | 0 | 2 |
| Medium | 0 | 0 | 8 |
| Low | 1 | 8 | 22 |
The report gives no description here.
Open the reportThe report gives no description here.
Open the reportMigrated NegRisk events can be resolved through `resolveMigrationCondition()`, which is permissionless once the corresponding legacy condition has resolved. The function takes a structured V2 `conditionId`, reads the legacy result by deriving the event and condition index from that id, and then stores the V2 result under the raw `conditionId` key. This creates an identity mismatch. A non-canonical alias such as `canonicalCondition | 1` is a different raw key for V2 result storage, but it still decodes to the same event and condition index for legacy lookup. If the legacy market resolves normally with one YES winner, an attacker can call `resolveMigrationCondition(aliasId)` for that YES condition. This increases `resultsSum[eventId]` to `1_000_000`, while the canonical winning condition remains unresolved in V2. Once `resultsSum[eventId] == 1_000_000`, anyone can call `resolveConditionToNo()` on the canonical conditions. This lets the canonical V2 event appear all-NO even though the legacy event resolved normally with one YES winner. Because already resolved positions can still be split, a user can repeat the flow by splitting canonical conditions, redeeming all NO positions for principal, and horizontally merging the YES basket for extra pUSD each time. The PoC below demonstrates one iteration ending with `totalInput + amount` after supplying only `totalInput`, but the damage is not capped to that single iteration and can continue draining pUSD until the system is paused. The impact can extend across chains. Once the non-canonical alias has a stored result on the source chain, `bridgeResult()` can bridge that raw alias result to supported spoke chains because it only checks `module.getResult(_conditionId).length == 2`. On the receiving chain, `_handleResultReceive()` forwards the raw `conditionId` to `reportResult()` from the bridge role. Since bridged results skip the NegRisk sum invariant inside `reportResult()`, the alias can seed the same corrupted event state on the spoke chain. An attacker can then repeat the same `resolveConditionToNo()`, split, redeem, and horizontal merge flow on each affected chain, draining pUSD from multiple deployments before detection and pause. The source of the regression is that the older implementation resolved through `BaseModule._resolveCondition()`, which called `_requireConditionPrepared()`. Since NegRisk event preparation set an oracle only for each canonical condition index, non-canonical aliases had `oracle[alias] == address(0)` and reverted. The newer migration path no longer enforces this canonical preparation check before storing the result, while legacy lookup still maps the alias back to the same legacy condition.
Open the reportThe report gives no description here.
Open the reportLegacy NegRisk event resolution can finalize with zero winners, meaning every condition resolves to NO. This differs from the one-winner economic assumption used by the new NegRisk design. If users are able to split positions after such an all NO legacy resolution, they can hold both legacy YES and legacy NO for each condition, redeem the winning NO side for full collateral recovery, and still migrate the corresponding YES side into the new module. The new module YES basket can then be horizontally merged for additional collateral value. This means a user can recover collateral through the legacy NO redemption path and extract additional value through the new module YES merge path from the same post resolution split set. Even when strict one-winner checks prevent full migration resolution finalization, the collateral extraction path remains reachable before that finalization boundary. This creates a state where migration safety depends on a legacy oracle invariant that may not hold in practice, rather than being fully enforced by migration time economic constraints. The same attack is also feasible if an already resolved legacy NegRisk event with all NO outcomes is migrated, because migration enrollment does not change the underlying all NO payoff asymmetry between legacy redemption and migrated horizontal merge paths.
Open the reportIn 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.
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportBatch order matching updates the taker order status in the `_matchBatchOrders()` function with the gross `takerFillAmount` before settlement completes. For batch buy matches that produce a collateral refund, `_executeBatchBuyMatch()` later computes the net `takerMakingAmount` as `takerFillAmount - collateralRefund`, but the stored order remaining value has already been reduced by the gross amount. The remaining calculation consumes a taker order faster than the net settled amount, potentially marking an order as filled even though only part of the signed economic size was executed.
Open the reportBatch matches can settle a taker order for less than the gross amount reported in the taker `OrderFilled` and `OrdersMatched` events. In the batch path, `_matchOrders()` sets `takerMakingAmount` to `takerFillAmount` before settlement completes, then `_emitTakerEvents()` emits that gross amount even if the batch settlement refunds collateral or positions back to the taker. If builder rewards or volume accounting are calculated from taker `OrderFilled` events keyed by `builder`, downstream systems can attribute rewards using the gross requested fill instead of the net settled amount.
Open the reportStructured position IDs derive their `conditionId` / `eventId` from a `baseHash`, and the `baseHash` includes the condition's original chain ID. The functions `NegRiskModule.getEventId()` and `BinaryModule.getConditionId()` hard-code the `_chainId` component of the computed `conditionId` to be `block.chainid`. For conditions that originate on one chain and are later bridged to another, the bridged positions retain their original source-chain IDs. Calling these helpers on the destination chain with the same market data will instead compute a different destination-chain ID. As a result, external integrations, indexers, frontends, or other offchain systems relying on these helpers may display incorrect market IDs, query incorrect balances/results, or build transactions against the wrong condition/event.
Open the reportSeveral upgradeable contracts inherit from stateful base contracts/mixins that do not reserve storage space for future upgrades. This is especially relevant for `NegRiskModule`, whose storage layout places inherited state before state declared directly in `NegRiskModule`. If a future upgrade adds a state variable to `NegRiskMigrationMixin`, it will be inserted before the `NegRiskModule` contract’s own storage variables. This would shift the existing storage layout and corrupt the expected values stored in these slots. The same upgrade-safety concern applies to other stateful inherited contracts used by upgradeable implementations, including: - `BaseModule.sol` - `BaseMigrationMixin.sol` - `BinaryMigrationMixin.sol` - `OracleModule.sol` - `oracle/mixins/Pausable.sol` These contracts would also benefit from reserved storage space or an alternative storage isolation pattern.
Open the report`prepareMigrationEvent()` binds migration state to a structured `eventId` that includes the current legacy condition count (encoded as arity), then snapshots legacy condition index mappings for that count. The same legacy event can later be enrolled again under a different structured `eventId` if the legacy condition count changes, because uniqueness is enforced per structured `eventId` key rather than per legacy event ID. This creates a moving migration target for a single legacy market: previously-migrated positions continue to exist under the earlier condition-count namespace, while future migrations of the same legacy conditions can be redirected into a new condition-count namespace. If the legacy layout changes after initial enrollment (for example, additional conditions are appended), migrated accounting and resolution assumptions can diverge from the original snapshot, and the protocol can end up operating multiple incompatible migrated views of one legacy event. Under specific outcome patterns, this divergence can produce migrated states that conflict with one-winner assumptions used for pUSD safety and can reintroduce the same collateral extraction risk class as all NO style migration outcomes.
Open the reportThe 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.
Open the reportIn 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.
Open the reportThe 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.
Open the reportThe `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.
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportThe reports name a repository or files. Open a report to see its scope.
Is this your project? Show this record on your own website. The label states what the record holds, links to this page and updates by itself. It is free and needs no sign-in.
Automatic: the three strongest facts of this project’s own record (its last audit and two more), highlighted. They update with the record. Picking a fact turns Automatic off.
<head><script src="https://widget.trustblock.run/project-widget.min.js" defer></script></head><body><divdata-tb-project-widgetdata-tb-project-slug="polymarket"></div></body>
Paste the code where the label should appear. It uses no API credit and updates by itself.
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