- August 10, 2026
OpenZeppelin Security
OpenZeppelin Security
Security Audits
Summary
Type: DeFi
Timeline: 2026-07-21 → 2026-07-27
Languages: Solidity
Findings
Total issues: 13 (9 resolved)
Critical: 0 (0 resolved) · High: 0 (0 resolved) · Medium: 8 (8 resolved) · Low: 5 (2 resolved)
Notes & Additional Information
6 notes raised (5 resolved)
Client Reported Issues
0 reported issues (0 resolved)
Scope
OpenZeppelin conducted an audit of the UMAprotocol/managed-oracle repository covering the changes between base snapshot 5fffebb and target snapshot 13dabea. This change introduces a new OOReporter contract and changes to the ManagedOptimisticOracleV2 contract.
In scope were the following modified or added files:
pm-v2-oo-reporter
└── src
├── interfaces
│ ├── IOOReporter.sol
│ ├── IOptimisticOracleV2.sol
│ └── IOptimisticRequester.sol
└── OOReporter.sol
src
└── optimistic-oracle-v2
├── implementation
│ └── ManagedOptimisticOracleV2.sol
└── interfaces
└── ManagedOptimisticOracleV2Interface.sol
Following the initial review, three additional feature pull requests were reviewed on top of the audited target snapshot:
- Pull request #56: a reversible pre-proposal pause and a reward-recovery flow for active requests.
- Pull request #57: an automatic Polymarket report callback that relays a final result to the registering module.
- Pull request #58: bond and liveness update events on the Managed Optimistic Oracle.
System Overview
OOReporter is a UMA-owned contract that connects Polymarket's version 2 prediction market contracts or any other integrators to UMA's Managed Optimistic Oracle. The Managed Optimistic Oracle extends UMA's Optimistic Oracle: it keeps the propose-and-dispute settlement model of the base oracle and adds a layer of administrative control over individual price requests, including customizable bond sizes, liveness periods, proposer and requester whitelists, and permissioned settlement. The base Managed Optimistic Oracle contract was reviewed in a prior OpenZeppelin engagement. This engagement covers the new OOReporter package together with the accompanying changes to ManagedOptimisticOracleV2.
OOReporter: This reporter replaces the Conditional Token Framework adapter previously used to create markets, giving UMA control over how oracle requests are created, parameterized, re-requested, and read back. It sits between the integrated market contracts such as Polymarket v2 and the Managed Optimistic Oracle for smoother integration and better management of requests, such as initialization, re-request and callbacks from the Managed Optimistic Oracle. Responsibilities are split between two allowlisted actor classes: a requester, expected to be the Polymarket market module, that registers a request identifier along with its rules, and an oracle initializer, expected to be UMA automation, that opens and re-opens the underlying oracle requests. Registration is separated from initialization so that malformed requests can be filtered before any reward is committed to the oracle.
Markets and Requests: The OOReporter tracks one object per market, keyed by the pair of price identifier and request rules, while the Managed Optimistic Oracle operates on individual requests via msg.sender whose identity also includes a timestamp. A single market can therefore correspond to several successive oracle requests. Reusing an identical identifier and rules pair resolves to the same reporter object, which is the intended behavior and the mechanism that keeps request identities unique within a deployment.
Request Lifecycle: The flow proceeds in distinct phases. It begins when the requester calls registerRequest with its requestId, the price identifier, the request rules, and an allowed liveness range. Registration reserves the request identity and its rules but commits no funds and opens no oracle request, so a malformed or duplicate registration can be rejected before any reward is at stake. An oracle initializer then calls initializeRequest to open the underlying Managed Optimistic Oracle request, funding the reward from the reporter's balance and choosing a bond and a liveness within the registered range. Reporter requests are opened as event-based, with dispute and settlement callbacks enabled. A proposer submits an answer backed by a bond, which starts the liveness challenge window. If the window elapses without a dispute, a permissioned resolver settles the request; if a dispute is raised, the request escalates to the DVM and is settled after the DVM resolves it. On a settlement that produces a usable (non-P4) value, the reporter records the raw outcome under the requestId, and the Polymarket integration reads it back to translate into market payouts.
Disputes, Settlements and Rule Updates: When a request is disputed on the oracle, or when it settles to the P4 value, the reporter can replace it, either through a single automatic re-request or through a budgeted manual re-request performed by the oracle initializer. Only a non-P4 settlement performed by the trusted resolver records a final outcome for the market. These actions of resolving and re-requests are handled through callbacks sent from the Managed Optimistic Oracle. Market rule clarifications are forwarded by the requester to ManagedOptimisticOracleV2, which the in-scope changes extend to store an append-only history of rule updates keyed by the request. These updates are informational and do not change the value the oracle adjudicates.
The three additional feature pull requests were assessed. Pull request #56 adds a reversible pre-proposal pause and a reward-recovery flow that lets the reward of an active request be raised or lowered before any proposal, funding increases from the caller and refunding decreases to the requester. To compute the change, OOReporter reads the current reward from the oracle through a low-level staticcall to getRequest and decodes the reward field with inline assembly at a fixed offset. Pull request #57 adds an automatic Polymarket report callback through a PolymarketOOReporter variant that relays a final non-P4 outcome to the registering module in a nonblocking manner. Pull request #58 adds bond and liveness update events on the Managed Optimistic Oracle.
Security Model and Trust Assumptions
The OOReporter package and the accompanying changes to ManagedOptimisticOracleV2 are permissioned components whose security depends on a set of trusted operators and on the deployment configuration. This section records the roles and trust assumptions relied upon during the review. Any finding whose impact depends on one of these assumptions being violated is out of scope unless an integrator or unprivileged actor could plausibly breach it.
Privileged Roles
-
The OOReporter owner controls configuration, fund movement, and upgrades.
- It manages the requester and oracle-initializer allowlists, sets the default re-request budget and the automatic re-request setting, can withdraw tokens from the contract through the
sweepfunction, can claim deferred oracle payouts owed to the reporter, and authorizes contract upgrades. The reporter is upgradeable and uses single-step ownership.
- It manages the requester and oracle-initializer allowlists, sets the default re-request budget and the automatic re-request setting, can withdraw tokens from the contract through the
-
The oracle initializer creates and replaces oracle requests in
OOReporter.- It opens and re-opens requests and selects the reward, bond, and liveness within the range registered for each request, drawing the reward from the reporter's balance. The manual re-request budget limits how many replacement requests it can perform for a request.
- It can also change the reward of an active request before any proposal is submitted through
setRequestReward, funding an increase from the reporter's balance or refunding a decrease. This capability is not scoped to the initializer that created the request, so any enabled oracle initializer can raise or lower the reward of any active request. - This role is managed by the UMA Team.
-
The requester registers requests and posts rule updates in
OOReporter.- It registers request identifiers together with their rules and forwards rule clarifications. Request identities are shared across enabled requesters within a single deployment and are keyed by the identifier and rules pair.
- This role will be granted to integrators such as Polymarket.
-
The callback functions in OOReporter are privileged.
- The
priceDisputedandpriceSettledfunctions are callback functions inOOReporterthat can only be invoked by the optimistic oracle i.e.,ManagedOptimisticOracleV2. This privilege is granted during initialization ofOOReporter.
- The
-
The Managed Optimistic Oracle administrators that govern the underlying oracle.
- An upgrade administrator holds exclusive authority to upgrade the implementation, a configuration administrator sets system-wide defaults such as the whitelists and the bond and liveness bounds, and request managers can override the bond, liveness, and proposer whitelist for individual requests. The privileged roles of the Managed Optimistic Oracles are assumed to act in good faith.
- With the addition of pull request #56, request managers will have the ability to override rewards as well.
Trust Assumptions
-
All permissioned actors within the reporter's scope are fully trusted.
- There is no intermediate, partially trusted role. Each permissioned actor is relied upon to act honestly and competently, and findings whose only precondition is a permissioned actor behaving maliciously are out of scope.
-
The reporter owner is trusted to administer the contract safely.
- It is relied upon to configure allowlists and budgets correctly, to hold the upgrade and sweep authority securely, and not to transfer or renounce ownership incorrectly.
-
The oracle initializer is trusted to initialize and re-request faithfully.
- The re-request budget is treated as a safeguard that limits the impact of a compromised initializer key rather than as protection against the initializer itself.
-
The requester-supplied
requestIdandrequestRulesare trusted to be well-formed and unique.- The reporter treats
requestIdas an opaque identifier chosen by the requester and does not derive it or validate it against any underlying market data. It relies on the requester to supply arequestIdthat is correctly formed, unique, and faithfully bound to the intended market or condition. Deriving the identifier and checking that it corresponds to the correct market is the requester's responsibility, performed on the Polymarket side. - The callbacks from the Managed Optimistic Oracle are identified using
priceIdentifierandrequestRules. TheOOReporterassumes thatrequestRulesare also always unique.
- The reporter treats
-
The deployment enables a single coordinated integrator.
- Requesting is expected to be restricted to Polymarket per deployed instance to avoid request-identifier collisions. Running multiple requesters relies on that integrator to keep request identities unique, since identities are shared across enabled requesters.
-
Resolution and finality depend on the resolver acting.
- The reporter records an outcome only once a resolver settles a request to a non-P4 value. A dispute causes the reporter to open a fresh oracle request rather than binding the market to the eventual resolution of the disputed request.
- On a dispute, the reporter automatically opens a replacement request and advances its internal timestamp. The disputed request stays live and eventually resolves at the Data Verification Mechanism, but because a request's identity includes its timestamp, that resolution settles only the superseded round and is disregarded by the reporter as stale. Market resolution follows the outcome of the latest replacement round. This is the intended behavior and preserves the semantics of the legacy Conditional Token Framework adapter, in which the first dispute produces a single replacement request. The market is deliberately not bound to the disputed round's own Data Verification Mechanism result.
-
Price-result validation and payout translation are the responsibility of the Polymarket integration.
- The reporter records and exposes only the raw settled outcome. Interpreting that outcome and paying out markets happen on the Polymarket side and are not enforced by the reporter.
-
Updated rules are taken into consideration immediately.
- It is assumed that the Data Verification Mechanism (DVM) and the integrated oracles immediately process updated rules for all requests and provide the latest valid response based on those updated rules.
-
The
OOReportercontract is close to the maximum contract size.- The reporter is deployed with a very small margin under the EIP-170 of 24,576-bytes limit. Some implementation choices, such as reading the oracle reward through inline assembly rather than decoding the full request structure, are made to preserve this margin, and future changes must account for the limited remaining headroom.
-
The layout of the Managed Optimistic Oracle request structure is assumed to remain unchanged.
- When computing a reward change,
OOReporterdecodes therewardfield returned by the oracle'sgetRequestfunction using inline assembly at a hardcoded offset. This relies on theRequeststructure of the Managed Optimistic Oracle keeping its current field layout. A reordering of fields, or the introduction of a dynamic member beforereward, would move the field and cause the assembly to read an incorrect value.
- When computing a reward change,
-
The underlying UMA infrastructure is trusted and out of scope.
- The reporter and the Managed Optimistic Oracle rely on the Data Verification Mechanism (DVM) that resolves disputes, the Store, and the Finder used to locate these components, together with the ERC-20 token used for rewards and bonds. These systems are treated as trusted external dependencies.
Additional Considerations
- No on-chain mechanism bounds how long a request may remain unresolved.
- A request that repeatedly settles to the P4 value, or that a resolver never settles, can keep a market unresolved indefinitely, and the reward committed to it can remain locked. A reward clawback function for such requests was considered and intentionally not implemented at this stage, so recovery from these situations, together with recovery of deferred reward payouts and continued re-requesting, depends on the trusted owner and oracle initializer.
Medium Severity
Incorrect Liveness Range Validation in OOReporter
During the registration of a request, minimumLiveness is checked against MAXIMUM_CUSTOM_LIVENESS, while maximumLiveness is checked against oracleMinimumLiveness.
However, these checks are misplaced, leaving an edge case where minimumLiveness is less than oracleMinimumLiveness and maximumLiveness is greater than MAXIMUM_CUSTOM_LIVENESS. In this case, the oracle cannot initialize the request, which results in a denial of service for upstream integrations.
Consider checking minimumLiveness against oracleMinimumLiveness and maximumLiveness against MAXIMUM_CUSTOM_LIVENESS to ensure that the configured liveness range is respected.
Update: Resolved in pull request #54 at commit 7def0a8. The team stated:
Resolved through the companion changes in PRs #53 and #54.
Rather than requiring the complete registered target range to remain an onchain runtime range, PR #53 clarifies and enforces the intended semantics:
minimumLivenessis a hard onchain floor.maximumLivenessis an offchain initialization target, not an onchain ceiling.- The selected liveness remains subject to Managed OO's current
minimumDisputeWindowand exclusive technical maximum.Registration intentionally retains the overlap predicate:
minimumLiveness < MAXIMUM_CUSTOM_LIVENESS
maximumLiveness >= optimisticOracle.minimumDisputeWindow()
minimumLiveness <= maximumLiveness
This ensures the target range has a valid normal-path choice at registration. If
minimumDisputeWindowlater rises above the stored target maximum, initialization or manual recovery can use a higher liveness instead of permanently blocking the request.
Raising minimumDisputeWindow Above a Registered Liveness Range Permanently Blocks a Request
The registerRequest function of the OOReporter contract stores a fixed liveness range for each request. The Polymarket module forwards the requested range verbatim, and the reporter records minimumLiveness and maximumLiveness with no later means to amend them. At registration, the range is sanity-checked against the oracle's current minimumDisputeWindow and MAXIMUM_CUSTOM_LIVENESS. When a request is opened, _requestPrice calls oracle.setCustomLiveness, which validates the chosen liveness through _validateLiveness and requires it to be at least the current minimumDisputeWindow.
The minimumDisputeWindow is adjustable at any time by the configuration administrator through setMinimumDisputeWindow. If it is raised above a request's stored maximumLiveness after registration, every liveness the initializer can select becomes invalid. The selected value is bounded to the stored range by _requireValidRequestLiveness, so it can never reach the new floor, and setCustomLiveness reverts. This blocks initializeRequest.
The same condition blocks the re-request flow. Both the automatic re-request, which reuses the stored request.liveness, and the manual rerequest function route through _requestPrice and the same validation, still capped by the immutable maximumLiveness. Therefore a request that was initialized before the change also becomes unrecoverable once it is disputed or settles to the P4 value. The automatic re-request fails inside its try/catch in the P4 branch of priceSettled and only opens the manual gate, and the manual re-request then reverts. In every such case, the affected market can no longer be initialized or re-requested through the reporter. Because the stored range is immutable and no function can clear or amend it, the market can be resolved only through the Polymarket administrator or arbitrator resolveResult path. Only requests that resolve on their first round, without a dispute and without a P4 outcome, are unaffected. The impact is limited to availability, however a single global configuration change can silently strand any number of pre-registered markets on the UMA side.
Consider revalidating and clamping the stored liveness range against the oracle's current bounds at initialization and re-request time, rather than only at registration. Alternatively, consider providing a gated path to clear or amend a request's stored liveness bounds, so that an out-of-bounds range can be corrected without redeploying the reporter. Furthermore, it is advisable to document that raising minimumDisputeWindow above the maximumLiveness of any outstanding registered request will prevent that request from being initialized or re-requested.
Update: Resolved in pull request #53 at commit e27d8c1. The team stated:
Fixed in the linked PR by removing the runtime maximum-liveness cap, so a registered range can no longer become unsatisfiable after a configuration change.
minimumLivenessremains the reporter-enforced onchain floor.maximumLivenessremains stored, returned, and emitted as an offchain initialization target, but it no longer blocksinitializeRequestor manualrerequest; the effective onchain floor ismax(request.minimumLiveness, optimisticOracle.minimumDisputeWindow()).Managed OO keeps enforcing its own bounds (
minimumDisputeWindowand< 5200 weeks) throughsetCustomLiveness.If
minimumDisputeWindowlater rises above a registered maximum, the request is no longer stranded: a failed automatic re-request opens the existing manual gate, and an enabled oracle initializer can recover with a liveness above the stale registered maximum.Registration-time consistency checks and all public ABI shapes are unchanged, and the interface NatSpec and README now distinguish the onchain-enforced minimum from the offchain target maximum.
Low Severity
Custom Bond and Liveness Overrides Are Not Revalidated Against Updated Constraints
ManagedOptimisticOracleV2 allows request managers to pre-configure per-request parameters that will be applied at proposal time, including custom liveness and proposal bonds. These values are validated when set via requestManagerSetCustomLiveness (calling _validateLiveness) and requestManagerSetBond (calling _validateBond). The bounds are intended to be adjustable over time, for example by updating minimumDisputeWindow and per-currency allowedBondRanges.
However, proposePriceFor copies the stored customBonds[...] and customLivenessValues[...] directly into the live request through request.requestSettings.bond and request.requestSettings.customLiveness values when the stored values are non-zero, without re-checking them against the current constraints. As a result, previously valid overrides can remain effective across later security-policy updates: an outdated too-short custom liveness can reduce the dispute window below the updated floor enforced by _validateLiveness, and an outdated custom bond can violate updated allowedBondRanges. Too-low bonds reduce economic security for proposals and disputes, while too-high bonds can reduce participation and create liveness risk, which undermines policy tightening for any managed request IDs with pre-existing overrides.
Consider revalidating stored overrides inside proposePriceFor before applying them, and reverting (or ignoring the override) if it violates the current configuration.
Update: Acknowledged, not resolved. The team stated:
Acknowledged. We decided not to revalidate the stored overrides in
proposePriceFor. Reverting would block proposals for any managed request ID with a stale override right after a configuration change — and since the managed request ID omits the timestamp, this would affect all recurring re-requests of a feed until manually remediated. Silently ignoring or substituting the override would change the effective bond/liveness relative to what the requester and proposer expect at proposal time; note the fallback bond would be the requester-set value, which is not validated againstallowedBondRanges.The scenario is handled operationally: request managers can already correct stale overrides at any time via
requestManagerSetBond/requestManagerSetCustomLiveness, which validate against the constraints in force at call time. Updates tominimumDisputeWindow/allowedBondRangeswill be paired with a review of active overrides (enumerable viaCustomBondSet/CustomLivenessSetevents), overwriting any affected managed request IDs. As part of this process, active bond overrides are corrected before anallowedBondRangeis closed to(0,0), since a closed range also prevents overwriting pre-existing overrides.
Rules Updates Have No Length Bound
registerRequest, which caps requestRules at MAX_REQUEST_RULES, neither the reporter's nor the oracle's updateRequestRules bounds the length of the submitted data.
Consider applying the same MAX_REQUEST_RULES cap, or an equivalent bound, in the reporter and oracle updateRequestRules functions.
Update: Acknowledged, not resolved. The team stated:
Acknowledged, no code change planned.
MAX_REQUEST_RULESis derived from OOv2's 8192-byte ancillary-data limit minus the 53-byte,ooRequester:<address>stamp. It protects calls that pass the registered rules torequestPrice.Rules updates do not replace
request.requestRules, participate in request identity, or become ancillary data:
In
OOReporter,updateRequestRulesleaves the registered rules unchanged, so re-requests continue using the rules that were bounded at registration.In
ManagedOptimisticOracleV2,updateRequestRulesappends torequestRulesUpdatesand emitsRequestRulesUpdatedfor off-chain consumers.Applying
MAX_REQUEST_RULESto updates would therefore not enforce an onchain request constraint. We nevertheless acknowledge that oversized or repeated updates can increase contract storage and event/indexer/RPC load. Requester whitelisting restricts who can cause that growth but does not bound it, and the initiating caller pays the transaction cost. We accept this residual operational risk under the trusted-requester model.
Zero Default Budget With Automation Disabled Strands a Request After a P4 Verdict
The OOReporter contract recovers from an unresolvable (P4) verdict either by an automatic re-request or, when automation is unavailable, by a manual re-request drawn from a per-request budget. In the P4 branch of priceSettled, the manual budget is first refreshed to the configured default. The _shouldAttemptAutomaticRerequest function is then consulted, and the manual gate is opened for the oracle initializer.
When automatic re-requests are disabled and the default re-request budget is zero, this recovery path collapses. The budget is refreshed to zero, the automatic attempt is skipped because automation is disabled, and the gate is opened with no budget behind it. A subsequent manual rerequest call then reverts with RequestRerequestBudgetExhausted, so the request has no forward path and its market remains unresolved. This state is reachable only under an owner configuration that deviates from the deployed defaults, in which automation has been disabled and the default budget set to zero.
Recovery is possible but is not a single per-request budget adjustment. The setRequestRerequestBudget function caps the per-request budget at the current default, so while the default is zero any positive value reverts with RequestRerequestBudgetAboveDefault. The owner must therefore first raise the global default through setDefaultRerequestBudget, then set the per-request budget, after which the oracle initializer can re-request. Re-enabling automation alone does not rescue the already-settled request, since no callback fires for it again.
Consider treating a zero default budget while automation is disabled as an unsupported configuration and guarding against it, for example by rejecting that combination at configuration time or by surfacing a distinct signal when a P4 verdict leaves a request with an open gate and no available budget. Alternatively, consider documenting that the default budget must remain positive whenever automatic re-requests are disabled to reduce the likelihood of reaching this state.
Update: Acknowledged, not resolved. The team stated:
Acknowledged. Automatic re-requests and the manual re-request budget are intentionally independent, owner-controlled recovery mechanisms. Setting the default budget to zero disables manual re-requests, while disabling automation disables automatic re-requests; configuring both therefore deliberately pauses all re-request paths.
This does not permanently strand the request. The owner can restore a positive default budget and top up the affected request (both owner updates can be batched via
multicall), after which an enabled oracle initializer can manually re-request it. Re-enabling automation does not replay an already processed callback by design. We prefer to preserve this full-stop operational configuration, so no code changes are planned.
Reward Increases and Decreases Use Asymmetric Counterparties, Preventing Recovery of Over-Funding
The internal _setReward function funds a reward increase by pulling the difference from msg.sender, but it returns a reward decrease to the original requester. In the base contract these are the same account. However, through requestManagerSetReward the caller is a request manager, which is distinct from the requester.
As a result, a sequence of an increase followed by a decrease is not value-neutral for a request manager. The manager funds the increase from its own balance, but the subsequent decrease is refunded to the requester rather than to the manager. A manager that raises a reward and later lowers it therefore transfers the difference to the requester and cannot recover an accidental over-increase. The behavior is intentional, since refunding decreases to the requester avoids pulling against the requester's token allowance, and no theft is possible because the manager cannot move the requester's funds. It nonetheless remains a footgun for the privileged caller.
Consider documenting at the interface that a request manager cannot recover an over-funded reward, so that operators of that role understand that increases they fund are recoverable only by the requester.
Update: Resolved in pull request #62 at commit c3cb247. The team stated:
We are taking the documentation path from the recommendation. The linked PR updates the
requestManagerSetRewardinterface NatSpec to make the asymmetric counterparties explicit:
Reward increases are funded by the request manager calling the function.
Reward decreases are returned only to the original requester.
Consequently, lowering the reward cannot recover an amount previously over-funded by the request manager.
The existing reward-accounting behavior is intentionally preserved. This is a documentation-only change with no behavior, ABI, event/error signature, storage, or bytecode impact.
Avoid Hardcoded Assembly If Possible
The new setRequestReward function of the OOReporter contract reads the current reward from the Optimistic Oracle to compute the funding delta. To avoid the bytecode cost of decoding the full Request struct, since the contract sits close to the EIP-170 limit, it performs a raw staticcall to getRequest and extracts a single word with inline assembly. It reads Request.reward from a hardcoded offset of 0x1c0 (word 14) and only checks that the returned data is at least 0x1e0 bytes long.
This decode is correct for the current struct, because Request is fully static and reward is the fifteenth word. However, the offset is tightly coupled to the exact layout of a struct defined in a separate contract. If a future version of the Optimistic Oracle reorders the fields, or creates an entirely new storage with a new defined struct such as a reordering that moves reward outright. In these cases, assembly reads a different, wrong value as oldReward without reverting. A wrong oldReward produces a wrong funding delta, which either over-pulls tokens from the reporter or refunds an incorrect amount. Because the trigger is a change to a different contract, a reviewer of that change is unlikely to connect it back to this decode.
The preferred remediation is to remove the assembly entirely, rather than harden it. Consider adding a dedicated getRequestReward view to the base OptimisticOracleV2 that returns _getRequest(...).reward as a uint256 (mirroring the nonReentrantView guard of getRequest), declaring it on the reporter's IOptimisticOracleV2 interface, and reading it with a normal typed call. This lets the compiler decode a single uint256 safely and eliminates the hardcoded offset, the ABI-layout coupling, and the mock-versus-production parity gap in one change. Measurements at the current configuration show this is comfortably within the size budget: the getter adds 56 bytes to ManagedOptimisticOracleV2 (24,319 to 24,375 bytes, leaving 201 bytes of margin), while removing the assembly reduces OOReporter by 27 bytes (24,490 to 24,463 bytes), increasing its margin from 86 to 113 bytes. The change therefore moves cost onto the contract with more headroom while relieving the more constrained one.
If the assembly is retained despite the above, a test that decodes the full Request through the standard ABI and asserts that its reward equals the value extracted by the assembly would also catch a reordering that preserves the size. Thorough unit testing of this path is essential.
Update: Resolved in pull request #63 at commit a503da1. The team stated:
Fixed in the linked PR by following the recommended scalar-getter approach.
Added
getRequestReward(...)toOptimisticOracleV2, returning_getRequest(...).rewardwith the samenonReentrantViewprotection used by the full-request getter.
OOReporter.setRequestRewardnow uses a typed scalar call, removing the rawstaticcall, return-data length check, and hardcoded0x1c0assembly offset.
Notes & Additional Information
Avoid Unbounded Allowance To The Oracle To Limit Exposure
When opening or re-opening a price request, the _requestPrice function of the OOReporter contract funds the reward from its own balance. Before calling requestPrice, it checks the current allowance to the oracle and, when that allowance is below the reward, calls forceApprove with type(uint256).max. As a result, the reporter grants the oracle an unlimited allowance over its reward token rather than an amount scoped to the request being created.
The Optimistic Oracle is a trusted component, so this approval is acceptable under the current trust model. However, an unlimited allowance persists across all subsequent requests and grants the oracle standing authority to transfer the entire reward-token balance of the reporter at any time. Should the oracle ever be compromised, for example through a leaked key or a malicious implementation, the unlimited allowance would let it drain the reporter's full balance in a single transfer, whereas a request-sized allowance would cap the immediate exposure to the amount actually committed.
Consider approving only the reward amount for each request, so that the allowance is consumed by the corresponding requestPrice call and no residual allowance remains. This is a defense-in-depth measure that reduces the reporter's exposure in the event that the oracle is compromised, at the cost of one approval per request. If the unlimited approval is retained deliberately for gas reasons, consider documenting that decision alongside the trust assumptions placed on the oracle.
Update: Resolved in pull request #55 at commit a1e780d. The team stated:
We are taking the documentation path from the recommendation: the unlimited approval is retained for gas savings, and the decision plus the oracle trust assumptions are now documented in code and integration docs (linked PR).
Rationale for retaining the unbounded approval:
The Managed OO address is fixed at
initialize(...)and the reporter has no oracle setter, so the allowance is only ever granted to the UMA-governed Managed OO proxy — infrastructure in the same trust domain as the UMA-owned reporter itself.Managed OO pulls only each request's committed reward via
requestPrice; the standing approval avoids an extra approval call on every subsequent request and re-request.Exposure is capped by the reporter's reward-currency balance. Operationally the reporter is funded with a short-term working reward float rather than a treasury balance, which bounds the worst case of an oracle compromise to that float.
Constants Not Using UPPER_CASE Format
Throughout the codebase there are constants not declared using UPPER_CASE format.
-
The
OOReporterStorageLocationconstant declared in line 60 ofOOReporter.sol. -
The
ancillaryBytesLimitconstant declared in line 130 ofOptimisticOracleV2Interface.sol.
According to the Solidity Style Guide, constants should be named with all capital letters with underscores separating words. For better readability, consider following this convention.
Update: Resolved in pull request #59. The team stated:
Fixed in the linked PR by renaming both nonconforming constants to
UPPER_CASEand updating every code reference while preserving their values.
OOReporterStorageLocationis nowOO_REPORTER_STORAGE_LOCATION; the ERC-7201 storage-slot value is unchanged, with no behavior, storage-layout, or ABI impact.
ancillaryBytesLimitis now represented byANCILLARY_BYTES_LIMIT;OO_ANCILLARY_DATA_LIMITuses the renamed symbol, so the base limit remains 8192 bytes and the derived limit remains 8139 bytes.The new uppercase getter is additive. A deprecated
ancillaryBytesLimit()compatibility getter preserves the previously published selector and still returns 8192.
Single-Step Ownership Transfer Can Lead to Accidental Loss Of Admin Control
The OOReporter contract utilizes a single-step ownership transfer mechanism for transferring ownership. This implementation immediately assigns the owner role to the newOwner address without requiring acceptance from the new account. If the current admin accidentally provides an incorrect address (for example, due to a typo or copy-paste error), ownership of the contract will be permanently lost.
Consider implementing a two-step ownership transfer pattern so that the new owner must explicitly accept the role before the transfer is finalized.
Update: Resolved in pull request #60. The team stated:
Fixed in the linked PR by replacing
OwnableUpgradeablewithOwnable2StepUpgradeableforOOReporter.
transferOwnershipnow nominates apendingOwner; the current owner retains control of configuration, funds, and upgrades until the nominee callsacceptOwnership().An incorrect pending nominee can be replaced before acceptance, preventing a typo or pasted wrong address from immediately losing admin control.
The existing owner slot and
OOReporterstorage remain unchanged. OpenZeppelin stores the pending owner in a separate ERC-7201 namespace.The existing
transferOwnership(address)selector is preserved, whilependingOwner()andacceptOwnership()are added for the two-step flow.
Superseded Disputed Rounds Can Strand Proposer and Disputer Bonds
When a dispute or a P4 verdict triggers a re-request, the reporter advances to a new request timestamp and thereafter ignores the superseded oracle request's callbacks as stale. That superseded request nonetheless still holds the proposer and disputer bonds, which can be recovered only through settle, a function restricted to the RESOLVER_ROLE with no permissionless fallback. An honest disputer who correctly challenged a bad proposal therefore depends entirely on the resolver eventually settling a request that the reporter has already abandoned.
Consider providing a permissionless settlement path after an extended grace period following DVM resolution, or maintaining an authoritative index of superseded but unsettled requests so that resolver operations can clear them systematically.
Update: Acknowledged, not resolved. The team stated:
Acknowledged. Superseding a disputed request changes only the round currently authoritative for
OOReporter; it does not remove the previous Managed OO request. That request remains addressable by its exact(requester, identifier, timestamp, ancillaryData)tuple and can be settled by the resolver after DVM resolution.Managed OO transfers the winner's payout, or records a deferred payout if the transfer fails, before invoking
priceSettled. Consequently,OOReporterintentionally ignoring the stale callback cannot interfere with proposer or disputer bond recovery.Resolver-only settlement is an intentional property of
ManagedOptimisticOracleV2, and a permissionless fallback would change that trust model. Oracle events preserve the exact tuple required by the existing resolver workflow. We acknowledge resolver liveness as an operational dependency, but no contract change is planned. P4-triggered re-requests do not create this condition because settlement and payout processing occur before the callback.
De-Whitelisting a Price Identifier Mid-Request Can Render It Undisputable
Price identifier support is checked when a request is created, but it is not rechecked when a proposal is made or when a request is disputed. If a whitelisted identifier is de-whitelisted after a request has been opened, escalating a dispute reverts, because the dispute path forwards the request to the Data Verification Mechanism, which rejects the unsupported identifier. The request can then reach the Expired state with no dispute possible.
In ManagedOptimisticOracleV2 this is materially mitigated, because settlement is permissioned to the resolver through settleAndGetPrice, and Expired is treated as Proposed, so an undisputable request does not auto-finalize. The trusted resolver is the backstop, and on the Polymarket side the aggregator's administrator or arbitrator resolveResult override can still resolve the affected market. De-whitelisting an identifier requires a bonded governance proposal and Data Verification Mechanism approval, so the scenario is rare.
Consider documenting that identifier support is enforced only at request creation, and that the permissioned resolver, together with the Polymarket resolveResult path, is the intended backstop for any request rendered undisputable by a later de-whitelisting. Revalidating identifier support at proposal or settlement time may also be considered.
Update: Resolved in pull request #61. The team stated:
We are taking the documentation path from the recommendation. The linked PR adds a focused inline comment at the Managed OO request site documenting that identifier support is checked only when the request is created, and that resolver-gated settlement plus the Polymarket administrator/arbitrator
resolveResultoverride are the intended operational backstops if governance removes the identifier later.The existing behavior is retained: revalidating at settlement would remove the resolver backstop. This is a documentation-only change with no behavior, ABI, event/error signature, or storage impact.
Reward Cache in setRequestReward Is Updated After External Oracle Call
The setRequestReward function of the OOReporter contract writes its cached request.reward only after calling oracle.setReward, and the reporter applies no reentrancy guard of its own. A reward increase is pulled by oracle.setReward, whose token transfer could re-enter the reporter if the reward currency executed a transfer callback, so the effect on reporter state occurs after the external interaction rather than before it. This deviates from the checks-effects-interactions pattern.
In practice the flow is safe with the intended non-hookable reward currency, because any nested re-entry reaches the Managed Optimistic Oracle while its reentrancy lock is set and reverts. However, this relies on the external oracle's lock and on the currency being non-callback.
Consider writing request.reward = newReward before the external oracle.setReward call, so the reporter's own state follows checks-effects-interactions independently of the oracle guard and the token behavior.
Update: Resolved in pull request #64 at commit 4cec3d1. The team stated:
Fixed in the linked PR by updating the reporter's cached reward before calling the external oracle, following checks-effects-interactions.
A callback during
oracle.setRewardnow observes the new cached reward rather than stale reporter state.Managed OO remains the authoritative source for the old reward, preserving the existing increase/refund delta calculation.
If the oracle rejects the update, transaction atomicity restores the previous cached value.
The reward-flow documentation now explains the call ordering and rollback behavior.
Focused regression coverage makes the mock oracle inspect the reporter cache during the external call; the test fails before the fix and passes afterward.
Conclusion
The OOReporter package creates a layer between a prediction market and UMA's Managed Optimistic Oracle, replacing the previous Conditional Token Framework adapter and giving UMA control over how oracle requests are created, parameterized, re-requested, and read back. OpenZeppelin audited the new package together with the accompanying changes to ManagedOptimisticOracleV2.
The reviewed code follows a permissioned design in which a small set of trusted operators, namely the reporter owner, the oracle initializer, the requester, and the resolver, the govern the request lifecycle. The security of the system depends heavily on these operators being configured and operated correctly, and request data is managed upstream by the integrator such as Polymarket-v2.
The issues identified during the audit were limited in severity and oriented toward hardening rather than direct loss of funds. They center on validating request parameters more strictly, tightening the recovery paths available when a request cannot be disputed or settles to an unusable value, and reducing standing authority and configuration footguns.
OpenZeppelin thanks the UMA team for their cooperation and responsiveness throughout the engagement.
Appendix
Issue Classification
OpenZeppelin classifies smart contract vulnerabilities on a 5-level scale:
- Critical
- High
- Medium
- Low
- Note/Information
Critical Severity
This classification is applied when the issue’s impact is catastrophic, threatening extensive damage to the client's reputation and/or causing severe financial loss to the client or users. The likelihood of exploitation can be high, warranting a swift response. Critical issues typically involve significant risks such as the permanent loss or locking of a large volume of users' sensitive assets or the failure of core system functionalities without viable mitigations. These issues demand immediate attention due to their potential to compromise system integrity or user trust significantly.
High Severity
These issues are characterized by the potential to substantially impact the client’s reputation and/or result in considerable financial losses. The likelihood of exploitation is significant, warranting a swift response. Such issues might include temporary loss or locking of a significant number of users' sensitive assets or disruptions to critical system functionalities, albeit with potential, yet limited, mitigations available. The emphasis is on the significant but not always catastrophic effects on system operation or asset security, necessitating prompt and effective remediation.
Medium Severity
Issues classified as being of medium severity can lead to a noticeable negative impact on the client's reputation and/or moderate financial losses. Such issues, if left unattended, have a moderate likelihood of being exploited or may cause unwanted side effects in the system. These issues are typically confined to a smaller subset of users' sensitive assets or might involve deviations from the specified system design that, while not directly financial in nature, compromise system integrity or user experience. The focus here is on issues that pose a real but contained risk, warranting timely attention to prevent escalation.
Low Severity
Low-severity issues are those that have a low impact on the client's operations and/or reputation. These issues may represent minor risks or inefficiencies to the client's specific business model. They are identified as areas for improvement that, while not urgent, could enhance the security and quality of the codebase if addressed.
Notes & Additional Information Severity
This category is reserved for issues that, despite having a minimal impact, are still important to resolve. Addressing these issues contributes to the overall security posture and code quality improvement but does not require immediate action. It reflects a commitment to maintaining high standards and continuous improvement, even in areas that do not pose immediate risks.
Looking for a security partner?