Protocol Overview

The SBET Protocol is a decentralized sports betting platform built on Ethereum. It provides P2P order-book trading, multi-outcome pool betting, NFT-based wagering, LMSR prediction markets, and a modular treasury system — all governed by on-chain smart contracts with no off-chain custody of funds.

24
Contracts
25K+
Lines of Solidity
883
Tests Passing
146/146
Audit Findings Addressed

Design Philosophy

  • Non-custodial: Users retain control of their assets until the moment of trade execution. No deposits to a central pool are required for P2P trading.
  • Modular: Each betting primitive (P2P, pools, NFTs, prediction markets) is an independent contract. The treasury uses a hub-and-spoke module system.
  • Defense-in-depth: Every contract uses OpenZeppelin's ReentrancyGuard, SafeERC20, and AccessControl. State changes follow the Checks-Effects-Interactions (CEI) pattern throughout.
  • Gas-efficient: Packed order encoding, batch operations on all endpoints, and via_ir compilation with 200 optimizer runs.
  • Formally verified: Critical invariants checked with Halmos (symbolic execution), Echidna (fuzzing), and SMTChecker. See the full audit report.

Architecture

The protocol is organized into six layers: core trading, betting primitives, prediction markets, treasury management, integrator infrastructure, and donations. Each layer is composed of independent contracts connected through well-defined interfaces.

Contract Hierarchy

CORE TRADING SBET (SBETTrading + SBETClaims) BETTING PRIMITIVES SBETPool (module of SBET) SBETNFT (module of SBET) NFTVault PREDICTION MARKETS PredictionMarket PredictionAMM SUPPORT SBETQuery SBETMath DONATIONS DonationManager TREASURY SYSTEM SBETTreasury (Hub) FeeManager Vesting Budgets MultiSig Yield NFTManager NFTFees INTEGRATOR INFRASTRUCTURE IntegratorHub SHARED: OpenZeppelin ReentrancyGuard + AccessControl + SafeERC20 + Pausable
Figure 1 — Contract architecture hierarchy. The crimson dashed line shows the core-to-treasury fee flow.

Security Primitives

Every contract in the protocol inherits from a common set of security primitives.

Primitive Source Purpose
ReentrancyGuard OpenZeppelin Prevents re-entrant calls on all state-mutating functions
SafeERC20 OpenZeppelin Safe token transfers — handles non-standard return values
AccessControl OpenZeppelin Role-based permission system for admin operations
Pausable OpenZeppelin Emergency circuit breaker for all user-facing endpoints
CEI Pattern Convention Checks-Effects-Interactions ordering in every function
Timelocks Custom 24h delay on match finalization; 2-day delay on treasury migration

Compiler Configuration

// foundry.toml
[profile.default]
solc_version = "0.8.34"
evm_version = "prague"
via_ir = true
optimizer = true
optimizer_runs = 200

P2P Trading

The core trading engine uses EIP-712 typed-data signatures for gasless order creation. Orders are signed off-chain and settled on-chain when matched. The system supports both taker-initiated trades (trade()) and permissionless order matching (matchOrders()). See the Trading API reference for full function signatures.

EIP-712 Order Struct

Every order is an EIP-712 typed struct with 14 fields. The domain separator uses the name "Sports BET (SBET) Protocol", version "1.0", a dynamic chainId, the deployed contract address as verifyingContract, and a deterministic salt derived from keccak256("53019554...").

// EIP-712 Order type — matches SBETStorage.EIP712_ORDER_SCHEMA_HASH
struct Order {
    address maker;       // Order creator
    address taker;       // Designated counterparty (address(0) = anyone)
    address token;       // ERC-20 settlement token
    uint256 matchId;     // Sporting event identifier
    uint256 amount;      // Maximum trade size in token base units
    uint256 price;       // Price in range [0, MAX_PRICE]
    uint256 direction;   // 0 = Back (long), 1 = Lay (short)
    uint256 expiry;      // Unix timestamp — order invalid after this
    uint256 timestamp;   // Creation time — used by cancelAll()
    uint256 orderGroup;  // Logical grouping for batch cancellation
    uint256 nonce;       // Unique per-maker, prevents replay
    bool    isNFT;       // Whether this order involves NFT collateral
    address nftAddress;  // NFT contract (if isNFT)
    uint256 nftId;       // NFT token ID (if isNFT)
}

// EIP-712 Domain
{
    name: "Sports BET (SBET) Protocol",
    version: "1.0",
    chainId: <dynamic>,
    verifyingContract: <SBET contract address>,
    salt: keccak256("53019554eadfcd6f3ee53ec6d0bcd18db643b20cfcf8655aae83229cdeb079bd")
}

Price Model

Prices are expressed as integers in the range [0, MAX_PRICE] where MAX_PRICE = 1,000,000,000 (1 billion). This provides sub-cent granularity for any settlement token. A price of 500,000,000 represents even odds (50/50).

For two orders to be matchable, their prices must satisfy the spread condition: leftPrice + rightPrice >= MAX_PRICE. This ensures the combined collateral from both sides covers the full payout regardless of the outcome.

Order Lifecycle

Sign Order EIP-712 off-chain Submit to relayer / UI Match On-Chain trade() / matchOrders() Finalize Match grader sigs + timelock Claim claim() / batchClaim() Cancel (nonce/all)
Figure 2 — Order lifecycle from off-chain signing to on-chain settlement and payout.

Nonce System

The protocol provides three levels of order cancellation granularity:

  • cancelOrderNonce(nonce) — Cancel a single specific order by nonce. See API.
  • cancelUpTo(nonce) — Invalidate all orders with nonces below the given value. See API.
  • cancelAll() — Invalidate all orders signed before the current block timestamp. See API.
Audit note: Finding C-01 identified that cancelAll() originally did not prevent order execution in the trade() path. This was remediated by adding a cancelTimestamps check to _unpackOrderSoft(). See audit findings for details.

Match Finalization

Matches are finalized through M-of-N grader signatures via finalizeWithGraders(). The grader configuration (quorum size and addresses) is encoded in the matchId itself. Finalization initiates a 24-hour timelock before the result can be committed via executeFinalization(). The timelock checks oracle staleness before committing, providing a window to detect and respond to incorrect results.

Pool Betting

Pool betting provides a simpler, mutuel-style wagering system. Unlike P2P trading, pools aggregate all stakes and distribute payouts pro-rata to winners. This model works well for multi-outcome events (e.g., tournament winners, exact score predictions). See the Pool Betting API for function details.

Pool Lifecycle

  1. Create: An admin creates a pool with createPool(token, numOutcomes), specifying the settlement token and number of possible outcomes (minimum 2).
  2. Join: Users place bets by calling joinPoolBet(poolId, outcome, amount). Tokens are transferred into the pool contract.
  3. Finalize: After the event concludes, an admin calls finalizePool(poolId, winningOutcome) to lock in the result.
  4. Claim: Winners call claimPoolWinnings(poolId) to receive their pro-rata share. Batch claiming across multiple pools is supported via batchClaimPools().

Payout Formula

Payouts are calculated using a straightforward pro-rata formula. The total pool is distributed among winners in proportion to their stake on the winning outcome.

// Pool payout calculation
payout = (userStakeOnWinningOutcome / totalStakeOnWinningOutcome) * totalPoolStake
Total Pool: 1,000 USDC Outcome A (Winner) 600 USDC (60%) Outcome B 300 USDC (30%) Outcome C 100 USDC (10%) Example Payout (Outcome A wins): Alice staked 300 of 600 on A Payout = (300/600) * 1000 = 500 USDC Bob staked 150 of 600 on A Payout = (150/600) * 1000 = 250 USDC
Figure 3 — Pool payout distribution. The entire pool (including losing stakes) is divided among winners.

Batch Operations

Users with positions across multiple pools can claim all winnings in a single transaction using batchClaimPools(), reducing gas costs significantly for active bettors.

NFT Betting

The NFT betting module enables users to wager ERC-721 and ERC-1155 tokens on sporting events. NFTs are held in escrow by the NFTVault contract during the bet and transferred to the winner upon match finalization. See the NFT Betting API for full function signatures.

Bet Lifecycle

  1. Create (Side A): A user calls createBetWithNFT(matchId, nftAddress, nftId) to stake their NFT. The token is transferred to NFTVault for escrow.
  2. Join (Side B): A counterparty stakes their NFT via joinBetWithNFT().
  3. Finalize: After the match result is determined, finalizeMatchNFTs() transfers both NFTs to the winner. Batch finalization is available via finalizeMatchNFTsBatch() for matches with many NFT bets.

NFT Vault Escrow

The NFTVault contract provides secure escrow for both ERC-721 and ERC-1155 tokens. It supports batch deposit/withdraw operations and provides view functions for querying vault holdings. An emergency transfer function (admin-only) exists for recovering stuck assets in edge cases.

Blacklist System

Admins can blacklist NFT collections that are fraudulent, compromised, or otherwise unsuitable for betting via setNFTBlacklist(). Blacklisted collections cannot be used in new bets. Existing bets with blacklisted NFTs can still be finalized and claimed.

Per-Collection Fee Configuration

The TreasuryNFTFees module allows per-collection fee configuration. Fees can be fixed-amount or percentage-based (with min/max bounds). Users can be granted permanent or temporary fee exemptions. Fee configuration is managed through the treasury system.

Prediction Markets

The prediction market module implements a full LMSR (Logarithmic Market Scoring Rule) automated market maker for multi-outcome event prediction. Markets support creation, share trading, resolution, and dispute handling — all on-chain. See the PredictionMarket API and PredictionAMM API for function details.

LMSR Cost Function

The AMM uses Hanson's Logarithmic Market Scoring Rule to price outcome shares. The cost function C(q) for a quantity vector q of n outcomes is:

// LMSR cost function
// b = liquidity parameter (higher = less price impact)
// q = vector of outstanding shares per outcome

C(q) = b * ln( sum( e^(q_i / b) ) )    for i = 1..n

// Price of buying Δ shares of outcome k:
cost = C(q_1, ..., q_k + Δ, ..., q_n) - C(q_1, ..., q_k, ..., q_n)

// Implied probability of outcome k:
p_k = e^(q_k / b) / sum( e^(q_i / b) )

The b parameter controls price sensitivity. A larger b means the market can absorb bigger trades before prices move significantly, but requires more initial liquidity.

0.0 0.25 0.50 0.75 1.0 0 25 50 75 100 Shares purchased for Outcome A (binary market) Price of A b=10 b=50 b=200 50% (equal odds start)
Figure 4 — LMSR price curves for different b parameters. Lower b values cause prices to move faster with each trade.

Market Creation

Markets are created via createMarket(params) with a MarketParams struct specifying the question, outcome labels, resolution source, timing parameters, settlement token, fee configuration, and grader setup. The creator provides initial liquidity which is seeded into the LMSR pool via seedLiquidity().

Share Trading

Users buy outcome shares via buy(marketId, outcomeIndex, shares, maxCost) and sell via sell(marketId, outcomeIndex, shares, minPayout). Both functions include slippage protection parameters (maxCost and minPayout) to prevent front-running losses.

Preview functions getCostToBuy() and getPayoutToSell() allow UIs to display estimated costs and payouts before submitting a transaction.

Resolution

Markets are resolved via resolveMarket() using M-of-N grader signatures, similar to match finalization in P2P trading. After resolution, holders of the winning outcome can redeem their shares for a pro-rata portion of the collateral pool via redeem(). The liquidity provider reclaims remaining collateral via removeLiquidity().

Dispute System

Resolved markets can be disputed by posting a bond via disputeResolution(). Disputes are adjudicated by a separate M-of-N dispute council through resolveDispute(). If the dispute succeeds, the bond is returned and the market outcome is corrected. If it fails, the bond is forfeited. Markets can also be voided entirely by an admin via voidMarket(), which refunds all participants their original deposits.

Treasury System

The treasury uses a hub-and-spoke architecture. The central SBETTreasury contract manages token allowlists, deposits, withdrawals, daily limits, and delegates specialized functionality to pluggable modules. See the Treasury API and Treasury Modules API for function details.

SBET Treasury FeeManager Fee recipients + distribution Vesting Token vesting schedules Budgets Department spending limits MultiSig Governance proposals Yield Strategy deployment NFTManager NFT asset management NFTFees Per-collection fee config IntegratorHub Fee accrual + payout
Figure 5 — Treasury hub-and-spoke architecture. Each module is a separate contract connected to the central treasury hub.

RBAC Roles

The treasury uses OpenZeppelin AccessControl with the following roles:

Role Permissions Typical Holder
ADMIN_ROLE Deposits, fee config, module management, migration queuing Protocol multisig
WITHDRAWER_ROLE Standard withdrawals (subject to daily limits) Operations wallet
EMERGENCY_ADMIN_ROLE Emergency withdrawals (bypass limits), emergency pause Cold storage multisig
TOKEN_MANAGER_ROLE Add/remove allowed tokens Protocol admin
LOCK_MANAGER_ROLE Lock/unlock treasury (required for migration) Protocol multisig

Daily Limits

Withdrawals are subject to per-token daily limits. Each token can have an individual daily cap, and there is also a global daily limit across all tokens. These limits provide a safety net against compromised admin keys — an attacker with WITHDRAWER_ROLE can only drain up to the daily limit before the emergency admin intervenes.

Treasury Modules

The treasury hub delegates functionality to 8 specialized modules:

  • FeeManager: Configures fee recipients with percentage allocations. Fee operations require a timelock. Distributes accumulated fees to all recipients.
  • Vesting: Creates token vesting schedules with cliff periods. Supports revocable and irrevocable schedules, batch release, and per-beneficiary queries.
  • Budgets: Creates time-bounded budgets with department allocations and spending approvals. Enables controlled, auditable expenditure.
  • MultiSig: Governance module for proposing and voting on treasury operations (transfers, signer changes, threshold adjustments). Requires M-of-N signer approval.
  • Yield: Deploys treasury funds to yield-generating strategies. Supports target allocations, rebalancing, and harvest operations across multiple strategies.
  • NFTManager: Manages NFT deposits and withdrawals through the NFTVault on behalf of the treasury. Supports single and batch operations for ERC-721 and ERC-1155 tokens.
  • NFTFees: Per-collection fee configuration for NFT betting. Supports fixed and percentage-based fees with min/max bounds, plus user exemptions.
  • IntegratorHub: Manages integrator fee accrual and distribution. See the Integrators section.

Migration System

The treasury supports migration to a new contract via a two-step timelocked process:

  1. queueMigration(newTreasury) — Starts a 2-day timelock. The treasury must be locked (setLock(true)) at this point.
  2. executeMigration() — After the timelock elapses, executes the migration. Requires the treasury to have been continuously locked since the migration was queued.

The continuous lock requirement prevents a scenario where an admin unlocks the treasury during the timelock period to drain funds before the migration completes.

Integrator Hub

The IntegratorHub enables third-party applications to earn a share of protocol fees for trades routed through them. It provides self-service registration, configurable fee sharing, periodic sweep payouts, and per-integrator analytics. See the IntegratorHub API for function details.

Self-Registration

Third-party developers register their app by calling registerMyApp(payout, period), specifying a payout address and a sweep period (Daily, Weekly, or Monthly). A registration fee may be required (paid via msg.value). Admins can also register integrators on their behalf via ownerRegister().

Fee Accrual Flow

Trade Executes via integrator router Fee Accrued accrue(router, token) Sweep Period Daily / Weekly / Monthly Integrator 95% (default) Treasury 5% (default) Payout ERC-20
Figure 6 — Integrator fee flow. Fees accrue per-trade, accumulate until the sweep period elapses, then split between integrator and treasury.

Fee Split

The default split is 95% to the integrator and 5% to the protocol treasury. Admins can adjust the split per-integrator by modifying their bps setting. Fees are consumed (split and paid out) by calling consumeAccrued() or in batch via batchConsume().

Sweep Periods

Each integrator chooses a sweep period at registration time:

  • Daily — Fees can be consumed once per day.
  • Weekly — Fees accumulate for 7 days before becoming consumable.
  • Monthly — Fees accumulate for 30 days before becoming consumable.

The isDue(router) view function checks whether an integrator's sweep period has elapsed and fees are ready for consumption.

View Functions

Integrators can query their status and pending fees using:

Donations

The DonationManager contract provides a transparent, on-chain donation system for charitable organizations. It supports ERC-20 and ETH donations to verified organizations, cause-based distribution, and donation-from-winnings — enabling users to donate a portion of their betting winnings directly to charity.

Donation Lifecycle

  1. Register: A DONATION_MANAGER_ROLE holder registers organizations with name, cause, description, image, and wallet address. Limited to MAX_ORGANIZATIONS (500).
  2. Verify: The contract owner verifies organizations before they can receive donations.
  3. Donate: Users donate ERC-20 tokens via donate() or ETH via donateETH() to a verified organization.
  4. Cause-based: donateByCause() distributes a donation evenly among all verified organizations for a given cause. The last organization receives the remainder to eliminate rounding dust.
  5. From winnings: donateFromWinnings() allows the protocol to donate a portion of a user's winnings on their behalf.

Supporting Contracts

SBETQuery

A read-only diagnostic contract that wraps calls to the main SBET contract for convenient frontend consumption. Provides 29 view functions across 7 categories: user positions, match queries, pool queries, NFT queries, order/nonce queries, system state, and aggregated portfolio views. All functions are view or pure and never modify state. See the SBETQuery API for full function details.

SBETMath

Internal math library used across SBET contracts. Provides safe arithmetic for position calculations including exposureDelta, effectiveBalance, priceDivide, and position-safe add/subtract operations. All functions are pure and internal. See the SBETMath API for function signatures.

Security

Security is a first-class concern across the entire protocol. Every contract follows defense-in-depth principles, and the codebase has been analyzed with 5 different security tools plus manual review. See the full audit report for all 125 findings and their remediations.

Defense-in-Depth Summary

  • Reentrancy protection: Every state-mutating external function uses OpenZeppelin's ReentrancyGuard.
  • Safe token handling: All ERC-20 interactions use SafeERC20 to handle non-standard return values and revert on failure.
  • Access control: Role-based permissions via OpenZeppelin AccessControl on all privileged operations.
  • CEI pattern: All functions follow Checks-Effects-Interactions ordering to prevent cross-function reentrancy.
  • Pausability: Emergency circuit breaker via Pausable on all user-facing endpoints.
  • Integer safety: Solidity 0.8.34 built-in overflow/underflow checks. Additional safe math in SBETMath for position calculations.

Timelock Protections

Operation Timelock Purpose
Match finalization 24 hours Window to detect incorrect results before committing
Treasury migration 2 days Community review period before funds move to a new contract
Fee recipient changes Configurable Prevents instant fee redirection by a compromised admin

Formal Verification

Critical protocol invariants have been verified using multiple tools:

  • Slither: Static analysis for common vulnerability patterns (reentrancy, unchecked returns, access control issues).
  • Foundry Forge: 883 unit and integration tests across 20 test suites covering all contract functions and edge cases.
  • Halmos: Symbolic execution for verifying mathematical invariants (e.g., LMSR price bounds, position accounting).
  • Echidna: Property-based fuzzing for state machine invariants (e.g., pool totals, nonce monotonicity).
  • SMTChecker: Compiler-integrated formal verification for overflow/underflow and assertion checking.

Deployment

Current Status

The protocol is fully developed, tested, and audited. Contracts are prepared for deployment to Ethereum mainnet and Sepolia testnet. The SBET token is already deployed on mainnet.

Mainnet Token

Contract Network Address
SBET Token (ERC-20) Ethereum Mainnet 0x2e8209267cf2F839d0Ec42a3E61e4Bf…

Compiler Configuration

Setting Value
Solidity version 0.8.34
EVM target prague
IR pipeline via_ir = true
Optimizer Enabled, 200 runs

Contract Addresses

Core protocol contracts will be deployed to Sepolia first for integration testing, then to Ethereum mainnet. Addresses will be published here upon deployment.

Contract Sepolia Mainnet
SBET (Trading + Pools + NFT + Claims) Pending Pending
NFTVault Pending Pending
PredictionMarket Pending Pending
PredictionAMM Pending Pending
SBETTreasury Pending Pending
IntegratorHub Pending Pending
TreasuryFeeManager Pending Pending
TreasuryVesting Pending Pending
TreasuryYield Pending Pending
TreasuryBudgets Pending Pending
TreasuryMultiSig Pending Pending
TreasuryNFTFees Pending Pending
TreasuryNFTManager Pending Pending
SBETQuery Pending Pending
DonationManager Pending Pending