Game guide

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:
        uint256 indexed reportId,
        address indexed token1Address,
        address indexed token2Address,
        uint256 feePercentage,
        uint256 multiplier,
        uint256 exactToken1Report,
        uint256 ethFee,
        address creator,
        uint256 settlementTime,
        uint256 escalationHalt,
        uint256 disputeDelay,
        uint256 protocolFee,
        uint256 settlerReward,
        bool timeType,
        address callbackContract,
        bytes4 callbackSelector,
        bool trackDisputes,
        uint256 callbackGasLimit,
        bool keepFee,
        bytes32 stateHash,
        uint256 blockTimestamp

    ReportDisputed and InitialReportSubmitted:
        uint256 indexed reportId,
        address reporter,
        uint256 amount1,
        uint256 amount2,
        address indexed token1Address,
        address indexed token2Address,
        uint256 swapFee,
        uint256 protocolFee,
        uint256 settlementTime,
        uint256 disputeDelay,
        uint256 escalationHalt,
        bool timeType,
        address callbackContract,
        bytes4 callbackSelector,
        bool trackDisputes,
        uint256 callbackGasLimit,
        bytes32 stateHash,
        uint256 blockTimestamp

    ReportDisputed2 and InitialReportSubmitted2:
        uint256 reportId
        uint256 multiplier
        address protocolFeeRecipient

    ReportInstanceCreated2:
        uint256 reportId
        address protocolFeeRecipient            

When you see an oracle game created (event 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 must be less than the single-transaction gas limit of the given network because in the settle() function, we force the settler to provide enough gas for the callback to be fully attempted (use up to callbackGasLimit) or the entire settle reverts. If the callbackGasLimit is too high, the settle can never complete, locking funds potentially permanently or until the underlying blockchain gas limit per transaction or block increases enough to free the 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 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.

If using these overloads the disputer and reporter input should be an address YOU control, NOT the address of the previous reporter or you will lose your tokens. At the end of the round, tokens are pulled from msg.sender and returned at the end of the round to the specified reporter or disputer address.

Playing the game

Settler Reward Filter

The settlerReward 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 see ReportInstanceCreated, 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 initial 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. You must wait at most the settlementTime to get your money out. If timeType is True, settlementTime is in seconds, if false, blocks. submitInitialReport() costs around 250k gas per transaction. There is an optional submitInitialReport() function with an additional "reporter" overload to specify who gets the tokens back on dispute or settle. The italicized function specified above just sends them back to msg.sender. 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 you see ReportInstanceCreated and one of InitialReportSubmitted or ReportDisputed events for a given reportId, that reportId has an active report that is eligible to be disputed. Both initial report submitted and report disputed events are equivalent from the perspective of your dispute bot: they both represent a disputable reportId. A reportId is disputable if the timestamp or block number of the latest InitialReportSubmitted or ReportDisputed event is less than settlementTime old and disputeDelay has passed since the report. If timeType true, settlementTime and disputeDelay are in seconds, if false, blocks. Once settlementTime has passed, a reportId can no longer be disputed. The dispute's timing is checked against settlementTime as follows:

Note reportTimestamp is stored as blocks if timeType is False. To submit a dispute, you can call disputeAndSwap(reportId, tokenToSwap, newAmount1, newAmount2, amt2Expected, stateHash). To figure out "tokenToSwap", you look at the amount1 of token1Address and compare its value to the amount2 of token2Address. If amount1 is worth less than amount2, tokenToSwap is token1Address. If the reverse is true, tokenToSwap is token2Address. Choosing the right tokenToSwap allows you earn the absolute difference in value between the two standing limit orders in the swap portion of the dispute, less any swap or protocol fees paid. 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:

All amounts are in lowest level ERC-20 contract units. The "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 you get disputed, you receive two times the lower valued token amount plus swap fees. For example, if you dispute with newAmount1 and newAmount2 of $100 of WETH and $100 of USDC respectively, and WETH drops -1% after you dispute, 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. You must wait at most the settlementTime to get your money out. If timeType is True, settlementTime is in seconds, if false, blocks. 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. The exact way disputeDelay is checked when you dispute is as follows:

As part of the dispute, the token flows from you as the disputer are as follows:

disputeAndSwap() costs around 210k gas per transaction. There is an optional disputeAndSwap() function with an additional "disputer" overload to specify who gets the tokens back on next dispute or settle. The italicized function specified above just sends them back to msg.sender. 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):

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.

Settling

Once settlementTime 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). The specific rules for settle timing are as follows:

Note that disputes are allowed in the first settle eligibility block or timestamp, and may kick out your settle depending on the inclusion tip (prio fee). The gas cost to settle is at most ~120k + 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 contractarrow-up-right where you pass in the following oracleParams:

to:

or:

These functions check the passed parameters against what is actually on-chain before the initial report or dispute is allowed to fire. If the parameters don't match, the transaction reverts. This way, if an RPC lies to you, all you do is revert instead of torch your capital. Everything you need to construct oracleParams is in the events.

Expected value

Assuming the settler reward is high enough (discussed above), we can write the ~ 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 of tokenToSwap 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.64 * (standard deviation of settlementTime returns of the token1/token2 ratio) from our work in the "Other considerations" page. You can increase the 0.64 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 given reportId. You can claim by submitting the initial report through the bounty contract instead 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 bountStartAmt. 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. Let's walk through a simple example:

The bounty was created at timestamp 1000. Assume blocks are 2 seconds each. The claimable bounty for reportId is as follows, for each timestamp: 1000: not claimable 1002: 1 ETH 1004: 1.21 ETH (1 * 1.1^(1004-1002) = 1.1^2) 1006: 1.4541 ETH (1.21 * 1.1^(1006 - 1004) = 1.21 * 1.1^2) 1008: 1.759461 ETH 1010: 2.12894781 ETH 1012: 2.5760268501 ETH 1014: 2.5760268501 ETH 1016: 2.5760268501 ETH The bounty stops escalating once 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:

This works the same as the initial report function in the oracle contract. There are additional overloads for game validation just like in the "Oracle game validation" section above to protect against adversarial RPCs and delayed transaction inclusion. Sometimes, a bounty for a reportId can be retargeted:

This is the same as the BountyCreated event except the parameters now apply to newReportId and the bounty for oldReportId is no longer claimable.

Last updated