Every claim on this page is backed by a specific line of deployed code. No vague promises — each section references the exact file and mechanism that enforces it.
initializeGame
Creates a PDA escrow account and a separate PDA vault to hold lamports. Entry fee, house fee bps, and max players are locked into the on-chain state at this point. on-chain
deposit from Phantom
The program enforces: game must be Open, not full, wallet must not have already deposited. The exact entry_fee lamports move from player → vault PDA. Server cannot intercept. on-chain
verifyDeposit
Reads the on-chain GameEscrow account and checks the player's public key is in the registered players array. Only then does the server grant lobby access. escrow.js
finalize
Payouts array must sum to exactly 10 000 bps (enforced by the contract). Contract verifies each recipient wallet matches a registered player at that index. Arithmetic uses checked_mul / checked_div to prevent overflow. on-chain
cancel
Full entry fee returned to every registered player. No house fee on cancel. Server must be the original authority (has_one = authority) to call this. on-chain
[b"vault", game_id]. Only the Anchor program can sign transfers out of it — no EOA has the private key.finalize instruction asserts total_bps == 10_000 before moving any funds. If the payouts array doesn't sum to 100%, the transaction reverts on-chain.WrongMint error).AlreadyDeposited). This is enforced at the program level, not just by the server.Clock::get()?.unix_timestamp >= escrow.created_at + 86_400. No authority signature is required — any deposited player can initiate. on-chain
players array. Non-players cannot call it (NotAPlayer error). Each player must call it separately for their own refund.
entry_fee stored at deposit time is transferred back. Callable from Solana Explorer, any wallet, or a simple script — zero Cryptan server involvement. on-chain
ESCROW_PROGRAM_ID — token wagers without it are refused at the server level before any funds are touched.used_signatures with ON CONFLICT (signature) DO NOTHING. A UNIQUE INDEX on the signature column ensures this is enforced at the database level, not just in code.refundLocks Set entry keyed to refund:{username}:{type}. If already held, the second transfer is skipped. Prevents disconnect handler racing the escrow monitor on the same room.UPDATE tournaments SET prize_distributed=true WHERE id=$1 AND prize_distributed=false RETURNING id. Only one concurrent path can get a non-zero rowCount — the second path exits immediately.BigInt(Math.round(amount * LAMPORTS_PER_SOL)) instead of float multiplication. Prevents IEEE-754 drift (e.g. 0.1 SOL × 1e9 = 99,999,999 instead of the correct 100,000,000).JWT_SECRET is missing — there is no fallback to an insecure default key.crypto.randomInt. Room IDs use crypto.randomBytes(3). Neither relies on Math.random(), whose V8 internal state can be partially recovered from observed outputs.otp_store table on every mutation. A server restart does not invalidate a code the user is mid-way through entering. Codes expire after 10 minutes and have a max of 5 attempts.crypto.timingSafeEqual() — not a string equality operator. This prevents timing side-channel attacks that could leak password length or prefix through response time differences.| Header | Value / Policy | Protects Against |
|---|---|---|
| Content-Security-Policy | strict sha256 hashes for inline scripts; default-src 'self' |
XSS, script injection, data exfiltration |
| HSTS | production only max-age=31536000, includeSubDomains, preload | SSL stripping, protocol downgrade |
| Referrer-Policy | strict-origin-when-cross-origin |
URL leakage to third-party resources |
| frame-ancestors | 'none' |
Clickjacking via iframe embedding |
| object-src | 'none' |
Flash/Silverlight plugin attacks |
| base-uri | 'self' |
<base> injection attacks |
| CORS | env-configured ALLOWED_ORIGIN; defaults to block-all in production |
Cross-origin API abuse |
| Body limit | express.json({ limit: '2mb' }) |
JSON payload DoS |
| Scenario | Guarantee Level | Condition |
|---|---|---|
| SOL wager — correct payout | Trustless | ESCROW_PROGRAM_ID configured |
| SOL wager — refund if server disappears | Trustless | Call force_cancel (any deposited player, all refunded at once) or emergency_refund (per-player) after 24 h — no server needed |
| CRYPTAN token — correct payout (on-chain) | Trustless | Both ESCROW_PROGRAM_ID and CRYPTAN_MINT_ADDRESS set |
| CRYPTAN token wager — payout | Trustless | PDA token vault enforced by contract — same guarantees as SOL wagers. Room creation refused without ESCROW_PROGRAM_ID |
| Tournament prizes | Trust-minimised | Atomic DB guard prevents double-pay; on-chain transfer per winner |
| Free-play game integrity | Server-side | No crypto at stake; game logic runs server-side, not on-chain |
| Account password security | Strong | bcrypt-12; JWT signed with env secret; OTPs CSPRNG-generated |
ESCROW_PROGRAM_ID — no custodial fallback exists. Both SOL and CRYPTAN wagers use PDA vaults with identical guarantees.refundLocks Set replaced with pg_try_advisory_lock. The lock is now held at the Postgres level and is visible across all server instances sharing the same database.force_cancel / force_token_cancel instructions added to the contract. Any single deposited player can call them after 24 h to refund all players at once in one transaction.["vault", roomId] and the program address. Check its lamport balance on Solana Explorer. The balance should equal entry_fee × player_count while the game is active.GameEscrow PDA (seeds: ["escrow", roomId]) with Anchor CLI or program.account.gameEscrow.fetch(). You can see players, entry_fee, status, and created_at directly.escrow.created_at + 86400 <= Clock::unix_timestamp. If true and the game isn't finalized or cancelled, call emergency_refund from any Solana wallet client using only the room ID and your wallet — no server needed.GameFinalized, GameCancelled, EmergencyRefunded) that are permanently visible on any Solana explorer using the program address.