Skip to main content

Core idea

An openOracle report is two limit orders at one implied price. If nobody takes either side before the timer expires, the price can settle. If someone takes one side, they must replace the report with a new two-sided report at a larger size, and the timer restarts. It is recommended to read Oracle Life Cycle before engaging with this page. The oracle contract stores a hash of the live oracle game instead of storing the whole game in normal public storage. Bots and indexers should keep the event data needed to reconstruct the current preimage.
The oracle is permissionless and the contract performs almost no parameter sanity checking on its own. Anyone can create a game with any token pair, any fee, any settlement time, any callback contract, any escalation halt, etc. It is on you, the participant, to validate every parameter of a game before you report on it or dispute it. Bad parameters can lock or drain your funds, make the game economically impossible to play, or otherwise put you in a position you didn’t intend. Treat every new game as untrusted input.

Oracle game

Each oracle game is keyed by a reportId. The contract stores oracleGame[reportId], a keccak256 hash of the OracleGame struct below together with binding context (creator, block number, block timestamp):
Each field operates as follows:

Events and packed layout

There are three main oracle game events:
ReportSubmitted is emitted when a new game starts. ReportDisputed is emitted when the live report is replaced. ReportSettled only tells you the report settled; final amounts come from the preimage supplied to settle() or from optional storage flags. The packed payloads are tightly packed bytes instead of normal ABI-encoded event fields. Operators need a decoder for the packed layout.
Decoded packed values populate the OracleGame struct above and a small PreimageHelper. Every PreimageHelper field except reportId lives in the packed payload; reportId comes from the indexed event topic. report(), dispute(), and settle() all take this pair as calldata.

Critical filters

If a game does not match your filters, ignore it.
Outside native ETH with address(0) as the token address, only normal ERC-20s are supported. USDC and USDT-style return behavior is supported. Fee-on-transfer, rebasing, hook, and other unusual token mechanics are not supported. Inspect callbackGasLimit before entering a game. Settlement attempts a fixed callback if callbackContract is set. If the gas limit is impossible to fully attempt, settlement reverts and the current reporter’s funds can remain locked. Every disputer inherits the callback parameters from the original report. Inspect settlementTime with the same care. It is an arbitrary uncapped value set by the game creator, and reporter or disputer funds can be locked for at least that long. If the lock time is too long relative to the value of reporting or disputing, ignore the game. These are critical safety filters, but they are not the only parameters with serious consequences. Swap fees, protocol fees, settler reward, multiplier, escalation halt, and dispute delay can all make reporting or disputing unprofitable if misconfigured.

Timing Boundaries

report() and dispute() both accept timing boundaries:
This lets a caller bind the transaction to the block number and timestamp range they expected when building the transaction. If the transaction is included in a block outside the allowed block number or timestamp bound, the call reverts. Timing validation is optional. If blockTimestamp is zero, the contract skips the entire check.

Playing the game

Reporting / Creating a game

Anyone can call report() to open a new oracle game with a chosen token pair and initial price:
report() sets the game parameters and submits the first live report. When calling report(), pass reportTimestamp, lastReportOppoTime, settlementTimestamp, and numReports as zero inside params. OracleGame parameters behave as described in the Oracle game section above. If you fund a report with currentReporter set to another address, the report funds are credited to that address when the report settles or is later disputed. Funding can come from external token transfers, native ETH, or internal balances. If tryInternalBalance1 or tryInternalBalance2 is true, the contract tries to use currentReporter’s internal balance before pulling the rest externally. If currentReporter != msg.sender and an internal-balance flag is true, that token must be fully covered by internal balance and allowance or the call reverts. Any ETH required for the report, including settlerReward, must be supplied through msg.value.

Token flows

To create a report, the game must be funded with:
If a token is native ETH, the required external amount is paid through msg.value. If a token is an ERC-20, any required external amount is pulled from msg.sender. Any excess ETH is credited to currentReporter. After reporting, you are exposed to being disputed before the timer expires. See Counter-dispute risk below for the loss profile.

Paying someone else to report

If an integrating application wants to pay someone to create a report, it can commit to the OracleGame parameters it wants and compensate whichever caller supplies a matching report.

Disputing

A reportId is disputable from reportTimestamp + disputeDelay up to (but not past) reportTimestamp + settlementTime, where reportTimestamp is from the most recent ReportSubmitted or ReportDisputed event for that reportId. Both events publish the current live preimage; for dispute bots they are equivalent signals. Anyone can call dispute() to replace the live report with a larger two-sided report at a new implied price. A disputer earns the absolute difference in USD value between the two sides of the prior report, less the swap fee, the protocol fee and gas. The disputer also has exposure to being counter-disputed themselves. To dispute, call:
params and helper must reconstruct the current stored hash for reportId. If they do not, the call reverts. By passing in the matching preimage, the disputer is agreeing to the oracle game encoded in that hash. See the Oracle game section above for the rules baked into each parameter. The swap side is derived from your price revision: if newAmount2 / newAmount1 is greater than the live currentAmount2 / currentAmount1, the swapped token is token2; otherwise it is token1. This makes the swapped token the lower-valued side of the live report at your new price. The previous reporter is paid out in the swapped token, which forces them into the cheap side and lets you capture the mispricing spread minus fees. Your new ratio newAmount1 / newAmount2 becomes the next reported price. newAmount1 is determined by the oracle rules:
newAmount2 is chosen by the disputer and sets the new price. Once the dispute lands, newAmount1 and newAmount2 become the next live report’s currentAmount1 and currentAmount2. Disputes are too late once the game reaches the settlement boundary:
Fees are charged on the old amount of the swapped token. The swap fee goes to the previous reporter. The protocol fee goes to protocolFeeRecipient if one is set. Both swap fee (feePercentage) and protocolFee are in 1e7 units, meaning 1000 corresponds to a fee of 0.01%. In a true self-dispute, where the previous reporter is also msg.sender and disputer, the previous position is netted into the new one instead of being paid out as a normal third-party dispute. The protocol fee is still charged. tryInternalBalance1 and tryInternalBalance2 work the same way as in report(). If true, the contract tries to fund that token from disputer’s internal balance. If disputer != msg.sender, the internal balance and allowance must fully cover that token’s required contribution or the call reverts. If false, any required external ERC-20 contribution is pulled from msg.sender, and any required ETH must come from msg.value.

Token flows

If the swapped token is token1:
If the swapped token is token2:
Any excess ETH is credited to disputer. The protocol fee recipient receives protocolFee of the swapped token if one is set. After disputing, your own report is exposed to being disputed in turn. See Counter-dispute risk below for the loss profile.

Settling

Once the latest report has survived for settlementTime, anyone can settle:
params and helper must match the current stored state hash. The contract then marks the game settled, credits the current reporter with both report amounts, credits the settler with settlerReward, optionally stores the final price or full final game, and emits ReportSettled(reportId). Game payouts are credited to internal balances. Users can later withdraw, or callers can use internal-balance helpers where appropriate. If callbackContract is set, settlement calls:
Callback failure itself does not undo settlement, but an impossible callback gas limit can still make settlement revert.

Counter-dispute risk

Any live report, whether submitted as an initial report or as a dispute, can itself be disputed before the timer expires. Your payout depends on which side the next dispute’s price implies is swapped. In the normal case, that is the lower-valued side. For example, if you reported 100 USD of WETH against 100 USD of USDC, and WETH then drops 1%, there is now roughly 99 USD of WETH and 100 USD of USDC in the report. A dispute at the corrected price takes the WETH side, and you receive 2 * 99 USD worth of WETH plus the swap fee on currentAmount1. If the swap fee is 1%, you additionally receive about 1% of 99 USD denominated in WETH. The protocol fee on currentAmount1 does not flow to you. Your realized loss versus the true price is the spread (1 USD) net of the swap fee credit.

Internal balances

Internal balances let repeat participants report or dispute without moving tokens externally on every call. Functions to manage internal balances:
Pulls amount of token from msg.sender and credits beneficiary’s internal balance. If depositing ETH, msg.value must match amount.
Same as deposit, but funded by from via a Permit2 witness signature. The signature is only valid when relayed by the intended caller, crediting the intended beneficiary, attributed to the intended from, and matching the intended intent hash. ETH is not supported through this path.
Moves internal balance from from to to. If from != msg.sender, the caller must hold sufficient internal allowance (see Delegation below).
Sends amount of token from the caller’s internal balance out as a real transfer, either to msg.sender or to the specified to. If amount exceeds the available balance, the contract sends what’s available instead of reverting.
Debits the caller’s internal balance and tries to push the tokens to to externally. If the external transfer fails (ETH call reverts, ERC-20 misbehaves, or runs out of gas within a 50k budget), the contract falls back to crediting to’s internal balance instead. Useful when you don’t want a recipient’s malfunction to brick your transaction. address(0) as token means native ETH throughout. Internal balances keep a one-unit sentinel in touched slots, so the displayed raw storage amount may include dust that is not withdrawable.

Delegation

approveInternal allows a spender to use the owner’s internal balance:
This approval only applies to balances already inside the oracle. It does not approve external ERC-20 transfers. A spender can use approved internal balance through internalTransferFrom, or through delegated report() / dispute() funding when the corresponding tryInternalBalance flag is true. If the flag is false, the contract expects msg.sender to fund that token externally. If the flag is true but the internal balance or internal allowance is insufficient, the call reverts. Only approve addresses you trust. A spender with internal allowance can move or spend your internal balance according to that allowance.

Expected value

For the initial reporter, compensation must come from the application or strategy that asked for the report. A simple initial report() caller EV model is:
For a disputer:
This ignores swap fees received when your dispute is itself disputed, which strictly help you as a disputer. swap_fee_usd and protocol_fee_usd are levied on the swapped token’s amount in the current report before you dispute, which is the smaller-valued side. volatility_loss captures the expected loss from being counter-disputed before the timer expires. When the token1/token2 ratio drifts during the settlement window, the next disputer can take the cheap side from you at the stale price, and the spread becomes your loss. The longer settlementTime is and the more the pair moves, the more loss you should price in. A fuller treatment of this term lives in Oracle Accuracy & Cost. An appropriate volatility_loss term is approximately:
You can increase the coefficient for more buffer, but that reduces competitiveness. In some distributions, variance and standard deviation are not finite. In those cases, realized median absolute deviation divided by about 0.6745 may be more stable than standard deviation. Estimating short-window volatility has a high skill cap. If volatility spikes faster than your model adjusts, you can lose money.