Do not open a public GitHub issue for security vulnerabilities.
Use one of the following private disclosure channels:
- GitHub Private Security Advisory (preferred): Go to the Security Advisories tab of this repository and open a draft advisory. This keeps all communication private and lets us coordinate a fix before public disclosure.
- Email: Send full details to the address listed in the GitHub organisation profile. Encrypt the message with our PGP key if the disclosure contains exploit code or proof-of-concept.
Please include:
- A clear description of the vulnerability and its potential impact.
- Steps to reproduce or a proof-of-concept (PoC).
- The affected component(s) and version/commit hash.
- Your suggested severity (Critical / High / Medium / Low).
We will acknowledge receipt within 48 hours and provide an initial triage assessment within 7 days. We aim to ship a fix within 90 days of confirmed triage for High/Critical issues and within 30 days for Medium/Low issues, depending on complexity.
We credit researchers who report valid vulnerabilities in the release notes and CHANGELOG unless they prefer to remain anonymous.
| Stage | Target timeline |
|---|---|
| Acknowledgement | Within 48 hours of report receipt |
| Initial triage | Within 7 calendar days |
| Fix for Critical/High | Within 90 calendar days of triage |
| Fix for Medium/Low | Within 30 calendar days of triage |
| Public disclosure | Coordinated with reporter after fix ships |
There is no formal bug bounty program at this time. We recognise and credit researchers who report valid vulnerabilities; financial rewards are evaluated case-by-case for Critical severity findings at the maintainers' discretion.
The following components are in scope for vulnerability reports:
| Component | Location |
|---|---|
| Escrow smart contract | contracts/escrow/src/ |
| Oracle smart contract | contracts/oracle/src/ |
| Off-chain oracle service | apps/backend/ |
| Frontend dApp | apps/frontend/ |
The following are not in scope:
- Vulnerabilities in the Lichess or Chess.com APIs themselves.
- Vulnerabilities in third-party libraries not introduced by this project.
- Stellar/Soroban protocol-level bugs (report those to the Stellar Development Foundation directly).
- Social-engineering attacks against maintainers.
- Denial-of-service attacks that only affect development/testnet deployments.
- Issues already publicly disclosed or present in a dependency's own advisory database.
| Actor | Trusted for | Not trusted for |
|---|---|---|
| Stellar network | Correct Soroban contract execution | — |
| Oracle service | Accurate game result reporting | Custody of player funds |
| Admin key | Pause/unpause, oracle rotation, result override | Accessing escrowed funds unilaterally |
| Players | Their own key management | Each other |
No single party can unilaterally steal funds. All payout rules are enforced on-chain.
Threat: Attacker calls initialize a second time to overwrite oracle/admin.
Mitigation: Both contracts check storage for an existing key and panic on a second call.
Threat: Attacker replaces the oracle address to redirect payouts.
Mitigation: Oracle address can only be rotated by the admin via update_oracle.
Threat: Non-oracle address calls submit_result.
Mitigation: submit_result compares caller against the stored oracle address and returns Error::Unauthorized before any state change.
Threat: Oracle submits a result twice to change the winner.
Mitigation: Match state is checked for Active before processing; once in PendingResult, submit_result is rejected with InvalidState. Oracle contract additionally rejects duplicates with Error::AlreadySubmitted.
Threat: Compromised oracle submits a result for the correct match_id but a different game_id.
Mitigation: submit_result validates game_id against the value stored at match creation. Mismatch returns Error::GameIdMismatch.
Threat: Multiple matches reference the same chess game_id; one oracle result pays out all of them.
Mitigation: create_match stores each game_id in persistent storage and returns Error::DuplicateGameId on collision.
Threat: Oracle submits an incorrect result (due to API error or key compromise), causing irreversible fund loss.
Mitigation: Results enter a PendingResult dispute window for DISPUTE_WINDOW_LEDGERS (~24 hours). During this window the admin can call override_result to correct the outcome. After the window expires, finalize_result executes the payout.
Threat: Oracle service crashes permanently; player funds are locked in Active state forever.
Mitigation: Either player may call claim_timeout after TIMEOUT_LEDGERS (~7 days) without a result. Both players receive their stake_amount back and the match is Cancelled.
Threat: Player deposits into a cancelled or completed match, locking funds.
Mitigation: deposit requires state == Pending; any other state returns Error::InvalidState.
Threat: Match created with stake_amount = 0 wastes ledger storage.
Mitigation: create_match returns Error::InvalidAmount if stake_amount <= 0.
Threat: Single address creates a match against itself.
Mitigation: create_match checks player1 != player2 and returns Error::InvalidPlayers.
Threat: MatchCount wraps silently, reusing match IDs.
Mitigation: create_match uses checked_add(1).ok_or(Error::Overflow)?.
Threat: A persistent Match entry expires mid-game.
Mitigation: Every persistent write calls extend_ttl with MATCH_TTL_LEDGERS (~30 days).
Threat: Critical bug discovered post-deployment with no way to halt damage.
Mitigation: Admin can call pause() to block create_match, deposit, and submit_result. cancel_match and claim_timeout remain available so players can recover funds.
| Function | Caller |
|---|---|
initialize |
Anyone (once only) |
create_match |
player1 (requires auth) |
deposit |
player1 or player2 (requires auth) |
cancel_match |
player1 or player2 (requires auth) |
submit_result |
Registered oracle only |
override_result |
Admin only |
finalize_result |
Anyone (callable after dispute window) |
claim_timeout |
player1 or player2 (requires auth, after timeout) |
pause / unpause |
Admin only |
update_oracle |
Admin only |
get_match / is_funded / get_escrow_balance |
Anyone (read-only) |
- All state-mutating functions require
require_auth()from the appropriate caller - Oracle address is validated before any payout is executed
- Admin-only functions reject non-admin callers before any storage write
-
override_resultis restricted to admin and enforces the dispute window boundary
- All state transitions are guarded; invalid transitions return
Error::InvalidState -
CompletedandCancelledare terminal — no further transitions possible -
depositis rejected on non-Pendingmatches -
submit_resulttransitions toPendingResult, not directly toCompleted -
finalize_resultcan only execute afterDISPUTE_WINDOW_LEDGERShave elapsed
- Payout amounts are exactly
stake_amount * 2(winner) orstake_amounteach (draw) -
cancel_matchrefunds only deposited players; non-depositors receive nothing -
claim_timeoutrefunds both players their originalstake_amount - No code path allows funds to be sent to an address other than
player1,player2, or back to depositor -
get_escrow_balancereturns 0 forCompletedandCancelledmatches
-
stake_amount <= 0is rejected -
player1 == player2is rejected -
game_idlength is bounded byMAX_GAME_ID_LEN - Duplicate
game_idacross matches is rejected
-
game_idis verified against the stored value on everysubmit_resultcall - Oracle address can only be changed by the admin
- Incorrect results can be corrected within
DISPUTE_WINDOW_LEDGERSviaoverride_result -
override_resultis blocked after the dispute window expires
-
activated_ledgeris recorded when match transitions toActive -
claim_timeoutcheckscurrent_ledger - activated_ledger > TIMEOUT_LEDGERS -
claim_timeoutis rejected before the timeout period elapses - Only
player1orplayer2can callclaim_timeout
- All persistent entries call
extend_ttlon every write -
MatchCountincrement useschecked_addto prevent overflow
-
pause()blockscreate_match,deposit, andsubmit_result -
cancel_matchandclaim_timeoutremain callable while paused - Only admin can pause/unpause
- The oracle is a centralised component; a compromised oracle key can submit incorrect results within the dispute window. Mitigated by the
override_resultadmin function andupdate_oraclekey rotation. - The dispute window admin override is itself a centralised control. A compromised admin key could manipulate results during the window. Mitigated by making
override_resultonly callable during the window and emitting anoracle:result_overriddenevent for transparency.