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.
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, andAccessControl. State changes follow the Checks-Effects-Interactions (CEI) pattern throughout. - Gas-efficient: Packed order encoding, batch operations on all endpoints, and
via_ircompilation 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
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
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 thatcancelAll()originally did not prevent order execution in thetrade()path. This was remediated by adding acancelTimestampscheck 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
- Create: An admin creates a pool with
createPool(token, numOutcomes), specifying the settlement token and number of possible outcomes (minimum 2). - Join: Users place bets by calling
joinPoolBet(poolId, outcome, amount). Tokens are transferred into the pool contract. - Finalize: After the event concludes, an admin calls
finalizePool(poolId, winningOutcome)to lock in the result. - Claim: Winners call
claimPoolWinnings(poolId)to receive their pro-rata share. Batch claiming across multiple pools is supported viabatchClaimPools().
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
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
- Create (Side A): A user calls
createBetWithNFT(matchId, nftAddress, nftId)to stake their NFT. The token is transferred to NFTVault for escrow. - Join (Side B): A counterparty stakes their NFT via
joinBetWithNFT(). - Finalize: After the match result is determined,
finalizeMatchNFTs()transfers both NFTs to the winner. Batch finalization is available viafinalizeMatchNFTsBatch()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.
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.
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:
queueMigration(newTreasury)— Starts a 2-day timelock. The treasury must be locked (setLock(true)) at this point.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
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:
appOf(router)— Full app info (payout address, period, bps split, status).dueOf(router, token)— Pending amounts due to integrator and treasury.payoutAddressOf(router)— Configured payout address.
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
- Register: A
DONATION_MANAGER_ROLEholder registers organizations with name, cause, description, image, and wallet address. Limited toMAX_ORGANIZATIONS(500). - Verify: The contract owner verifies organizations before they can receive donations.
- Donate: Users donate ERC-20 tokens via
donate()or ETH viadonateETH()to a verified organization. - 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. - 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
SafeERC20to handle non-standard return values and revert on failure. - Access control: Role-based permissions via OpenZeppelin
AccessControlon all privileged operations. - CEI pattern: All functions follow Checks-Effects-Interactions ordering to prevent cross-function reentrancy.
- Pausability: Emergency circuit breaker via
Pausableon 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 |