Blockchain / Global / Address
Contract
0x6b2FeE58F90AeE79BE03E417213c547526791102
- Balance
- 0 SOMI
- Transactions sent
- 1
- Type
- Contract · 4,964 bytes of code
BinaryMarket Verified
- Compiler
- solc 0.8.34+commit.80d5c536
- Language
- Solidity
- Optimization
- Yes, 100 runs
- EVM version
- cancun
- License
- —
- Verified
- 2026-10-12
src/clob/markets/BinaryMarket.sol
and 12 more files
Solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.34;
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {CloseState, IBinaryPool} from "somnia-dex/interfaces/IBinaryPool.sol";
import {PAYOUT_VECTOR_DENOMINATOR} from "../../interfaces/IOracleAnswerReceiver.sol";
import {IBinaryMarket} from "../interfaces/IBinaryMarket.sol";
import {IMarket} from "../interfaces/IMarket.sol";
import {MarketStatus, MarketType, VoidPolicy, VoidVectorSource} from "../interfaces/MarketTypes.sol";
/// @notice Per-market lifecycle + oracle contract for a binary
/// market. Carries no collateral and no outcome supply — those live entirely
/// on the linked `BinaryPool` from the somnia-dex submodule. The market
/// exposes a stable lifecycle status (Listed → Trading → Locked →
/// Resolved/Voided), the oracle's `resolve` / `voidMarket` entry points, and
/// view passthroughs (`backing`, `outcomeCount`, `outcomeTokens`) so consumers
/// can read market state without knowing about the pool split.
///
/// Settlement v3 (DESIGN §2B/§7.7): the terminal state is a PAYOUT VECTOR —
/// `resolve(uint256[])` validates `Σ == PAYOUT_VECTOR_DENOMINATOR` raw and
/// stores it; void stores the uniform vector. The pool snapshots the vector at
/// finalize; all redemption math downstream reads only the vector.
/// @dev Status is derived from `block.timestamp` + a terminal `_finalState`
/// flag — `poke()` is a no-op kept for ABI stability. Behind EIP-1167 minimal
/// proxies via `BinaryClobFactory.deployFamily`.
contract BinaryMarket is Initializable, IBinaryMarket {
/// @notice Terminal state once the oracle finalises the market.
enum FinalState {
None,
Resolved,
Voided
}
/// @notice Market type tag per clob-design §4. Stage 1 binary markets are
/// fixed at `BINARY_SINGLE_BOOK`.
MarketType public constant override marketType = MarketType.BINARY_SINGLE_BOOK;
/// @notice ERC-20 collateral the linked pool quotes prices in.
/// @dev Mirrors `IMarket.collateral()`; the interface owns the canonical NatSpec.
IERC20 public override collateral;
/// @notice Settler authorised to call `resolve` / `voidMarket` (the BinaryMarketsModule
/// that owns resolution — NOT the price oracle).
address public settler;
/// @notice Wallet that created the market (threaded from BinaryMarketsModule's `msg.sender`).
address public creator;
/// @notice Origin operator this market is attributed to.
uint32 public operatorId;
/// @notice Origin venue (within the operator) this market is attributed to.
bytes32 public venueId;
/// @notice Unix-seconds timestamp at which trading begins.
uint64 public override tradingStart;
/// @notice Unix-seconds timestamp at which trading halts and Locked begins.
uint64 public override expiry;
/// @notice Seconds after `expiry` within which the oracle must resolve.
uint64 public settlementWindow;
/// @notice Shared ERC-6909 outcome-token singleton. Set once via `setOutcomeToken`.
address public override outcomeToken;
/// @notice BinaryPool proxy hosting the order book. Set once via `setPool`.
address public override pool;
/// @inheritdoc IBinaryMarket
/// @dev Frozen at `initialize` — the factory threads the policy the module resolved against
/// the venue's configuration and the provisioned pool's capability. The void paths read
/// THIS field (never the registry), so the module-bypassing `voidExpired` is covered.
VoidPolicy public override voidPolicy;
/// @notice The pool's market nonce this market clone is bound to (part of the outcome-id
/// encoding). A pool serves successive markets, each a fresh clone with its own nonce;
/// the nonce disambiguates the outcome ids of one pool's successive markets. Set once
/// via `setPool`.
uint64 public poolNonce;
FinalState internal _finalState;
/// @dev Terminal payout vector (settlement v3). Empty until Resolved/Voided;
/// one-hot on a resolved binary market, uniform on a void. Raw sum ==
/// `PAYOUT_VECTOR_DENOMINATOR` — validated at `resolve`, by construction
/// on the void paths.
uint256[] internal _payoutNumerators;
/// @notice Thrown when a one-shot setter is called more than once.
error AlreadyInitialised();
/// @notice Thrown when a required address argument is the zero address.
error ZeroAddress();
/// @notice Thrown when the trading window timestamps are not strictly increasing.
error InvalidTradingWindow();
/// @notice Thrown when the settlement window is zero.
error InvalidSettlementWindow();
/// @notice Thrown when an oracle-only function is called by a non-oracle address.
error NotOracle();
/// @notice Thrown when the market is in the wrong status for the attempted action.
error WrongStatus(MarketStatus expected, MarketStatus actual);
/// @notice Thrown when a resolve call carries a payout vector whose length is not
/// `outcomeCount()` or whose raw sum is not `PAYOUT_VECTOR_DENOMINATOR`.
error InvalidPayoutVector();
/// @notice Thrown when `voidExpired` is called before `expiry + settlementWindow`.
error SettlementWindowOpen();
/// @notice Thrown when `initialize` is handed the legacy `AMM_SNAPSHOT` policy — it has no
/// meaning on the CLOB and exists only for encoding stability.
error InvalidVoidPolicy();
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
/// @notice One-shot initializer for clones. The impl contract is deployed
/// once and locked via `_disableInitializers()`; BinaryClobFactory clones it per
/// market via `Clones.cloneDeterministic` and calls this from the same tx
/// as the clone deploy.
/// @param collateral_ ERC-20 collateral the linked pool will quote in.
/// @param settler_ Address authorised to call `resolve` / `voidMarket`.
/// @param creator_ Wallet that created the market.
/// @param operatorId_ Origin operator this market is attributed to.
/// @param venueId_ Origin venue (within the operator) this market is attributed to.
/// @param tradingStart_ Unix-seconds timestamp at which Listed → Trading.
/// @param expiry_ Unix-seconds timestamp at which Trading → Locked.
/// @param settlementWindow_ Seconds after `expiry` within which the oracle must resolve.
/// @param voidPolicy_ Void payout policy frozen into this market (already resolved by the
/// module against the venue's wish AND the pool's capability; `AMM_SNAPSHOT` rejected).
function initialize(
IERC20 collateral_,
address settler_,
address creator_,
uint32 operatorId_,
bytes32 venueId_,
uint64 tradingStart_,
uint64 expiry_,
uint64 settlementWindow_,
VoidPolicy voidPolicy_
) external initializer {
if (settler_ == address(0)) revert ZeroAddress();
if (creator_ == address(0)) revert ZeroAddress();
if (address(collateral_) == address(0)) revert ZeroAddress();
// Reverts when expiry == tradingStart (zero-length window) — strict `<`
// would allow that degenerate case.
// solhint-disable-next-line gas-strict-inequalities
if (tradingStart_ != 0 && expiry_ != 0 && expiry_ <= tradingStart_) {
revert InvalidTradingWindow();
}
if (settlementWindow_ == 0) revert InvalidSettlementWindow();
if (voidPolicy_ == VoidPolicy.AMM_SNAPSHOT) revert InvalidVoidPolicy();
collateral = collateral_;
settler = settler_;
creator = creator_;
operatorId = operatorId_;
venueId = venueId_;
tradingStart = tradingStart_;
expiry = expiry_;
settlementWindow = settlementWindow_;
voidPolicy = voidPolicy_;
}
/// @notice One-shot setter called by the deployer to wire the shared
/// outcome-token singleton.
/// @param outcomeToken_ Address of the ERC-6909 outcome-token singleton.
function setOutcomeToken(address outcomeToken_) external {
if (outcomeToken != address(0)) revert AlreadyInitialised();
if (outcomeToken_ == address(0)) revert ZeroAddress();
outcomeToken = outcomeToken_;
}
/// @notice One-shot setter called by the deployer after the BinaryPool
/// proxy is created (fresh deploy) or cloned onto a recycled pool.
/// @param pool_ Address of the linked BinaryPool proxy.
/// @param nonce_ The pool's market nonce for this market (1 on a fresh pool, the incremented
/// value on a recycled one). Frozen into the outcome-id encoding via `_idFor`.
function setPool(address pool_, uint64 nonce_) external {
if (pool != address(0)) revert AlreadyInitialised();
if (pool_ == address(0)) revert ZeroAddress();
pool = pool_;
poolNonce = nonce_;
}
/// @inheritdoc IMarket
/// @dev No-op — status is derived from time + finalState, no lazy
/// transition state to advance. Kept for ABI stability with adapters that
/// may still call it.
function poke() external override {}
/// @inheritdoc IMarket
/// @dev Validates the raw-sum invariant `Σ == PAYOUT_VECTOR_DENOMINATOR` here — the
/// settlement layer's solvency (`Σ redeemable <= backing` under floor division)
/// is proven against exactly this precondition (DESIGN §7.7).
function resolve(uint256[] calldata payoutNumerators_) external override {
if (msg.sender != settler) revert NotOracle();
if (_finalState != FinalState.None) revert WrongStatus(MarketStatus.Locked, _liveStatus());
// forge-lint: disable-next-line(block-timestamp)
if (block.timestamp < expiry) revert WrongStatus(MarketStatus.Locked, _liveStatus());
// A binary market always has exactly 2 outcome slots (`outcomeCount()`).
if (payoutNumerators_.length != 2) revert InvalidPayoutVector();
uint256 sum;
for (uint256 i; i < payoutNumerators_.length; ++i) {
sum += payoutNumerators_[i];
}
if (sum != PAYOUT_VECTOR_DENOMINATOR) revert InvalidPayoutVector();
_payoutNumerators = payoutNumerators_;
_finalState = FinalState.Resolved;
emit Resolved(PAYOUT_VECTOR_DENOMINATOR, payoutNumerators_);
emit StatusChanged(MarketStatus.Locked, MarketStatus.Resolved);
}
/// @inheritdoc IMarket
function voidMarket() external override {
if (msg.sender != settler) revert NotOracle();
if (_finalState != FinalState.None) revert WrongStatus(MarketStatus.Locked, _liveStatus());
MarketStatus prev = _liveStatus();
_storeVoidVector();
_finalState = FinalState.Voided;
emit Voided();
emit StatusChanged(prev, MarketStatus.Voided);
}
/// @notice Permissionless escape hatch: void a market the oracle never resolved
/// once `expiry + settlementWindow` has elapsed. Without this a dead /
/// unresponsive oracle would strand the pool's entire backing forever
/// (there is no admin override — resolution is oracle-only by design).
/// After void, both sides redeem at 0.5 collateral per token via the pool.
/// @dev Anyone may call; the time gate is the only guard. No-op-safe: reverts if
/// the market is already resolved or voided.
function voidExpired() external {
if (_finalState != FinalState.None) revert WrongStatus(MarketStatus.Locked, _liveStatus());
// forge-lint: disable-next-line(block-timestamp)
if (block.timestamp < expiry + settlementWindow) revert SettlementWindowOpen();
MarketStatus prev = _liveStatus();
_storeVoidVector();
_finalState = FinalState.Voided;
emit Voided();
emit StatusChanged(prev, MarketStatus.Voided);
}
/// @inheritdoc IMarket
function status() external view override returns (MarketStatus) {
return _liveStatus();
}
/// @notice Pool gate — true only while the market is in the Trading state.
function tradingActive() external view override returns (bool) {
return _liveStatus() == MarketStatus.Trading;
}
/// @notice True once the oracle has resolved the market to a concrete winning outcome.
function isResolved() external view override returns (bool) {
return _finalState == FinalState.Resolved;
}
/// @notice True once the oracle has voided the market.
function isVoided() external view override returns (bool) {
return _finalState == FinalState.Voided;
}
/// @inheritdoc IMarket
/// @dev Single declaration satisfies both `IMarket` and the somnia-dex
/// base `IBinaryMarket` (same signature on both).
function payoutNumerators() external view override(IBinaryMarket) returns (uint256[] memory) {
return _payoutNumerators;
}
/// @inheritdoc IMarket
/// @dev Delegate to the linked pool's `setBacking` — the pool owns the
/// canonical backing state WHILE the market is live. Returns 0 when the
/// pool hasn't been wired up yet (between initialize and setPool) AND after
/// the market is finalized: `finalize` sweeps the pool's entire `setBacking`
/// (zeroing it) over to the `BinarySettlement` singleton, where the market's
/// net pot then lives at `settlement.getSettlement(marketKey)` — read that for
/// the post-finalize backing (marketKey = yesId >> 8).
function backing() external view override returns (uint256) {
return pool == address(0) ? 0 : IBinaryPool(pool).setBacking();
}
/// @inheritdoc IBinaryMarket
function yesId() external view override returns (uint256) {
return _idFor(0);
}
/// @inheritdoc IBinaryMarket
function noId() external view override returns (uint256) {
return _idFor(1);
}
/// @inheritdoc IMarket
function outcomeIds() external view override returns (uint256[] memory ids) {
ids = new uint256[](2);
ids[0] = _idFor(0);
ids[1] = _idFor(1);
}
/// @inheritdoc IMarket
function outcomeCount() external pure override returns (uint8) {
return 2;
}
/// @inheritdoc IMarket
function payoutDenominator() external pure override returns (uint32) {
return PAYOUT_VECTOR_DENOMINATOR;
}
/// @dev Store the void payout vector per the market's frozen policy and emit `VoidVector`.
/// Shared by the oracle-driven `voidMarket` and the permissionless `voidExpired` backstop.
///
/// `UNIFORM` → the uniform vector, by rule. `CLOB_SNAPSHOT` → `[p, D-p]` from the pool's
/// closing price: the stored `captureClose` mid when present, else the raw top of the
/// closing book, which the pool's closing-book lock guarantees intact until terminal or
/// captured (dual regime: a pre-expiry early void reads the LIVE top). `p` is clamped to
/// `[1, D-1]` so neither side is ever zeroed by an extreme close. The ENTIRE pool
/// interaction is failure-isolated (try/catch on the external pool + guards) → uniform on
/// any failure: `voidExpired` is the dead-oracle escape hatch and must never brick, and an
/// old-implementation pool without the capture surface simply falls back. (External-call
/// try/catch on the pool — a different contract — not a banned self-call.)
function _storeVoidVector() internal {
if (voidPolicy != VoidPolicy.CLOB_SNAPSHOT) {
_storeUniformVector();
emit VoidVector(_payoutNumerators, voidPolicy, VoidVectorSource.UNIFORM_POLICY);
return;
}
(uint256 p, VoidVectorSource source, bool ok) = _closingPayout();
if (!ok) {
_storeUniformVector();
emit VoidVector(_payoutNumerators, voidPolicy, VoidVectorSource.FALLBACK_UNIFORM);
return;
}
_payoutNumerators.push(p);
_payoutNumerators.push(uint256(PAYOUT_VECTOR_DENOMINATOR) - p);
emit VoidVector(_payoutNumerators, voidPolicy, source);
}
/// @dev Derive the YES payout numerator `p` from the pool's closing price. Returns
/// `ok = false` for every case that must fall back to uniform: unwired/old/reverting
/// pool, degenerate (one-sided/empty) close, or a zero price scale. Preference order:
/// 1. `Captured` → the stored `captureClose` mid (the normal dead-oracle path — someone
/// captured to free maker escrow before `voidExpired` ran).
/// 2. `Open` → `closingTop(0)`, the raw intact-book top (early void mid-trading reads
/// the live regime; a post-expiry void before any capture reads the closing book the
/// lock preserved). Two-sided → mid of best bid/ask.
/// `CapturedDegenerate` has no two-sided close; `TerminalLatch` cannot be observed here
/// (the market is not terminal yet — this runs BEFORE `_finalState` flips) but maps to
/// fallback defensively.
function _closingPayout() internal view returns (uint256 p, VoidVectorSource source, bool ok) {
if (pool == address(0)) return (0, VoidVectorSource.FALLBACK_UNIFORM, false);
uint256 mid;
uint256 oneCollateral;
try IBinaryPool(pool).closingPrice() returns (uint256 storedMid, uint256 one, CloseState state) {
if (state == CloseState.Captured) {
(mid, oneCollateral) = (storedMid, one);
source = VoidVectorSource.CAPTURED_MID;
} else if (state == CloseState.Open) {
// No capture stored — read the intact book directly (bounded default walk; a
// stale-stuffed book reverts inside and lands in the catch → uniform).
try IBinaryPool(pool).closingTop(0) returns (
uint256 bestBid, uint256 bestAsk, bool bidFound, bool askFound
) {
if (!bidFound || !askFound) return (0, VoidVectorSource.FALLBACK_UNIFORM, false);
// Overflow guard (audit M-1): with exotic high-decimals collateral two near-1.0
// book prices can sum past uint256 max. A panic here would escape the failure
// isolation (try/catch does not catch reverts raised in its own success block)
// and brick the void -- treat as degenerate instead.
if (bestBid > type(uint256).max - bestAsk) {
return (0, VoidVectorSource.FALLBACK_UNIFORM, false);
}
(mid, oneCollateral) = ((bestBid + bestAsk) / 2, one);
source = VoidVectorSource.RAW_TOP;
} catch {
return (0, VoidVectorSource.FALLBACK_UNIFORM, false);
}
} else {
// CapturedDegenerate (no two-sided close) or defensive TerminalLatch.
return (0, VoidVectorSource.FALLBACK_UNIFORM, false);
}
} catch {
// Old pool implementation without the capture surface, or any read failure.
return (0, VoidVectorSource.FALLBACK_UNIFORM, false);
}
if (oneCollateral == 0) return (0, VoidVectorSource.FALLBACK_UNIFORM, false);
// Overflow guard (audit M-1): `D * mid` must fit uint256 or the void tx would panic OUTSIDE
// the try/catch and permanently brick `voidExpired` (collateral decimals are creator-supplied
// and unbounded below the pool's own 10**dec limit; ~71+ decimals reach this). The void paths
// must never revert on pool data -- fall back to uniform like every other degenerate case.
if (mid > type(uint256).max / uint256(PAYOUT_VECTOR_DENOMINATOR)) {
return (0, VoidVectorSource.FALLBACK_UNIFORM, false);
}
// Scale the raw book mid onto the payout denominator and clamp to [1, D-1]: book prices
// are strictly inside (0, oneCollateral), but flooring could still hit 0 and an extreme
// close must never zero a side outright on a VOID (that is resolution's job).
p = (uint256(PAYOUT_VECTOR_DENOMINATOR) * mid) / oneCollateral;
if (p == 0) p = 1;
if (p > uint256(PAYOUT_VECTOR_DENOMINATOR) - 1) p = uint256(PAYOUT_VECTOR_DENOMINATOR) - 1;
ok = true;
}
/// @dev Store the uniform payout vector (each outcome pays 1/N). The `UNIFORM`-policy rule and
/// the universal fallback of the `CLOB_SNAPSHOT` policy (degenerate close / old pool /
/// any read failure — see `_storeVoidVector`).
/// For N=2 the split `[D/2, D/2]` is exact (D is even), so the raw-sum
/// invariant `Σ == PAYOUT_VECTOR_DENOMINATOR` holds by construction.
function _storeUniformVector() internal {
uint8 n = 2; // binary — matches `outcomeCount()`.
uint256 share = uint256(PAYOUT_VECTOR_DENOMINATOR) / n;
for (uint8 i; i < n; ++i) {
_payoutNumerators.push(share);
}
// Fold any flooring remainder into outcome 0 so the raw sum stays exact for
// hypothetical N that does not divide D (never the case at N=2).
_payoutNumerators[0] += uint256(PAYOUT_VECTOR_DENOMINATOR) - share * n;
}
/// @dev ERC-6909 id of this market's outcome at `outcomeIdx` (v2 encoding, matching the dex
/// `BinaryPool._idFor` and `OutcomeToken6909`): the pool address in the upper 160 bits, the
/// pool's market nonce in the next 64, the outcome index in the low 8.
function _idFor(uint8 outcomeIdx) internal view returns (uint256) {
return (uint256(uint160(pool)) << 72) | (uint256(poolNonce) << 8) | outcomeIdx;
}
function _liveStatus() internal view returns (MarketStatus) {
if (_finalState == FinalState.Resolved) return MarketStatus.Resolved;
if (_finalState == FinalState.Voided) return MarketStatus.Voided;
// forge-lint: disable-next-line(block-timestamp)
if (block.timestamp < tradingStart) return MarketStatus.Listed;
// forge-lint: disable-next-line(block-timestamp)
if (block.timestamp < expiry) return MarketStatus.Trading;
return MarketStatus.Locked;
}
}
Other files
lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol
// SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.0.0) (proxy/utils/Initializable.sol) pragma solidity ^0.8.20; /** * @dev This is a base contract to aid in writing upgradeable contracts, or any kind of contract that will be deployed * behind a proxy. Since proxied contracts do not make use of a constructor, it's common to move constructor logic to an * external initializer function, usually called `initialize`. It then becomes necessary to protect this initializer * function so it can only be called once. The {initializer} modifier provided by this contract will have this effect. * * The initialization functions use a version number. Once a version number is used, it is consumed and cannot be * reused. This mechanism prevents re-execution of each "step" but allows the creation of new initialization steps in * case an upgrade adds a module that needs to be initialized. * * For example: * * [.hljs-theme-light.nopadding] * ```solidity * contract MyToken is ERC20Upgradeable { * function initialize() initializer public { * __ERC20_init("MyToken", "MTK"); * } * } * * contract MyTokenV2 is MyToken, ERC20PermitUpgradeable { * function initializeV2() reinitializer(2) public { * __ERC20Permit_init("MyToken"); * } * } * ``` * * TIP: To avoid leaving the proxy in an uninitialized state, the initializer function should be called as early as * possible by providing the encoded function call as the `_data` argument to {ERC1967Proxy-constructor}. * * CAUTION: When used with inheritance, manual care must be taken to not invoke a parent initializer twice, or to ensure * that all initializers are idempotent. This is not verified automatically as constructors are by Solidity. * * [CAUTION] * ==== * Avoid leaving a contract uninitialized. * * An uninitialized contract can be taken over by an attacker. This applies to both a proxy and its implementation * contract, which may impact the proxy. To prevent the implementation contract from being used, you should invoke * the {_disableInitializers} function in the constructor to automatically lock it when it is deployed: * * [.hljs-theme-light.nopadding] * ``` * /// @custom:oz-upgrades-unsafe-allow constructor * constructor() { * _disableInitializers(); * } * ``` * ==== */ abstract contract Initializable { /** * @dev Storage of the initializable contract. * * It's implemented on a custom ERC-7201 namespace to reduce the risk of storage collisions * when using with upgradeable contracts. * * @custom:storage-location erc7201:openzeppelin.storage.Initializable */ struct InitializableStorage { /** * @dev Indicates that the contract has been initialized. */ uint64 _initialized; /** * @dev Indicates that the contract is in the process of being initialized. */ bool _initializing; } // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.Initializable")) - 1)) & ~bytes32(uint256(0xff)) bytes32 private constant INITIALIZABLE_STORAGE = 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00; /** * @dev The contract is already initialized. */ error InvalidInitialization(); /** * @dev The contract is not initializing. */ error NotInitializing(); /** * @dev Triggered when the contract has been initialized or reinitialized. */ event Initialized(uint64 version); /** * @dev A modifier that defines a protected initializer function that can be invoked at most once. In its scope, * `onlyInitializing` functions can be used to initialize parent contracts. * * Similar to `reinitializer(1)`, except that in the context of a constructor an `initializer` may be invoked any * number of times. This behavior in the constructor can be useful during testing and is not expected to be used in * production. * * Emits an {Initialized} event. */ modifier initializer() { // solhint-disable-next-line var-name-mixedcase InitializableStorage storage $ = _getInitializableStorage(); // Cache values to avoid duplicated sloads bool isTopLevelCall = !$._initializing; uint64 initialized = $._initialized; // Allowed calls: // - initialSetup: the contract is not in the initializing state and no previous version was // initialized // - construction: the contract is initialized at version 1 (no reininitialization) and the // current contract is just being deployed bool initialSetup = initialized == 0 && isTopLevelCall; bool construction = initialized == 1 && address(this).code.length == 0; if (!initialSetup && !construction) { revert InvalidInitialization(); } $._initialized = 1; if (isTopLevelCall) { $._initializing = true; } _; if (isTopLevelCall) { $._initializing = false; emit Initialized(1); } } /** * @dev A modifier that defines a protected reinitializer function that can be invoked at most once, and only if the * contract hasn't been initialized to a greater version before. In its scope, `onlyInitializing` functions can be * used to initialize parent contracts. * * A reinitializer may be used after the original initialization step. This is essential to configure modules that * are added through upgrades and that require initialization. * * When `version` is 1, this modifier is similar to `initializer`, except that functions marked with `reinitializer` * cannot be nested. If one is invoked in the context of another, execution will revert. * * Note that versions can jump in increments greater than 1; this implies that if multiple reinitializers coexist in * a contract, executing them in the right order is up to the developer or operator. * * WARNING: Setting the version to 2**64 - 1 will prevent any future reinitialization. * * Emits an {Initialized} event. */ modifier reinitializer(uint64 version) { // solhint-disable-next-line var-name-mixedcase InitializableStorage storage $ = _getInitializableStorage(); if ($._initializing || $._initialized >= version) { revert InvalidInitialization(); } $._initialized = version; $._initializing = true; _; $._initializing = false; emit Initialized(version); } /** * @dev Modifier to protect an initialization function so that it can only be invoked by functions with the * {initializer} and {reinitializer} modifiers, directly or indirectly. */ modifier onlyInitializing() { _checkInitializing(); _; } /** * @dev Reverts if the contract is not in an initializing state. See {onlyInitializing}. */ function _checkInitializing() internal view virtual { if (!_isInitializing()) { revert NotInitializing(); } } /** * @dev Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call. * Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized * to any version. It is recommended to use this to lock implementation contracts that are designed to be called * through proxies. * * Emits an {Initialized} event the first time it is successfully executed. */ function _disableInitializers() internal virtual { // solhint-disable-next-line var-name-mixedcase InitializableStorage storage $ = _getInitializableStorage(); if ($._initializing) { revert InvalidInitialization(); } if ($._initialized != type(uint64).max) { $._initialized = type(uint64).max; emit Initialized(type(uint64).max); } } /** * @dev Returns the highest version that has been initialized. See {reinitializer}. */ function _getInitializedVersion() internal view returns (uint64) { return _getInitializableStorage()._initialized; } /** * @dev Returns `true` if the contract is currently initializing. See {onlyInitializing}. */ function _isInitializing() internal view returns (bool) { return _getInitializableStorage()._initializing; } /** * @dev Returns a pointer to the storage namespace. */ // solhint-disable-next-line var-name-mixedcase function _getInitializableStorage() private pure returns (InitializableStorage storage $) { assembly { $.slot := INITIALIZABLE_STORAGE } } }lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol
// SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/IERC20.sol) pragma solidity ^0.8.20; /** * @dev Interface of the ERC-20 standard as defined in the ERC. */ interface IERC20 { /** * @dev Emitted when `value` tokens are moved from one account (`from`) to * another (`to`). * * Note that `value` may be zero. */ event Transfer(address indexed from, address indexed to, uint256 value); /** * @dev Emitted when the allowance of a `spender` for an `owner` is set by * a call to {approve}. `value` is the new allowance. */ event Approval(address indexed owner, address indexed spender, uint256 value); /** * @dev Returns the value of tokens in existence. */ function totalSupply() external view returns (uint256); /** * @dev Returns the value of tokens owned by `account`. */ function balanceOf(address account) external view returns (uint256); /** * @dev Moves a `value` amount of tokens from the caller's account to `to`. * * Returns a boolean value indicating whether the operation succeeded. * * Emits a {Transfer} event. */ function transfer(address to, uint256 value) external returns (bool); /** * @dev Returns the remaining number of tokens that `spender` will be * allowed to spend on behalf of `owner` through {transferFrom}. This is * zero by default. * * This value changes when {approve} or {transferFrom} are called. */ function allowance(address owner, address spender) external view returns (uint256); /** * @dev Sets a `value` amount of tokens as the allowance of `spender` over the * caller's tokens. * * Returns a boolean value indicating whether the operation succeeded. * * IMPORTANT: Beware that changing an allowance with this method brings the risk * that someone may use both the old and the new allowance by unfortunate * transaction ordering. One possible solution to mitigate this race * condition is to first reduce the spender's allowance to 0 and set the * desired value afterwards: * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 * * Emits an {Approval} event. */ function approve(address spender, uint256 value) external returns (bool); /** * @dev Moves a `value` amount of tokens from `from` to `to` using the * allowance mechanism. `value` is then deducted from the caller's * allowance. * * Returns a boolean value indicating whether the operation succeeded. * * Emits a {Transfer} event. */ function transferFrom(address from, address to, uint256 value) external returns (bool); }lib/openzeppelin-contracts/contracts/utils/introspection/IERC165.sol
// SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.1.0) (utils/introspection/IERC165.sol) pragma solidity ^0.8.20; /** * @dev Interface of the ERC-165 standard, as defined in the * https://eips.ethereum.org/EIPS/eip-165[ERC]. * * Implementers can declare support of contract interfaces, which can then be * queried by others ({ERC165Checker}). * * For an implementation, see {ERC165}. */ interface IERC165 { /** * @dev Returns true if this contract implements the interface defined by * `interfaceId`. See the corresponding * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[ERC section] * to learn more about how these ids are created. * * This function call must use less than 30 000 gas. */ function supportsInterface(bytes4 interfaceId) external view returns (bool); }lib/somnia-dex-protocol/src/common/Common.sol
// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.34; /// @notice Reusable slot index assigned to each order for storage mapping. /// @dev Index 0 is reserved as `EMPTY_ORDER_INDEX` (sentinel value). /// Indices are recycled via a LIFO free list in `OrderIndexAllocator`. type OrderIndex is uint64; // solhint-disable private-vars-leading-underscore /// @notice Equality comparison for OrderIndex. /// @param a The first OrderIndex to compare. /// @param b The second OrderIndex to compare. function eqOrderIndex(OrderIndex a, OrderIndex b) pure returns (bool) { return OrderIndex.unwrap(a) == OrderIndex.unwrap(b); } /// @notice Inequality comparison for OrderIndex. /// @param a The first OrderIndex to compare. /// @param b The second OrderIndex to compare. function neOrderIndex(OrderIndex a, OrderIndex b) pure returns (bool) { return OrderIndex.unwrap(a) != OrderIndex.unwrap(b); } using {eqOrderIndex as ==, neOrderIndex as !=} for OrderIndex global; // Sentinel value representing an empty/unoccupied order index. OrderIndex constant EMPTY_ORDER_INDEX = OrderIndex.wrap(0); /// @notice Thrown when a function receives `EMPTY_ORDER_INDEX` (the sentinel) where a real order index was expected. error ZeroOrderIndex(); /// @notice Globally unique, permanent identifier for an order. /// @dev Composed as `(OrderIndex << 64) | uniqueCounter`. The upper 64 bits /// encode the reusable slot index; the lower 64 bits are a monotonically /// increasing counter that guarantees uniqueness even after index reuse. type OrderId is uint128; /// @notice Equality comparison for OrderId. /// @param a The first OrderId to compare. /// @param b The second OrderId to compare. function eqOrderId(OrderId a, OrderId b) pure returns (bool) { return OrderId.unwrap(a) == OrderId.unwrap(b); } /// @notice Inequality comparison for OrderId. /// @param a The first OrderId to compare. /// @param b The second OrderId to compare. function neOrderId(OrderId a, OrderId b) pure returns (bool) { return OrderId.unwrap(a) != OrderId.unwrap(b); } using {eqOrderId as ==, neOrderId as !=} for OrderId global; /// @notice Creates an OrderId by packing an OrderIndex and a unique counter value /// @param orderIndex The reusable slot index (upper 64 bits) /// @param uniqueId A monotonically increasing counter (lower 64 bits) /// @return The packed OrderId function createOrderId(OrderIndex orderIndex, uint64 uniqueId) pure returns (OrderId) { return OrderId.wrap((uint128(OrderIndex.unwrap(orderIndex)) << 64) | (uint128(uniqueId))); } /// @notice Extracts the OrderIndex from an OrderId by right-shifting 64 bits /// @param orderId The packed order identifier /// @return The OrderIndex encoded in the upper 64 bits function getOrderIndexFromOrderId(OrderId orderId) pure returns (OrderIndex) { return OrderIndex.wrap(uint64(OrderId.unwrap(orderId) >> 64)); } /// @notice Returns the current block timestamp converted to nanoseconds /// @dev Uses unchecked arithmetic — safe because block.timestamp fits in uint64 for centuries /// @return timestamp The current timestamp in nanoseconds function currentTimestampNs() view returns (uint64 timestamp) { unchecked { // Timestamps are not big so its safe to do unchecked math timestamp = uint64(block.timestamp) * 1e9; } } /// @notice Returns the smaller of two uint256 values /// @param a First value /// @param b Second value /// @return The minimum of a and b function minUint256(uint256 a, uint256 b) pure returns (uint256) { if (a < b) { return a; } else { return b; } } /// @notice Divides a by b, rounding up (ceiling division) /// @dev Returns 0 when a is 0 (avoids underflow in the (a-1)/b+1 pattern) /// @param a Numerator /// @param b Denominator (must be non-zero) /// @return The ceiling of a / b function divCeil(uint256 a, uint256 b) pure returns (uint256) { if (a == 0) return a / b; return ((a - 1) / b) + 1; } /// @notice Divides a by b, rounding down (standard Solidity division) /// @param a Numerator /// @param b Denominator (must be non-zero) /// @return The floor of a / b function divFloor(uint256 a, uint256 b) pure returns (uint256) { return a / b; } /// @notice Signed integer division rounding toward +infinity (ceiling), for any operand signs /// @dev Solidity `/` truncates toward zero; this corrects upward when a positive true quotient was floored. /// Used for protocol-favoring settlement rounding (ceil the debit/payer magnitude). Reverts on b == 0. /// @param a Numerator (signed) /// @param b Denominator (signed, non-zero) /// @return q The ceiling of a / b function divCeilInt(int256 a, int256 b) pure returns (int256 q) { q = a / b; // Remainder exists AND the true quotient is positive (operands share sign) => trunc floored it, round up. if (a % b != 0 && (a < 0) == (b < 0)) ++q; } /// @notice Signed integer division rounding toward -infinity (floor), for any operand signs /// @dev Solidity `/` truncates toward zero; this corrects downward when a negative true quotient was rounded up. /// Used for protocol-favoring settlement rounding (ceil the loss/debit magnitude via a signed floor). /// Reverts on b == 0. /// @param a Numerator (signed) /// @param b Denominator (signed, non-zero) /// @return q The floor of a / b function divFloorInt(int256 a, int256 b) pure returns (int256 q) { q = a / b; // Remainder exists AND the true quotient is negative (operands differ in sign) => trunc rounded up, round down. if (a % b != 0 && (a < 0) != (b < 0)) --q; } /// @notice Linearly interpolates between two output values based on an input's position within an input range /// @dev Assumes inputStart <= input <= inputEnd and outputStart <= outputEnd. Division is deferred /// to the end for maximum integer precision. For a decreasing output range (outputStart > outputEnd), /// use `linearInterpolationReversed` instead. When `inputStart == inputEnd` (degenerate input /// range), returns the midpoint of the output range rather than dividing by zero. /// @param inputStart The start of the input range /// @param inputEnd The end of the input range /// @param input The input value (must be within [inputStart, inputEnd]) /// @param outputStart The output value corresponding to inputStart /// @param outputEnd The output value corresponding to inputEnd /// @return The interpolated output value function linearInterpolation( uint256 inputStart, uint256 inputEnd, uint256 input, uint256 outputStart, uint256 outputEnd ) pure returns (uint256) { if (inputStart == inputEnd) return outputStart + ((outputEnd - outputStart) / 2); return outputStart + (input - inputStart) * (outputEnd - outputStart) / (inputEnd - inputStart); } /// @notice Linearly interpolates between two output values where the output decreases as the input increases /// @dev Assumes inputStart <= input <= inputEnd and outputStart >= outputEnd. Division is deferred /// to the end for maximum integer precision. Use this instead of `linearInterpolation` when /// the output range is decreasing — swapping outputStart/outputEnd in `linearInterpolation` /// would underflow on `outputEnd - outputStart`. When `inputStart == inputEnd` (degenerate /// input range), returns the midpoint of the output range rather than dividing by zero. /// @param inputStart The start of the input range /// @param inputEnd The end of the input range /// @param input The input value (must be within [inputStart, inputEnd]) /// @param outputStart The output value corresponding to inputStart (the larger output) /// @param outputEnd The output value corresponding to inputEnd (the smaller output) /// @return The interpolated output value function linearInterpolationReversed( uint256 inputStart, uint256 inputEnd, uint256 input, uint256 outputStart, uint256 outputEnd ) pure returns (uint256) { if (inputStart == inputEnd) return (outputEnd + (outputStart - outputEnd) / 2); return outputStart - (input - inputStart) * (outputStart - outputEnd) / (inputEnd - inputStart); } /// @notice Calculates a percentage of a value using BPS_TIMES_1K units, rounding up /// @dev 1 BPS = 1000 in BPS_TIMES_1K units; 100% = 10,000,000. /// Formula: divCeil(bpsTimes1k * value, 10_000_000). /// Precondition: `bpsTimes1k` MUST be <= 10,000,000 (100%). SpotPool enforces this cap on /// `takerFeeBpsTimes1k` and `makerFeeBpsTimes1k` at `initialize`. The internal /// `bpsTimes1k * value` multiplication is unchecked for overflow; under the cap, overflow /// requires `value > 2^256 / 10^7 ~= 1.16e70`, unreachable for any standard ERC-20 supply. /// A future caller that passes an uncapped `bpsTimes1k` would risk reverts on extreme inputs. /// @param value The base value /// @param bpsTimes1k The fee rate in basis points times 1000 /// @return The fee amount, rounded up function getBpsOfValueCeil(uint256 value, uint256 bpsTimes1k) pure returns (uint256) { return divCeil(bpsTimes1k * value, 10000 * 1000); } /// @notice Calculates a percentage of a value using BPS_TIMES_1K units, rounding down /// @dev 1 BPS = 1000 in BPS_TIMES_1K units; 100% = 10,000,000. /// Formula: divFloor(bpsTimes1k * value, 10_000_000). /// Precondition: `bpsTimes1k` MUST be <= 10,000,000 (100%). SpotPool enforces this cap on /// `takerFeeBpsTimes1k` and `makerFeeBpsTimes1k` at `initialize`. The internal /// `bpsTimes1k * value` multiplication is unchecked for overflow; under the cap, overflow /// requires `value > 2^256 / 10^7 ~= 1.16e70`, unreachable for any standard ERC-20 supply. /// A future caller that passes an uncapped `bpsTimes1k` would risk reverts on extreme inputs. /// @param value The base value /// @param bpsTimes1k The fee rate in basis points times 1000 /// @return The fee amount, rounded down function getBpsOfValueFloor(uint256 value, uint256 bpsTimes1k) pure returns (uint256) { return divFloor(bpsTimes1k * value, 10000 * 1000); } function absInt128(int128 value) pure returns (uint256) { return value < 0 ? uint256(uint128(~value)) + 1 : uint256(uint128(value)); } /// @notice Returns the absolute value of an int256 as a uint256 /// @dev Uses bitwise complement plus one for the negative branch to avoid overflow on /// `type(int256).min`, since `-type(int256).min` exceeds `type(int256).max`. /// @param value The signed value /// @return The absolute value function absInt256(int256 value) pure returns (uint256) { return value < 0 ? uint256(~value) + 1 : uint256(value); } /// @notice Extracts the 4-byte error selector from raw revert data caught by a `catch` clause. /// @dev For callers that tolerate a rejected external call and want to LOG which rejection it was. /// Custom errors are ABI-encoded selector-first, so the leading 4 bytes identify the error /// uniquely — enough to tell `PostOnlyWouldCross` from `SelfMatchCancelTaker` without carrying the /// full payload into a log. That matters on Somnia, where logs cost roughly 10x Ethereum: a /// `bytes4` occupies one word where an equivalent `bytes` needs three (offset, length, data). /// /// Returns `0x00000000` for revert data shorter than a selector, which covers a bare `revert()`, /// an empty-returndata revert, and an out-of-gas bubble. Callers should treat zero as "unknown /// reason" rather than as a specific error. /// @param reason The raw revert data from `catch (bytes memory reason)` /// @return selector The leading 4 bytes of `reason`, or zero if it is shorter than 4 bytes function revertSelector(bytes memory reason) pure returns (bytes4 selector) { if (reason.length < 4) { return bytes4(0); } return bytes4(reason[0]) | (bytes4(reason[1]) >> 8) | (bytes4(reason[2]) >> 16) | (bytes4(reason[3]) >> 24); } /// @dev Selector of the compiler-generated `Panic(uint256)` error. bytes4 constant PANIC_SELECTOR = 0x4e487b71; /// @notice Decodes raw revert data as the compiler-generated `Panic(uint256)`. /// @dev A panic is a BUG IN THE CALLEE, not a verdict about the caller's request: a failed `assert`, /// arithmetic overflow, division by zero, an out-of-bounds index. It is worth separating because /// it is ABI-shaped exactly like an ordinary rejection — 36 bytes, selector-first — so a caller /// that buckets "any non-empty revert" as a rejection files a genuine invariant violation as a /// routine no-fill. /// /// For callers that MUST NOT re-raise (a liquidation path that may never be the thing that /// reverts; a reactivity callback whose revert would take down every other order in the pass), /// this is how the alarm gets raised without changing control flow. /// /// A well-formed panic carries exactly 36 bytes: the 4-byte selector plus a 32-byte code — 0x01 /// assert, 0x11 arithmetic overflow/underflow, 0x12 division by zero, 0x32 array out-of-bounds. /// Anything else (a custom error, `Error(string)`, empty returndata) returns false. Note the /// converse does NOT hold: a custom error can also signal an invariant violation /// (`DryRunDifference`), and no selector test can distinguish those from a merit rejection. /// @param reason The raw revert data from `catch (bytes memory reason)` /// @return isPanic True when `reason` is a well-formed `Panic(uint256)` /// @return code The panic code, or zero when `isPanic` is false function decodePanic(bytes memory reason) pure returns (bool isPanic, uint256 code) { if (reason.length != 36 || revertSelector(reason) != PANIC_SELECTOR) { return (false, 0); } // `reason` points at its length word; +0x20 reaches the data, +4 more skips the selector. // solhint-disable-next-line no-inline-assembly assembly { code := mload(add(reason, 0x24)) } return (true, code); } // 100% expressed in basis points (10,000 BPS). uint256 constant ONE_HUNDRED_PERCENT_BPS = 10_000; // keccak256(abi.encode(uint256(keccak256("somnia.sentinel.ERC20VaultNativeTokenSentinel")) - 1)) & ~bytes32(uint256(0xff)) /// @dev Sentinel address representing the chain's native token (ETH/SOMI) in the vault balance mapping address constant NATIVE_TOKEN = 0x28f34DeFd2b4CB48d9eE6d89f2Be4Bc601694c00; // solhint-enable private-vars-leading-underscore /// @title OrderIndexAllocator /// @notice LIFO free-list allocator for OrderIndex values. /// @dev Reuses freed indices (via a stack) to minimize new storage slot creation. /// Index 0 is reserved as the empty sentinel and is never allocated. library OrderIndexAllocator { struct Allocator { OrderIndex[] freeIndices; OrderIndex nextNewOrderIndex; } /// @notice Thrown when the allocator has already been initialised (freeIndices is not empty). error AllocatorAlreadyInitialised(); /// @notice Thrown when the allocator's nextNewOrderIndex is not zero, indicating prior initialisation. error AllocatorNotAtZeroIndex(); /// @notice Thrown when freeOrderIndex is called with the reserved zero sentinel. error AllocatorZeroFree(); /// @notice Initializes the allocator, setting the first allocatable index to 1 /// @dev Must be called exactly once; reverts if already initialized /// @param self The allocator storage reference function initialise(Allocator storage self) internal { // We never allocate index 0, it is reserved to mean no index. require(self.freeIndices.length == 0, AllocatorAlreadyInitialised()); require(self.nextNewOrderIndex == OrderIndex.wrap(0), AllocatorNotAtZeroIndex()); self.nextNewOrderIndex = OrderIndex.wrap(1); } /// @notice Allocates an OrderIndex, reusing a freed index if available /// @dev Pops from the free stack (LIFO) when non-empty, otherwise increments the counter /// @param self The allocator storage reference /// @return The allocated OrderIndex (always non-zero) function allocateOrderIndex(Allocator storage self) internal returns (OrderIndex) { if (self.freeIndices.length == 0) { // There are no free indices, allocate a new one. OrderIndex orderIndex = self.nextNewOrderIndex; self.nextNewOrderIndex = OrderIndex.wrap(OrderIndex.unwrap(self.nextNewOrderIndex) + 1); return orderIndex; } else { // Pop off a free order index. OrderIndex orderIndex = self.freeIndices[self.freeIndices.length - 1]; self.freeIndices.pop(); return orderIndex; } } /// @notice Returns an OrderIndex to the free list for future reuse /// @dev Reverts with `AllocatorZeroFree` if `orderIndex` is the reserved zero sentinel. /// The no-double-free invariant is maintained by upstream callers via owner / /// orderId / liveness checks; this library does not track per-index free state to /// avoid an SSTORE on Somnia's hot path. /// @param self The allocator storage reference /// @param orderIndex The index to free (pushed onto the LIFO stack) function freeOrderIndex(Allocator storage self, OrderIndex orderIndex) internal { require(orderIndex != OrderIndex.wrap(0), AllocatorZeroFree()); self.freeIndices.push(orderIndex); } }lib/somnia-dex-protocol/src/interfaces/IBinaryMarket.sol
// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.34; /// @title IBinaryMarket /// @notice Read-only surface a binary prediction market must expose so that a `BinaryPool` can /// gate trading and route redeems. Deliberately tiny: the pool delegates ALL resolution /// state — what the market is, when it expires, who the oracle is, what counts as a /// valid answer — to whatever contract implements this. The pool just polls these views /// on order placement and on finalize. /// @dev The split keeps the pool implementation in the dex repo prediction-market-agnostic: /// market lifecycle (Listed/Trading/Locked/Resolved/Voided) and oracle integration live in /// the consuming protocol (e.g. prophecy-markets), not in the dex. /// /// Settlement v3: resolution state is a CTF-style PAYOUT VECTOR (`payoutNumerators`), /// scaled against the family denominator `ONE_HUNDRED_PERCENT_BPS * 1_000` (10_000_000). /// A resolved binary market stores a one-hot vector; a voided market stores the uniform /// vector. All settlement math downstream reads only the vector — `isVoided()` survives as /// the explicit fee-waiver / observability flag, never as a payout input. /// /// Implementations MUST keep these views cheap (pool calls them in the hot path of every /// `placeOrder` and at finalize). The pool reads them with low-gas eth_call semantics — no /// writes, no external calls of their own. interface IBinaryMarket { /// @notice Whether new orders may be placed on the linked pool. /// @dev The pool's `_onOrderPlaced` hook reverts when this returns false. Existing orders /// remain on the book until cancelled / filled — the gate only blocks new placement. /// Typical implementations return `true` in the Trading state and `false` everywhere /// else (Listed before tradingStart, Locked after expiry, Resolved/Voided). function tradingActive() external view returns (bool); /// @notice Whether the market has been resolved to a concrete payout vector. /// @dev When `true`, `payoutNumerators()` MUST return a valid vector whose raw sum equals /// the family denominator. Mutually exclusive with `isVoided()`. function isResolved() external view returns (bool); /// @notice Whether the market has been voided (no winner — uniform payout vector). /// @dev Mutually exclusive with `isResolved()`. The settlement layer waives the /// settlement fee exactly when this flag is set (explicit flag, never inferred /// from the vector shape). function isVoided() external view returns (bool); /// @notice Per-outcome payout numerators after settlement (resolve OR void), indexed by /// outcome index. `Σ == ONE_HUNDRED_PERCENT_BPS * 1_000` raw. /// @dev Empty until the market settles. The pool snapshots this at `finalize` and hands it /// to the settlement singleton, which fee-scales and records it permanently. function payoutNumerators() external view returns (uint256[] memory); }lib/somnia-dex-protocol/src/interfaces/IBinaryPool.sol
// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.34; import {OrderId} from "../common/Common.sol"; import {IBinaryMarket} from "./IBinaryMarket.sol"; import {IOrderBook, OrderBookParameters} from "./IOrderBook.sol"; import {IOutcomeToken6909} from "./IOutcomeToken6909.sol"; /// @title OrderKind /// @notice The four concrete binary order kinds a caller places through `placeBinaryOrder`. v2 no /// longer decodes the kind from `(isBid, userData)` — the caller states it explicitly and the /// pool maps it onto the underlying book's `(isBid, price)` wire convention: /// - `BUY_YES` → bid on the YES book at the YES-side `price`. /// - `SELL_YES` → ask on the YES book at the YES-side `price`. /// - `BUY_NO` → ask on the (inverted) NO book; submitted price is the YES-side complement. /// - `SELL_NO` → bid on the (inverted) NO book; submitted price is the YES-side complement. /// `price` supplied to `placeBinaryOrder` is ALWAYS the YES-side price (identical to v1's /// on-the-wire storage), so the mapping is byte-identical to v1's `_kindOf` inverse. enum OrderKind { BUY_YES, SELL_YES, BUY_NO, SELL_NO } /// @title CloseState /// @notice Lifecycle of the current market's CLOSING BOOK — the set of resting orders whose /// `expireTimestampNs == marketExpiryNs` (placement caps every order's expiry at the market's, /// so exactly these orders were alive at the close). The pool guarantees that book cannot be /// dismantled until the market is terminal or its closing price has been captured, which is /// what makes a closing-price read at void time always possible ("lock until terminal or /// captured" — see `captureClose`). /// - `Open`: no capture yet. Post-expiry removals of closing-book orders revert /// `CloseNotCaptured` unless the market is already terminal (which latches below). /// - `Captured`: `captureClose` found both a best bid and a best ask among the closing /// orders; their mid is stored in `closingPrice`. The lock is lifted. /// - `CapturedDegenerate`: `captureClose` ran but a side had no closing order — there is no /// two-sided close, no mid is stored, consumers fall back to a uniform void. Lock lifted. /// - `TerminalLatch`: a post-expiry removal observed the market already terminal /// (resolved or voided) and latched that fact — the lock is lifted without a capture /// (a terminal market's payout vector is already fixed; nothing needs the closing book). /// @dev State is per-market: `recycle` resets it to `Open` for the fresh market. The numeric values /// are load-bearing for the packed storage (`Open` must be the zero value). enum CloseState { Open, Captured, CapturedDegenerate, TerminalLatch } /// @title BinaryPoolParameters /// @notice Fee + recipient config a BinaryPool is initialized with. /// @param makerFeeBpsTimes1k Protocol maker fee, basis-points × 1000 (sub-bps precision). /// @param takerFeeBpsTimes1k Protocol taker fee. /// @param maxBuilderFeeBpsTimes1k Per-order builder fee cap enforced at placeOrder. Mirrors the /// spot-pool convention so builder + protocol fees combined never exceed 100%. /// @param settlementFeeBpsTimes1k Fee skimmed ONCE from the winning payout pool when a resolved /// market is first redeemed (basis-points × 1000; 0 = none). Charged on the whole winning /// backing (incl. never-redeemed tokens), then every winner redeems for `(1 - fee)`. Never /// charged on a voided market. /// @param feeRecipient Address that receives protocol fees on every fill (credited to its vault /// balance; pulled out via the inherited `ERC20Vault.withdraw`). struct BinaryPoolParameters { uint256 makerFeeBpsTimes1k; uint256 takerFeeBpsTimes1k; uint256 maxBuilderFeeBpsTimes1k; uint256 settlementFeeBpsTimes1k; address feeRecipient; } /// @title BinaryPoolOrderInfo /// @notice Per-order auxiliary state that BinaryPool tracks alongside the base OrderBook order. /// Exposed for consumers that want to inspect specific orders (e.g. off-chain indexers, /// invariant tests). Keyed by `OrderIndex` in the pool's storage; the slot is reused /// across order lifetimes. /// @param orderId The full `OrderId` this info belongs to (used to validate against stale reads). /// @param lockedCollateral Collateral escrowed for a BUY order — mutually exclusive with `lockedOutcome`. /// @param lockedOutcome Outcome tokens escrowed for a SELL order — mutually exclusive with `lockedCollateral`. /// @param outcomeIdx 0 = YES, 1 = NO. Derived from the explicit `OrderKind` supplied at placement (v2); /// NOT from `userData`, which is opaque market-maker bookkeeping and never interpreted by the pool. /// @param isBuy True for BUY_YES / BUY_NO; false for SELL_YES / SELL_NO. /// @param builder Optional builder code address (zero if none). /// @param builderFeeBpsTimes1k Per-order builder fee cap approved at placement. struct BinaryPoolOrderInfo { OrderId orderId; uint256 lockedCollateral; uint256 lockedOutcome; uint8 outcomeIdx; bool isBuy; address builder; uint96 builderFeeBpsTimes1k; } /// @title BinaryPoolInfo /// @notice Bundled snapshot of a BinaryPool's on-chain state — returned by `getBinaryPoolParams()` /// for tooling that needs to read many pools in one RPC round-trip. /// @param collateralToken ERC-20 collateral backing the pool. /// @param market Linked `IBinaryMarket` contract that owns resolution state. /// @param outcomeToken Shared ERC-6909 outcome-token singleton this pool mints / burns against. /// @param yesId ERC-6909 id of the YES outcome (index 0) on the singleton. /// @param noId ERC-6909 id of the NO outcome (index 1) on the singleton. /// @param oneCollateral Snapshot of `10 ** collateralToken.decimals()` — the price scaling base. /// @param setBacking Total collateral held as backing for outstanding YES + NO supply. /// @param feeRecipient Address that receives protocol fees. /// @param makerFeeBpsTimes1k Protocol maker fee, basis-points × 1000. /// @param takerFeeBpsTimes1k Protocol taker fee, basis-points × 1000. /// @param maxBuilderFeeBpsTimes1k Per-order builder fee cap. /// @param settlementFeeBpsTimes1k One-time settlement fee on the winning payout, basis-points × 1000. /// @param settlement The permanent `BinarySettlement` singleton that finalizes + redeems this pool's markets. /// @param marketNonce The pool's own per-market counter (starts at 1, ++ on `recycle`). Combined with /// the pool address it disambiguates the outcome ids of successive markets on a recycled pool. /// @param finalized True once the current market's backing + resolution snapshot have been swept to the /// settlement singleton; the pool cannot be recycled until this is true (and the books are empty). struct BinaryPoolInfo { address collateralToken; address market; address outcomeToken; uint256 yesId; uint256 noId; uint256 oneCollateral; uint256 setBacking; address feeRecipient; uint256 makerFeeBpsTimes1k; uint256 takerFeeBpsTimes1k; uint256 maxBuilderFeeBpsTimes1k; uint256 settlementFeeBpsTimes1k; address settlement; uint64 marketNonce; bool finalized; } /// @title BinaryFinalizeSnapshot /// @notice The full per-market record `BinaryPool.finalize()` hands to the `BinarySettlement` singleton /// in the same call that sweeps the market's collateral backing over to it. Captured from the /// pool's own storage BEFORE any `recycle` can overwrite `parameters`/`market`, so the /// settlement singleton owns a frozen, self-sufficient copy of everything redemption needs. /// @dev The settlement contract balance-diff-verifies that it actually received exactly `backing` /// collateral of `collateralToken` in the same transaction, so the snapshot is trust-minimised. /// @param collateralToken The ERC-20 the market's backing is denominated in (fixed for the pool's life). /// @param backing The exact collateral amount transferred to settlement (== `setBacking` pre-finalize). /// @param yesId The YES outcome id (index 0) for the finalized market's nonce. /// @param noId The NO outcome id (index 1) for the finalized market's nonce. /// @param voided True if the market voided (uniform payout vector; NEVER a settlement fee). Explicit /// fee-waiver flag — settlement math reads only the vector, this flag only gates the fee skim. /// @param payoutNumerators RAW per-outcome payout numerators snapshotted from the market /// (`Σ == ONE_HUNDRED_PERCENT_BPS * 1_000`); one-hot on a resolved binary market, uniform on /// a void. The settlement singleton validates the raw sum, fee-scales, and records them. /// @param settlementFeeBpsTimes1k The one-time settlement fee rate to skim at finalize on a resolved /// (non-voided) market, in basis-points × 1000. /// @param feeRecipient The address that receives the settlement fee skim. struct BinaryFinalizeSnapshot { address collateralToken; uint256 backing; uint256 yesId; uint256 noId; bool voided; uint256[] payoutNumerators; uint256 settlementFeeBpsTimes1k; address feeRecipient; } /// @title IBinaryPool /// @notice Pool-facing surface for a binary prediction-market CLOB. Trading goes through the /// inherited `IOrderBook.placeOrder` family; this interface covers the prediction- /// specific extensions: complete-set mint / burn (user-facing arbitrage) and outcome- /// token redeem after resolution. /// @dev v2 order placement: callers use `placeBinaryOrder` / `placeBinaryOrderFor` and state the YES/NO /// side EXPLICITLY via `OrderKind` (BUY_YES / SELL_YES / BUY_NO / SELL_NO). The pool maps the kind /// to the base `(isBid, price)` wire convention internally and carries it to the fill hooks; it is /// the sole side-attribution source (also emitted as `BinaryOrderPlaced`). `userData` is opaque /// market-maker bookkeeping — passed to the base engine verbatim and NEVER decoded for side, fees, /// or settlement. The base generic `placeOrder` / `placeOrderFor` / `amendOrder` entries are /// rejected (`UseBinaryPlacement`) because they do not set the explicit kind. /// The pool's 4-case settlement matrix (direct YES fill, direct NO fill, mint-a-pair, /// burn-a-pair) is internal to `_onOrderFilled`; callers just place binary orders normally. interface IBinaryPool { /*////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////*/ /// @notice Emitted when a pair of outcome tokens is minted from collateral — either /// user-triggered via `mintSet` or pool-triggered during a BUY_YES × BUY_NO fill. /// @param payer Address whose collateral backed the freshly minted pair (the caller in /// user-triggered mints, the pool itself in fill-triggered mints). /// @param yesTo Recipient of the YES half. /// @param noTo Recipient of the NO half. /// @param amount Quantity of collateral consumed and of each outcome side minted. event SetMinted(address indexed payer, address indexed yesTo, address indexed noTo, uint256 amount); /// @notice Emitted when a pair of outcome tokens is burned back to collateral — either /// user-triggered via `burnSet` or pool-triggered during a SELL_YES × SELL_NO fill. /// @param holder Holder whose YES + NO pair was burned (the caller in user-triggered burns, /// the pool itself in fill-triggered burns). /// @param amount Quantity of each outcome side burned and of collateral released. event SetBurned(address indexed holder, uint256 amount); /// @notice Emitted for every order placed through `placeBinaryOrder` / `placeBinaryOrderFor`, /// carrying the explicit YES/NO order kind the caller chose. /// @dev This is the ONLY authoritative side-attribution source in v2: `userData` is now opaque /// market-maker bookkeeping and MUST NOT be decoded by indexers/SDKs to infer the side. Pair /// this with the base `OrderPlaced(orderId, order)` (same `orderId`) to attribute a fill's /// side. Fires from `_onOrderPlaced`, i.e. for every order that passes placement (including /// IOC/FOK that never rest). /// @param orderId The base OrderBook order id assigned to the placed order. /// @param kind The explicit binary order kind (BUY_YES / SELL_YES / BUY_NO / SELL_NO). event BinaryOrderPlaced(OrderId indexed orderId, OrderKind kind); /// @notice Emitted when the pool's current market is finalized: its entire `setBacking` (plus the /// resolution + fee-term snapshot) is swept to the `BinarySettlement` singleton, which /// becomes the permanent redemption home for that market's holders. /// @dev Redemption itself now lives on `BinarySettlement` — the pool no longer has a `redeem` /// function. After this fires, `setBacking == 0` and `finalized == true` for the current /// nonce, and the pool can be `recycle`d onto a fresh market once its books are empty. /// @param marketNonce The pool's market counter for the market being finalized. /// @param backing The collateral amount swept to settlement (== the pre-finalize `setBacking`). event PoolFinalized(uint64 indexed marketNonce, uint256 backing); /// @notice Emitted when a finalized, drained pool is re-pointed at a fresh market — the reuse /// mechanism that lets one pool (same address, warm order-slot arena) serve successive /// markets without a fresh deploy. /// @param marketNonce The NEW market counter after the increment (the nonce of the fresh market). /// @param market The fresh `IBinaryMarket` the pool now trades against. event PoolRecycled(uint64 indexed marketNonce, address indexed market); /// @notice Emitted when an auto-deliver collateral payout could not be pushed to the recipient's /// wallet (e.g. recipient is a contract that reverts on receive) and was credited to /// their internal vault balance instead. The recipient can withdraw via /// `ERC20Vault.withdraw(collateralToken, amount)` afterwards. /// @param owner The recipient whose vault was credited. /// @param token The token that fell back (always `collateralToken` — the vault only accepts collateral). /// @param amount Amount credited to the vault. event PayoutFallbackToVault(address indexed owner, address indexed token, uint256 amount); /// @notice Emitted when a user grants (or revokes) a per-order builder-fee approval. /// @dev Setting `maxFeeBpsTimes1k = 0` revokes an existing approval. The stored value is a raw /// cap; at order-placement time it's clamped by the pool's protocol-wide /// `maxBuilderFeeBpsTimes1k`, so a stored value that exceeds the current cap is silently /// capped there. See `getEffectiveBuilderApproval` for the enforced ceiling. /// @param user Address granting (or revoking) the approval. /// @param builder Builder address being approved. /// @param maxFeeBpsTimes1k Maximum per-order builder fee the builder may charge (0 = revoked). event BuilderApproved(address indexed user, address indexed builder, uint256 maxFeeBpsTimes1k); /// @notice Emitted on every fill of a builder-tagged order with the per-fill fee amount routed /// to the builder. Off-chain reconcilers can sum this stream per `builder` to attribute /// routing revenue without joining `OrderFilled` against `BinaryPoolOrderInfo.builder`. /// @dev Fires once per side per fill (maker side and taker side each emit if they carry a /// builder). Skipped when the order has no builder. `amount` may be zero on small fills /// because the fee is floor-rounded; the event still fires so consumers can detect that /// the builder path executed. `token` is always the pool's `collateralToken` — every /// BinaryPool fee routes in the collateral leg. /// @param orderId The order whose fill credited the builder. /// @param builder Builder address that received the fee. /// @param token Collateral token the fee was paid in. /// @param amount Fee amount routed to the builder (floor-rounded, may be zero). event BuilderFeeCharged(OrderId indexed orderId, address indexed builder, address indexed token, uint256 amount); /// @notice Emitted on every fill leg that pays a protocol fee, mirroring `BuilderFeeCharged` /// so indexers get one stream per revenue channel. /// @dev Fires exactly at every fill leg where BinaryPool computes a protocol fee: once per /// direct YES / NO fill (the seller's leg, taker-rate), twice per burn-a-pair fill (each /// seller's leg, maker-rate), and never on mint-a-pair (mint creates fresh backing — /// buyer-only, no protocol fee). `amount` may be zero when the rate is zero or the fill /// is small enough for floor-rounding to zero; the event still fires so consumers see /// the protocol path executed. `token` is always the pool's `collateralToken`. /// @param orderId The seller-side order whose proceeds funded the fee. /// @param recipient Protocol fee recipient (`BinaryPoolParameters.feeRecipient`). /// @param token Collateral token the fee was paid in. /// @param amount Fee amount routed to `recipient` (floor-rounded, may be zero). /// @param isTakerSide True if the taker rate was applied (direct fill), false for the maker /// rate (burn-a-pair). Lets indexers split protocol revenue by rate class. event ProtocolFeeCharged( OrderId indexed orderId, address indexed recipient, address indexed token, uint256 amount, bool isTakerSide ); /// @notice Emitted when the current market's closing price is captured (permissionless /// `captureClose`), lifting the closing-book lock. /// @dev Fires exactly once per market (the capture is latched; a second `captureClose` reverts /// `CloseAlreadyCaptured`). On a two-sided close (`CloseState.Captured`) `closingMid` is the /// arithmetic mid of the best closing bid/ask in raw book-price units (scale: the pool's /// `oneCollateral`). On a one-sided/empty close (`CloseState.CapturedDegenerate`) all three /// prices are 0 for the missing side(s) and consumers must fall back to a uniform void. /// @param marketNonce The pool's market counter for the market whose close was captured. /// @param bestBid Best closing-book bid price (YES-side, raw book units; 0 if none found). /// @param bestAsk Best closing-book ask price (YES-side, raw book units; 0 if none found). /// @param closingMid `(bestBid + bestAsk) / 2` when two-sided, else 0. /// @param state The resulting `CloseState` (`Captured` or `CapturedDegenerate`). event ClosingCaptured( uint64 indexed marketNonce, uint256 bestBid, uint256 bestAsk, uint256 closingMid, CloseState state ); /*////////////////////////////////////////////////////////////// ERRORS //////////////////////////////////////////////////////////////*/ /// @notice Thrown when an action requires the linked market to allow trading. error TradingNotActive(); /// @notice Thrown when `finalize` is attempted on a market that hasn't resolved or voided yet. error MarketNotSettled(); /// @notice Thrown when the outcome index parameter is outside the binary range [0, 1]. error InvalidOutcomeIndex(); /// @notice Thrown when a settlement-gated function (`finalize`) is called by any address other /// than the pool's init-frozen `settlement` singleton. error NotSettlement(); /// @notice Thrown when `finalize` is called on a market that has already been finalized. error AlreadyFinalized(); /// @notice Thrown when `recycle` is called before the current market has been finalized. error NotFinalized(); /// @notice Thrown when `recycle` is called while orders still rest on either side of the book. /// The market's collateral cannot be repurposed until the book is fully drained (via the /// permissionless expiry sweeps, which every order is eligible for after market expiry). error BooksNotEmpty(); /// @notice Thrown at order placement when an order's `expireTimestampNs` is zero (never-expiring) /// or beyond the linked market's expiry converted to the book's nanosecond timescale. /// Bounding every order to the market's expiry is what makes the book drainable purely /// via the existing permissionless expiry sweeps after the market locks. error OrderExpiryBeyondMarket(); /// @notice Thrown when `recycle` is handed a market that is missing (zero address) or already /// settled (resolved or voided) — a recycled pool must start from a live, unsettled market. error FreshMarketRequired(); /// @notice Thrown when the settlement singleton passed to `initialize` is the zero address. error InvalidSettlement(); /// @notice Thrown when the linked market exposes an expiry timestamp that is zero (a market must /// have a finite expiry for the order-expiry cap + expiry-sweep drain to be well-defined). error InvalidMarketExpiry(); /// @notice Thrown when a vault `deposit` / `withdraw` targets a token the pool does not /// custody. The vault only accepts the pool's `collateralToken`; outcome tokens live /// in raw pool balance and are not deposit-eligible. error InvalidDepositOrWithdrawal(); /// @notice Thrown when a mint/burn/redeem is called with `amount == 0`. Zero-amount calls /// previously succeeded as no-ops but wasted gas + emitted misleading zero-amount events. error ZeroAmount(); /// @notice Thrown when a redeem or burn path would debit `setBacking` below zero. In a healthy /// pool this is impossible (backing tracks supplies), but the explicit check surfaces a /// readable error if any future refactor breaks the invariant. error InsufficientBacking(); /// @notice Thrown when `_onOrderFilled` dispatches to a taker/maker kind pair not covered by /// the 4-case fill matrix. Defensive: today the OrderBook's matching rules and /// `_kindOf`'s decoding prevent unreachable pairs, but this guarantees loud failure /// if any future kind is added without extending settlement. /// @param takerKind The taker order's decoded OrderKind (as uint8). /// @param makerKind The maker order's decoded OrderKind (as uint8). error UnexpectedFillPair(uint8 takerKind, uint8 makerKind); /// @notice Thrown when an order's `price` is not strictly within `(0, oneCollateral)`. /// @dev A binary prediction-market share redeems for at most 1 unit of collateral, so any /// YES-equivalent `price <= 0` or `price >= oneCollateral` is economically impossible. /// Rejecting at the placement boundary blocks: /// - `price == 0`: dust orders that fill for 0 collateral (creates untracked outflow of outcome tokens); /// - `price >= oneCollateral`: BUY_NO branch's `oneCollateral_ - order.price` underflows in Solidity 0.8; /// - accounting drift for mint-a-pair edge cases where floor(qty * price / oneCollateral) rounds to zero /// on one side while the other side over-locks — the domain invariant `0 < price < oneCollateral` /// is the security boundary. Applies uniformly to BUY_YES / SELL_YES / BUY_NO / SELL_NO because /// `price` is always the YES-equivalent value on the order. error PriceOutOfBounds(); /// @notice Thrown when an order reaches the pool's placement hook WITHOUT a pending binary /// order-kind — i.e. it came through the base `IOrderBook.placeOrder` / `placeOrderFor` / /// `amendOrder` generic entries rather than `placeBinaryOrder` / `placeBinaryOrderFor`. /// @dev v2 stops interpreting `userData` (it is opaque market-maker bookkeeping now); the YES/NO /// order kind is carried explicitly through the binary entries via a transient slot. The /// generic entries never set that slot, so they are rejected here. Consumers MUST use the /// binary placement entries. error UseBinaryPlacement(); /// @notice Thrown when the outcome-token singleton passed to `initialize` is the zero address. error InvalidOutcomeToken(); /// @notice Thrown when an ERC-6909 outcome-token transfer returns false (escrow / settlement). error OutcomeTransferFailed(); /// @notice Thrown when fee parameters exceed the 100% bpsTimes1k precondition. error FeeTooHigh(); /// @notice Thrown when the builder fee on a placed order exceeds the configured cap. error BuilderFeeExceedsCap(); /// @notice Thrown when `approveBuilder` is called with the zero address. A non-zero builder is /// required — the zero address is the sentinel for "no builder" on placed orders and /// approving it would create ambiguity with unbuilder-tagged orders. error InvalidBuilder(); /// @notice Thrown when the configured fee recipient is the zero address. error InvalidFeeRecipient(); /// @notice Thrown when native value is attached to a placement entry. BinaryPool collateral is /// always ERC-20 and no path consumes native, so any `msg.value != 0` would strand the /// funds permanently. The `payable` placement entries reject it up front. Mirrors PerpPool. error UnexpectedNativeDeposit(); /// @notice Thrown when `getOrderInfo` is queried with an `OrderId` whose stored slot has been /// reused by a later order at the same index — the stored `orderId` no longer matches the /// requested one, so the returned info would be half-stale. Mirrors the base `getOrder` /// guard, which reverts on the same mismatch. error OrderInfoIdMismatch(); /// @notice Thrown when `initialize` or `recycle` is given a `marketExpiryNs` that is not strictly in /// the future relative to the current block time (converted to nanoseconds). A past expiry /// would brick the (fresh) generation — every order placement caps at the market expiry, so no /// order could ever rest. Minimal sanity bound on the supplied market clock. error MarketExpiryInPast(); /// @notice Thrown when a payout's external ERC-20 send is attempted with less than /// `_PAYOUT_FALLBACK_GAS_RESERVE` gas remaining, so the vault-fallback bookkeeping could not /// complete on a failed send. Mirrors SpotPool; guards the common cheap-revert failure mode. /// @param gasLeft The gas remaining when the reserve check ran. error InsufficientGasForPayout(uint256 gasLeft); /// @notice Thrown when `lotSize * tickSize < oneCollateral`, i.e. a single-lot fill at the minimum /// price tick would round down to zero collateral. /// @dev The matcher fills in `lotSize` increments, so the smallest fillable quantity is `lotSize` and /// the smallest non-zero fill price is `tickSize`. Bounding `lotSize * tickSize >= oneCollateral` /// guarantees every fill moves at least 1 unit of collateral — otherwise outcome tokens (which /// redeem for real collateral) could change hands for free. Mirrors SpotPool's /// `ZeroQuoteFillsAllowed` guard; enforced at `initialize` and on every `updateOrderBookParameters`. error ZeroCollateralFillsAllowed(); /// @notice Thrown when a post-expiry removal targets a CLOSING-BOOK order (one whose /// `expireTimestampNs == marketExpiryNs`) while the market is neither terminal nor /// captured. The closing book is the only on-chain record of the market's closing price; /// it may not be dismantled before that price is safe ("lock until terminal or captured"). /// @dev Remedy: call the permissionless `captureClose` first (or wait for resolution) — a /// multicall of capture + cancel releases the escrow in one transaction. Only unfilled /// resting-order escrow is ever held back; position holders redeem from settlement and are /// never affected. Stale orders that expired before the market close sweep freely at all /// times. error CloseNotCaptured(); /// @notice Thrown when `captureClose` is called but the close is no longer `Open` — it was /// already captured, or a post-expiry removal already latched the market terminal. error CloseAlreadyCaptured(); /// @notice Thrown when `captureClose` is called before the market's expiry — the closing book /// is still forming and a capture would freeze a pre-close price. error CaptureTooEarly(); /// @notice Thrown when a closing-book scan exhausts its step budget before finding a closing /// order (or proving the side empty). The walk is best-first, so steps are only consumed /// by stale (pre-close-expired, not-yet-swept) orders sitting above the closing book. /// @dev Remedy: retry with a larger `maxSteps` — capture is a standalone transaction, so the gas /// budget is entirely the caller's choice — or sweep the stale levels first (they are never /// locked) and retry. error CaptureStepsExhausted(); /*////////////////////////////////////////////////////////////// PREDICTION-SPECIFIC API //////////////////////////////////////////////////////////////*/ /// @notice Mint a complete YES+NO pair to the caller (or split between two recipients) by /// depositing `amount` collateral. /// @dev Mirrors the dex's `mintSetSplit` pool-internal hook but exposed user-facing for /// arbitrage / liquidity-bootstrap flows. Increments `setBacking` by `amount`, mints /// `amount` of each outcome token. Auto-pull semantics — caller must have approved the /// pool for `amount` collateral. /// @dev Reverts with `ZeroAmount` if `amount == 0` (closes C10). /// @dev Reverts with `TradingNotActive` if `market.tradingActive()` returns false. /// @param yesTo Recipient of the YES half. /// @param noTo Recipient of the NO half. /// @param amount Quantity of collateral to deposit and of each outcome token to mint. function mintSet(address yesTo, address noTo, uint256 amount) external; /// @notice Burn `amount` of YES and `amount` of NO from the caller and return `amount` /// collateral to the caller. /// @dev Symmetric inverse of `mintSet`. Collateral is delivered wallet-first with vault /// fallback via `_deliverCollateral` — see the `PayoutFallbackToVault` event. /// @dev Reverts with `ZeroAmount` if `amount == 0` (closes C10). /// @dev Reverts with `TradingNotActive` if `market.tradingActive()` returns false. /// @dev Reverts with `InsufficientBacking` if `amount > setBacking` (drift guard, closes C7). /// @dev Reverts (via outcome-token's own `_burn`) if caller doesn't hold both sides. /// @param amount Quantity of each outcome token to burn and of collateral to release. function burnSet(uint256 amount) external; /// @notice Place a binary order stating the YES/NO order kind EXPLICITLY (rather than encoding it /// in `userData`). This is the ONLY supported placement entry on a BinaryPool — the base /// `IOrderBook.placeOrder` / `placeOrderFor` / `amendOrder` generic entries revert /// `UseBinaryPlacement` because v2 leaves `userData` free for market-maker bookkeeping. /// @dev `price` is ALWAYS the YES-side price; the pool maps `kind` onto the base book's /// `(isBid, price)` per the v1 wire convention (see the `OrderKind` NatSpec). `userData` is /// passed through to the base engine verbatim — stored on the `Order` and emitted in /// `OrderPlaced` — and is never interpreted by the pool. Escrow / fees / fills are identical /// to v1. The order's `kind` is emitted in `BinaryOrderPlaced` for side attribution. /// @dev Reverts with `PriceOutOfBounds` if the YES-side `price` is not strictly within /// `(0, oneCollateral)`; with `TradingNotActive` if the market is not trading; with /// `OrderExpiryBeyondMarket` if `expireTimestampNs` is zero or beyond the market's expiry; /// with `BuilderFeeExceedsCap` on a builder-fee/approval violation. Base-engine reverts /// (tick/lot/min-quantity) apply as in v1. /// @param kind The explicit binary order kind (BUY_YES / SELL_YES / BUY_NO / SELL_NO). /// @param price The YES-side limit price (in collateral units, strictly within `(0, oneCollateral)`). /// @param quantity The outcome-token quantity to trade. /// @param expireTimestampNs Order expiry in nanoseconds; must be `> 0` and `<= marketExpiryNs`. /// @param orderType Base order type (Normal / FillOrKill / ImmediateOrCancel / PostOnly). /// @param selfMatchingOption Base self-matching behaviour (CancelTaker / CancelMaker). /// @param builder Optional builder code (zero for none). /// @param builderFeeBpsTimes1k Per-order builder fee rate (BPS_TIMES_1K); must respect the caps. /// @param userData Opaque passthrough (market-maker bookkeeping); stored + emitted verbatim, never /// interpreted by the pool. /// @return success True if the order was placed or (fully/partially) filled per `orderType`. /// @return id The base OrderBook order id (zero when `success` is false). function placeBinaryOrder( OrderKind kind, uint256 price, uint256 quantity, uint64 expireTimestampNs, IOrderBook.OrderType orderType, IOrderBook.SelfMatchingOption selfMatchingOption, address builder, uint96 builderFeeBpsTimes1k, uint64 userData ) external payable returns (bool success, OrderId id); /// @notice `placeBinaryOrder` on behalf of `owner`. Delegated placement on a binary pool is /// authorized SOLELY by the approved-contracts allowlist: the caller MUST be an approved /// placing-contract (`isApprovedContractToPlaceOrders`). This is the path the clob module's /// orchestrator uses. Unlike the base `placeOrderFor`, the per-user operator registry is /// intentionally NOT consulted here — BinaryPool never overrides `_operatorGate`, so that /// branch is fail-closed (`false`) and would never admit anyone. Delegation for binary pools /// is exclusively the approved-contracts model; see the BinaryPool design-decisions. /// @dev Same semantics + reverts as `placeBinaryOrder`, but escrow is pulled from `owner`; reverts /// `OnlyApprovedContracts` if the caller is not an approved placing-contract. /// @param owner The order owner whose funds are escrowed and who owns the resulting order. /// @param kind The explicit binary order kind (BUY_YES / SELL_YES / BUY_NO / SELL_NO). /// @param price The YES-side limit price (strictly within `(0, oneCollateral)`). /// @param quantity The outcome-token quantity to trade. /// @param expireTimestampNs Order expiry in nanoseconds; must be `> 0` and `<= marketExpiryNs`. /// @param orderType Base order type. /// @param selfMatchingOption Base self-matching behaviour. /// @param builder Optional builder code (zero for none). /// @param builderFeeBpsTimes1k Per-order builder fee rate (BPS_TIMES_1K). /// @param userData Opaque passthrough; stored + emitted verbatim, never interpreted by the pool. /// @return success True if the order was placed or filled per `orderType`. /// @return id The base OrderBook order id (zero when `success` is false). function placeBinaryOrderFor( address owner, OrderKind kind, uint256 price, uint256 quantity, uint64 expireTimestampNs, IOrderBook.OrderType orderType, IOrderBook.SelfMatchingOption selfMatchingOption, address builder, uint96 builderFeeBpsTimes1k, uint64 userData ) external payable returns (bool success, OrderId id); /// @notice Finalize the pool's current market — sweep its entire `setBacking` (plus the resolution /// + fee-term snapshot) to the `BinarySettlement` singleton, which becomes the permanent /// redemption home. ONLY the settlement singleton may call this (it is permissionless at /// the settlement layer, but funnelled through it so the balance-diff check is trust-minimised). /// @dev Requires `market.isResolved() || market.isVoided()` and `!finalized`. Effects: snapshot the /// backing + resolution + fee terms (read from `parameters` BEFORE any recycle overwrites /// them), zero `setBacking`, set `finalized = true`, transfer exactly `backing` collateral to /// the settlement singleton, emit `PoolFinalized`. Returns the snapshot for the settlement /// contract to record. /// @dev Reverts `NotSettlement` if the caller is not the settlement singleton; `AlreadyFinalized` /// if already finalized; `MarketNotSettled` if the market is neither resolved nor voided. /// @return snapshot The frozen per-market record (see `BinaryFinalizeSnapshot`). function finalize() external returns (BinaryFinalizeSnapshot memory snapshot); /// @notice Re-point a finalized, fully-drained pool at a fresh market — the reuse mechanism that /// lets one pool (same address, warm order-slot arena) serve successive markets. /// @dev onlyOwner. Requires `finalized`, `booksEmpty()`, `setBacking == 0`, and a fresh /// (non-zero, not-yet-settled) `newMarket`. Effects: validate + store `newParams`; validate + /// store `newBookParams` (re-running the base tick/lot/min + the A2 zero-collateral-fill /// guard); store the new market's expiry cap; set `market = newMarket`; `++marketNonce`; /// `finalized = false`; emit `PoolRecycled`. The new nonce shifts every outcome id, so the /// previous market's positions remain redeemable on the settlement singleton forever without /// collision. Vault balances + builder approvals persist across recycles by design (same /// collateral token, user-owned). /// @dev Reverts `NotFinalized` / `BooksNotEmpty` / `FreshMarketRequired` on the respective guards, /// `InvalidMarketExpiry` if `newMarketExpiryNs == 0`, `MarketExpiryInPast` if it is not strictly /// in the future (block time converted to ns), plus the base parameter-validation reverts. /// @param newMarket The fresh market to trade against. /// @param newParams The fresh fee + recipient configuration. /// @param newBookParams The fresh order-book (tick / lot / min-quantity) parameters. /// @param newMarketExpiryNs The fresh market's expiry, in the book's nanosecond timescale (> 0). function recycle( IBinaryMarket newMarket, BinaryPoolParameters calldata newParams, OrderBookParameters calldata newBookParams, uint64 newMarketExpiryNs ) external; /// @notice Approves (or revokes) a builder for the caller, granting the builder the right to /// charge a per-fill fee up to `maxFeeBpsTimes1k` on orders the caller submits with /// that builder code. Set `maxFeeBpsTimes1k = 0` to revoke. /// @dev Mirrors `ISpotPool.approveBuilder`. Fee routing is a separate concern from approval: /// this call only records the per-user cap that `_onOrderPlaced` validates against; the /// per-order fee rate is supplied at placement and clamped by both this cap and the /// protocol-wide `maxBuilderFeeBpsTimes1k`. Init-frozen fee params — there is no /// admin-facing setter for the protocol-wide cap on BinaryPool. /// @dev Reverts with `InvalidBuilder` if `builder == address(0)` — the zero address is the /// sentinel for "no builder" on placed orders. /// @dev Reverts with `BuilderFeeExceedsCap` if `maxFeeBpsTimes1k` exceeds the protocol-wide /// cap (`maxBuilderFeeBpsTimes1k`). /// @param builder Builder address to approve. /// @param maxFeeBpsTimes1k Maximum per-order builder fee rate (BPS_TIMES_1K units). function approveBuilder(address builder, uint256 maxFeeBpsTimes1k) external; /// @notice Capture the current market's closing price from the still-intact closing book and lift /// the closing-book lock. Permissionless once the market's expiry has passed. /// @dev The closing book (orders with `expireTimestampNs == marketExpiryNs`) is guaranteed intact /// here: post-expiry removals of its orders revert `CloseNotCaptured` until this runs (or the /// market turns terminal). Self-incentivized — whoever wants a pre-terminal book dismantled /// (a maker reclaiming escrow, a sweeper draining for `recycle`) is exactly the party that /// captures first. Walks each side best-first, skipping stale (earlier-expired) orders; the /// first closing order per side is that side's best. Both sides found ⇒ stores their mid and /// latches `CloseState.Captured`; a side proven empty ⇒ latches `CloseState.CapturedDegenerate` /// (no mid — consumers fall back to a uniform void). Emits `ClosingCaptured` either way. /// Runs fine on an already-terminal market (harmless — the vector is decided elsewhere); a /// capture before a normal resolution is simply never consumed. /// @dev Reverts with `CaptureTooEarly` before `marketExpiryNs`, `CloseAlreadyCaptured` when the /// close is no longer `Open`, and `CaptureStepsExhausted` when `maxSteps` runs out before a /// side is decided (retry with a bigger budget — gas is the caller's choice). /// @param maxSteps Per-side scan budget in orders visited; 0 selects the pool default (256). function captureClose(uint256 maxSteps) external; /*////////////////////////////////////////////////////////////// VIEWS //////////////////////////////////////////////////////////////*/ /// @notice Collateral token (typically a stablecoin) backing all YES + NO supply 1:1. function collateralToken() external view returns (address); /// @notice The shared ERC-6909 outcome-token singleton this pool mints / burns against. /// @return The outcome-token singleton. function outcomeToken() external view returns (IOutcomeToken6909); /// @notice ERC-6909 id of the outcome at `outcomeIdx` for this pool's CURRENT market nonce. /// @dev `id = (uint256(uint160(address(this))) << 72) | (uint256(marketNonce) << 8) | outcomeIdx`. /// The pool address occupies the upper 160 bits (storage-free mint/burn authority preserved: /// `msg.sender == address(uint160(id >> 72))`), the market nonce the next 64 bits, and the /// outcome index the low 8. Successive markets on a recycled pool therefore get disjoint id /// ranges, so old markets stay redeemable on settlement without collision. /// `marketKey = id >> 8 = (uint160(pool) << 64) | marketNonce` keys settlement records. /// @param outcomeIdx Outcome index to look up (must be 0 or 1). /// @return The outcome id for the current nonce. function outcomeId(uint8 outcomeIdx) external view returns (uint256); /// @notice Linked market that owns resolution + lifecycle state. function market() external view returns (IBinaryMarket); /// @notice Total collateral held in the pool as backing for outstanding YES + NO supply /// (= `outcomeToken(0).totalSupply()` = `outcomeToken(1).totalSupply()` during /// Trading). Stored separately from the inherited vault's per-user balances so the /// vault's withdraw cannot touch backing reserves. function setBacking() external view returns (uint256); /// @notice The permanent `BinarySettlement` singleton this pool finalizes into. Init-frozen. /// @return The settlement singleton address. function settlement() external view returns (address); /// @notice The pool's per-market counter — starts at 1, increments on every `recycle`. Combined /// with the pool address it disambiguates successive markets' outcome ids (see `outcomeId`). /// @return The current market nonce. function marketNonce() external view returns (uint64); /// @notice Whether the pool's CURRENT market has been finalized (its backing swept to settlement). /// True between `finalize` and the next `recycle`; false while trading. /// @return True if finalized. function finalized() external view returns (bool); /// @notice The current market's expiry in the book's nanosecond timescale. Every order placed on /// the pool must expire at or before this, so the book is fully drainable via the /// permissionless expiry sweeps once the market locks. /// @return The market expiry in nanoseconds. function marketExpiryNs() external view returns (uint64); /// @notice Whether both sides of the order book are empty (no resting bids or asks). A pool must /// be book-empty (and finalized) before it can be recycled onto a fresh market. /// @return True if there are no resting orders on either side. function booksEmpty() external view returns (bool); /// @notice The current market's captured closing price and close state. /// @dev `closingMid` is meaningful ONLY when `state == CloseState.Captured` (two-sided capture); /// it is 0 in every other state. Consumers deriving a payout from it scale by /// `oneCollateral` (returned alongside so one call suffices). This getter also serves as the /// capture-capability feature probe: an old-implementation pool has no such selector, so a /// caller feature-detects with a try/catch around this call. /// @return closingMid Captured closing mid in raw book-price units (0 unless `Captured`). /// @return oneCollateral The pool's price scale (`10 ** collateralToken.decimals()`). /// @return state The current `CloseState`. function closingPrice() external view returns (uint256 closingMid, uint256 oneCollateral, CloseState state); /// @notice Best currently-alive bid/ask relevant to the market close, read directly from the /// intact book — the dual-regime raw top used by void handlers when no capture is stored. /// @dev Pre-expiry (early void): a side's best is its best LIVE order (`expireTimestampNs >= now`) /// — the book is live and its top IS the current price. Post-expiry: a side's best is its /// best CLOSING order (`expireTimestampNs == marketExpiryNs`), which the closing-book lock /// guarantees is still resting while the close is `Open`. Reverts `CaptureStepsExhausted` /// when a side's scan exhausts `maxSteps` before it is decided; a caller wrapping this in /// try/catch (the void path does) then falls back to a uniform void. /// @param maxSteps Per-side scan budget in orders visited; 0 selects the pool default (256). /// @return bestBid Best relevant bid price (YES-side, raw book units; 0 if none). /// @return bestAsk Best relevant ask price (YES-side, raw book units; 0 if none). /// @return bidFound True when a relevant bid exists. /// @return askFound True when a relevant ask exists. function closingTop(uint256 maxSteps) external view returns (uint256 bestBid, uint256 bestAsk, bool bidFound, bool askFound); /// @notice Bundled view of the pool's on-chain state. Lets tooling read every configured /// address + fee rate + `setBacking` in a single RPC call. Mirrors the shape of /// `ISpotPool.getPoolParams` for consistent off-chain indexer coverage. /// @return info Snapshot of the pool's public state. `outcomeToken` is the shared ERC-6909 /// singleton and `yesId`/`noId` are this pool's outcome ids on it. function getBinaryPoolParams() external view returns (BinaryPoolInfo memory info); /// @notice Per-order info as tracked by BinaryPool (locked collateral / outcome / builder). /// @dev Use `getOrderIndexFromOrderId(orderId)` to derive the `OrderIndex` from an `OrderId`. /// Indexes are LIFO-reused, so a freed order's slot can be re-occupied by a later order. This /// call reverts `OrderInfoIdMismatch` when the stored slot's id no longer matches the queried /// one (mirroring the base `getOrder` guard) rather than returning half-stale data. /// @param orderId The full `OrderId` of the order to look up. /// @return info The per-order info tracked alongside the base OrderBook order. function getOrderInfo(OrderId orderId) external view returns (BinaryPoolOrderInfo memory info); /*////////////////////////////////////////////////////////////// BUILDER APPROVALS //////////////////////////////////////////////////////////////*/ /// @notice Returns the protocol-wide cap on per-user→builder approvals. /// @dev Reads `BinaryPoolParameters.maxBuilderFeeBpsTimes1k`. Init-frozen — no admin setter. /// Zero disables builder codes entirely (every non-zero approval / order would revert). /// @return maxFeeBpsTimes1k Current cap in BPS_TIMES_1K units. function getMaxBuilderFeeBpsTimes1k() external view returns (uint256 maxFeeBpsTimes1k); /// @notice Returns the raw per-user builder approval as stored by `approveBuilder`. /// @dev The stored value may exceed the current protocol-wide cap in future upgrades if the /// cap ever changes; today the cap is init-frozen so raw == effective in practice. Off- /// chain consumers that want the value actually enforced at order time should call /// `getEffectiveBuilderApproval`. /// @param user Order owner. /// @param builder Builder address. /// @return maxFeeBpsTimes1k Raw approved max (0 if not approved). function getBuilderApproval(address user, address builder) external view returns (uint256 maxFeeBpsTimes1k); /// @notice Returns the effective per-order builder fee a user has approved for a given builder, /// clamped by the current protocol-wide cap. /// @dev Mirrors the bound `_onOrderPlaced` enforces at order placement, so off-chain consumers /// (UIs, builder dashboards) read the same ceiling the chain will validate. /// @param user Order owner. /// @param builder Builder address. /// @return maxFeeBpsTimes1k `min(getBuilderApproval(user, builder), getMaxBuilderFeeBpsTimes1k())`. function getEffectiveBuilderApproval(address user, address builder) external view returns (uint256 maxFeeBpsTimes1k); }lib/somnia-dex-protocol/src/interfaces/IOrderBook.sol
// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.34; import {OrderId} from "../common/Common.sol"; /// @notice Represents a current order, which may or may not be partially filled /// @param orderId The unique identifier for this order /// @param isBid True if this is a buy order, false if sell /// @param owner The address that owns this order /// @param userData Arbitrary 64-bit user data attached to the order /// @param price The limit price for the order /// @param fullQuantity The original full quantity of the order /// @param quantityRemaining The quantity still remaining to be filled /// @param expireTimestampNs Expiration timestamp in nanoseconds struct Order { OrderId orderId; bool isBid; address owner; uint64 userData; uint256 price; uint256 fullQuantity; uint256 quantityRemaining; uint64 expireTimestampNs; } /// @notice Configuration parameters for the order book /// @param tickSize The minimum price increment in quote token units /// @param minQuantity The minimum order quantity in base token units /// @param lotSize The minimum quantity increment in base token units struct OrderBookParameters { uint256 tickSize; uint256 minQuantity; uint256 lotSize; } /// @notice Represents an aggregated price level in the order book /// @param price The price at this level /// @param quantity The total quantity at this price level struct OrderBookLevel { uint256 price; uint256 quantity; } /// @title IOrderBook /// @notice Interface for a central limit order book supporting various order types interface IOrderBook { /// @notice Specifies the execution behavior of an order enum OrderType { /// @dev This order will be filled or placed into the book depending on the current book NormalOrder, /// @dev This order will only be created if it can be fully immediately filled, otherwise no action takes place FillOrKill, /// @dev This order will only be created if it can be partially immediately filled, and the rest of the order will be cancelled ImmediateOrCancel, /// @dev This order will be cancelled if any of it would be filled immediately PostOnly } /// @notice Specifies behavior when a taker order matches a maker order from the same owner enum SelfMatchingOption { /// @dev If a taker order matches a maker order with the same owner, cancel the remaining part of the taker order CancelTaker, /// @dev If a taker order matches a maker order with the same owner, cancel the full maker order CancelMaker } /// @notice Why a placement did not result in a live or filled order. /// @dev These are LANDING-TIME BOOK-STATE rejections — the order was well-formed but the book it /// arrived at could not honour it. They are distinct from the malformed-input class /// (`QuantityBelowMinimum` / `InvalidPrice` / `InvalidQuantity`), which a caller can /// pre-validate off-chain and which always reverts regardless of entry point. /// /// Surfaced two ways, per entry-point arity: /// - SINGULAR entries (`placeOrder` / `placeOrderFor`) REVERT with the matching typed /// error, so a transaction can never report success for an order that achieved nothing. /// - BATCH entries (`placeOrders` / `placeOrdersFor`) keep their per-request `bool[]` and /// emit `OrderRejected` carrying this reason, so one bad request cannot kill the batch. /// One exception: the REAL-RUN FillOrKill divergence check reverts on every surface, /// batch included, because it rolls back partial fills that were already settled. It fires /// only when `_shouldCancelMakerPreFill` retracts a maker the dry-run had counted (PerpPool /// only today), so `FillOrKillUnfillable` is the batch-reported reason in every ordinary /// case and `FillOrKillNotFillable` the revert in that rare one. /// /// Append-only: new reasons MUST be added at the END so existing off-chain decoders keep /// their numbering. `None` is the zero value and means the order was placed or filled. /// /// ADDING A REASON? It also needs a branch in `OrderBook._requirePlaced`, which converts this /// enum into the singular surface's typed revert. That function ends in an exhaustive /// fall-through rather than a final `if`, so a value with no branch does not fail loudly — it /// is silently reported as `ImmediateOrCancelNoFill`. That is the deliberately safe failure /// mode (the alternative shape would let an unhandled reason return NORMALLY and resurrect the /// phantom-success bug this enum exists to kill), but it is silent, so the branch is easy to /// forget and nothing will remind you. Add the reason, the branch, and its error together. enum OrderRejectionReason { /// @dev Not a rejection — the order rested on the book or filled. None, /// @dev `expireTimestampNs` was already in the past when the order landed. AlreadyExpired, /// @dev The order crossed the owner's own resting order under `SelfMatchingOption.CancelTaker`. SelfMatchCancelTaker, /// @dev A `PostOnly` order would have matched against the book instead of resting. PostOnlyWouldCross, /// @dev A `FillOrKill` order could not be filled in full. FillOrKillUnfillable, /// @dev An `ImmediateOrCancel` order crossed nothing and so filled none of its quantity. /// Determined on the dry-run, then re-checked after the real-run: pre-fill maker /// retractions can drop a predicted fill to zero, and that case reports this reason too /// rather than a phantom success. Unlike the FillOrKill divergence above it needs no /// revert — zero fill means nothing settled — so the batch keeps its per-item flag. ImmediateOrCancelNoFill } /// @notice Parameters describing a single order in a batch placement (`placeOrders` / /// `placeOrdersFor`). /// @dev Mirrors the per-order arguments of `placeOrder` / `placeOrderFor`; the order `owner` is /// supplied once by the batch entry point (the caller for `placeOrders`, the `owner` /// argument for `placeOrdersFor`) rather than per request. /// @param isBid True for a buy order, false for a sell order /// @param userData Arbitrary 64-bit user data attached to the order /// @param price The limit price for the order /// @param quantity The quantity of the base asset to trade /// @param expireTimestampNs Expiration timestamp in nanoseconds /// @param orderType The execution behavior of the order /// @param selfMatchingOption Behavior when matching against own orders /// @param builder Optional builder address that earns a fee on this order's fills (address(0) for none) /// @param builderFeeBpsTimes1k Per-order builder fee rate in BPS_TIMES_1K units (must be 0 if `builder` is address(0)) struct PlaceOrderRequest { bool isBid; uint64 userData; uint256 price; uint256 quantity; uint64 expireTimestampNs; OrderType orderType; SelfMatchingOption selfMatchingOption; address builder; uint96 builderFeeBpsTimes1k; } /// @notice Parameters describing a single order in a batch reduction (`reduceOrders` / /// `reduceOrdersFor`). /// @param orderId The unique identifier of the order to reduce /// @param newQuantityRemaining The new remaining quantity (must be >= minQuantity) struct ReduceOrderRequest { OrderId orderId; uint256 newQuantityRemaining; } /// @notice Parameters describing a single amend: the order to cancel plus the replacement to /// place. Shared by `amendOrder` / `amendOrderFor` (one request) and `amendOrders` / /// `amendOrdersFor` (an array of requests). /// @param oldOrderId The unique identifier of the order being amended (cancelled, then replaced) /// @param alwaysPlace Race handling for a `oldOrderId` that is already filled/cancelled by the /// time the tx lands. When false (the default, industry-standard posture) the amend /// reverts `AmendOldOrderGone` and nothing is placed. When true the cancel leg is skipped /// and the replacement is placed anyway (opt-in upsert, mirroring Binance `cancelReplace` /// ALLOW_FAILURE / Hyperliquid `always_place`). `alwaysPlace` only tolerates a *gone* /// order: it never skips an ownership failure (a live order owned by someone else still /// reverts `IncorrectSender`) and never relaxes the replacement's no-gap check /// on the replacement itself. With no old order to free, a skipped-cancel replacement /// sources its funds/margin fresh (auto-pull from the wallet, or a manual-vault balance). /// @param newOrder The replacement order to place; its owner is the amend caller (`msg.sender`), /// or the operator-declared `owner` on the `amendOrderFor` / `amendOrdersFor` paths struct AmendOrderRequest { OrderId oldOrderId; bool alwaysPlace; PlaceOrderRequest newOrder; } /*////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////*/ /// @notice Emitted when a new order is placed /// @dev Emitted for every accepted order, regardless of whether any quantity /// ultimately rests on the book. /// @param orderId The unique identifier of the placed order /// @param placedOrder The full order details event OrderPlaced(OrderId indexed orderId, Order placedOrder); /// @notice Emitted when an order comes to rest on the book after placement /// @dev Only emitted for the residual quantity that is actually inserted into /// the priority index. Orders that fully fill on placement, that are /// ImmediateOrCancel residuals, or that are FillOrKill never emit this. /// The resting order's details are available on the paired OrderPlaced. /// @param orderId The unique identifier of the resting order event OrderRested(OrderId indexed orderId); /// @notice Emitted when an order is cancelled by its owner /// @param orderId The unique identifier of the cancelled order event OrderCancelled(OrderId indexed orderId); /// @notice Emitted when an order expires during matching /// @param orderId The unique identifier of the expired order event OrderExpired(OrderId indexed orderId); /// @notice Emitted when a resting maker order is cancelled because an incoming same-owner /// taker crossed it with `SelfMatchingOption.CancelMaker`. /// @dev Distinct from `OrderCancelled` (owner-initiated): this is the protocol removing a /// maker due to a same-owner self-match. The maker's locked funds are returned to the /// owner exactly as in any other removal — this event is purely the observability /// signal that lets off-chain consumers keep their reconstructed book in sync (DEX-1236). /// It is otherwise the only path by which an order leaves the book without a lifecycle /// event. The cancelled order's details remain available on its original `OrderPlaced`. /// @param orderId The unique identifier of the maker order cancelled by the self-match event OrderCancelledSelfMatch(OrderId indexed orderId); /// @notice Emitted when a resting maker order is cancelled by the pre-fill guard /// (`_shouldCancelMakerPreFill`) instead of being filled. /// @dev Emitted unconditionally by the base matching loop, so every pre-fill removal is /// observable no matter which pool triggered it — a pool that overrides /// `_shouldCancelMakerPreFill` cannot create an eventless removal path (DEX-1236 /// hardening). A pool-specific reason event may accompany it (e.g. PerpPool's /// `MakerOrderCancelledExceedsPosition`). The maker's locked funds are returned to the /// owner exactly as in any other removal. /// @param orderId The unique identifier of the maker order cancelled by the pre-fill guard event OrderCancelledPreFill(OrderId indexed orderId); /// @notice Emitted when an order's quantity is reduced /// @param orderId The unique identifier of the reduced order /// @param newQuantity The new remaining quantity after reduction event OrderReduced(OrderId indexed orderId, uint256 newQuantity); /// @notice Emitted when an order is amended: `oldOrderId` is replaced by the freshly placed /// `newOrderId` /// @dev Fires alongside the replacement's `OrderPlaced(newOrderId)` / `OrderRested(newOrderId)`, /// linking the amended order to its replacement so off-chain book reconstruction sees the /// amend as one action. Normally also paired with `OrderCancelled(oldOrderId)` from the /// cancel leg; when `alwaysPlace` skipped the cancel (the old order was already gone) there /// is no paired `OrderCancelled` in this tx — that removal was observed earlier. The /// replacement always carries a fresh id (see `amendOrder`), so `oldOrderId != newOrderId`. /// @param oldOrderId The unique identifier of the amended (old) order /// @param newOrderId The unique identifier of the replacement order event OrderAmended(OrderId indexed oldOrderId, OrderId indexed newOrderId); /// @notice Emitted when a taker order fills against a maker order /// @param takerOrderId The unique identifier of the taker order /// @param makerOrderId The unique identifier of the maker order /// @param quantityFilled The quantity of base asset filled /// @param takerRemainingQuantity The taker order's remaining quantity after this fill /// @param makerRemainingQuantity The maker order's remaining quantity after this fill /// @param fillPrice The fill price for the taker which is derived from the maker resting order event OrderFilled( OrderId indexed takerOrderId, OrderId indexed makerOrderId, uint256 quantityFilled, uint256 takerRemainingQuantity, uint256 makerRemainingQuantity, uint256 fillPrice ); /// @notice Emitted when the order book parameters are updated /// @param newParameters The new order book parameters event OrderBookParametersUpdated(OrderBookParameters newParameters); /// @notice Emitted for each contract whose approval to place orders on behalf of users is updated /// @dev Emitted once per array entry processed by `updateIsApprovedContractToPlaceOrders`, /// regardless of whether the new approval state differs from the prior state. /// @param contractAddress The address whose approval state was updated /// @param isApproved True if the address is now approved, false if it was revoked event ContractApprovalUpdated(address indexed contractAddress, bool isApproved); /// @notice Emitted for each request in a BATCH placement that was rejected at landing time. /// @dev Emitted only by `placeOrders` / `placeOrdersFor`. The singular entries revert instead, and /// a revert discards its own logs — emitting there would burn gas (logs are ~10x Ethereum on /// Somnia) for a record no one can read. This is therefore the one place a landing-time /// rejection reason survives in event space, pairing with the `bool[]` the batch returns. /// @param owner The order owner the rejected request was placed for /// @param reason Why the request did not rest or fill /// @param requestIndex Index of the rejected request in the submitted `requests` array event OrderRejected(address indexed owner, OrderRejectionReason indexed reason, uint256 requestIndex); /*////////////////////////////////////////////////////////////// ERRORS //////////////////////////////////////////////////////////////*/ /// @notice Thrown when a non-approved contract attempts to place an order on behalf of another address error OnlyApprovedContracts(); /// @notice Thrown when minQuantity is zero error InvalidMinQuantity(); /// @notice Thrown when tickSize is zero error InvalidTickSize(); /// @notice Thrown when lotSize is zero error InvalidLotSize(); /// @notice Thrown when minQuantity is not a multiple of lotSize, which would silently raise the effective minimum order size error InconsistentMinQuantityAndLotSize(); /// @notice Thrown when order quantity is below the minimum quantity /// @param quantity The provided quantity /// @param minimum The minimum required quantity error QuantityBelowMinimum(uint256 quantity, uint256 minimum); /// @notice Thrown when price is zero or not aligned to tick size /// @param price The provided price /// @param tickSize The required tick size alignment error InvalidPrice(uint256 price, uint256 tickSize); /// @notice Thrown when quantity is not aligned to lot size or exceeds remaining /// @param quantity The provided quantity /// @param constraint The lot size or maximum allowed quantity error InvalidQuantity(uint256 quantity, uint256 constraint); /// @notice Thrown when dry-run and real-run fill quantities diverge error DryRunDifference(); /// @notice Thrown when a FillOrKill order cannot be filled in full /// @dev Covers BOTH FillOrKill rejection paths, which were previously inconsistent: the dry-run /// determination (which used to return `(false, 0)` from the singular entries) and the /// real-run case where maker cancellations undo a fill the dry-run predicted. Corresponds to /// `OrderRejectionReason.FillOrKillUnfillable` on the batch surface. error FillOrKillNotFillable(); /// @notice Thrown when a placed order's `expireTimestampNs` is already in the past /// @dev Corresponds to `OrderRejectionReason.AlreadyExpired`. Parameterless like its four /// siblings: the submitted expiry is in the caller's own calldata and the comparison /// timestamp is the block's, so echoing either back adds no information. error OrderAlreadyExpired(); /// @notice Thrown when a taker order crosses its own resting order under `SelfMatchingOption.CancelTaker` /// @dev Corresponds to `OrderRejectionReason.SelfMatchCancelTaker`. The `CancelMaker` option /// instead removes the resting maker and emits `OrderCancelledSelfMatch`, so this error is /// reachable only on the `CancelTaker` path. error SelfMatchCancelTaker(); /// @notice Thrown when a `PostOnly` order would have matched against the book instead of resting /// @dev Corresponds to `OrderRejectionReason.PostOnlyWouldCross`. error PostOnlyWouldCross(); /// @notice Thrown when an `ImmediateOrCancel` order crossed nothing and filled none of its quantity /// @dev Corresponds to `OrderRejectionReason.ImmediateOrCancelNoFill`. A PARTIAL fill is a /// success: the filled portion settles and the residual is discarded rather than rested. /// Raised whether the book was unfillable up front or a pre-fill maker retraction dropped a /// predicted fill to zero during the real-run — the same outcome deserves the same error. error ImmediateOrCancelNoFill(); /// @notice Thrown when attempting to store an order with zero remaining quantity error CannotStoreZeroOrder(); /// @notice Thrown when caller is not the order owner /// @param sender The actual sender /// @param expected The expected owner error IncorrectSender(address sender, address expected); /// @notice Thrown when the order ID does not match the stored order error IncorrectOrder(); /// @notice Thrown when getAllOpenOrdersOffChain is called from a non-zero address error MustBeSentFromZeroAddress(); /// @notice Thrown when a stored order has zero remaining quantity (invariant violation) error ZeroOrder(); /// @notice Thrown when an unexpected dry-run state is encountered during matching error IncorrectDryRun(); /// @notice Thrown when `reduceOrder` is called on an order that has already passed its /// `expireTimestampNs`. Callers must use `cancelOrder` to recover funds from an /// expired order; that path returns the full locked balance via `_onOrderRemoved`. /// @param orderId The unique identifier of the expired order error ExpiredOrderMustBeCancelled(OrderId orderId); /// @notice Thrown when a batch order-management method is called with an empty request array error EmptyBatch(); /// @notice Thrown when a replacement in a BATCH amend did not rest or fill /// @dev The batch counterpart of the bare landing-time reason `amendOrder` raises. A batch amend is /// atomic (a rejected replacement reverts the whole re-ladder rather than leaving a gap), so /// unlike `placeOrders` there is no per-item flag to carry the index — and a naked /// `PostOnlyWouldCross()` out of a 20-rung re-ladder would not say WHICH rung crossed. This /// wrapper keeps both the index and the reason. /// @param requestIndex Index of the failing amend in the submitted `requests` array /// @param reason Why that request's replacement did not rest or fill error AmendReplacementRejected(uint256 requestIndex, OrderRejectionReason reason); /// @notice Thrown when the `oldOrderId` of an amend is already filled or cancelled (a benign /// decision-vs-landing race) and the request did not opt into `alwaysPlace`. Kept /// distinct from `IncorrectSender` / `IncorrectOrder` so a maker racing a fill can /// branch to a re-quote cheaply instead of treating it as an ownership bug. Set /// `alwaysPlace = true` on the request to place the replacement anyway instead. /// @param oldOrderId The identifier of the order that was to be amended but is already gone error AmendOldOrderGone(OrderId oldOrderId); /*////////////////////////////////////////////////////////////// FUNCTIONS //////////////////////////////////////////////////////////////*/ /// @notice Places an order on the order book for the caller /// @param isBid True for a buy order, false for a sell order /// @param userData Arbitrary 64-bit user data attached to the order /// @param price The limit price for the order /// @param quantity The quantity of the base asset to trade /// @param expireTimestampNs Expiration timestamp in nanoseconds /// @param orderType The execution behavior of the order /// @param selfMatchingOption Behavior when matching against own orders /// @param builder Optional builder address that earns a fee on this order's fills (address(0) for none) /// @param builderFeeBpsTimes1k Per-order builder fee rate in BPS_TIMES_1K units (must be 0 if `builder` is address(0)) /// @return success Always true on return — a rejected order reverts. Retained in the ABI so /// existing callers and the `(bool, OrderId)` return shape are unchanged /// @return id The unique identifier for the placed order /// @dev Payable to support native-input auto-pull on subclasses (e.g., SpotPool). Implementations /// that do not consume native must reject non-zero `msg.value` via the /// `_assertMsgValueAllowed` hook. /// @dev REJECTION IS A REVERT. A transaction can never report success for an order that achieved /// nothing; inclusion is not effect. Malformed input reverts as it always has, and every /// landing-time book-state rejection now reverts with its own typed error: /// - `OrderAlreadyExpired` — `expireTimestampNs` already in the past /// - `SelfMatchCancelTaker` — crossed the owner's own order under `CancelTaker` /// - `PostOnlyWouldCross` — a `PostOnly` order would have matched instead of resting /// - `FillOrKillNotFillable` — a `FillOrKill` order could not be filled in full /// - `ImmediateOrCancelNoFill` — an `ImmediateOrCancel` order crossed nothing /// All five are CUSTOM errors: a caller that must tolerate a rejection has to use /// `catch (bytes memory)` or a bare `catch`, never `catch Error(string)`. A caller that wants /// per-item tolerance without try/catch should use `placeOrders` instead, which keeps its /// `bool[]` and reports the reason via `OrderRejected`. /// @dev Reverts with `QuantityBelowMinimum` if quantity < minQuantity /// @dev Reverts with `InvalidPrice` if price is zero or not a multiple of tickSize /// @dev Reverts with `InvalidQuantity` if quantity is not a multiple of lotSize /// @dev Attached `msg.value` needs no refund path: a rejection reverts, which rolls the auto-pull /// transfer, the vault credit and the lock back together, leaving the caller's wallet and any /// pre-existing vault balance untouched /// @dev Pool implementations may revert with builder-validation errors (e.g., `BuilderNotApproved`) function placeOrder( bool isBid, uint64 userData, uint256 price, uint256 quantity, uint64 expireTimestampNs, OrderType orderType, SelfMatchingOption selfMatchingOption, address builder, uint96 builderFeeBpsTimes1k ) external payable returns (bool success, OrderId id); /// @notice Places an order on the order book on behalf of another address /// @dev Two-gate authorization: the call is admitted if the caller is either a system-level /// allowlisted contract (legacy gate, configured by /// `updateIsApprovedContractToPlaceOrders`) OR is approved by the order's `owner` via the /// per-user `_operatorGate` hook (a subclass extension that defaults to false on the /// abstract OrderBook). /// @param owner The address that will own the order /// @param isBid True for a buy order, false for a sell order /// @param userData Arbitrary 64-bit user data attached to the order /// @param price The limit price for the order /// @param quantity The quantity of the base asset to trade /// @param expireTimestampNs Expiration timestamp in nanoseconds /// @param orderType The execution behavior of the order /// @param selfMatchingOption Behavior when matching against own orders /// @param builder Optional builder address that earns a fee on this order's fills (address(0) for none) /// @param builderFeeBpsTimes1k Per-order builder fee rate in BPS_TIMES_1K units (must be 0 if `builder` is address(0)) /// @return success Always true on return — a rejected order reverts. Retained in the ABI so /// existing callers and the `(bool, OrderId)` return shape are unchanged /// @return id The unique identifier for the placed order /// @dev Payable to support native-input auto-pull on subclasses (e.g., SpotPool). Implementations /// that do not consume native must reject non-zero `msg.value` via the /// `_assertMsgValueAllowed` hook. /// @dev REJECTION IS A REVERT, with the same five typed errors as `placeOrder` — see its `@dev` /// block for the list and the `catch (bytes memory)` requirement. Operators that must survive /// a rejected placement (a router leg, a stop-order trigger, a market-maker ladder rung) MUST /// wrap this call in try/catch or move to `placeOrdersFor`, which keeps per-request flags. /// Note that a caught revert discards the inner call's logs but leaves the CALLER's prior /// state intact, so a per-item loop can continue safely. /// @dev Reverts with `OnlyApprovedContracts` if caller satisfies neither gate /// @dev Reverts with `QuantityBelowMinimum` if quantity < minQuantity /// @dev Reverts with `InvalidPrice` if price is zero or not a multiple of tickSize /// @dev Reverts with `InvalidQuantity` if quantity is not a multiple of lotSize /// @dev Attached `msg.value` needs no refund path — see `placeOrder` /// @dev Pool implementations may revert with builder-validation errors (e.g., `BuilderNotApproved`) function placeOrderFor( address owner, bool isBid, uint64 userData, uint256 price, uint256 quantity, uint64 expireTimestampNs, OrderType orderType, SelfMatchingOption selfMatchingOption, address builder, uint96 builderFeeBpsTimes1k ) external payable returns (bool success, OrderId id); /// @notice Cancels an existing order owned by the caller /// @param orderId The unique identifier of the order to cancel /// @dev Reverts with `IncorrectSender` if caller is not the order owner /// @dev Reverts with `IncorrectOrder` if the order ID does not match function cancelOrder(OrderId orderId) external; /// @notice Cancels an existing order on behalf of its owner /// @dev Per-user gate ONLY: the caller must be approved by `owner` for this function's /// selector via the `_operatorGate` hook (e.g., SpotPool's `OperatorPermissionsRegistry`). /// Unlike `placeOrderFor`, the system-level contract allowlist /// (`updateIsApprovedContractToPlaceOrders`) does NOT admit callers here — cancellation /// authority is granted exclusively by the order owner. Released funds are always /// returned to `owner` (never the caller), so an operator cannot redirect balances. /// @param owner The owner of the order being cancelled /// @param orderId The unique identifier of the order to cancel /// @dev Reverts with `OnlyApprovedContracts` if the caller is not operator-approved by `owner` /// for `cancelOrderFor` /// @dev Reverts with `IncorrectSender` if `owner` is not the order's owner /// @dev Reverts with `IncorrectOrder` if the order ID does not match function cancelOrderFor(address owner, OrderId orderId) external; /// @notice Reduces the remaining quantity of an existing order /// @param orderId The unique identifier of the order to reduce /// @param newQuantityRemaining The new remaining quantity (must be >= minQuantity) /// @dev Reverts with `IncorrectSender` if caller is not the order owner /// @dev Reverts with `IncorrectOrder` if the order ID does not match /// @dev Reverts with `ExpiredOrderMustBeCancelled` if the order has passed its /// `expireTimestampNs`; callers must use `cancelOrder` to recover funds from an /// expired order /// @dev Reverts with `QuantityBelowMinimum` if newQuantityRemaining < minQuantity /// @dev Reverts with `InvalidQuantity` if newQuantityRemaining is not a multiple of lotSize or exceeds current remaining function reduceOrder(OrderId orderId, uint256 newQuantityRemaining) external; /// @notice Reduces the remaining quantity of an existing order on behalf of its owner /// @dev Per-user gate ONLY: the caller must be approved by `owner` for this function's /// selector via the `_operatorGate` hook (e.g., SpotPool's `OperatorPermissionsRegistry`). /// Unlike `placeOrderFor`, the system-level contract allowlist /// (`updateIsApprovedContractToPlaceOrders`) does NOT admit callers here — reduction /// authority is granted exclusively by the order owner. Funds freed by the reduction are /// always returned to `owner` (never the caller), so an operator cannot redirect balances. /// @param owner The owner of the order being reduced /// @param orderId The unique identifier of the order to reduce /// @param newQuantityRemaining The new remaining quantity (must be >= minQuantity) /// @dev Reverts with `OnlyApprovedContracts` if the caller is not operator-approved by `owner` /// for `reduceOrderFor` /// @dev Reverts with `IncorrectSender` if `owner` is not the order's owner /// @dev Reverts with `IncorrectOrder` if the order ID does not match /// @dev Reverts with `ExpiredOrderMustBeCancelled` if the order has passed its /// `expireTimestampNs`; use `cancelOrderFor` to recover funds from an expired order /// @dev Reverts with `QuantityBelowMinimum` if newQuantityRemaining < minQuantity /// @dev Reverts with `InvalidQuantity` if newQuantityRemaining is not a multiple of lotSize or exceeds current remaining function reduceOrderFor(address owner, OrderId orderId, uint256 newQuantityRemaining) external; /// @notice Places multiple orders for the caller in a single transaction /// @dev Thin batch wrapper: each request is applied through the same path as `placeOrder`, in /// array order, within one reentrancy-guarded call. NON-PAYABLE — batch placement does not /// support native (`msg.value`) auto-pull; fund native-token orders by pre-depositing to /// the vault (auto-pull then consumes the vault balance) or use per-order `placeOrder`. /// ERC20 auto-pull (wallet `transferFrom`) works normally per request. /// @dev THE BATCH SURFACE DOES NOT REVERT ON A LANDING-TIME REJECTION — this is the deliberate /// counterpart to `placeOrder`, which does. `successes[i]` is false (with `ids[i]` zero) for a /// benign non-placement (already-expired / PostOnly-crossed / unfilled FillOrKill / IOC /// no-fill / CancelTaker self-match), and an `OrderRejected` event carries the specific /// `OrderRejectionReason` for that request index. This is what lets a market maker re-ladder /// 2N rungs where a routine self-match on one rung must not discard the other 2N-1. A hard /// validation error on any request (bad price/lot/quantity, insufficient funds, builder /// rejection) still reverts the ENTIRE batch. /// @param requests The orders to place, applied in array order /// @return successes Per-request success flag, aligned index-for-index with `requests` /// @return ids Per-request order ID (zero where the corresponding `successes` entry is false) /// @dev Reverts with `EmptyBatch` if `requests` is empty function placeOrders(PlaceOrderRequest[] calldata requests) external returns (bool[] memory successes, OrderId[] memory ids); /// @notice Places multiple orders on behalf of `owner` in a single transaction /// @dev Same batch semantics as `placeOrders`, including the non-reverting per-request rejection /// flags plus `OrderRejected`. Authorization is checked ONCE for the whole batch using the /// same dual gate as `placeOrderFor` (system-level allowlist OR the per-user `_operatorGate` /// for `placeOrderFor`). NON-PAYABLE — see `placeOrders`. Prefer this over a try/catch loop /// around `placeOrderFor` when an operator places many orders for one owner: one call frame, /// one gate check, one reentrancy acquisition, and no swallowed reverts. /// @param owner The address that will own every order in the batch /// @param requests The orders to place, applied in array order /// @return successes Per-request success flag, aligned index-for-index with `requests` /// @return ids Per-request order ID (zero where the corresponding `successes` entry is false) /// @dev Reverts with `OnlyApprovedContracts` if the caller satisfies neither gate /// @dev Reverts with `EmptyBatch` if `requests` is empty function placeOrdersFor(address owner, PlaceOrderRequest[] calldata requests) external returns (bool[] memory successes, OrderId[] memory ids); /// @notice Cancels multiple orders owned by the caller in one transaction, best-effort /// @dev Cancels every still-live order owned by the caller and SKIPS (does not revert on) any id /// that is no longer cancellable — already filled, cancelled, expired-and-swept, or not /// owned by the caller. This suits the primary use case (a market maker pulling a whole /// ladder in a fast market): a single rung filling or being swept in the race window no /// longer reverts the entire cancel and leaves the rest of the ladder exposed. Only the /// payload-level `EmptyBatch` pre-check reverts. Runs within one reentrancy-guarded call. /// @dev A `false` entry intentionally does NOT disambiguate WHY the id was skipped: a benign race /// (filled / cancelled / expired-and-swept) and a caller-side bug (a wrong `owner` on the /// operator variant, or a malformed / stale id) both surface as `false`. Integrators must /// not read `false` as proof of a race; re-check the id independently if that distinction /// matters. /// @dev Reverts with `EmptyBatch` if `orderIds` is empty /// @param orderIds The orders to cancel, processed in array order /// @return cancelled Per-id flag aligned index-for-index with `orderIds`: true where the order /// was live and cancelled, false where it was skipped. Only a contract caller can read /// this return value; an EOA reconciles from the per-cancel `OrderCancelled` / /// `OrderExpired` events (a skipped id emits nothing). function cancelOrders(OrderId[] calldata orderIds) external returns (bool[] memory cancelled); /// @notice Cancels multiple orders on behalf of `owner` in one transaction, best-effort /// @dev Same best-effort skip-stale semantics as `cancelOrders` — including that a `false` entry /// does not disambiguate a benign race from a caller-side bug (such as passing the wrong /// `owner`). Authorization is checked ONCE for the whole batch via the per-user /// `_operatorGate` for `cancelOrderFor` — the system-level allowlist does NOT admit callers /// here, matching `cancelOrderFor`. Freed funds always flow to `owner`, never the caller. /// @dev Reverts with `OnlyApprovedContracts` if the caller is not operator-approved by `owner` /// for `cancelOrderFor` /// @dev Reverts with `EmptyBatch` if `orderIds` is empty /// @param owner The owner of every order in the batch /// @param orderIds The orders to cancel, processed in array order /// @return cancelled Per-id flag aligned index-for-index with `orderIds` (true = cancelled, /// false = skipped). Readable only by a contract caller; see `cancelOrders`. function cancelOrdersFor(address owner, OrderId[] calldata orderIds) external returns (bool[] memory cancelled); /// @notice Reduces multiple orders owned by the caller in a single transaction /// @dev Thin batch wrapper over `reduceOrder`, applied in array order within one /// reentrancy-guarded call. ATOMIC: if any reduction is invalid (order not owned, id /// mismatch, expired, below minimum, not lot-aligned, or above current remaining) the /// whole batch reverts. /// @param requests The reductions to apply, processed in array order /// @dev Reverts with `EmptyBatch` if `requests` is empty /// @dev Reverts with `IncorrectSender` / `IncorrectOrder` / `ExpiredOrderMustBeCancelled` / /// `QuantityBelowMinimum` / `InvalidQuantity` on the first invalid reduction function reduceOrders(ReduceOrderRequest[] calldata requests) external; /// @notice Reduces multiple orders on behalf of `owner` in a single transaction /// @dev Same atomic semantics as `reduceOrders`. Authorization is checked ONCE for the whole /// batch via the per-user `_operatorGate` for `reduceOrderFor` — the system-level allowlist /// does NOT admit callers here. Funds freed by the reductions always flow to `owner`. /// @param owner The owner of every order in the batch /// @param requests The reductions to apply, processed in array order /// @dev Reverts with `OnlyApprovedContracts` if the caller is not operator-approved by `owner` /// for `reduceOrderFor` /// @dev Reverts with `EmptyBatch` if `requests` is empty function reduceOrdersFor(address owner, ReduceOrderRequest[] calldata requests) external; /// @notice Atomically cancels an existing order and places a replacement, for the caller /// @dev Cancels `oldOrderId` (freeing its locked funds/collateral) and then places `newOrder`, /// in that order, within one reentrancy-guarded call. Cancel-first frees the old order's /// funds so the replacement reuses them and the two can never self-match. /// @dev Returns a NEW order id: the replacement is a fresh order (the freed order index may be /// reused, but the id's unique component always differs), so callers MUST update any local /// tracking to `newOrderId`. `getOrder(oldOrderId)` reverts `IncorrectOrder` afterward. /// @dev Decision-vs-landing race: if `oldOrderId` is already filled/cancelled when the tx lands, /// then by default (`request.alwaysPlace == false`) the amend reverts `AmendOldOrderGone` /// and nothing is placed — the industry-standard reject; a caller wanting a quote regardless /// catches this and calls `placeOrder`. Setting `request.alwaysPlace == true` instead skips /// the (impossible) cancel and places the replacement anyway (opt-in upsert). `alwaysPlace` /// never skips an ownership failure (a live order owned by another still reverts /// `IncorrectSender`) and never relaxes the no-gap check below. /// @dev No-gap guarantee: if `newOrder` does not rest or fill (a benign non-placement such as a /// crossed PostOnly, an unfilled FillOrKill, an IOC that took nothing, an already-expired /// order, or a CancelTaker self-match), the whole amend reverts with that landing-time reason /// and (when the cancel ran) the original order is left in place. Any hard error in the /// cancel or place also reverts the amend. Non-payable, so native (`msg.value`) auto-pull is /// not supported — fund native-token replacements from a MANUAL-VAULT balance. For an /// auto-pull owner the cancel leg auto-delivers the freed native to the wallet, leaving /// nothing for the non-payable place to draw on, so a native auto-pull amend reverts. /// @dev This is not a priority-preserving resize: the replacement is inserted at the back of the /// price-time queue for its price. To only shrink quantity while keeping queue priority, use /// `reduceOrder`. /// @param request The order to cancel (`oldOrderId`) and the replacement to place (`newOrder`) /// @return newOrderId The unique identifier of the replacement order /// @dev Reverts with `IncorrectSender` if a live `oldOrderId` is owned by someone else /// @dev Reverts with `AmendOldOrderGone` if `oldOrderId` is already gone and `alwaysPlace` is false /// @dev Reverts with the replacement's landing-time reason (`PostOnlyWouldCross`, /// `SelfMatchCancelTaker`, `ImmediateOrCancelNoFill`, `FillOrKillNotFillable`, /// `OrderAlreadyExpired`) if it does not rest or fill, rolling the cancel back with it function amendOrder(AmendOrderRequest calldata request) external returns (OrderId newOrderId); /// @notice Atomically cancels and replaces multiple orders for the caller (re-ladder) /// @dev All `oldOrderId`s are cancelled first, then all `newOrder`s are placed, within one /// reentrancy-guarded call. Cancel-all-first guarantees a replacement never matches against /// an old order that has not yet been cancelled. It does NOT shield replacements from each /// other: a replacement placed earlier in the batch rests immediately, so a later /// replacement whose price crosses it can self-match it (per that request's /// `selfMatchingOption`). A re-ladder of non-overlapping rungs never hits this. Fully /// atomic: if any cancel or replacement fails (including a replacement that does not rest or /// fill), the whole batch reverts and no change takes effect — replacing a ladder never /// leaves a gap. Per-request `alwaysPlace` applies the same race rule as `amendOrder`. /// Non-payable. /// @param requests The amends to apply; `newOrderIds[i]` corresponds to `requests[i]` /// @return newOrderIds The replacement order ids, aligned index-for-index with `requests` /// @dev Reverts with `EmptyBatch` if `requests` is empty /// @dev Reverts with `IncorrectSender` on a live id owned by another, `AmendOldOrderGone` on a /// gone id whose request did not set `alwaysPlace`, or `AmendReplacementRejected` for the first /// replacement that does not rest or fill function amendOrders(AmendOrderRequest[] calldata requests) external returns (OrderId[] memory newOrderIds); /// @notice Atomically cancels and replaces an order on behalf of `owner` /// @dev Same semantics as `amendOrder`. Authorization: the caller must hold BOTH the per-user /// `cancelOrderFor` and `placeOrderFor` approvals for `owner` /// (amend = cancel + place, so it composes the two existing capabilities and needs no /// separate grant). Requiring the `placeOrderFor` grant also lets the replacement's /// auto-pull draw from `owner`'s wallet, exactly as `placeOrderFor` does. The system-level /// allowlist does NOT admit callers here. Funds always flow to `owner`. /// @param owner The owner of the order being amended /// @param request The order to cancel (`oldOrderId`) and the replacement to place (`newOrder`) /// @return newOrderId The unique identifier of the replacement order /// @dev Reverts with `OnlyApprovedContracts` if the caller lacks either the `cancelOrderFor` or /// `placeOrderFor` approval from `owner` /// @dev Reverts with `IncorrectSender` if a live `oldOrderId` is not owned by `owner`, or /// `AmendOldOrderGone` if it is already gone and `request.alwaysPlace` is false /// @dev Reverts with the replacement's landing-time reason (`PostOnlyWouldCross`, /// `SelfMatchCancelTaker`, `ImmediateOrCancelNoFill`, `FillOrKillNotFillable`, /// `OrderAlreadyExpired`) if it does not rest or fill, rolling the cancel back with it function amendOrderFor(address owner, AmendOrderRequest calldata request) external returns (OrderId newOrderId); /// @notice Atomically cancels and replaces multiple orders on behalf of `owner` (re-ladder) /// @dev Same cancel-all-first, fully-atomic semantics as `amendOrders`, with the same dual-grant /// authorization as `amendOrderFor` (caller must hold both `cancelOrderFor` and /// `placeOrderFor` approvals for `owner`). Funds always flow to `owner`. /// @param owner The owner of the orders being amended /// @param requests The amends to apply; `newOrderIds[i]` corresponds to `requests[i]` /// @return newOrderIds The replacement order ids, aligned index-for-index with `requests` /// @dev Reverts with `OnlyApprovedContracts` if the caller lacks either approval from `owner` /// @dev Reverts with `EmptyBatch` if `requests` is empty /// @dev Reverts with `AmendOldOrderGone` on a gone id whose request did not set `alwaysPlace`, or /// `AmendReplacementRejected(requestIndex, reason)` if any replacement does not rest or fill function amendOrdersFor(address owner, AmendOrderRequest[] calldata requests) external returns (OrderId[] memory newOrderIds); /// @notice Cleans up the listed orders that have expired /// @dev Permissionless. Best-effort: orders that are not expired, or whose stored orderId /// no longer matches the supplied orderId (stale entries), are silently skipped rather /// than reverted on. Each successfully cleaned order emits `OrderExpired` and returns /// its locked balances to the owner's withdrawable vault balance. /// @param orderIds The orders to attempt to clean up function cancelExpiredOrders(OrderId[] calldata orderIds) external; /// @notice Cleans up the CALLER's own expired orders, without needing to know their ids /// @dev The self-service counterpart to `cancelExpiredOrders` (which needs ids) and /// `sweepExpiredAtLevel` (which needs a price). Walks the caller's own order list and frees up to /// `maxOrders` expired entries, returning each one's locked funds to the caller — to their wallet /// or their vault, per that order's delivery mode — and emitting `OrderExpired` per removal. /// /// Added because expired orders are now garbage-collected LESS often than they used to be. A /// placement sweeps the caller's expired orders before matching, but since DEX-1761 a REJECTED /// placement reverts, which rolls that sweep back with it. Nothing is ever stuck — this, plus /// `cancelOrder` and the two permissionless sweeps, all still free them — but before this a user /// whose placements kept being rejected had no way to reclaim their own locked funds without /// enumerating order ids off-chain. /// /// `maxOrders` bounds the orders REMOVED, not the entries inspected: non-expired orders are /// skipped and still cost a read, so a user with a long list of live orders and few expired ones /// pays for the walk either way. Pass zero for unbounded. Returns the count so a caller can loop /// until it yields zero. /// @param maxOrders The maximum number of the caller's expired orders to remove; zero means unbounded /// @return swept The number of the caller's expired orders that were removed function sweepMyExpiredOrders(uint256 maxOrders) external returns (uint256 swept); /// @notice Cleans up expired orders at a single price level /// @dev Permissionless. Walks the priority chain from the best order on the requested side, /// cleaning up to `maxCount` expired orders found at exactly `price`. Stops when /// `maxCount` is reached or once the walker moves past the target level. Each cleaned /// order emits `OrderExpired` and returns its locked balances to the owner's /// withdrawable vault balance. /// @param isBid True to sweep the bid side, false to sweep the ask side /// @param price The price level to sweep /// @param maxCount The maximum number of expired orders to clean in this call /// @return cleaned The number of orders that were cleaned up function sweepExpiredAtLevel(bool isBid, uint256 price, uint256 maxCount) external returns (uint256 cleaned); /// @notice Cleans up expired orders walking one side of the book best-first, with no price targeting /// @dev Permissionless. The keeper-ergonomic counterpart to `sweepExpiredAtLevel`: it needs **no price, no /// oracle, and no off-chain level enumeration** — just a side and a work budget. Each cleaned order /// emits `OrderExpired` and returns its locked balances to the owner's withdrawable vault balance. /// /// Expired orders are deliberately NOT removed by a crossing taker (they linger until swept — see /// `_matchOrderToBook`), so on a busy book they accumulate on their own with no attacker involved. Left /// unswept they inflate the cost of every subsequent match, including the liquidation IOC that must /// traverse them. This is the primitive that removes them; sweeping is expected to be routine keeper /// background work, not an incident response. /// @param isBid True to sweep the bid side, false to sweep the ask side /// @param maxVisit The maximum number of resting orders to TRAVERSE (not clean) in this call. Bounding the /// traversal rather than the cleanup count is what lets a caller cap its own gas spend up front: a /// cleaned-count bound leaves the walk unbounded whenever the expired entries sit behind live ones. /// @return cleaned The number of orders that were cleaned up function sweepExpiredFromBest(bool isBid, uint256 maxVisit) external returns (uint256 cleaned); /// @notice Updates the order book configuration parameters /// @param newParameters The new order book parameters /// @dev Reverts with `OwnableUnauthorizedAccount` if caller is not the contract owner /// @dev Reverts with `InvalidMinQuantity` if minQuantity is zero /// @dev Reverts with `InvalidTickSize` if tickSize is zero /// @dev Reverts with `InvalidLotSize` if lotSize is zero /// @dev Reverts with `InconsistentMinQuantityAndLotSize` if minQuantity is not a multiple of lotSize function updateOrderBookParameters(OrderBookParameters calldata newParameters) external; /// @notice Updates the approval status for contracts allowed to place orders on behalf of users /// @param contractsApprovedToPlaceOrders The addresses to update /// @param isApproved True to approve, false to revoke /// @dev Reverts with `OwnableUnauthorizedAccount` if caller is not the contract owner function updateIsApprovedContractToPlaceOrders(address[] calldata contractsApprovedToPlaceOrders, bool isApproved) external; /// @notice Returns the details of an order by its ID /// @param orderId The unique identifier of the order /// @return The order details /// @dev Reverts with `IncorrectOrder` if the order ID does not match any active order function getOrder(OrderId orderId) external view returns (Order memory); /// @notice Returns all open (non-expired) order IDs owned by the caller /// @return An array of order IDs function getOwnOpenOrders() external view returns (OrderId[] memory); /// @notice Returns aggregated price levels from the order book /// @param isBid True to get bid levels, false to get ask levels /// @param numLevels The maximum number of price levels to return /// @return An array of aggregated price levels function getBookLevels(bool isBid, uint64 numLevels) external view returns (OrderBookLevel[] memory); /// @notice Returns the order book configuration parameters /// @return The order book parameters function getOrderBookParameters() external view returns (OrderBookParameters memory); /// @notice Allows iteration of all open orders, but only off chain (msg.sender must be zero). /// @param isBid True to get bid orders, false to get ask orders /// @param maxCount The maximum number of orders to collect in this call. /// @param startCursor The starting cursor of the iteration. /// @return orders An array of open orders. /// @return hasMoreOrders If true, there are more orders (to be fetched using the cursor). /// @return nextCursor The next cursor to iterate with. function getAllOpenOrdersOffChain(bool isBid, uint256 maxCount, uint64 startCursor) external view returns (Order[] memory orders, bool hasMoreOrders, uint64 nextCursor); }lib/somnia-dex-protocol/src/interfaces/IOutcomeToken6909.sol
// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.34; /// @title IOutcomeToken6909 /// @notice ERC-6909 surface the BinaryPool + its periphery (routers) need from the shared /// outcome-token singleton. One protocol-level contract holds every outcome position /// of every binary market as an ERC-6909 id; pools never deploy per-market token /// contracts. /// @dev Id scheme (v2 — load-bearing, shared with the implementation and the settlement singleton): /// /// id = (uint256(uint160(pool)) << 72) | (uint256(marketNonce) << 8) | outcomeIdx /// /// The upper 160 bits carry the pool address, the next 64 bits the pool's per-market /// `marketNonce` (starts at 1, ++ on `recycle`), and the low 8 bits the outcome index /// (0 = YES, 1 = NO). The pool-of-id (`address(uint160(id >> 72))`) makes MINT authority /// *storage-free*: the only address allowed to mint an id is the pool encoded in it. A third /// party can mint only inside its own address namespace, so the partition is airtight without a /// registry. The nonce field means a recycled pool's successive markets occupy DISJOINT id /// ranges — past markets never collide with the pool's current one, so redemption on the /// settlement singleton stays valid forever. `marketKey = id >> 8 = (uint160(pool) << 64) | nonce` /// keys per-market settlement records. /// /// (v1 used `id = (uint160(pool) << 8) | outcomeIdx` with no nonce. v2 pools deploy fresh — the /// layout change is NOT an upgrade of a live v1 token; deploy a fresh singleton.) /// /// BURN authority (v2 exception): burn is permitted by the pool-of-id OR by ONE registered /// `settlement` address (owner-set once via a `setSettlement`-style one-shot on the /// implementation). This is required because redemption moved OUT of the pool and into the /// permanent `BinarySettlement` singleton: after a pool is recycled it no longer represents the /// old market, so the settlement contract — not the (possibly recycled) pool — burns redeemers' /// winning/voided tokens. MINT stays pool-of-id ONLY. Any batch-burn "is-this-market-settled" /// gate MUST consult `settlement.isFinalized(id)` rather than a pool's `market()` (which breaks /// on a recycled pool). /// /// Token implementation lives in the consumer (prophecy-markets) since metadata + /// upgrade governance are venue-specific. The dex repo only needs this interface so /// the pool + settlement can typecheck mint / burn / escrow calls. /// /// Implementations MUST enforce the namespace check on `mint` (and on `burn` modulo the /// settlement exception above); leaking either breaks the pool's `setBacking` invariant /// (collateral held by the pool == total YES supply == total NO supply during Trading). interface IOutcomeToken6909 { /// @notice Mint `amount` of `id` to `to`. MUST revert unless `msg.sender` is the pool /// encoded in the upper 160 bits of `id` (`address(uint160(id >> 72))`). MINT is /// pool-of-id ONLY — the settlement burn exception does NOT extend to mint. /// @param to Recipient of the freshly minted outcome balance. /// @param id Outcome id being minted. /// @param amount Quantity to mint. function mint(address to, uint256 id, uint256 amount) external; /// @notice Burn `amount` of `id` from `from`. MUST revert unless `msg.sender` is the pool encoded /// in `id` (`address(uint160(id >> 72))`) OR the ONE registered `settlement` singleton /// (the v2 exception — see the interface NatSpec). No allowance is consumed — the burner /// is trusted to burn only from accounts whose tokens it settles (pool: burnSet / /// burn-a-pair escrow it holds; settlement: redemption of the caller's own winning/voided /// position). /// @param from Holder whose balance is burned. /// @param id Outcome id being burned. /// @param amount Quantity to burn. function burn(address from, uint256 id, uint256 amount) external; /// @notice ERC-6909 transfer of the caller's balance. /// @param to Recipient. /// @param id Outcome id. /// @param amount Quantity to move. /// @return True on success (ERC-6909 mandates revert on failure). function transfer(address to, uint256 id, uint256 amount) external returns (bool); /// @notice ERC-6909 transfer consuming allowance / operator approval of `from`. /// @param from Holder whose balance is moved. /// @param to Recipient. /// @param id Outcome id. /// @param amount Quantity to move. /// @return True on success (ERC-6909 mandates revert on failure). function transferFrom(address from, address to, uint256 id, uint256 amount) external returns (bool); /// @notice ERC-6909 operator grant — lets `spender` move all of the caller's ids. Used by /// periphery (e.g. a collateral router granting the core operator status) and by /// end users approving the pool / core once for all markets. /// @param spender Operator to grant / revoke. /// @param approved New operator status. /// @return True on success. function setOperator(address spender, bool approved) external returns (bool); /// @notice Balance of `owner` for outcome `id`. /// @param owner Account to query. /// @param id Outcome id. /// @return The balance. function balanceOf(address owner, uint256 id) external view returns (uint256); /// @notice Total supply of outcome `id`. Backs the pool's `setBacking` invariant /// (`setBacking == totalSupply(yesId) == totalSupply(noId)` during Trading). /// @param id Outcome id. /// @return The total supply of that id. function totalSupply(uint256 id) external view returns (uint256); }src/clob/interfaces/IBinaryMarket.sol
// SPDX-License-Identifier: MIT pragma solidity 0.8.34; import {IMarket} from "./IMarket.sol"; import {VoidPolicy, VoidVectorSource} from "./MarketTypes.sol"; import {IBinaryMarket as IBinaryMarketBase} from "somnia-dex/interfaces/IBinaryMarket.sol"; /// @notice Prophecy-side binary market interface. Composes: /// - `IMarket`: the family-agnostic lifecycle + view surface shared by every /// market variant (binary today, multi-outcome later). /// - `IBinaryMarketBase`: the minimal pool-gate surface read by the somnia-dex /// `BinaryPool` on every `placeOrder` / at `finalize` — kept tiny upstream so /// the dex repo stays market-type-agnostic. /// Plus binary-only convenience getters (`yesId`, `noId`, `pool`) that /// only make sense at N=2. /// @dev `payoutNumerators()` is declared on both base interfaces; implementers /// satisfy both with a single `override(IMarket, IBinaryMarketBase)` declaration. interface IBinaryMarket is IMarket, IBinaryMarketBase { /// @notice Emitted alongside `Voided` with the payout vector the void actually stored and its /// provenance. Additive to the existing event surface: `Voided()` stays as the terminal /// signal, this event carries the WHAT and WHY (the module's resolution fan-out /// re-emits the numerators too, but `voidExpired` bypasses the module — this event is /// the one shape every void path emits). /// @param payoutNumerators The stored vector (raw sum == `PAYOUT_VECTOR_DENOMINATOR`). /// @param policy The market's frozen void policy. /// @param source Where the vector came from (see `VoidVectorSource`). event VoidVector(uint256[] payoutNumerators, VoidPolicy policy, VoidVectorSource source); /// @notice The void payout policy frozen into this market at creation. /// @dev Resolved at creation against the venue's configuration AND the provisioned pool's /// actual capability (a market never advertises a rule its pool cannot honor); the void /// paths read this field — never the registry — so `voidExpired` is covered. function voidPolicy() external view returns (VoidPolicy); /// @notice Per-outcome payout numerators after settlement. Re-declared here to /// resolve the diamond inheritance: both `IMarket` and `IBinaryMarketBase` /// declare the same signature, and Solidity needs the derived interface /// to surface it explicitly so implementers can `override` it once. function payoutNumerators() external view override(IMarket, IBinaryMarketBase) returns (uint256[] memory); /// @notice ERC-6909 id of the YES outcome (index 0) on the shared outcome-token singleton. /// @dev Convenience alias for `outcomeIds()[0]`. function yesId() external view returns (uint256); /// @notice ERC-6909 id of the NO outcome (index 1) on the shared outcome-token singleton. /// @dev Convenience alias for `outcomeIds()[1]`. function noId() external view returns (uint256); /// @notice CLOB pool that hosts the order book for this market. /// @dev Binary uses a single pool; multi-outcome variants may expose /// family-specific pool topology (potentially N pools). function pool() external view returns (address); }src/clob/interfaces/IMarket.sol
// SPDX-License-Identifier: MIT pragma solidity 0.8.34; import {MarketStatus, MarketType} from "./MarketTypes.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @notice Shared lifecycle + oracle surface for every market /// variant — binary (N=2) today, multi-outcome (N>2) later. Settlement (mint /// complete set, burn, redeem) lives on the linked pool, not here; this /// interface is intentionally settlement-free so the indexer and frontend can /// treat any future N-outcome family the same way as today's binary markets. /// @dev By design type-agnostic: speaks in `outcomeCount` and `outcomeIds[]` on /// the shared outcome-token singleton rather than `yesToken`/`noToken`. /// Family-specific conveniences belong on the per-family interface that extends /// this one (see `IBinaryMarket`). /// /// Oracle v2 / settlement v3: resolution is a CTF-style PAYOUT VECTOR /// (`resolve(uint256[])` + `payoutNumerators()`), not a winning index — one /// redemption rule for win / void / (future) scalar. Void survives as a /// structurally distinct terminal state (uniform vector + explicit fee-waiver /// flag), but all settlement math reads only the vector. interface IMarket { /// @notice Emitted whenever the market lifecycle status changes. /// @param oldStatus Prior lifecycle status. /// @param newStatus New lifecycle status. event StatusChanged(MarketStatus indexed oldStatus, MarketStatus indexed newStatus); /// @notice Emitted when the oracle resolves the market to a payout vector. /// @param payoutDenominator Denominator the numerators are scaled against. /// @param payoutNumerators Per-outcome payout numerators; `Σ == payoutDenominator` raw. event Resolved(uint32 payoutDenominator, uint256[] payoutNumerators); /// @notice Emitted when the oracle voids the market. The stored payout vector is /// the uniform vector (each outcome redeems 1/N, settlement fee waived). event Voided(); /// @notice Advance Listed→Trading at `tradingStart` and Trading→Locked at /// `expiry`. Anyone may poke; idempotent. Current binary implementation /// derives status from time + finalState, so this is a no-op kept for /// ABI stability with adapters that may still call it. function poke() external; /// @notice Oracle-only: settle the market to a payout vector. /// @dev Reverts unless `Σ payoutNumerators_ == payoutDenominator()` and /// `payoutNumerators_.length == outcomeCount()` — the raw-sum invariant is /// what makes `Σ redeemable <= backing` hold downstream under floor division. /// @param payoutNumerators_ Per-outcome payout numerators (one-hot for a binary win). function resolve(uint256[] calldata payoutNumerators_) external; /// @notice Oracle-only: void the market. Stores the uniform payout vector; each /// outcome redeems for 1/N collateral with the settlement fee waived. function voidMarket() external; /// @notice ERC-20 collateral that backs outcome supply 1:1. function collateral() external view returns (IERC20); /// @notice Number of outcomes this market has (binary = 2, multi = N). function outcomeCount() external view returns (uint8); /// @notice The shared ERC-6909 outcome-token singleton this market's positions live on. /// @return The outcome-token singleton address. function outcomeToken() external view returns (address); /// @notice ERC-6909 ids of every outcome, in outcome-index order. /// @return ids Array of outcome ids on the singleton, indexed by outcome index. function outcomeIds() external view returns (uint256[] memory ids); /// @notice Current lifecycle status. function status() external view returns (MarketStatus); /// @notice Per-outcome payout numerators; empty until the market settles /// (Resolved stores the oracle vector, Voided stores the uniform vector). function payoutNumerators() external view returns (uint256[] memory); /// @notice Denominator every payout vector is scaled against. A protocol /// constant (`PAYOUT_VECTOR_DENOMINATOR` = 10_000_000, DESIGN §7.7), /// surfaced per-market so consumers never hardcode it. function payoutDenominator() external view returns (uint32); /// @notice Collateral currently locked backing the outstanding outcome /// sets. Tracked on the linked pool; this view is a delegate so consumers /// can read it off the market like in the AMM era. function backing() external view returns (uint256); /// @notice Unix-seconds timestamp at which trading begins. function tradingStart() external view returns (uint64); /// @notice Unix-seconds timestamp at which trading halts and Locked begins. function expiry() external view returns (uint64); /// @notice Type + book topology of this market, fixed at construction. /// @dev Stage 1 binary markets return `BINARY_SINGLE_BOOK`; later stages /// flip this for dual-book + native-multi variants. function marketType() external view returns (MarketType); }src/clob/interfaces/MarketTypes.sol
// SPDX-License-Identifier: MIT pragma solidity 0.8.34; /// @notice Lifecycle status of a binary market. /// @dev Transitions are monotone: Listed → Trading → Locked → (Resolved | Voided). /// Settling is reserved for a future on-chain dispute window; today resolve and /// void short-circuit from Locked. enum MarketStatus { Listed, Trading, Locked, Settling, Resolved, Voided } /// @notice Market type + book topology, stored as an immutable on every market. /// @dev Per clob-design §4 "On-chain type tag per market": the indexer, /// frontend, and BinaryMarketsModule read this field to branch by market shape instead of /// hard-coding behaviour by address. Adding a new market type later is a /// single new enum value, not a new event family. /// /// Values: /// - `BINARY_SINGLE_BOOK` — binary YES/NO market trading on one book with /// price inversion (Stage 1; the only value used today). /// - `BINARY_DUAL_BOOK` — binary market trading on two separate books /// (Stage 2 — reserved). /// - `NATIVE_MULTI` — native multi-outcome market with N books, one per /// outcome (Stage 3 — reserved). enum MarketType { BINARY_SINGLE_BOOK, BINARY_DUAL_BOOK, NATIVE_MULTI } /// @notice Policy applied when a market voids rather than resolving normally — frozen per market /// at creation, stored on the `BinaryMarket` clone itself (the void paths read the clone's /// own field, never the registry, so the module-bypassing `voidExpired` is covered too). /// @dev Shared by `BinaryMarket` (frozen field + vector construction), `BinaryClobFactory` /// (threading) and `BinaryMarketsModule` (venue params v3 + policy resolution at creation). /// Values are append-only — the numeric encoding rides venue `feeParams` bytes and events. /// - `UNIFORM`: every outcome pays 1/N — `[D/2, D/2]` for binary. The default, and the fallback /// for every degenerate/failure case of the snapshot policy. /// - `AMM_SNAPSHOT`: legacy AMM-era slot, no meaning on the CLOB — retained for encoding /// stability only; rejected wherever a policy is accepted as input. /// - `CLOB_SNAPSHOT`: pay `[p, D-p]` where `p` is derived from the market's closing YES price on /// its pool ("lock until terminal or captured", dex PR #218): the stored `captureClose` mid when /// present, else the raw top of the guaranteed-intact closing book; uniform when one-sided/empty /// or on any pool read failure. enum VoidPolicy { UNIFORM, AMM_SNAPSHOT, CLOB_SNAPSHOT } /// @notice Where a stored void payout vector came from — emitted in `VoidVector` so indexers and /// UIs can show WHY a voided market paid what it paid without re-deriving pool state. /// - `UNIFORM_POLICY`: the market's policy is `UNIFORM` — the uniform vector by rule. /// - `CAPTURED_MID`: `CLOB_SNAPSHOT` consumed the pool's stored `captureClose` mid. /// - `RAW_TOP`: `CLOB_SNAPSHOT` read the intact book's top directly (early void, or post-expiry /// void before anyone captured). /// - `FALLBACK_UNIFORM`: `CLOB_SNAPSHOT` could not produce a two-sided price (degenerate/empty /// close, old pool without the capture surface, or any pool read failure) — uniform fallback. enum VoidVectorSource { UNIFORM_POLICY, CAPTURED_MID, RAW_TOP, FALLBACK_UNIFORM }src/interfaces/IOracleAnswerReceiver.sol
// SPDX-License-Identifier: MIT pragma solidity 0.8.34; import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; /// @dev Canonical payout-vector denominator (Oracle v2, DESIGN §7.7): 100% in /// basis-points × 1000 — the SAME unit the dex prediction family's fee math /// uses (`ONE_HUNDRED_PERCENT_BPS * 1_000`), pinned here so the wire, the /// market storage, and the settlement math all divide by one constant. /// Every delivered payout vector satisfies `Σ numerators == 10_000_000` raw; /// a binary win is `[D, 0]` / `[0, D]`, a void is uniform `[D/2, D/2]`. uint32 constant PAYOUT_VECTOR_DENOMINATOR = 10_000_000; /// @title IOracleAnswerReceiver /// @notice Implemented by the market-type module (e.g. BinaryMarketsModule). /// Called by the oracle adapter (the hub) to resolve ONE market at a time. /// Only the adapter registered for a given market may resolve it (the module /// skips markets bound to a different adapter). /// /// Oracle v2 wire (DESIGN §2B/§8b/§8e): resolution is delivered as a CTF-style /// PAYOUT VECTOR instead of a single winning index — one redemption rule for /// win / void / (future) scalar. The adapter translates the raw oracle answer /// (discrete winner → one-hot, reference comparison → one-hot, every /// `VoidReason` → uniform vector) and drives the per-market fan-out itself: the /// hub's bounded-drain loop calls `resolveOne` per market so it can meter each /// market's gas and debit the operator exactly (no unbounded module-side loop). /// /// Implements ERC-165 so adapters can verify the receiver at construction /// time rather than discovering a misconfiguration at resolution time. interface IOracleAnswerReceiver is IERC165 { /// @notice Resolve ONE market to the delivered payout vector. Called by the /// registered adapter (the hub) from its bounded-drain loop, once per bound /// market, so the hub can meter each call's gas and debit the operator. /// @dev `voided` is the EXPLICIT void discriminator (DESIGN §7.7: the fee waiver /// is flag-driven, never inferred from the vector shape). When `voided` is /// true the numerators are the uniform vector and the receiver applies its /// void path (uniform payout, settlement fee waived). Reference-mode markets /// ignore the delivered vector and recompute a one-hot from the market's own /// numeric answer vs its reference (pulled from the adapter=hub). Non-reverting /// per market: a per-market obstruction is skipped (retryable via `pokeOracle`) /// rather than reverting the hub's callback. /// @param marketId Module-scoped market id to resolve. /// @param payoutDenominator Denominator the numerators are scaled against /// (always `PAYOUT_VECTOR_DENOMINATOR`; self-describing for future families). /// @param payoutNumerators Per-outcome payout numerators; `Σ == payoutDenominator`. /// @param voided True if the oracle voided the question rather than resolving it. function resolveOne( bytes32 marketId, uint32 payoutDenominator, uint256[] calldata payoutNumerators, bool voided ) external; }
ABI · 48 entries
json
[
{
"type": "constructor",
"inputs": [],
"stateMutability": "nonpayable"
},
{
"name": "AlreadyInitialised",
"type": "error",
"inputs": []
},
{
"name": "InvalidInitialization",
"type": "error",
"inputs": []
},
{
"name": "InvalidPayoutVector",
"type": "error",
"inputs": []
},
{
"name": "InvalidSettlementWindow",
"type": "error",
"inputs": []
},
{
"name": "InvalidTradingWindow",
"type": "error",
"inputs": []
},
{
"name": "InvalidVoidPolicy",
"type": "error",
"inputs": []
},
{
"name": "NotInitializing",
"type": "error",
"inputs": []
},
{
"name": "NotOracle",
"type": "error",
"inputs": []
},
{
"name": "SettlementWindowOpen",
"type": "error",
"inputs": []
},
{
"name": "WrongStatus",
"type": "error",
"inputs": [
{
"name": "expected",
"type": "uint8",
"internalType": "enum MarketStatus"
},
{
"name": "actual",
"type": "uint8",
"internalType": "enum MarketStatus"
}
]
},
{
"name": "ZeroAddress",
"type": "error",
"inputs": []
},
{
"name": "Initialized",
"type": "event",
"inputs": [
{
"name": "version",
"type": "uint64",
"indexed": false,
"internalType": "uint64"
}
],
"anonymous": false
},
{
"name": "Resolved",
"type": "event",
"inputs": [
{
"name": "payoutDenominator",
"type": "uint32",
"indexed": false,
"internalType": "uint32"
},
{
"name": "payoutNumerators",
"type": "uint256[]",
"indexed": false,
"internalType": "uint256[]"
}
],
"anonymous": false
},
{
"name": "StatusChanged",
"type": "event",
"inputs": [
{
"name": "oldStatus",
"type": "uint8",
"indexed": true,
"internalType": "enum MarketStatus"
},
{
"name": "newStatus",
"type": "uint8",
"indexed": true,
"internalType": "enum MarketStatus"
}
],
"anonymous": false
},
{
"name": "VoidVector",
"type": "event",
"inputs": [
{
"name": "payoutNumerators",
"type": "uint256[]",
"indexed": false,
"internalType": "uint256[]"
},
{
"name": "policy",
"type": "uint8",
"indexed": false,
"internalType": "enum VoidPolicy"
},
{
"name": "source",
"type": "uint8",
"indexed": false,
"internalType": "enum VoidVectorSource"
}
],
"anonymous": false
},
{
"name": "Voided",
"type": "event",
"inputs": [],
"anonymous": false
},
{
"name": "backing",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"name": "collateral",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "contract IERC20"
}
],
"stateMutability": "view"
},
{
"name": "creator",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "address"
}
],
"stateMutability": "view"
},
{
"name": "expiry",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint64",
"internalType": "uint64"
}
],
"stateMutability": "view"
},
{
"name": "initialize",
"type": "function",
"inputs": [
{
"name": "collateral_",
"type": "address",
"internalType": "contract IERC20"
},
{
"name": "settler_",
"type": "address",
"internalType": "address"
},
{
"name": "creator_",
"type": "address",
"internalType": "address"
},
{
"name": "operatorId_",
"type": "uint32",
"internalType": "uint32"
},
{
"name": "venueId_",
"type": "bytes32",
"internalType": "bytes32"
},
{
"name": "tradingStart_",
"type": "uint64",
"internalType": "uint64"
},
{
"name": "expiry_",
"type": "uint64",
"internalType": "uint64"
},
{
"name": "settlementWindow_",
"type": "uint64",
"internalType": "uint64"
},
{
"name": "voidPolicy_",
"type": "uint8",
"internalType": "enum VoidPolicy"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "isResolved",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "view"
},
{
"name": "isVoided",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "view"
},
{
"name": "marketType",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint8",
"internalType": "enum MarketType"
}
],
"stateMutability": "view"
},
{
"name": "noId",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"name": "operatorId",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint32",
"internalType": "uint32"
}
],
"stateMutability": "view"
},
{
"name": "outcomeCount",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint8",
"internalType": "uint8"
}
],
"stateMutability": "pure"
},
{
"name": "outcomeIds",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "ids",
"type": "uint256[]",
"internalType": "uint256[]"
}
],
"stateMutability": "view"
},
{
"name": "outcomeToken",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "address"
}
],
"stateMutability": "view"
},
{
"name": "payoutDenominator",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint32",
"internalType": "uint32"
}
],
"stateMutability": "pure"
},
{
"name": "payoutNumerators",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256[]",
"internalType": "uint256[]"
}
],
"stateMutability": "view"
},
{
"name": "poke",
"type": "function",
"inputs": [],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "pool",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "address"
}
],
"stateMutability": "view"
},
{
"name": "poolNonce",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint64",
"internalType": "uint64"
}
],
"stateMutability": "view"
},
{
"name": "resolve",
"type": "function",
"inputs": [
{
"name": "payoutNumerators_",
"type": "uint256[]",
"internalType": "uint256[]"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "setOutcomeToken",
"type": "function",
"inputs": [
{
"name": "outcomeToken_",
"type": "address",
"internalType": "address"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "setPool",
"type": "function",
"inputs": [
{
"name": "pool_",
"type": "address",
"internalType": "address"
},
{
"name": "nonce_",
"type": "uint64",
"internalType": "uint64"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "settlementWindow",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint64",
"internalType": "uint64"
}
],
"stateMutability": "view"
},
{
"name": "settler",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "address"
}
],
"stateMutability": "view"
},
{
"name": "status",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint8",
"internalType": "enum MarketStatus"
}
],
"stateMutability": "view"
},
{
"name": "tradingActive",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "view"
},
{
"name": "tradingStart",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint64",
"internalType": "uint64"
}
],
"stateMutability": "view"
},
{
"name": "venueId",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "bytes32",
"internalType": "bytes32"
}
],
"stateMutability": "view"
},
{
"name": "voidExpired",
"type": "function",
"inputs": [],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "voidMarket",
"type": "function",
"inputs": [],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "voidPolicy",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint8",
"internalType": "enum VoidPolicy"
}
],
"stateMutability": "view"
},
{
"name": "yesId",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
}
]