Skip to main content
  • Bots listen for oracle-trigger conditions (e.g. asset price ≥ target).
  • Upon trigger they call MarketContract.resolve() passing Pyth price ID.
  • Contract sets outcome ⟶ users burn winning tokens for $1 each.
/**
     * @notice Push a Pyth update, evaluate the condition, and settle a market.
     * @param market      The prediction-market contract being settled.
     * @param updateData  Raw Pyth price-feed update packets.
     */
    function updatePriceAndFulfill(
        address market,
        bytes[] calldata updateData
    ) external onlyRelayer payable {
        require(market != address(0), "Oracle: zero market");
        require(updateData.length > 0,  "Oracle: empty update");
        /*── 1. Pay Pyth fee and publish the update ───────────────────*/
        uint256 fee = PYTH.getUpdateFee(updateData);
        require(msg.value >= fee, "Oracle: fee too low");
        /*── 2. Pull market parameters ────────────────────────────────*/
        IMarket.TriggerCondition memory tc = IMarket(market).getMarketParams();
        bytes32[] memory priceIds = new bytes32[](1);
        priceIds[0] = tc.assetId;
        /*── 3. Fetch and validate a fresh price ──────────────────────*/
        PythStructs.PriceFeed[] memory feeds =
            PYTH.parsePriceFeedUpdates{ value: fee }(
                updateData,
                priceIds,
                0,
                uint64(block.timestamp)
            );
        /*── 4. Use the single feed we requested ───────────────────────────────────*/
        PythStructs.PriceFeed memory feed = feeds[0];
        _validatePriceStruct(feed.price.price, feed.price.conf, feed.price.expo);
        int256 triggerPx = int256(uint256(tc.triggerPrice));
        int256 oraclePx  = int256(feed.price.price);
        uint256 winningToken;
        if (tc.op == IMarket.Operator.LT) {
            winningToken = (oraclePx <= triggerPx) ? 1 : 2;
        } else { // GT
            winningToken = (oraclePx >= triggerPx) ? 1 : 2;
        }
        /*── 5. Resolve the market ───────────────────────────────────*/
        IMarket(market).resolveMarketOracle(winningToken);
        /*── 6. Refund any excess ETH ────────────────────────────────*/
        uint256 refund = msg.value - fee;
        if (refund != 0) payable(msg.sender).transfer(refund);
    }

Typical resolution timeline

  1. A market’s trigger condition is met and a bot submits the relevant Pyth update.
  2. The contract fetches a fresh price, validates it, and determines the winning side.
  3. The market resolves on-chain and claims open immediately for the winning token.
Most markets finalize within seconds to a few minutes after the oracle reports the triggering price.

Oracle failure contingency

  • If Pyth is unavailable or returns stale data, updatePriceAndFulfill reverts.
  • Bots continually retry the call until a valid price is published.
  • Until then, the market remains unresolved and funds stay locked.
The retry and escalation flow is:

SEDA Resolution (Oracle Program)

SEDA resolution uses a Data Request (DR) executed by the SEDA network. The Oracle Program in this repo fetches fixture results, outputs an ABI-encoded payload, and the EVM contract verifies a signed result before resolving the market.

What the Oracle Program returns

  • Primary path (resolve_outcome): ABI (uint8 winnerToken, string resultInfo)
  • winnerToken is 1 (yes/win/draw) or 2 (no/lose/not draw)
  • resultInfo is a human-readable string from the fixture API

Execution + Tally

  • Execution phase fetches SportMonks fixture data and computes the winning token.
  • Tally phase collects reveals and requires unanimous agreement. If any mismatch exists, the tally returns an error and the DR is not finalized.

Execution inputs and outputs

Exec inputs (JSON)
  • operation (optional, default resolve_outcome)
  • fixture_id (required)
  • predicted_participant_id (required if draw = false)
  • draw (bool)
  • market_id (required if operation = cancel_market)
Outputs
  • resolve_outcome -> (uint8 winnerToken, string resultInfo)
  • cancel_market -> uint256 yesMarketPrice (6 decimals)

End-to-end flow

On-chain verification (fulfillAndResolve)

Failure and retry behavior

  • If the tally phase detects mismatched reveals, it returns an error and no consensus is produced.
  • If result.consensus is false or exitCode != 0, fulfillAndResolve reverts.
  • Relayers can re-submit the DR after the underlying data source stabilizes.
Info: Resolution is deterministic. The EVM contract only accepts results that pass SEDA consensus and FAST signature verification.
/**
     * @notice Verify the FAST signature, decode the winning token, and resolve the market.
     * @dev Expects the Oracle Program payload to be: (uint8 winnerToken, string resultInfo).
     */
    function fulfillAndResolve(
        address market,
        SedaDataTypes.Result calldata result,
        bytes calldata signature
    ) external onlyRelayer {
        if (market == address(0)) revert InvalidMarket();
        if (!result.consensus) revert NoConsensusYet();
        if (result.exitCode != 0) revert OracleExecutionError(result.exitCode);
        bytes memory rawResult = fastVerifier.verifyAndGetResult(result, signature);
        (uint8 parsedToken, string memory info) = abi.decode(rawResult, (uint8, string));
        if (parsedToken != 1 && parsedToken != 2) {
            revert InvalidWinningToken(parsedToken);
        }
        IMarket(market).resolveMarketOracle(parsedToken);
        lastWinningToken = parsedToken;
        lastResultInfo = info;
        lastUpdated = uint64(block.timestamp);
        lastResolvedRequestId = result.drId;
        emit OutcomeResolved(result.drId, market, parsedToken, info);
    }