Integrate OOv3 from a smart contract
The standard pattern: your contract holds funds, calls assertTruth()
when a market closes, and listens for the result via callback. This is
the same model used by leading prediction-market platforms.
Develop against the sandbox: deploy your integration contract to X Layer Testnet (chainId 1952), point it at the same OOv3 address (Contracts), and use
app-dev.xtruth.xyzfor assertion / dispute / settle UI while you iterate. Move toapp.xtruth.xyzonce you're done. Both environments hit the same chain today — see Environments.
Minimal prediction-market contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import { OptimisticOracleV3Interface } from
"./interfaces/OptimisticOracleV3Interface.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract YesNoMarket {
OptimisticOracleV3Interface public immutable oo;
IERC20 public immutable bondCurrency;
bytes32 public constant identifier = "ASSERT_TRUTH";
struct Market {
bool resolved;
bool outcome; // true = YES
bytes32 assertionId;
}
mapping(bytes32 => Market) public markets;
constructor(address oov3, address currency) {
oo = OptimisticOracleV3Interface(oov3);
bondCurrency = IERC20(currency);
}
/// Anyone can resolve a market by posting an assertion + bond.
function resolve(bytes32 marketId, string calldata claim, uint64 liveness) external {
require(!markets[marketId].resolved, "already resolved");
uint256 bond = oo.getMinimumBond(address(bondCurrency));
bondCurrency.transferFrom(msg.sender, address(this), bond);
bondCurrency.approve(address(oo), bond);
bytes32 assertionId = oo.assertTruth(
bytes(claim),
msg.sender, // asserter
address(this), // callbackRecipient
address(0), // escalationManager
liveness,
bondCurrency,
bond,
identifier,
bytes32(0)
);
markets[marketId].assertionId = assertionId;
}
/// Called by OOv3 when the assertion settles (positive or negative).
function assertionResolvedCallback(
bytes32 assertionId,
bool assertedTruthfully
) external {
require(msg.sender == address(oo), "only OO");
// Find the market this assertion belongs to and store the outcome.
// Pay out winners. (Indexing details elided.)
}
/// Optional: a hook for when somebody disputes (escalates to the DVM).
function assertionDisputedCallback(bytes32 assertionId) external {
require(msg.sender == address(oo), "only OO");
// Surface "in dispute" UI; freeze any pending payouts.
}
}
Key choices
Bond amount
getMinimumBond(currency) returns the network minimum for that token; you
can post a higher bond for stronger economic security. Higher bonds make
disputes more expensive, which is exactly what you want for high-value
markets.
Liveness window
Trade-off:
- Short (e.g. 1 hour) — fast settlement, but disputers might miss the window if the question is non-obvious or contentious.
- Long (e.g. 7 days) — slower payouts but very safe; appropriate for large markets or governance-style questions.
The default in OOv3 is 2 hours. Polymarket uses values from 2 hours up to several days depending on the market.
Identifier
For OOv3 the most common choice is ASSERT_TRUTH. It says "the claim text
is a true/false proposition; resolve true if undisputed". Other identifiers
exist (YES_OR_NO_QUERY, NUMERICAL, MULTIPLE_CHOICE_QUERY) but only
matter if your escalation manager interprets them differently.
Custom escalation manager
Pass address(0) for the default DVM-only behavior. To customize (e.g.
auto-veto certain assertions, require multi-sig before disputing), deploy
a contract implementing EscalationManagerInterface and pass its address.
Callback gotchas
assertionResolvedCallbackis called insidesettleAssertion(). Keep it cheap — it must finish in the gas budget of whoever called settle.- A revert in your callback does not revert the settlement; OOv3 wraps it in a try/catch. But you might end up with a settled assertion that your contract doesn't know about. Use a fallback "claim by id" function.
- Idempotency: do not assume the callback fires exactly once. Defend with a
markets[marketId].resolvedflag.
Approving from a contract
When the contract itself posts the bond (vs. pulling from msg.sender
like the example above), you typically pre-fund the contract with bond
currency and approve the OOv3 inside resolve(). To avoid wasted gas on
every call, approve once at deployment with type(uint256).max.
ABI
The OptimisticOracleV3 interface for both reads and writes is bundled in xtruth-app/src/lib/contracts/oov3-abi.ts. The exact function we use:
function assertTruth(
bytes memory claim,
address asserter,
address callbackRecipient,
address escalationManager,
uint64 liveness,
IERC20 currency,
uint256 bond,
bytes32 identifier,
bytes32 domainId
) external returns (bytes32 assertionId);
For a copy you can drop into your project, see the xtruth-app source.