Solidity Integration

This guide shows how to integrate the OptimisticOracle into your Solidity smart contracts for fast, low-cost oracle queries with dispute mechanisms.

Contract Addresses (BSC Mainnet)

  • OptimisticOracle: 0xA83689161DFa9d5992fBa658d3148C6f72E1419E
  • FarmTruthToken: 0x948d7a6f18511df422184360d1cd5d56f5be4444
  • DecentralizedOracle (for disputes): 0xFA4595F636887CA28FCA3260486e44fdcc8c8A71

View on BSCScan:

Interface

Note: For the complete interface with all functions, events, and detailed documentation, see IOptimisticOracle Interface Reference.

The minimal interface below includes only the essential functions for basic integration:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/// @notice Minimal interface for OptimisticOracle on-chain integration
/// @dev For complete interface, see: /docs/interfaces/ioptimisticoracle.md
interface IOptimisticOracle {
    enum QueryState { Awaiting, Proposed, Disputed, Resolved }

    // Mutations
    function createQuery(
        string memory question,
        string[] memory options
    ) external returns (uint256);

    function proposeAnswer(
        uint256 queryId,
        uint8[] memory selectedOptions
    ) external;

    function dispute(
        uint256 queryId,
        bool disputeAsNotClear
    ) external;

    function resolveUndisputed(uint256 queryId) external;

    function resolveDisputed(uint256 queryId) external;

    // Reads
    function getAnswer(uint256 queryId) external view returns (uint8[] memory);

    function getQueryState(uint256 queryId) external view returns (QueryState);

    function getQueryDetails(uint256 queryId) external view returns (
        address creator,
        string memory question,
        string[] memory options,
        uint256 createdAt,
        QueryState state,
        address proposer,
        uint8[] memory proposedAnswer,
        uint256 proposalBlock,
        bool disputed,
        address challenger,
        uint256 mainOracleQueryId,
        bool resolved,
        uint8[] memory finalAnswer,
        bool isNotClearYet
    );

    function getDisputeWindowRemaining(uint256 queryId) external view returns (uint256);

    function canDispute(uint256 queryId) external view returns (bool);

    function getQueriesByState(
        QueryState state,
        uint256 offset,
        uint256 limit
    ) external view returns (uint256[] memory);

    // Constants
    function CREATION_COST() external view returns (uint256);
    function PROPOSAL_COLLATERAL() external view returns (uint256);
    function DISPUTE_COLLATERAL() external view returns (uint256);
    function DISPUTE_WINDOW() external view returns (uint256);
}

Constants

  • CREATION_COST: 30,000 tokens (18 decimals)
  • PROPOSAL_COLLATERAL: 1,000,000 tokens
  • DISPUTE_COLLATERAL: 1,000,000 tokens
  • DISPUTE_WINDOW: 7,200 blocks (~1.5 hours at 0.75s/block on BSC)

Creating Queries

Basic Query Creation

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

interface IOptimisticOracle {
    function createQuery(string memory, string[] memory) external returns (uint256);
}

contract OptimisticQueryCreator {
    IOptimisticOracle public immutable oracle;
    IERC20 public immutable token;

    uint256 constant CREATION_COST = 30_000 * 10**18;

    constructor(address oracle_, address token_) {
        oracle = IOptimisticOracle(oracle_);
        token = IERC20(token_);
    }

    /// @notice Create a simple yes/no query
    /// @dev Assumes contract holds tokens and has approved oracle
    function createYesNoQuery(string calldata question) external returns (uint256) {
        string[] memory options = new string[](2);
        options[0] = "Yes";
        options[1] = "No";

        return oracle.createQuery(question, options);
    }

    /// @notice Create a query with custom options
    function createCustomQuery(
        string calldata question,
        string[] calldata options
    ) external returns (uint256) {
        require(options.length >= 2 && options.length <= 254, "Invalid options count");
        return oracle.createQuery(question, options);
    }

    /// @notice Approve oracle to pull creation cost
    function approveOracle() external {
        token.approve(address(oracle), CREATION_COST);
    }
}

Requirements

  • Your contract must hold 30,000 $TRUTH tokens
  • Approve OptimisticOracle to spend tokens before calling createQuery
  • Provide 2-254 non-empty option strings
  • Each option must be unique

Proposing Answers

Basic Proposal

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

interface IOptimisticOracle {
    function proposeAnswer(uint256, uint8[] memory) external;
    function getQueryState(uint256) external view returns (QueryState);
    enum QueryState { Awaiting, Proposed, Disputed, Resolved }
}

contract OptimisticProposer {
    IOptimisticOracle public immutable oracle;
    IERC20 public immutable token;

    uint256 constant PROPOSAL_COLLATERAL = 1_000_000 * 10**18;

    constructor(address oracle_, address token_) {
        oracle = IOptimisticOracle(oracle_);
        token = IERC20(token_);
    }

    /// @notice Propose a single option answer
    function proposeSingleOption(uint256 queryId, uint8 optionIndex) external {
        require(
            oracle.getQueryState(queryId) == IOptimisticOracle.QueryState.Awaiting,
            "Not awaiting proposal"
        );

        uint8[] memory answer = new uint8[](1);
        answer[0] = optionIndex;

        oracle.proposeAnswer(queryId, answer);
    }

    /// @notice Propose multiple options (multi-select)
    function proposeMultipleOptions(
        uint256 queryId,
        uint8[] calldata optionIndices
    ) external {
        oracle.proposeAnswer(queryId, optionIndices);
    }

    /// @notice Propose "no correct choice"
    function proposeNoCorrectChoice(uint256 queryId) external {
        uint8[] memory emptyAnswer = new uint8[](0);
        oracle.proposeAnswer(queryId, emptyAnswer);
    }

    /// @notice Approve oracle for proposal collateral
    function approveOracle() external {
        token.approve(address(oracle), PROPOSAL_COLLATERAL);
    }
}

Requirements

  • Your contract must hold 1,000,000 $TRUTH tokens
  • Approve OptimisticOracle before calling proposeAnswer
  • Query must be in Awaiting state
  • Option indices must be valid (0 to N-1, where N = number of options)

Disputing Proposals

Basic Dispute

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

interface IOptimisticOracle {
    function dispute(uint256, bool) external;
    function canDispute(uint256) external view returns (bool);
    function getDisputeWindowRemaining(uint256) external view returns (uint256);
}

contract OptimisticDisputer {
    IOptimisticOracle public immutable oracle;
    IERC20 public immutable token;

    uint256 constant DISPUTE_COLLATERAL = 1_000_000 * 10**18;

    constructor(address oracle_, address token_) {
        oracle = IOptimisticOracle(oracle_);
        token = IERC20(token_);
    }

    /// @notice Dispute a proposal (standard dispute)
    /// @dev Use when proposer selected wrong option(s)
    function disputeProposal(uint256 queryId) external {
        require(oracle.canDispute(queryId), "Cannot dispute");
        oracle.dispute(queryId, false);
    }

    /// @notice Dispute as "not clear yet"
    /// @dev Use when answer isn't determinable yet
    function disputeAsNotClear(uint256 queryId) external {
        require(oracle.canDispute(queryId), "Cannot dispute");
        oracle.dispute(queryId, true);
    }

    /// @notice Check if dispute window is still open
    function isDisputeWindowOpen(uint256 queryId) external view returns (bool) {
        return oracle.getDisputeWindowRemaining(queryId) > 0;
    }

    /// @notice Approve oracle for dispute collateral
    function approveOracle() external {
        token.approve(address(oracle), DISPUTE_COLLATERAL);
    }
}

Requirements

  • Your contract must hold 1,000,000 $TRUTH tokens
  • Approve OptimisticOracle before calling dispute
  • Query must be in Proposed state
  • Dispute must be within 7,200 blocks (~1.5 hours) of proposal

Reading Query Data

Get Query Answer

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IOptimisticOracle {
    function getAnswer(uint256) external view returns (uint8[] memory);
    function getQueryState(uint256) external view returns (QueryState);
    enum QueryState { Awaiting, Proposed, Disputed, Resolved }
}

contract OptimisticReader {
    IOptimisticOracle public immutable oracle;

    constructor(address oracle_) {
        oracle = IOptimisticOracle(oracle_);
    }

    /// @notice Get the final answer for a resolved query
    /// @dev Reverts if query is not resolved
    function getFinalAnswer(uint256 queryId) external view returns (uint8[] memory) {
        require(
            oracle.getQueryState(queryId) == IOptimisticOracle.QueryState.Resolved,
            "Query not resolved"
        );
        return oracle.getAnswer(queryId);
    }

    /// @notice Check if answer is a single option
    function isSingleOptionAnswer(uint256 queryId) external view returns (bool, uint8) {
        uint8[] memory answer = oracle.getAnswer(queryId);
        if (answer.length == 1) {
            return (true, answer[0]);
        }
        return (false, 0);
    }

    /// @notice Check if answer is "no correct choice"
    function isNoCorrectChoice(uint256 queryId) external view returns (bool) {
        uint8[] memory answer = oracle.getAnswer(queryId);
        return answer.length == 0;
    }
}

Get Query Details

contract OptimisticDetailsReader {
    IOptimisticOracle public immutable oracle;

    constructor(address oracle_) {
        oracle = IOptimisticOracle(oracle_);
    }

    /// @notice Get complete query information
    function getFullDetails(uint256 queryId) external view returns (
        address creator,
        string memory question,
        string[] memory options,
        IOptimisticOracle.QueryState state
    ) {
        (
            creator,
            question,
            options,
            , // createdAt
            state,
            , // proposer
            , // proposedAnswer
            , // proposalBlock
            , // disputed
            , // challenger
            , // mainOracleQueryId
            , // resolved
            , // finalAnswer
              // isNotClearYet
        ) = oracle.getQueryDetails(queryId);
    }
}

Complete Integration Example

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

interface IOptimisticOracle {
    enum QueryState { Awaiting, Proposed, Disputed, Resolved }

    function createQuery(string memory, string[] memory) external returns (uint256);
    function proposeAnswer(uint256, uint8[] memory) external;
    function getAnswer(uint256) external view returns (uint8[] memory);
    function getQueryState(uint256) external view returns (QueryState);
}

/// @title PredictionMarket
/// @notice Example prediction market using OptimisticOracle
contract PredictionMarket {
    IOptimisticOracle public immutable oracle;
    IERC20 public immutable token;

    uint256 constant CREATION_COST = 30_000 * 10**18;

    struct Market {
        uint256 queryId;
        string question;
        bool settled;
    }

    mapping(uint256 => Market) public markets;
    uint256 public marketCount;

    event MarketCreated(uint256 indexed marketId, uint256 indexed queryId, string question);
    event MarketSettled(uint256 indexed marketId, uint8[] answer);

    constructor(address oracle_, address token_) {
        oracle = IOptimisticOracle(oracle_);
        token = IERC20(token_);

        // Approve oracle for future queries
        token.approve(oracle_, type(uint256).max);
    }

    /// @notice Create a new prediction market
    function createMarket(
        string calldata question,
        string[] calldata options
    ) external returns (uint256 marketId) {
        require(options.length >= 2, "Need at least 2 options");

        // Transfer creation cost from user
        token.transferFrom(msg.sender, address(this), CREATION_COST);

        // Create optimistic query
        uint256 queryId = oracle.createQuery(question, options);

        // Store market
        marketId = marketCount++;
        markets[marketId] = Market({
            queryId: queryId,
            question: question,
            settled: false
        });

        emit MarketCreated(marketId, queryId, question);
    }

    /// @notice Settle a market after oracle resolves
    function settleMarket(uint256 marketId) external {
        Market storage market = markets[marketId];
        require(!market.settled, "Already settled");

        // Check if oracle is resolved
        require(
            oracle.getQueryState(market.queryId) == IOptimisticOracle.QueryState.Resolved,
            "Oracle not resolved"
        );

        // Get final answer
        uint8[] memory answer = oracle.getAnswer(market.queryId);

        market.settled = true;

        emit MarketSettled(marketId, answer);

        // TODO: Distribute payouts based on answer
    }

    /// @notice Check if market can be settled
    function canSettle(uint256 marketId) external view returns (bool) {
        Market storage market = markets[marketId];
        return !market.settled &&
               oracle.getQueryState(market.queryId) == IOptimisticOracle.QueryState.Resolved;
    }
}

Common Patterns

Approve Once, Use Many Times

constructor(address oracle_, address token_) {
    oracle = IOptimisticOracle(oracle_);
    token = IERC20(token_);

    // Approve maximum amount for unlimited queries
    token.approve(oracle_, type(uint256).max);
}

Safe Resolution with Try/Catch

function tryResolveUndisputed(uint256 queryId) external returns (bool success) {
    try this._resolveUndisputed(queryId) {
        return true;
    } catch {
        return false;
    }
}

function _resolveUndisputed(uint256 queryId) external {
    oracle.resolveUndisputed(queryId);
}

Wait for Resolution

function waitForResolution(uint256 queryId) external view returns (
    bool isResolved,
    uint8[] memory answer
) {
    if (oracle.getQueryState(queryId) == IOptimisticOracle.QueryState.Resolved) {
        return (true, oracle.getAnswer(queryId));
    }
    return (false, new uint8[](0));
}

Error Handling

Common revert reasons:

  • "Question cannot be empty": Provide a non-empty question string
  • "Must have 2-254 options": Option array length must be 2-254
  • "Option cannot be empty": All option strings must be non-empty
  • "Query not in awaiting state": Can only propose to queries in Awaiting state
  • "Invalid option index": Option index must be < options.length
  • "Duplicate options": Cannot propose same option twice
  • "Query not in proposed state": Can only dispute Proposed queries
  • "Dispute window closed": Must dispute within 7,200 blocks
  • "Query not resolved": Cannot get answer until resolved

Best Practices

  1. Approve Once: Use type(uint256).max for unlimited approvals
  2. Check State: Always verify query state before interacting
  3. Handle Reverts: Use try/catch for resolution attempts
  4. Validate Options: Ensure option indices are valid before proposing
  5. Monitor Timing: Track dispute windows for time-sensitive operations
  6. Economic Incentives: Only propose/dispute when you have high confidence

Gas Considerations

  • createQuery: ~150k-300k gas (depends on question/options length)
  • proposeAnswer: ~100k-150k gas
  • dispute: ~300k-500k gas (creates main oracle query)
  • resolveUndisputed: ~50k-100k gas
  • resolveDisputed: ~100k-200k gas

Reference Documentation

Next Steps

Back to: