Core events
There are three main event structures defining the limit order game. The first,ReportInstanceCreated, is when a new game is created. The second structure, which is equivalent for ReportDisputed and InitialReportSubmitted, tells you how much money is in the limit orders, which instance (reportId) the limit orders correspond to, and the rules of that specific game. The third structure, ReportInstanceCreated2, ReportDisputed2 and InitialReportSubmitted2, contains extra game variables that couldn’t fit in the larger events.
All of this data is available in on-chain storage as well, but it is generally faster to read from the events.
ReportInstanceCreated), it’s important to have a few sanity check filters.
Critical filters (if these are not followed your funds may be lost forever)
callbackGasLimit below the chain’s per-tx gas limit. settle() reverts if the callback cannot be fully attempted, so an oversized value can make settlement impossible and lock funds.
settlementTime is more user preference: when you submit a report, your funds are locked in the limit orders for up to this amount of time before the settle() function can free them. timeType true means this time is measured in seconds, timeType false means this time is measured in blocks.
Add normal ERC-20 token addresses like WETH and USDC (no rebasing tokens or anything like that) to your whitelist and check the reportId’s token1Address and token2Address against these.
If the reportId doesn’t match these filters, ignore it.
Overloads in the oracle contract
There are some overload functions in the oracle contract with optional reporter and disputer inputs, mainly for external contracts.msg.sender and returned at the end of the round to the specified reporter or disputer address.
Both submitInitialReport() and disputeAndSwap() have overloads that let you set the beneficiary (reporter/disputer). The base forms return funds to msg.sender.
Playing the game
Settler Reward Filter
ThesettlerReward event item is quoted in wei and should be greater than 120k + callbackGasLimit worth of gas at prevailing gas prices in order for you to participate in a given reportId. If the settlerReward is not high enough, ignore the reportId. This way you don’t have to worry about collecting your report amounts yourself, the network will take care of it for you at settlement.
Initial reporting
If you only seeReportInstanceCreated, and don’t see any InitialReportSubmitted event, the reportId is eligible for an initial report. There can only be one successful initial report per reportId.
To submit an initial report, you can call submitInitialReport(reportId, amount1, amount2, stateHash).
Looking at the ReportInstanceCreated event, you earn ethFee minus settlerReward as an initial reporter reward. If keepFee is true, you keep the initial reporter reward if later disputed. If keepFee is false, you don’t receive the initial reporter reward if later disputed. Filtering keepFee to true is reasonable.
amount1 in the function input must equal exactToken1Report. As part of the initial report, you send in exactToken1Report of token1Address.
amount2 in the function input is the amount of token2Address that equals exactToken1Report in value. As part of the initial report, you send in amount2 of token2Address.
stateHash in the function input should equal stateHash from the ReportInstanceCreated event you saw and validated.
If you get disputed, you receive two times the lower valued token amount plus swap fees. For example, if you initially reported $100 of WETH and $100 of USDC, and WETH drops -1%, there is now $99 WETH and $100 USDC in the report. If someone comes and disputes, you will receive 2 * $99 + swap fees in WETH. If swap fees are 1%, you receive 1% * $99 in additional swap fees from the disputer, in WETH.
The swap fees you get paid are controlled by the feePercentage event item. Here, 1% = 100000 in contract units. 0.01% = 1000, etc. A higher swap fee protects you from market moves.
Funds unlock on settle if undisputed for settlementTime (timeType true: seconds, false: blocks).
submitInitialReport() costs around 250k gas per transaction.
Initial reporters should be aware they must be able to receive ETH, or the initial reporter reward will be burned.
Keep in mind anyone can dispute you before the end of the settlementTime, and you will lose the absolute difference in value between your reported token amounts at that time, less the swap fee.
Disputing
If areportId has either InitialReportSubmitted or ReportDisputed, treat it as active and disputable. For dispute bots, those two events are equivalent signals.
A reportId is disputable if settlementTime has not yet passed and disputeDelay has elapsed since the last report. Disputes are allowed in the last eligible block or timestamp. Contract check:
reportTimestamp is stored as blocks if timeType is false.
To submit a dispute, you can call disputeAndSwap(reportId, tokenToSwap, newAmount1, newAmount2, amt2Expected, stateHash).
Choose tokenToSwap as the side with lower USD value (amount1 vs amount2). That choice captures the current mispricing spread, minus swap/protocol fees.
The newAmount1 input depends on amount1 and escalationHalt from the InitialReportSubmitted or ReportDisputed event and multiplier from the ReportInstanceCreated event (or ReportDisputed2 / InitialReportSubmitted2). Multiplier in the contract is coded so 140 = 1.4x (pseudocode):
newAmount2 input is the amount of token2Address that equals newAmount1 in value.
amt2Expected is simply the amount2 in the InitialReportSubmitted or ReportDisputed event you are reacting to. This helps protect disputers against chain reorganization attacks.
stateHash in the function input should equal stateHash from the ReportInstanceCreated event you saw and validated.
When you dispute, you pay swapFee + protocolFee times the amount of the token corresponding to your choice of tokenToSwap. For example, if there is $99 WETH (token1) and $100 USDC (token2) in a report you are disputing, you would choose tokenToSwap as token1Address, and swap and protocol fees paid would be multiplied by amount1 ($99 weth).
For both swap and protocol fees, a 1% fee would be 100000 in contract units. 0.01% = 1000, etc. A higher swap fee protects you from market moves. Protocol fees (protocolFee) can be considered burned, while swap fees (swapFee) are paid to the reporter you are disputing. swapFee in the initial report and dispute events is equivalent to feePercentage in ReportInstanceCreated.
Your dispute can itself be disputed by someone else. If your dispute is later disputed, you receive 2 * lower-valued side + swap fee, denominated in tokenToSwap. Example: from $99/$100, payout is 2 * $99 + fee in WETH.
The disputeDelay parameter controls how long you must wait from the last initial report or dispute before disputes are allowed. If timeType is true, disputeDelay is in seconds, if false, blocks. Contract check:
disputeAndSwap() costs around 210k gas per transaction.
Finally, the new implied price by your dispute (ratio of newAmount1 to newAmount2) must be outside the fee barrier around the previous report’s price, which includes both swap fees (feePercentage) and protocol fees (protocolFee):
settlementTime, and you will lose the absolute difference in value between your reported token amounts at that time, less the swap fee.
Settling
OncesettlementTime has passed since the last InitialReportSubmitted or ReportDisputed event, a reportId can be settled. The settler earns the settlerReward in wei.
To settle a reportId, you can call settle(reportId). Calling settle on an already-settled report returns the stored values rather than reverting.
Contract check:
callbackGasLimit, provided token1 and token2 are normal ERC-20 tokens. More complex ERC-20s may cost more.
Settlers should be aware malicious ERC-20 tokens in the reportId can cause their settle to revert and waste gas. Generally, there is no incentive other than griefing to play oracle games with malicious ERC-20 tokens, but it is a good practice to filter oracle game tokens for settles.
Settlers should also be aware they must be able to receive ETH when calling settle, or the settlerReward will be burned.
Oracle game validation
Some may want to run oracle game bots using RPC and websocket data without running a full node. One issue here is if the data provider lies to you, they can trick you into a very bad trade in the oracle game. We have function entries in the batcher contract where you pass in the followingoracleParams:
Expected value
Assuming the settler reward is high enough (discussed above), we can write the approximate mean worst-case expected value for an initial reporter as: EV = initial_reporter_reward_usd - exactToken1ReportToUsd * volatility_loss - gas_fees_usd This ignores swap fees received when disputed, which strictly help you as the initial reporter. For a disputer, assuming tokenToSwap is chosen correctly: EV = abs(currentAmount1ToUsd - currentAmount2ToUsd) - swap_fee_usd - protocol_fee_usd - newAmount1ToUsd * volatility_loss - gas_fees_usd 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 amount oftokenToSwap in the current report (before you dispute), which should be the amount with a smaller USD value.
When EV > 0, an initial report or dispute is in theory profitable.
Volatility loss
An appropriate volatility_loss term appears to be something like 0.65 * (standard deviation ofsettlementTime returns of the token1/token2 ratio) from Oracle Accuracy & Cost. You can increase the 0.65 coefficient a bit to give yourself more of a buffer if desired but it may reduce competitiveness. This appears surprisingly robust across a number of distributions we tested, but real-world price action may differ.
In some distributions, variance and standard deviation are not finite. It may behoove you to use something like the realized median absolute deviation divided by ~0.6745 instead of standard deviation.
In general, estimating the scale of the returns has a high skill gap. With volatility estimation (and openOracle broadly), we are playing a dangerous game. If volatility spikes more quickly than your sampling adjusts, you can lose money. The best bots will have the best volatility predictions. If nobody is predicting volatility on that level, then the risk to a more naive bot is much lower.
Initial reporter bounty contract
An additional initial reporter reward may be available for a givenreportId. You can claim by submitting the initial report through the bounty contract instead of the oracle contract.
The bounty contract event is as follows:
reportId is the oracle game unique identifier the bounty applies to. totalAmtDeposited is the maximum bounty paid to the initial reporter. bountyStartAmt is the starting bounty amount.
After maxRounds, the bounty stops increasing. Each round is roundLength of time, where time is determined by timeType. If timeType is true, time is in seconds, and if false, blocks. Each round, the bounty increases by bountyMultiplier. For example, a bountyMultiplier of 15000 corresponds to an increase of 1.5x each round.
startTime is when the bounty starts escalating, starting from bountyStartAmt. Before this time, you cannot claim the bounty. Time here depends on timeType: if true, startTime is a timestamp, if false, a block height.
blockTimestamp is simply the block timestamp of the event and doesn’t impact the bounty math.
bountyToken is the address of the token the bounty is paid in. If bountyToken is address(0), this means the bounty is paid in ETH.
Contract logic:
reportId is as follows, for each timestamp:
maxRounds of time has passed, which is 10 seconds in this case, given timeType true, maxRounds of 10, and roundLength 1.
If roundLength were 2 here, the claimable bounty at time 1004 would be 1.1 ETH, at time 1006, 1.21 ETH, and so on. This bounty can be added to the initial reporter reward in the oracle contract to get your initial_reporter_reward_usd in the Expected Value section equation.
You can claim the bounty by calling the below function directly in the bounty contract:
reportId can be retargeted:
BountyCreated event except the parameters now apply to newReportId and the bounty for oldReportId is no longer claimable.