Source: src/periphery/ConditionalMarketOracle.sol
Per-proposal TWAP oracle used by market settlement.
- Maintains a cumulative price value in Uniswap-v2-style Q112.112 form.
- Anchors each proposal's scoring window at
tradingStartwith a zero cumulative baseline. - Calculates TWAP from
tradingStartto the current block timestamp (frozen attradingEnd).
The three-function interface is the upgrade seam: the Hub owner can point the core at a
replacement oracle implementing the same interface (see docs/UPGRADES.md) without changing
UmiaMarketCore. winningThresholdBps is accepted (and validated) at initialize so a future
implementation can calibrate its clamp to the same threshold settlement uses; this implementation's
fixed per-update band does not consume it.
initialize(proposalId, reserve0, reserve1, tradingStart, tradingEnd, winningThresholdBps)— one call at market creation: validates inputs, records the seed observation, anchorslastTimestampattradingStart. Reverts withAlreadyInitializedif repeated.update(proposalId, reserve0, reserve1)— called before every reserve mutation, so the elapsed interval is credited at the price that actually held over it. No-op beforetradingStart, within the same second, after thetradingEndfreeze, or for degenerate (zero) reserves — a degenerate interval is credited by the next well-formed update instead.calculateTWAP(proposalId, reserve0, reserve1)— view; extrapolates the current interval at the clamped price of the passed reserves.
oracleStates[proposalId]:price0CumulativeLast— ∫ observation dt, scored fromtradingStartlastPrice0X112— last accepted observationtradingStart,tradingEnd— scoring windowlastTimestamp— last recorded time; anchored attradingStarton initinitialized
initializeandupdateareonlyMarketCore.
Per-update price changes are clamped to a maximum ratio of 2.5x (and minimum of 0.4x) relative to the last recorded observation. This bounds the impact of single-block price manipulation on the TWAP accumulator.
maxPrice = lastPrice * 5 / 2 (2.5x ceiling)
minPrice = (lastPrice * 2 + 4) / 5 (0.4x floor, rounded up so it is never zero)
The clamped price (not the raw price) is accumulated into the cumulative sum, so even if an attacker moves the CPMM spot price by 100x in one block, the oracle only records a 2.5x move. Over multiple updates the clamp compounds, so sustained price changes still converge — only single-update spikes are bounded. Every accepted observation is saturated to MAX_PRICE_X112 = 2^208, so an extreme reserve ratio cannot store an observation that overflows a later update.
Both update() and calculateTWAP() cap their effective timestamp at min(block.timestamp, tradingEnd) via _effectiveTimestamp().
This means:
- After
tradingEnd, no further price accumulation occurs regardless of whenupdate()is called. calculateTWAP()returns the same value whether called attradingEndor hours later.- Settlement delay cannot dilute or extend the TWAP window beyond the trading period.
- Last-block manipulation at
tradingEnd - 1gets at most one block of influence rather than being extended by the settlement gap.
calculateTWAPreverts withProposalNotInitializedfor unknown proposals andTradingNotStartedbefore the scoring window opens.- Queried in the first second of the window,
calculateTWAPreturns the anchored observation rather than the raw spot of the passed reserves (which would be same-block manipulable).