This repository contains the core smart contracts for the BOXMEOUT boxing prediction market on Stellar/Soroban.
Build artifacts in target/ are not tracked in git.
cargo test # run all contract tests
stellar contract build # optimised wasm build (same as CI)The main BOXMEOUT_STELLA repo's CI runs cargo llvm-cov, uploads an
contracts-lcov artifact (lcov.info) and posts a summary to the job summary.
To run it locally:
rustup component add llvm-tools-preview
cargo install cargo-llvm-cov
cargo llvm-cov --workspace --summary-only # terminal summary
cargo llvm-cov --workspace --lcov --output-path lcov.info # lcov report
cargo llvm-cov --workspace --html --open # browsable HTML reportCI fails if any optimised .wasm exceeds MAX_WASM_SIZE_BYTES (65,536 bytes, the
Soroban contract code size limit).
Manages accumulated protocol fees and implements fee collection/withdrawal logic.
Key Methods:
initialize(admin, factory, token)- One-time setupset_fee_bps(admin, fee_bps)- Update protocol fee rate (admin-only)deposit_fees(market_id, amount)- Receive fees from marketswithdraw_fees(admin, recipient, amount)- Withdraw accrued feesemergency_drain(admin, recipient)- Emergency drain when pausedget_balance()- Current escrow balanceget_fee_bps()- Current fee rateget_total_fees_earned()- Lifetime feesget_withdrawal_log()- Last 50 withdrawals (ring buffer; events hold the full history)
Manages individual boxing prediction markets, bet placement, and claim resolution.
Key Methods:
initialize(...)- Create a new marketplace_bet(bettor, side, amount)- Place a bet on a fighterlock_market(oracle)- Close market to new betsresolve_market(oracle, outcome)- Set final outcomeclaim_winnings(bettor, bet_id)- Claim winnings or refund
Deploys new Market contracts and manages protocol configuration.
Key Methods:
initialize(admin, treasury, oracle)- Setup factorycreate_market(...)- Deploy a new market contractget_config()- Return current protocol configupdate_config(admin, ...)- Update protocol settings
Common types, utilities, and constants used across contracts.
Emitted when a new market contract is initialized.
Topics:
Symbol("market_created")- Event nameBytes- Market ID
Data:
- Full
Marketstruct with all fields:market_id: Bytes- Unique market identifierfighter_a: Fighter- First fighter details (name, record, nationality, weight_class)fighter_b: Fighter- Second fighter detailsscheduled_at: u64- Unix timestamp of scheduled fight timebetting_ends_at: u64- Unix timestamp after which no bets acceptedcreated_at: u64- Timestamp when market was createdcreated_by: Address- Factory address that created this marketstatus: MarketStatus- Market status (Open)pool_a: i128- Total bets on fighter A (0 at creation)pool_b: i128- Total bets on fighter B (0 at creation)total_pool: i128- Total pool size (0 at creation)protocol_fee_bp: u32- Protocol fee in basis pointsoracle_address: Address- Authorized oracle for resolutionoutcome: Option<Outcome>- Final outcome (None at creation)fee_collector_address: Address- Recipient of protocol feesresolved_at: u64- Timestamp when resolved (0 at creation)dispute_window_sec: u64- Duration of dispute period
Emitted when a market deposits accumulated protocol fees into treasury
(via shared::events::emit_fee_deposited).
Topics:
Symbol("fee_deposited")- Event name
Data:
Address- Market address depositing feesAddress- Token addressi128- Amount deposited (stroops)
Emitted when the treasury admin updates the protocol fee rate.
Topics:
Symbol("FeeBpsUpdated")- Event name
Data:
u32- New fee rate in basis points
Emitted when fees are withdrawn from the treasury
(via shared::events::emit_fee_withdrawn).
Topics:
Symbol("fee_withdrawn")- Event name
Data:
Address- Token withdrawni128- Amount withdrawn (stroops)Address- Recipient of withdrawn fees
Emitted when the treasury is drained during a protocol pause
(via shared::events::emit_emergency_drain).
Topics:
Symbol("emergency_drain")- Event name
Data:
Address- Token drainedi128- Total amount drained (stroops)Address- Admin executing the drain
Emitted when a bettor places a bet on a market.
Topics:
Symbol("bet_placed")- Event nameBytes- Market ID
Data:
BetPlacedEventstruct containing:bet_id: Bytes- Unique bet identifiermarket_id: Bytes- Market identifierbettor: Address- Address that placed the betside: BetSide- Fighter chosen (FighterA or FighterB)amount: i128- Bet amount in stroopsplaced_at: u64- Timestamp of bet placement
All three contracts keep every key in persistent storage and none extends
TTLs explicitly, so entries expire to the archive after the network's minimum
persistent TTL from their last write and must be restored before use. The full
per-contract key tables (key, value type, storage class, TTL), the TTL strategy
and operator guidance are in docs/contracts.md → Storage Layout.
| Key | Type | Class |
|---|---|---|
DataKey::MarketInfo |
Market |
persistent |
DataKey::Factory |
Address |
persistent |
DataKey::Bet(bet_id) |
Bet |
persistent |
DataKey::BetsByAddr(address) |
Vec<Bytes> |
persistent |
DataKey::Claimed(bet_id) |
bool |
persistent |
DataKey::DisputeRaised |
bool |
persistent |
DataKey::DisputeReason |
Bytes (≤ 256 bytes) |
persistent |
"BET_COUNT" |
u64 |
persistent |
| Key | Type | Class |
|---|---|---|
"ADMIN" / "FACTORY" / "TOKEN" / "FEE_RECIPIENT" |
Address |
persistent |
"FEE_BPS" |
u32 |
persistent |
"BALANCE" / "TOTAL_FEES" |
i128 |
persistent |
"WITHDRAWAL_LOG" |
Vec<(Address, i128, u64)> |
persistent |
| Key | Type | Class |
|---|---|---|
"ADMIN" / "TREASURY" |
Address |
persistent |
"MARKET_WASM_HASH" |
BytesN<32> |
persistent |
"PAUSED" |
bool |
persistent |
"MARKET_COUNT" |
u64 |
persistent |
"MARKET_MAP" |
Map<Bytes, MarketInfo> |
persistent |
"ALL_MARKETS" |
Vec<Bytes> |
persistent |
MarketFactory::upgrade_market_wasm only changes the wasm hash used by future
create_market calls. Already-deployed markets keep their original code and
storage; there is no migration path through the factory. See
docs/contracts.md → Upgrade Policy.
Canonical event topics and the legacy topics still emitted until C-60/C-61
land are listed in docs/contracts.md → Event Topics.
Authorization:
"not admin"- Caller is not the stored admin address"unauthorized: caller is not a registered market"- Market not registered in factory"not initialized"- Contract called before initialization
State Validation:
"already initialized"- Initialize called more than once"amount exceeds balance"- Withdrawal exceeds available balance"fee_bps exceeds ceiling"- Fee rate > 10000 basis points (100%)"protocol is not paused"- Emergency drain called while protocol active"market not open"- Bet placed on closed market"betting period has ended"- Bet placed after betting_ends_at
Open- Bets are being acceptedLocked- Fight started, no more betsResolved- Winner declared, claims openCancelled- Fight cancelled, full refundsDisputed- Result under review, claims frozen
FighterA- Bet on first fighterFighterB- Bet on second fighter
FighterA- Fighter A winsFighterB- Fighter B winsDraw- Match ends in drawNoContest- DQ or injury rulingUndetermined- Not yet resolved (sentinel; rejected byresolve_market/resolve_dispute)
All shared enums and structs (MarketStatus, BetSide, Outcome, Fighter, Bet,
ClaimReceipt, ProtocolConfig) are defined once in shared/src/types.rs. The Market
contract re-exports them from market/src/types.rs, which only defines market-only types
(the full Market state, SettledOutcome, MarketResolved, WinningsClaimed).