--- title: SBET V2 Contracts Reference description: API reference for every external function, event, and error in SBET V2 — SBETCoreV2, SBETTreasuryV2, GraderRegistryV2, DisputeManager, GuardianCouncil, SBETStakerRewards. canonical: https://sbettoken.org/docs/protocol-v2/contracts-reference version: 2.0.0 updated: 2026-04-05 --- # Contracts Reference Complete API reference for the nine V2 contracts. Signatures, events, errors, and roles are pulled directly from the Solidity source. This document is grouped by contract; functions within each contract are ordered as they appear in source. Source tree: ``` contracts/v2/ ├── SBETCoreV2.sol ├── SBETTreasuryV2.sol ├── GraderRegistryV2.sol ├── DisputeManager.sol ├── GuardianCouncil.sol ├── SBETStakerRewards.sol ├── PredictionMarketV2.sol ├── PredictionAMMV2.sol ├── interfaces/ │ ├── ISBETCoreV2.sol │ ├── ISBETTreasuryV2.sol │ ├── IGraderRegistryV2.sol │ ├── IDisputeManager.sol │ ├── IGuardianCouncil.sol │ ├── ISBETStakerRewards.sol │ ├── IPredictionMarketV2.sol │ └── IPredictionAMMV2.sol └── libraries/ ├── ChallengeWindowLib.sol └── SignedPositionLib.sol ``` --- ## Shared enums ### MatchState ```solidity enum MatchState { None, // 0 Open, // 1 Proposed, // 2 Disputed, // 3 Finalized, // 4 Voided // 5 } ``` ### PoolType ```solidity enum PoolType { SIGNED_POSITION, // 0 — V1 long/short, binary only PARIMUTUEL // 1 — N-outcome, proportional pot } ``` ### Direction ```solidity enum Direction { LONG, // 0 SHORT // 1 } ``` ### VoidReason ```solidity enum VoidReason { NATURAL, // 0 — game cancelled, postponed, never started PROTOCOL, // 1 — protocol-level abort DISPUTE // 2 — arbiter ruled outcome unresolvable } ``` ### PauseTier ```solidity enum PauseTier { None, // 0 Match, // 1 — bond-pause, 24h max Category, // 2 — 3-of-5 guardian, 72h max Global // 3 — 5-of-9 guardian, 14d max } ``` --- ## SBETCoreV2 Match registry, bet accounting, and two-phase finalization state machine. ### Constants ```solidity uint256 public constant ABSOLUTE_CAP = 225_000e18; // per-match cap uint256 public constant MATCH_LOCK_BOND = 1_000e18; // SBET uint256 public constant MATCH_LOCK_DURATION = 24 hours; uint16 public constant DEFAULT_PANEL_SIZE = 13; uint16 public constant DEFAULT_QUORUM_M = 9; uint256 public constant VRF_FALLBACK_DELAY = 24 hours; ``` ### Roles ```solidity bytes32 public constant GOVERNANCE_ROLE = keccak256("GOVERNANCE_ROLE"); bytes32 public constant MATCH_REGISTRAR_ROLE = keccak256("MATCH_REGISTRAR_ROLE"); bytes32 public constant ARBITER_ROLE = keccak256("ARBITER_ROLE"); bytes32 public constant GRADER_AGGREGATOR_ROLE = keccak256("GRADER_AGGREGATOR_ROLE"); bytes32 public constant VRF_FULFILLER_ROLE = keccak256("VRF_FULFILLER_ROLE"); ``` ### Immutable wiring ```solidity ISBETTreasuryV2 public immutable TREASURY; IGraderRegistryV2 public immutable GRADERS; IDisputeManager public immutable DISPUTES; IGuardianCouncil public immutable GUARDIANS; IERC20 public immutable SBET_TOKEN; address public immutable VRF_COORDINATOR; uint64 public immutable VRF_SUBSCRIPTION_ID; ``` ### `registerMatch` ```solidity function registerMatch( uint256 matchId, bytes32 categoryId, uint16 numOutcomes, PoolType poolType, bytes32 panelRoot, uint16 panelSize, uint16 panelQuorumM ) external onlyRole(MATCH_REGISTRAR_ROLE); ``` Registers a new match. `matchId` is chosen off-chain (typically `keccak256` of metadata) and must be unused. `poolType` is set **once** here and never mutated. `panelRoot` commits to the VRF-drawn grader panel; `panelSize`/`panelQuorumM` override the defaults (13/9) for this match when non-zero. **Emits**: `MatchRegistered`, `MatchStateTransitioned(None,Open)`, `VRFSeedRequested(matchId, 0)` **Reverts**: `SignedPositionMustBeBinary`, `InvalidPanelCommitment`, `"Core: matchId zero"`, `"Core: bad numOutcomes"`, `"Core: match exists"` ### `placeBet` ```solidity function placeBet( uint256 matchId, uint8 outcome, address token, uint256 amount ) external; ``` **PARIMUTUEL-only** bet placement. Pulls `amount` of `token` from the caller via Treasury's `receiveStake`. Reverts if user already has a position on a different outcome for this match (single-outcome constraint — Bug Fix #1). **Emits**: `BetPlaced(matchId, bettor, token, outcome, amount, totalAcceptedAfter)` **Reverts**: `MatchNotOpen`, `InvalidOutcome`, `WrongPoolType`, `ExistingPositionDifferentOutcome`, `PerMatchCapExceeded`, pause errors ### `openSignedPosition` ```solidity function openSignedPosition( uint256 matchId, Direction direction, address token, uint256 amount ) external; ``` **SIGNED_POSITION-only** position opening. Dual long+short is valid (a user may hold both directions simultaneously). Cap applies to `max(longPool, shortPool)`, not the sum (Bug Fix #4). **Emits**: `SignedPositionOpened(matchId, bettor, token, direction, amount, directionPoolAfter)` **Reverts**: `MatchNotOpen`, `WrongPoolType`, `PerMatchCapExceeded`, pause errors ### `proposeOutcome` ```solidity function proposeOutcome( uint256 matchId, uint8 outcome, uint16 confidenceBps, uint8 sourceAgreement, uint256 signers, uint256 dissents ) external onlyRole(GRADER_AGGREGATOR_ROLE); ``` Transition `Open → Proposed`. Computes the challenge-window tier via `ChallengeWindowLib.computeTier(N, M, signers, dissents, confidenceBps, sourceAgreement)` where N = `MatchInfo.panelSize` and M = `MatchInfo.quorumM` (Bug Fix #3). **Emits**: `OutcomeProposed(matchId, outcome, tier, unlockAt, confidenceBps, proposer)`, `MatchStateTransitioned(Open,Proposed)` **Reverts**: `MatchNotOpen`, `InvalidOutcome`, `"Core: tally overflows panel"`, `"Core: below quorum"`, pause errors ### `finalize` ```solidity function finalize(uint256 matchId) external; ``` Permissionless. Transitions `Proposed → Finalized` once `block.timestamp >= unlockAt`. **Emits**: `MatchFinalized`, `MatchStateTransitioned(Proposed,Finalized)` **Reverts**: `MatchNotProposed`, `ChallengeWindowActive(matchId, unlockAt)`, pause errors ### `openDispute` ```solidity function openDispute(uint256 matchId, uint8 counterOutcome) external returns (uint256 disputeId); ``` Permissionless. Challenger must have approved **DisputeManager** (not Core) for the initial bond. Transitions `Proposed → Disputed` and forwards to `DisputeManager.openDispute`. **Emits**: `MatchStateTransitioned(Proposed,Disputed)`, `DisputeEscalated(matchId, DISPUTES, bond)` **Reverts**: `MatchNotProposed`, `ChallengeWindowActive` (inverse — must be before unlock), `InvalidOutcome` ### `resolveDispute` ```solidity function resolveDispute( uint256 matchId, uint8 newOutcome_, bool void_, VoidReason voidReason ) external; ``` Called by DisputeManager only. Transitions `Disputed → Finalized` (if `!void_`) or `Disputed → Voided` (if `void_`). **Reverts**: `NotDisputeManager(caller)`, `InvalidMatchState`, `"Core: bad outcome"` ### `overrideOutcome` ```solidity function overrideOutcome( uint256 matchId, uint8 newOutcome, bytes32 evidenceHash ) external onlyRole(ARBITER_ROLE); ``` 5-of-9 arbiter emergency path. Transitions state from `Open`/`Proposed`/`Disputed` to `Finalized` with the arbiter's chosen outcome. `evidenceHash` is emitted via topic-index search (no dedicated event). **Emits**: `MatchFinalized`, `MatchStateTransitioned(prev,Finalized)` **Reverts**: `"Core: bad state for override"`, `"Core: bad outcome"` ### `voidMatch` ```solidity function voidMatch(uint256 matchId, VoidReason reason) external onlyRole(GOVERNANCE_ROLE); ``` Governance-only void path. `reason` must be `NATURAL` or `PROTOCOL` — dispute voids flow through `resolveDispute`. **Emits**: `MatchVoided(matchId, reason)`, `MatchStateTransitioned(prev,Voided)` **Reverts**: `"Core: void wrong state"`, `"Core: use resolveDispute"` ### `claimParimutuel` ```solidity function claimParimutuel(uint256 matchId, address token) external; ``` Payout formula: `userStake × totalAccepted / winningOutcomeStake` (if bettor's outcome matches resolved), `0` otherwise. Stake is zeroed before Treasury call (reentrancy-safe). **Reverts**: `MatchNotFinalized`, `WrongPoolType`, `"Core: no stake"` ### `claimSignedPosition` ```solidity function claimSignedPosition(uint256 matchId, address token) external; ``` Payout formulas (V1 parity): ``` payoff_long = longPos × finalPrice / MAX_PRICE payoff_short = shortPos × (MAX_PRICE - finalPrice) / MAX_PRICE finalPrice = outcome × MAX_PRICE / (numOutcomes - 1) ``` Both long and short are settled in a single call. **Reverts**: `MatchNotFinalized`, `WrongPoolType`, `"Core: no position"` ### `claimVoidRefund` ```solidity function claimVoidRefund(uint256 matchId, address token) external; ``` Refunds principal (+ fees, pending fee-schedule lock-in). Branches on `PoolType`: parimutuel returns `_stakes[...][token]`; signed-position returns `longPos + shortPos`. **Reverts**: `InvalidMatchState`, `"Core: no stake"`, `"Core: no position"` ### `lockMatch` / `releaseMatchLock` ```solidity function lockMatch(uint256 matchId) external; function releaseMatchLock(uint256 matchId, bool maliciousSlash) external; ``` Permissionless lock with $1k SBET bond (`MATCH_LOCK_BOND`). Lock auto-expires in 24h. `releaseMatchLock(matchId, true)` is `GOVERNANCE_ROLE`-only; slashes the bond to Treasury. `releaseMatchLock(matchId, false)` is permissionless after expiry. **Emits**: `MatchLocked(matchId, by, bond, unlockAt)`, `MatchUnlocked(matchId, bondSlashed)` ### VRF seeding ```solidity function fulfillVRFSeed(uint256 matchId, uint256 seed) external onlyRole(VRF_FULFILLER_ROLE); function seedFromBlockhashFallback(uint256 matchId) external; ``` First is the happy path; second is the 24h liveness fallback using `blockhash(matchCreationBlock)`. **Emits**: `VRFSeedFulfilled(matchId, seed, fromFallback)` ### Views ```solidity function getMatchInfo(uint256 matchId) external view returns (MatchInfo memory); function getMatchState(uint256 matchId) external view returns (MatchState); function perMatchPayoutCap() public view returns (uint256); function graderStakeFloor() external view returns (uint256); function quorumM() external view returns (uint256); function pauseTierOf(uint256 matchId) public view returns (PauseTier); function stakeOf(uint256 matchId, address bettor, address token) external view returns (uint256); function positionOf(uint256 matchId, address bettor, Direction direction, address token) external view returns (uint256); function vrfSeedOf(uint256 matchId) external view returns (uint256); function verifyPanelMembership(uint256 matchId, address grader, bytes32[] calldata proof) external view returns (bool); ``` `perMatchPayoutCap()` returns `min(graderStakeFloor × quorumM, ABSOLUTE_CAP)`. ### Governance setter ```solidity function setQuorumM(uint256 newM) external onlyRole(GOVERNANCE_ROLE); ``` ### Events ```solidity event MatchRegistered(uint256 indexed matchId, bytes32 indexed categoryId, uint16 numOutcomes, uint256 payoutCap, PoolType poolType, bytes32 panelRoot, uint16 panelSize, uint16 quorumM); event BetPlaced(uint256 indexed matchId, address indexed bettor, address indexed token, uint8 outcome, uint256 amount, uint256 totalAcceptedAfter); event SignedPositionOpened(uint256 indexed matchId, address indexed bettor, address indexed token, Direction direction, uint256 amount, uint256 directionPoolAfter); event OutcomeProposed(uint256 indexed matchId, uint8 outcome, uint8 tier, uint32 unlockAt, uint16 confidenceBps, address indexed proposer); event MatchFinalized(uint256 indexed matchId, uint8 outcome, uint32 finalizedAt); event MatchVoided(uint256 indexed matchId, VoidReason reason); event MatchStateTransitioned(uint256 indexed matchId, MatchState previous, MatchState next); event MatchLocked(uint256 indexed matchId, address indexed by, uint256 bond, uint256 unlockAt); event MatchUnlocked(uint256 indexed matchId, bool bondSlashed); event DisputeEscalated(uint256 indexed matchId, address indexed disputeManager, uint256 bond); event VRFSeedRequested(uint256 indexed matchId, uint256 requestId); event VRFSeedFulfilled(uint256 indexed matchId, uint256 seed, bool fromFallback); ``` ### Errors ```solidity error MatchNotOpen(uint256 matchId); error MatchNotProposed(uint256 matchId); error MatchNotFinalized(uint256 matchId); error InvalidMatchState(uint256 matchId, MatchState actual); error ChallengeWindowActive(uint256 matchId, uint256 unlockAt); error PerMatchCapExceeded(uint256 requested, uint256 cap); error MatchPaused(uint256 matchId, PauseTier tier); error CategoryPaused(bytes32 categoryId); error GlobalPaused(); error InvalidOutcome(uint8 outcome, uint16 numOutcomes); error NotDisputeManager(address caller); error NotArbiter(address caller); error NotGraderRegistry(address caller); error ExistingPositionDifferentOutcome(); error WrongPoolType(PoolType expected, PoolType actual); error InvalidPanelCommitment(); error NotOnPanel(); error SignedPositionMustBeBinary(); ``` --- ## SBETTreasuryV2 Token custody, payout release, auto-claim meta-tx, and slash distributor. Inherits from `ERC2771Context`, `AccessControl`, `ReentrancyGuard`, `Pausable`. ### Constants ```solidity uint256 public constant AUTO_CLAIM_FEE_CAP_BPS = 500; // 5% max uint256 public constant AUTO_CLAIM_FEE_SBET_BPS = 200; // 2% default for SBET uint256 public constant AUTO_CLAIM_FEE_OTHER_BPS = 350; // 3.5% default for other tokens uint256 public constant SLASH_TO_CHALLENGER_BPS = 5_000; // 50% uint256 public constant SLASH_TO_TREASURY_BPS = 2_000; // 20% uint256 public constant SLASH_TO_STAKERS_BPS = 2_000; // 20% uint256 public constant SLASH_TO_BOUNTY_BPS = 1_000; // 10% ``` ### Roles ```solidity bytes32 public constant GOVERNANCE_ROLE = keccak256("GOVERNANCE_ROLE"); bytes32 public constant CORE_ROLE = keccak256("CORE_ROLE"); bytes32 public constant GRADER_REGISTRY_ROLE = keccak256("GRADER_REGISTRY_ROLE"); bytes32 public constant DISPUTE_MANAGER_ROLE = keccak256("DISPUTE_MANAGER_ROLE"); bytes32 public constant TOKEN_MANAGER_ROLE = keccak256("TOKEN_MANAGER_ROLE"); ``` ### Custody entry points (Core-only) ```solidity function receiveStake(address token, address from, uint256 amount, uint256 matchId) external onlyRole(CORE_ROLE); function releasePayout(address token, address to, uint256 amount, uint256 matchId) external onlyRole(CORE_ROLE); function refundVoid(address token, address to, uint256 amount, uint256 matchId) external onlyRole(CORE_ROLE); function autoClaim(address user, address token, uint256 amount, uint256 matchId) external onlyRole(CORE_ROLE) returns (uint256 netPayout, uint256 fee); ``` `autoClaim` deducts the per-token fee (default 2% SBET, 3.5% other; governance- override per-token up to 500 bps) and forwards the fee to `stakerRewards` via `notifyReward` (wrapped in try/catch to prevent auto-claim from reverting on StakerRewards failure). ### Slash distributor ```solidity function distributeSlash( address bondToken, uint256 amount, address challenger, uint256 matchId, address grader ) external onlyRole(GRADER_REGISTRY_ROLE); ``` Splits `amount` per the 50/20/20/10 distribution. Emits `SlashDistributed(matchId, grader, total, toChallenger, toTreasury, toStakers, toBounty)`. ### Token allowlist ```solidity function addAllowedToken(address token) external onlyRole(TOKEN_MANAGER_ROLE); function removeAllowedToken(address token) external onlyRole(TOKEN_MANAGER_ROLE); function isAllowedToken(address token) external view returns (bool); function getAllowedTokens() external view returns (address[] memory); ``` SBET_TOKEN is added in the constructor and cannot be removed. ### User controls ```solidity function setAutoClaimOptIn(bool enabled) external; function isAutoClaimOptedIn(address user) external view returns (bool); ``` Uses `_msgSender()` (ERC-2771) so that users can opt in via meta-tx. ### Governance ```solidity function setAutoClaimFee(address token, uint256 bps) external onlyRole(GOVERNANCE_ROLE); function setStakerRewards(address staker) external onlyRole(GOVERNANCE_ROLE); function setBountyPool(address bounty) external onlyRole(GOVERNANCE_ROLE); function pause() external onlyRole(GOVERNANCE_ROLE); function unpause() external onlyRole(GOVERNANCE_ROLE); // REVERTS — forwarder is immutable function setTrustedForwarder(address) external onlyRole(GOVERNANCE_ROLE); ``` ### Views ```solidity function autoClaimFeeBps(address token) public view returns (uint256); function trustedForwarder() public view returns (address); ``` ### Events ```solidity event StakeHeld(address indexed token, address indexed from, uint256 amount, uint256 indexed matchId); event PayoutReleased(address indexed token, address indexed to, uint256 amount, uint256 indexed matchId, bool autoClaim); event VoidRefund(address indexed token, address indexed to, uint256 amount, uint256 indexed matchId); event AutoClaimFeeUpdated(address indexed token, uint256 oldBps, uint256 newBps); event AutoClaimOptIn(address indexed user, bool enabled); event SlashDistributed(uint256 indexed matchId, address indexed grader, uint256 total, uint256 toChallenger, uint256 toTreasury, uint256 toStakers, uint256 toBounty); event TrustedForwarderUpdated(address indexed previous, address indexed current); ``` --- ## GraderRegistryV2 Grader staking, grade submission, slashing coordination. ### Constants ```solidity uint256 public constant DEFAULT_STAKE_FLOOR = 25_000e18; // $25k SBET uint256 public constant DEREGISTER_COOLDOWN = 7 days; ``` ### Roles ```solidity bytes32 public constant GOVERNANCE_ROLE = keccak256("GOVERNANCE_ROLE"); bytes32 public constant SLASHER_ROLE = keccak256("SLASHER_ROLE"); bytes32 public constant CORE_ROLE = keccak256("CORE_ROLE"); ``` ### Grader lifecycle ```solidity function register(uint256 amount) external; function increaseStake(uint256 amount) external; function requestDeregister() external; function executeDeregister() external; ``` `register` requires `amount >= stakeFloor`. `requestDeregister` moves the grader into `Cooldown` for 7 days; `executeDeregister` can be called after. ### Grade submission ```solidity function submitGrade(uint256 matchId, uint8 outcome, uint16 confidenceBps) external; ``` Only `Active` graders with `stake >= stakeFloor` may submit. Each grader may submit at most once per `matchId`. `confidenceBps ∈ [0, 10_000]`. ### Slashing ```solidity function slashWithChallenger( address grader, address challenger, uint256 matchId, uint256 amount, bytes32 reasonCode ) external onlyRole(SLASHER_ROLE); // Compatibility shim — prefer slashWithChallenger. function slash( address grader, uint256 matchId, uint256 amount, bytes32 reasonCode ) external onlyRole(SLASHER_ROLE); ``` Both transfer `amount` of stake token to Treasury and call `Treasury.distributeSlash`. Use `slashWithChallenger` — the single-arg `slash` is retained for emergency arbiter-direct slashing but carries a wrong-semantic placeholder for the challenger address. ### Governance ```solidity function setGraderStakeFloor(uint256 newFloor) external onlyRole(GOVERNANCE_ROLE); function pause() external onlyRole(GOVERNANCE_ROLE); function unpause() external onlyRole(GOVERNANCE_ROLE); ``` ### Views ```solidity function isActive(address grader) external view returns (bool); function getGraderInfo(address grader) external view returns (GraderInfo memory); function getGradeSubmissions(uint256 matchId) external view returns (GradeSubmission[] memory); function graderStakeFloor() external view returns (uint256); function aggregateConfidence(uint256 matchId) external view returns (uint16 avgConfidenceBps, uint256 dissents, uint256 signers); ``` `aggregateConfidence` computes the **modal outcome** and reports: - `signers` — total submissions - `dissents` — submissions that disagree with the mode - `avgConfidenceBps` — unweighted mean of confidence across all submissions ### Events ```solidity event GraderRegistered(address indexed grader, uint256 stake); event StakeIncreased(address indexed grader, uint256 delta, uint256 newStake); event DeregisterRequested(address indexed grader, uint64 cooldownEndsAt); event DeregisterExecuted(address indexed grader, uint256 returnedStake); event GradeSubmitted(uint256 indexed matchId, address indexed grader, uint8 outcome, uint16 confidenceBps); event GraderSlashed(address indexed grader, uint256 indexed matchId, uint256 slashAmount, bytes32 reasonCode); event GraderStakeFloorUpdated(uint256 oldFloor, uint256 newFloor); ``` ### Structs ```solidity struct GraderInfo { uint256 stake; uint64 registeredAt; uint64 cooldownEndsAt; GraderStatus status; uint256 slashedTotal; uint256 gradesSubmitted; } struct GradeSubmission { address grader; uint8 outcome; uint16 confidenceBps; uint64 submittedAt; } enum GraderStatus { Inactive, Active, Cooldown, Slashed } ``` --- ## DisputeManager Bond-escalation dispute state machine with 5-of-9 arbiter override. ### Constants ```solidity uint256 public constant INITIAL_BOND_BPS = 100; // 1% of TVL uint256 public constant INITIAL_BOND_FLOOR = 5_000e18; uint256 public constant INITIAL_BOND_CEILING = 50_000e18; uint256 public constant ESCALATION_FACTOR = 2; // 2× per round uint8 public constant MAX_ROUNDS = 3; // 8× initial max uint256 public constant RESPONSE_WINDOW = 24 hours; ``` ### Roles ```solidity bytes32 public constant GOVERNANCE_ROLE = keccak256("GOVERNANCE_ROLE"); bytes32 public constant ARBITER_ROLE = keccak256("ARBITER_ROLE"); bytes32 public constant CORE_ROLE = keccak256("CORE_ROLE"); ``` ### `openDispute` ```solidity function openDispute( uint256 matchId, address challenger, uint8 proposedOutcome, uint8 counterOutcome, uint256 matchTvl, address bondToken ) external onlyRole(CORE_ROLE) returns (uint256 disputeId); ``` Called by Core only. Challenger must have approved **this contract** for the initial bond before Core invokes this function. **Reverts**: `DisputeAlreadyExists(matchId)`, `InvalidOutcomeChoice` ### `respond` ```solidity function respond(uint256 disputeId) external; ``` Caller must be the `awaiting` side (challenger or defender). On first response, the defender is locked in. Bond doubles each round. If `round >= MAX_ROUNDS` after this call, state transitions to `AwaitingArbiter`. **Reverts**: `DisputeNotFound`, `DeadlinePassed`, `NotAwaiting(expected, caller)` ### `claimByDefault` ```solidity function claimByDefault(uint256 disputeId) external; ``` Permissionless; callable after `deadline`. The defaulting side (whoever was `awaiting`) loses their cumulative deposit; the winner receives the full pot. Propagates the default-ruling to Core via `core.resolveDispute`. **Reverts**: `DisputeNotFound`, `DeadlineNotPassed` ### `arbiterResolve` ```solidity function arbiterResolve( uint256 disputeId, DisputeSide winner, uint8 finalOutcome, bool void_, bytes32 evidenceHash ) external onlyRole(ARBITER_ROLE); ``` 5-of-9 Safe multisig resolution. Loser's contributions flow to winner; `core.resolveDispute` is called to propagate the ruling. **Reverts**: `DisputeNotFound`, `InvalidOutcomeChoice` ### Views ```solidity function getDispute(uint256 disputeId) external view returns (Dispute memory); function disputeIdFor(uint256 matchId) external view returns (uint256); function computeInitialBond(uint256 matchTvl) public pure returns (uint256); ``` ### Structs ```solidity struct Dispute { uint256 matchId; address challenger; address defender; address bondToken; uint256 initialBond; uint256 currentBond; uint256 totalBonded; uint8 round; // 0..3 uint64 openedAt; uint64 deadline; DisputeState state; DisputeSide awaiting; DisputeSide winner; uint8 proposedOutcome; uint8 counterOutcome; } enum DisputeState { None, Open, Escalated, AwaitingArbiter, Resolved, Expired } enum DisputeSide { None, Challenger, Defender } ``` ### Events ```solidity event DisputeOpened(uint256 indexed disputeId, uint256 indexed matchId, address indexed challenger, uint256 bond, uint8 counterOutcome); event DisputeResponded(uint256 indexed disputeId, address indexed responder, uint8 round, uint256 bond); event DisputeEscalatedToArbiter(uint256 indexed disputeId, uint8 finalRound, uint256 totalBonded); event DisputeResolved(uint256 indexed disputeId, DisputeSide winner, uint8 finalOutcome, bool voided, bytes32 evidenceHash); event DisputeExpired(uint256 indexed disputeId, DisputeSide defaulter); ``` --- ## GuardianCouncil Three-tier pause controller. ### Constants ```solidity uint256 public constant CATEGORY_QUORUM = 3; uint256 public constant CATEGORY_SIGNERS = 5; uint256 public constant CATEGORY_MAX_DURATION = 72 hours; uint256 public constant GLOBAL_QUORUM = 5; uint256 public constant GLOBAL_SIGNERS = 9; uint256 public constant GLOBAL_MAX_DURATION = 14 days; uint256 public constant GLOBAL_UNPAUSE_TIMELOCK = 72 hours; ``` ### Roles ```solidity bytes32 public constant CATEGORY_GUARDIAN_ROLE = keccak256("CATEGORY_GUARDIAN_ROLE"); bytes32 public constant GLOBAL_GUARDIAN_ROLE = keccak256("GLOBAL_GUARDIAN_ROLE"); ``` ### Category pause ```solidity function voteCategoryPause(bytes32 categoryId, uint256 duration) external onlyRole(CATEGORY_GUARDIAN_ROLE); function liftCategoryPause(bytes32 categoryId) external; ``` `duration <= CATEGORY_MAX_DURATION (72h)`. Pause auto-activates when 3-of-5 guardians have voted. Pause may be lifted by admin at any time, or by anyone after expiry. ### Global pause ```solidity function voteGlobalPause(uint256 duration) external onlyRole(GLOBAL_GUARDIAN_ROLE); function voteGlobalUnpause() external onlyRole(GLOBAL_GUARDIAN_ROLE); function executeGlobalUnpause() external; ``` `duration <= GLOBAL_MAX_DURATION (14d)`. Pause auto-activates on 5-of-9. Unpause requires a separate 5-of-9 vote **plus** a 72h timelock before `executeGlobalUnpause` is callable. If the pause auto-expires (`block.timestamp >= expiresAt`), the timelock is bypassed and anyone can unpause. ### Signer rotation ```solidity function rotateVotes(bytes32[] calldata categoryIds) external onlyRole(DEFAULT_ADMIN_ROLE); ``` Increments all proposal nonces, invalidating in-flight votes from removed guardians. Must be called immediately after any guardian role grant/revoke. ### Views ```solidity function isCategoryPaused(bytes32 categoryId) external view returns (bool); function isGloballyPaused() external view returns (bool); function globalUnpauseUnlockAt() external view returns (uint256); ``` ### Events ```solidity event CategoryPauseVoted(bytes32 indexed categoryId, uint256 nonce, address indexed guardian); event CategoryPauseActivated(bytes32 indexed categoryId, uint64 expiresAt); event CategoryPauseLifted(bytes32 indexed categoryId, address indexed by); event GlobalPauseVoted(uint256 nonce, address indexed guardian); event GlobalPauseActivated(uint64 expiresAt); event GlobalUnpauseVoted(uint256 nonce, address indexed guardian); event GlobalUnpauseQueued(uint64 readyAt); event GlobalPauseLifted(address indexed by); event VotesRotated(); ``` --- ## SBETStakerRewards Pull-based MasterChef-style reward distributor. Stakers deposit SBET → earn multi-token rewards. ### Constants ```solidity uint256 public constant PRECISION = 1e18; ``` ### Roles ```solidity bytes32 public constant GOVERNANCE_ROLE = keccak256("GOVERNANCE_ROLE"); bytes32 public constant REWARD_NOTIFIER_ROLE = keccak256("REWARD_NOTIFIER_ROLE"); ``` ### Staking ```solidity function stake(uint256 amount) external; function unstake(uint256 amount) external; function restake() external; function claimRewards(address token) external returns (uint256 amount); ``` `stake` / `unstake` settle all pending rewards before mutating shares. `restake` compounds SBET rewards into shares without a token transfer. `claimRewards` pulls a single reward token. ### Reward notification ```solidity function notifyReward(address token, uint256 amount) external onlyRole(REWARD_NOTIFIER_ROLE); ``` Caller (Treasury) must have already transferred `amount` of `token` to this contract **before** calling. Bumps the per-share accumulator for `token`. ### Governance ```solidity function addRewardToken(address token) external onlyRole(GOVERNANCE_ROLE); function removeRewardToken(address token) external onlyRole(GOVERNANCE_ROLE); function setRewardNotifier(address notifier, bool allowed) external onlyRole(GOVERNANCE_ROLE); ``` SBET_TOKEN is added in the constructor and cannot be removed. ### Views ```solidity function pendingRewards(address user, address token) public view returns (uint256); function totalShares() external view returns (uint256); function sharesOf(address user) external view returns (uint256); function getRewardTokens() external view returns (address[] memory); mapping(address => uint256) public accRewardPerShare; mapping(address => mapping(address => uint256)) public rewardDebt; ``` Pending-rewards formula: ``` accrued = userShares × accRewardPerShare[token] / PRECISION pending = accrued - rewardDebt[user][token] ``` ### Events ```solidity event Staked(address indexed user, uint256 amount, uint256 newShares); event Unstaked(address indexed user, uint256 amount, uint256 newShares); event RewardNotified(address indexed token, uint256 amount, uint256 newAccPerShare); event RewardClaimed(address indexed user, address indexed token, uint256 amount); event Restaked(address indexed user, uint256 claimedAmount); ``` --- ## Library reference ### ChallengeWindowLib Pure library. No storage, no external calls. ```solidity function computeTier( uint256 n, uint256 m, uint256 signers, uint256 dissents, uint16 confidenceBps, uint8 sourceAgreement ) internal pure returns (Tier tier, uint32 windowSeconds); function unlockTimestamp(uint256 proposedAt, uint32 windowSeconds) internal pure returns (uint256 unlockAt); enum Tier { Fast, QuorumClean, OneDissent, Contentious } ``` **Reverts**: `InvalidQuorum`, `TooManyDissents`, `ConfidenceOutOfRange` ### SignedPositionLib Pure library. No storage, no external calls. V1 parity math. ```solidity uint256 internal constant MAX_PRICE = 1e9; uint256 internal constant MAX_POSITION_MAGNITUDE = 1e27; function longPayoff(uint256 magnitude, uint32 finalPrice) internal pure returns (uint256 payoff); function shortPayoff(uint256 magnitude, uint32 finalPrice) internal pure returns (uint256 payoff); function parimutuelPayoff(uint256 stake, uint256 totalAccepted, uint256 winningOutcomeStake) internal pure returns (uint256 payoff); ``` **Reverts**: `PositionTooLarge(magnitude, cap)`, `InvalidFinalPrice(finalPrice, maxPrice)` Uses OpenZeppelin `Math.mulDiv` (512-bit intermediate precision). --- ## PredictionMarketV2 Prediction market lifecycle — creation, metadata storage, and settlement orchestration built on the V2 Core state machine. Inherits `AccessControl`, `ReentrancyGuard`. ### Constants ```solidity uint256 public constant MAX_OUTCOMES = 10; uint256 public constant MIN_OUTCOMES = 2; uint256 public constant MAX_CREATOR_FEE_BPS = 50; // 0.5% uint256 public constant MAX_BATCH_SIZE = 100; bytes32 public constant PREDICTION_DOMAIN = keccak256("SBET.PredictionMarket.v2"); ``` ### Roles ```solidity bytes32 public constant GOVERNANCE_ROLE = keccak256("GOVERNANCE_ROLE"); bytes32 public constant MARKET_CREATOR_ROLE = keccak256("MARKET_CREATOR_ROLE"); ``` ### Immutable wiring ```solidity ISBETCoreV2 public immutable CORE; ISBETTreasuryV2 public immutable TREASURY; ``` ### `createMarket` ```solidity function createMarket(MarketParams calldata params) external nonReentrant returns (uint256 marketId); ``` Creates a prediction market. Validates parameters (outcome count, resolution time, expiry, creator fee cap, token allowlist). Registers a PARIMUTUEL match in Core with a domain-separated `coreMatchId`. Optionally seeds AMM liquidity if `params.initialLiquidity > 0` and an AMM is configured. The creator must have approved TreasuryV2 for the Core bond amount and (if seeding) this contract for `initialLiquidity` of the collateral token. **Emits**: `MarketCreated(marketId, coreMatchId, creator, question, numOutcomes)` **Reverts**: `NotMarketCreator`, `TooFewOutcomes`, `TooManyOutcomes`, `PastResolutionTime`, `ExpiryBeforeResolution`, `CreatorFeeTooHigh`, `ZeroAddress`, `TokenNotAllowed` ### `syncResolution` ```solidity function syncResolution(uint256 marketId) public nonReentrant; ``` Permissionless, idempotent. Reads Core's finalized outcome and marks the market as resolved. Calling on an already-synced market is a silent no-op (critical for keeper safety — `syncAndDistribute` depends on this). `resolvedAt` is set to Core's `unlockAt` (challenge window end), not `block.timestamp`, to ensure LP grace period accuracy. **Emits**: `MarketResolved(marketId, outcome, resolvedAt)`, `MarketStatusChanged(marketId, oldStatus, Resolved)` **Reverts**: `MarketDoesNotExist`, `MarketNotResolved` ### `syncVoid` ```solidity function syncVoid(uint256 marketId) public nonReentrant; ``` Same pattern as `syncResolution` for voided markets. Idempotent. **Emits**: `MarketVoided(marketId)`, `MarketStatusChanged(marketId, oldStatus, Voided)` **Reverts**: `MarketDoesNotExist`, `MarketNotVoided` ### `syncAndDistribute` ```solidity function syncAndDistribute(uint256 marketId, address[] calldata holders) external nonReentrant returns (uint256 totalPayout, uint256 failedCount); ``` Keeper convenience function. In a single transaction: 1. Calls `_syncResolution` (idempotent — safe if already synced) 2. Calls `amm.batchRedeemFor(marketId, holders)` to push payouts **Reverts**: `MarketDoesNotExist`, `AMMNotSet` ### `syncAndRefund` ```solidity function syncAndRefund(uint256 marketId, address[] calldata holders) external nonReentrant returns (uint256 totalRefunded, uint256 failedCount); ``` Same pattern for voided markets. Syncs void status, then batch-refunds. ### Admin ```solidity function setAMM(address amm_) external onlyRole(GOVERNANCE_ROLE); function setPermissionlessCreation(bool enabled) external onlyRole(GOVERNANCE_ROLE); ``` ### Views ```solidity function getMarket(uint256 marketId) external view returns (MarketView memory); function getMarketCount() external view returns (uint256); function isMarketTradeable(uint256 marketId) external view returns (bool); function getDerivedStatus(uint256 marketId) external view returns (DerivedStatus); function statusOf(uint256 marketId) external view returns (MarketStatus); function getCoreMatchId(uint256 marketId) external view returns (uint256); function getMarketCreator(uint256 marketId) external view returns (address); function getMarketToken(uint256 marketId) external view returns (address); function getMarketCreatorFeeBps(uint256 marketId) external view returns (uint256); function getMarketNumOutcomes(uint256 marketId) external view returns (uint256); function isResolved(uint256 marketId) external view returns (bool); function isVoided(uint256 marketId) external view returns (bool); function getWinningOutcome(uint256 marketId) external view returns (uint8); function getMarketTradeInfo(uint256 marketId) external view returns (bool tradeable, uint16 numOutcomes, uint16 creatorFeeBps); function getMarketsByCategory(string calldata category, uint256 offset, uint256 limit) external view returns (uint256[] memory); function getCreatorMarkets(address creator, uint256 offset, uint256 limit) external view returns (uint256[] memory); ``` `getMarketTradeInfo` is a combined view that replaces 3 separate cross-contract calls (saves ~6k gas per AMM trade). ### Enums ```solidity enum MarketType { Binary, MultiOutcome } enum MarketStatus { Active, // 0 — tradeable Paused, // 1 — pause tier active ResolutionPending, // 2 — outcome proposed Resolved, // 3 — finalized + synced Disputed, // 4 — bond-backed dispute Voided // 5 — market voided } enum DerivedStatus { NonExistent, Tradeable, ClosedAwaitingResolution, ResolutionPending, Disputed, Resolved, Voided } ``` ### Structs ```solidity struct MarketParams { string question; string[] outcomeLabels; string category; string resolutionSource; uint256 resolutionTime; uint256 expiryTime; address token; uint256 creatorFeeBps; uint256 initialLiquidity; uint256 liquidityParam; // LMSR b parameter (0 = auto-derive) } struct MarketView { uint256 marketId; uint256 coreMatchId; address creator; MarketType marketType; MarketStatus status; string question; string[] outcomeLabels; string category; string resolutionSource; uint256 resolutionTime; uint256 expiryTime; address token; uint256 creatorFeeBps; uint256 numOutcomes; uint256 createdAt; uint256 resolvedAt; uint8 winningOutcome; } ``` ### Events ```solidity event MarketCreated(uint256 indexed marketId, uint256 coreMatchId, address indexed creator, string question, uint256 numOutcomes); event MarketResolved(uint256 indexed marketId, uint8 winningOutcome, uint256 resolvedAt); event MarketVoided(uint256 indexed marketId); event MarketStatusChanged(uint256 indexed marketId, MarketStatus oldStatus, MarketStatus newStatus); event AMMSet(address indexed amm); event MarketCreatorRoleUpdated(bool permissionless); ``` ### Errors ```solidity error ZeroAddress(); error TooFewOutcomes(); error TooManyOutcomes(); error PastResolutionTime(); error ExpiryBeforeResolution(); error CreatorFeeTooHigh(uint256 actual, uint256 max); error TokenNotAllowed(address token); error NotMarketCreator(address caller, address expected); error MarketDoesNotExist(uint256 marketId); error MarketNotResolved(uint256 marketId); error MarketNotVoided(uint256 marketId); error AMMNotSet(); ``` --- ## PredictionAMMV2 LMSR automated market maker with ERC-1155 outcome shares, autonomous payouts, and fee routing to TreasuryV2. Inherits `ERC1155Supply`, `AccessControl`, `ReentrancyGuard`. ### Constants ```solidity uint256 public constant MAX_PRICE = 1e9; // price denominator uint256 public constant MAX_AMM_OUTCOMES = 20; uint256 public constant BPS_DENOMINATOR = 10_000; uint256 public constant MAX_PROTOCOL_FEE_BPS = 500; // 5% uint256 public constant LP_WITHDRAWAL_GRACE = 30 days; uint256 public constant MAX_BATCH_SIZE = 100; uint256 public constant MIN_B_PARAM = 1e18; uint256 public constant SAFE_RECOMMENDED_B = 1000e18; uint256 public constant MARKET_CHANGE_DELAY = 48 hours; // HIGH-1 timelock uint256 public constant MARKET_CHANGE_GRACE = 14 days; ``` ### Roles ```solidity bytes32 public constant GOVERNANCE_ROLE = keccak256("GOVERNANCE_ROLE"); ``` ### Immutable wiring ```solidity ISBETTreasuryV2 public immutable TREASURY; ``` ### Governance-settable reference (HIGH-1) ```solidity function PREDICTION_MARKET() public view returns (IPredictionMarketV2); function proposePredictionMarketChange(address newMarket) external onlyRole(GOVERNANCE_ROLE); function executePredictionMarketChange() external onlyRole(GOVERNANCE_ROLE); function cancelPredictionMarketChange() external onlyRole(GOVERNANCE_ROLE); ``` 48-hour timelock with 14-day grace window. Replaces the original immutable reference to enable independent Market contract upgrades. ### `seedLiquidity` ```solidity function seedLiquidity( uint256 marketId, uint256 amount, uint256 b, address provider ) external; ``` Called by PredictionMarketV2 or governance. Initializes the LMSR pool. `b` must be `>= MIN_B_PARAM (1e18)`. Collateral must already be transferred to this contract. Caches `creatorFeeBps` to avoid cross-contract reads on every trade. **Emits**: `LiquiditySeeded(marketId, provider, amount, b)` **Reverts**: `OnlyPredictionMarketOrOwner`, `PoolAlreadySeeded`, `BParamTooSmall(b, MIN_B_PARAM)`, `InvalidLiquidityParam`, `PoolNotActive`, `TooManyOutcomes` ### `buy` ```solidity function buy( uint256 marketId, uint256 outcomeIndex, uint256 shares, uint256 maxCost ) external nonReentrant; ``` LMSR-priced share purchase. Checks tradeability via `PredictionMarketV2 → Core`. Computes LMSR cost, applies symmetric fee (protocol + creator), transfers collateral with fee-on-transfer protection, mints ERC-1155 shares, routes protocol fee to Treasury and creator fee to market creator. **Emits**: `SharesBought`, `TradeExecuted`, `VolumeUpdated` **Reverts**: `PoolNotActive`, `InvalidShares`, `CostExceedsMaxCost`, `InsufficientReceived(expected, actual)` ### `sell` ```solidity function sell( uint256 marketId, uint256 outcomeIndex, uint256 shares, uint256 minPayout ) external nonReentrant; ``` LMSR-priced share sale. Fee computed on **average of pre-trade and post-trade price** (P0-3 fix — prevents sandwich attacks). Burns ERC-1155 shares **first** (P0-5 CEI fix), then updates pool state, then transfers payout. **Emits**: `SharesSold`, `TradeExecuted`, `VolumeUpdated` **Reverts**: `PoolNotActive`, `InvalidShares`, `InsufficientShares`, `PayoutBelowMinPayout`, `InsufficientLiquidity` ### `splitCollateral` / `mergeCollateral` ```solidity function splitCollateral(uint256 marketId, uint256 amount) external nonReentrant; function mergeCollateral(uint256 marketId, uint256 amount) external nonReentrant; ``` Split deposits collateral and mints one of each outcome token. Merge burns one of each and returns collateral. Split/merge tokens are NOT reflected in `pool.quantities` — they exist outside the LMSR pricing curve. ### Settlement — autonomous payouts ```solidity function redeem(uint256 marketId) external nonReentrant; function redeemFor(uint256 marketId, address holder) external nonReentrant returns (uint256 payout); function batchRedeemFor(uint256 marketId, address[] calldata holders) external nonReentrant returns (uint256 totalPayout, uint256 failedCount); function refund(uint256 marketId) external nonReentrant; function refundFor(uint256 marketId, address holder) external nonReentrant returns (uint256 refunded); function batchRefundFor(uint256 marketId, address[] calldata holders) external nonReentrant returns (uint256 totalRefunded, uint256 failedCount); ``` `redeemFor` / `refundFor` allow anyone to trigger payouts for any holder. Batch versions iterate up to 100 holders with per-holder failure isolation. Returns 0 (no revert) for holders with no shares — keeper-safe. Pro-rata payout: `(holderShares / totalWinningShares) * pool.collateral`. Total winning shares are lazily snapshotted on first call to prevent burn-reduces-totalSupply drift. ### `removeLiquidity` ```solidity function removeLiquidity(uint256 marketId) external nonReentrant; ``` LP withdrawal. Only callable by the original `provider`. Market must be resolved or voided. LP cannot withdraw collateral owed to unclaimed winners — if winning shares remain after the 30-day grace period, LP receives 0 and the pool is closed (but `redeem` still works). **Emits**: `LiquidityRemoved(marketId, provider, amount)` **Reverts**: `NotPoolProvider`, `MarketNotResolved`, `RedemptionGracePeriodActive` ### Admin ```solidity function setProtocolFeeBps(uint256 feeBps_) external onlyRole(GOVERNANCE_ROLE); ``` ### Views ```solidity function getTokenId(uint256 marketId, uint256 outcomeIndex) public pure returns (uint256); function getOutcomePrices(uint256 marketId) external view returns (uint256[] memory); function getPoolInfo(uint256 marketId) external view returns (Pool memory); function protocolFeeBps() external view returns (uint256); function marketTotalVolume(uint256 marketId) external view returns (uint256); function totalWinningShares(uint256 marketId) external view returns (uint256); ``` ### Structs ```solidity struct Pool { address token; // Slot 0: 20 bytes bool active; // Slot 0: +1 byte uint16 creatorFeeBps; // Slot 0: +2 bytes (packed) uint256 b; // Slot 1: LMSR liquidity parameter int256[] quantities; // Slot 2: shares per outcome uint256 collateral; // Slot 3: total collateral held address provider; // Slot 4: LP address } ``` 5 storage slots (optimized from 7 via struct packing — P2-4). ### Events ```solidity event LiquiditySeeded(uint256 indexed marketId, address indexed provider, uint256 amount, uint256 b); event LiquidityRemoved(uint256 indexed marketId, address indexed provider, uint256 amount); event SharesBought(uint256 indexed marketId, uint256 indexed outcomeIndex, address indexed buyer, uint256 shares, uint256 cost); event SharesSold(uint256 indexed marketId, uint256 indexed outcomeIndex, address indexed seller, uint256 shares, uint256 payout); event TradeExecuted(uint256 indexed marketId, uint256 outcomeIndex, address indexed trader, bool isBuy, uint256 shares, uint256 amount, uint256 protocolFee, uint256 creatorFee, uint256 price); event VolumeUpdated(uint256 indexed marketId, uint256 totalVolume); event CollateralSplit(uint256 indexed marketId, address indexed user, uint256 amount); event CollateralMerged(uint256 indexed marketId, address indexed user, uint256 amount); event SharesRedeemed(uint256 indexed marketId, address indexed holder, uint256 payout); event SharesRefunded(uint256 indexed marketId, address indexed holder, uint256 amount); event RedeemedFor(uint256 indexed marketId, address indexed holder, uint256 payout, address indexed keeper); event RefundedFor(uint256 indexed marketId, address indexed holder, uint256 amount, address indexed keeper); event AutoDistributed(uint256 indexed marketId, uint256 totalPayout, uint256 holderCount, uint256 failedCount); event AutoRefunded(uint256 indexed marketId, uint256 totalRefunded, uint256 holderCount, uint256 failedCount); event RedeemForFailed(uint256 indexed marketId, address indexed holder, bytes reason); event RefundForFailed(uint256 indexed marketId, address indexed holder, bytes reason); event ProtocolFeeUpdated(uint256 newFeeBps); event PredictionMarketChangeProposed(address indexed newMarket, uint256 executableAfter); event PredictionMarketChangeExecuted(address indexed oldMarket, address indexed newMarket); event PredictionMarketChangeCancelled(address indexed cancelled); ``` ### Errors ```solidity error ZeroAddress(); error PoolNotActive(); error PoolAlreadySeeded(); error InvalidShares(); error InvalidLiquidityParam(); error InsufficientShares(); error InsufficientLiquidity(); error InsufficientReceived(uint256 expected, uint256 actual); error CostExceedsMaxCost(); error PayoutBelowMinPayout(); error NoWinningShares(); error NotPoolProvider(); error MarketNotResolved(); error MarketNotVoided(); error FeeTooHigh(); error TooManyOutcomes(); error BatchTooLarge(uint256 actual, uint256 max); error RedemptionGracePeriodActive(); error OnlyPredictionMarketOrOwner(); error OnlySelf(); error BParamTooSmall(uint256 actual, uint256 min); error PredictionMarketChangeNotProposed(); error PredictionMarketChangeNotReady(); error PredictionMarketChangeExpired(); ``` --- ## Next - [State machine](./state-machine.md) — how these functions compose into the match lifecycle - [Security model](./security-model.md) — attack scenarios and economic guards - [User flows](./user-flows.md) — end-to-end journeys using these functions