This document defines the protocol guarantees, trust boundaries, and test coverage map for the Xelma Soroban prediction market contract. It is intended for maintainers, auditors, oracle operators, indexer authors, and contributors.
The implementation reference is contracts/src/contract.rs; the canonical data
types and errors live in contracts/src/types.rs and contracts/src/errors.rs.
Xelma is a virtual-token prediction market for XLM price movement. Users receive one initial vXLM allocation, stake it into one active round, and later claim pending winnings after oracle resolution or refund paths.
The contract supports two round modes:
| Mode | Value | Settlement rule |
|---|---|---|
| Up/Down | 0 |
Correct side receives stake plus a proportional share of the losing pool. Unchanged price refunds all participants. |
| Precision | 1 |
Closest price prediction wins the pot. Ties split the pot evenly; deterministic remainder goes to the first winner in resolution order. |
Precision rounds accept either a direct place_precision_prediction or a
commit-reveal flow (commit_prediction → reveal_prediction).
Commitment format. The on-chain commitment is a 32-byte SHA-256 digest:
commitment = sha256( predicted_price.to_xdr() || salt.to_xdr() )
- The all-zero hash is rejected at commit time (
InvalidCommitment). - Clients MUST sample
saltas 32 cryptographically random bytes. - The contract additionally rejects clearly weak salts at reveal time:
all-zero or constant-byte salts fail with
InvalidSalt.
Reveal window. Reveals are accepted only while
bet_end_ledger ≤ ledger < end_ledger.
Unrevealed commitments at settlement (deterministic):
| Situation | Policy |
|---|---|
| ≥1 prediction revealed (or placed directly) | Unrevealed commitments forfeit to the pot and count as losers (anti-griefing). |
| Nobody revealed | All committed stakes are refunded to pending winnings (conservation; no locked liquidity). |
Admin cancel_round / insufficient-participants fallback |
All stakes including unrevealed commitments are refunded. |
Pot conservation (I8) still holds: with mixed reveals,
Σ winner payouts + protocol fee == total pot (including forfeited
unrevealed stakes); with all-unrevealed refunds,
Σ refunds == total pot.
All token amounts are stored as i128 stroops where 1 vXLM = 10_000_000.
Prices are stored as u128 values scaled to 4 decimal places.
| Role | Trust level | Authority | Assumptions |
|---|---|---|---|
| Admin | Trusted operator | Initialize roles, create rounds, configure windows and risk controls, pause/unpause, cancel active rounds. | Admin key custody is secure; admin does not maliciously configure unusable windows or cancel rounds unfairly. |
| Oracle | Trusted data signer | Resolve rounds, submit liveness heartbeat. | Oracle reports accurate prices for the intended round and submits fresh payloads. |
| Users | Untrusted | Mint once, bet/predict, claim winnings, read state. | Users may attempt invalid auth, duplicate bets, timing abuse, overflow inputs, and replay-like calls. |
| Indexers/frontends | Off-chain consumers | Read events and contract state. | Consumers treat docs/EVENT_SCHEMA.md as canonical and handle additive events safely. |
The protocol does not currently prove oracle correctness on-chain. A valid oracle signature plus payload validation establishes authorization and freshness, not price truth. Mainnet use requires an operational oracle runbook, monitoring, and incident response policy.
At most one round can be active at a time. create_round must fail without
mutating round state when ActiveRound already exists.
Evidence:
- Code:
assert_no_active_round,create_round. - Tests:
guard_tests.rs,lifecycle.rs::test_create_round_while_active_fails. - Docs:
ROUND_LIFECYCLE.md.
State-changing entrypoints require the relevant signer:
| Entrypoint class | Required signer |
|---|---|
| Admin operations | Admin |
| Oracle resolution and heartbeat | Oracle |
| User mint, bet, prediction, claim | User |
Unauthorized calls must fail before meaningful state mutation.
Evidence:
- Code:
require_auth()calls in admin, oracle, and user entrypoints. - Tests:
initialization.rs,lifecycle.rs,pause.rs,windows.rs,security.rs.
When paused, high-risk mutating operations are rejected. Read-only queries remain available so operators can inspect contract state during an incident.
Evidence:
- Code:
_ensure_not_paused,pause_contract,unpause_contract. - Tests:
pause.rs,chaos_recovery.rs::test_chaos_pause_mid_round_then_unpause_resolve.
Bets and predictions are only accepted before bet_end_ledger. Resolution is
only accepted at or after end_ledger. Admin-configured windows must be
positive, bounded, and must close betting before resolution.
Evidence:
- Code:
set_windows,place_bet,place_precision_prediction,resolve_round. - Tests:
windows.rs.
A user may hold at most one Up/Down position or one Precision prediction in the active round. Duplicate submissions fail.
Evidence:
- Code: indexed keys
Position(round_id, user)andPrecisionPosition(round_id, user). - Tests:
betting.rs::test_place_bet_twice_same_round,mode_tests.rs::test_precision_prediction_already_bet. - Docs:
STORAGE_DESIGN.md.
Up/Down bets are only valid in Up/Down rounds. Precision predictions are only valid in Precision rounds.
Evidence:
- Code:
RoundModechecks in betting entrypoints. - Tests:
mode_tests.rs.
User balances cannot go negative. Bets deduct the staked amount before storing a
position. Resolution credits payouts to PendingWinnings(user). Claims move
pending winnings to balance atomically and clear the pending entry.
Evidence:
- Code:
balance,_set_balance,_accumulate_pending,claim_winnings. - Tests:
betting.rs,resolution.rs,overflow_tests.rs.
For each resolved round, credited payouts must not exceed the round pot. Refund paths return participant stake amounts. Precision tie remainders are assigned deterministically and no dust is intentionally lost.
Evidence:
- Code:
_resolve_updown_mode,_resolve_precision_mode, refund helpers. - Tests:
resolution.rs,property_invariants.rs,storage_benchmarks.rs::bench_large_round_resolves_correctly.
Arithmetic that can affect balances, pending winnings, pools, windows, round IDs, and stats must use checked operations or bounded validation. Overflow must return a contract error and avoid partial writes in covered payout paths.
Evidence:
- Code:
checked_*,payout_add,payout_mul. - Tests:
overflow_tests.rs,edge_cases.rs.
Oracle resolution payloads must bind to the active round, contain a non-zero price, use a fresh per-round nonce, and satisfy timestamp freshness checks.
Current payload semantics:
payload.round_idis matched againstRound.start_ledger.payload.noncemust not already be consumed forRound.round_id.payload.timestampmust not be future-dated.payload.timestampmust not be stale beyond the configured contract policy.
Evidence:
- Code:
resolve_round. - Tests:
security.rs. - Residual risk:
payload.round_idnaming is ambiguous because it currently refers toRound.start_ledger; this is tracked inSECURITY_REVIEW.md.
Admin cancellation and insufficient-participant fallback paths must refund participant stakes to pending winnings, remove active round state, and allow future rounds to be created.
Evidence:
- Code:
cancel_round,_refund_under_threshold. - Tests:
lifecycle.rs,resolution.rs,chaos_recovery.rs.
Resolution and cancellation must remove indexed participant keys and participant lists for the completed round. Legacy map keys remain readable for migration fallbacks but are not written by new betting paths.
Evidence:
- Code: indexed storage writes and cleanup in
contract.rs. - Tests:
storage_benchmarks.rs. - Docs:
STORAGE_DESIGN.md.
Events are an append-only observability interface. Existing event topic and
payload meanings must remain stable. Additive events are allowed when documented
in docs/EVENT_SCHEMA.md.
Canonical event classes:
- Round lifecycle:
("round", "created"),("round", "resolved"),("round", "cancelled"),("round", "fallback"). - User actions:
("mint", "initial"),("bet", "placed"),("predict", "price"),("claim", "winnings"). - Configuration/liveness:
("windows", "updated"),("oracle", "heartbeat").
Evidence:
- Code: event publishing calls in
contract.rs. - Tests:
lifecycle.rs,mode_tests.rs,resolution.rs,security.rs. - Docs:
docs/EVENT_SCHEMA.md.
- Unauthorized user, admin, or oracle calls.
- Duplicate bets and duplicate predictions.
- Late betting and premature resolution.
- Oracle payload replay across rounds or within a round.
- Stale or future-dated oracle timestamps.
- Invalid round mode usage.
- Arithmetic overflow in accounting paths.
- Storage growth and write amplification during betting.
- Indexer ambiguity caused by undocumented events.
- Malicious but authorized admin behavior.
- Compromised oracle signer submitting fresh but false prices.
- Off-chain price feed quality, exchange outages, or aggregation logic.
- Wallet UX, frontend signing prompts, and phishing resistance.
- Stellar network-level consensus failures.
- External economic/legal/regulatory risk of prediction markets.
- The admin can pause and cancel rounds. These controls are intended for recovery and are not trustless governance.
- The oracle is a single trusted signer in the current architecture.
- Resolution remains O(n) over participants; very large rounds may hit Soroban resource limits before protocol-level settlement logic completes.
- TypeScript bindings are generated artifacts and must be kept in parity with the Rust contract for safe client use.
- Backward-compatible documentation and additive events do not require a migration entry unless they alter consumer assumptions.
- Breaking changes to storage keys, public method signatures, event payload
order, error codes, or
OraclePayloadsemantics must be documented inMIGRATION.md. - Legacy storage fallbacks may be removed only after maintainers explicitly decide no deployed migration window depends on them.
- Any new public contract method must be reflected in TypeScript bindings and parity checks.
| ID | Invariant | Primary code | Test coverage | Status |
|---|---|---|---|---|
| I1 | Single active round | create_round, assert_no_active_round |
guard_tests.rs, lifecycle.rs |
Covered |
| I2 | Role authorization | require_auth() gates |
initialization.rs, lifecycle.rs, pause.rs, windows.rs, security.rs |
Covered |
| I3 | Pause safety | _ensure_not_paused |
pause.rs, chaos_recovery.rs |
Covered |
| I4 | Round timing | set_windows, betting/resolution ledger checks |
windows.rs |
Covered |
| I5 | One position per user | indexed position keys | betting.rs, mode_tests.rs |
Covered |
| I6 | Mode isolation | RoundMode checks |
mode_tests.rs |
Covered |
| I7 | Balance and pending accounting | _set_balance, _accumulate_pending, claim_winnings |
betting.rs, resolution.rs, overflow_tests.rs |
Covered |
| I8 | Settlement conservation | payout/refund helpers | resolution.rs, property_invariants.rs |
Covered |
| I9 | Checked arithmetic | checked_*, payout_add, payout_mul |
overflow_tests.rs, edge_cases.rs |
Covered with noted precision-error caveat in SECURITY_REVIEW.md |
| I10 | Oracle payload binding | resolve_round |
security.rs |
Covered |
| I11 | Cancellation/fallback refunds | cancel_round, _refund_under_threshold |
lifecycle.rs, resolution.rs, chaos_recovery.rs |
Covered |
| I12 | Storage cleanup/migration | indexed cleanup and legacy fallbacks | storage_benchmarks.rs |
Covered |
| I13 | Event semantics | event publishing calls | lifecycle.rs, mode_tests.rs, resolution.rs, security.rs |
Covered; canonical schema in docs/EVENT_SCHEMA.md |
When changing protocol behavior:
- Identify which invariant is affected.
- Update this document if the invariant, trust boundary, or compatibility guarantee changes.
- Add or update tests listed in the coverage matrix.
- Update
docs/EVENT_SCHEMA.mdfor event changes. - Update
MIGRATION.mdfor breaking ABI, storage, event, or error changes. - Regenerate and validate TypeScript bindings for public ABI changes.