This document describes the security measures implemented in Sol Battles / DegenDome and important considerations for future development.
- Solana Program Security
- PDA Balance System
- Session Key Security
- API Key Protection
- Wallet Authentication
- Input Validation
- Rate Limiting
- CORS Configuration
- Anti-Abuse Measures
- Environment Variables
- Common Security Mistakes to Avoid
- Future Improvements
The Session Betting program (6nEZfYvQQL4iyG2sF9rNvGhMy1T1GisUKXmBZA5pAJq2) implements multiple security layers:
- Authority-only price submission:
lock_roundrequires authority signature to prevent price manipulation - Authority-only settlement:
credit_winningsandtransfer_to_global_vaultare authority-only - Game pause functionality: Authority can pause all betting in emergencies
- Reentrancy protection: All state is updated BEFORE transfers
- Math overflow protection: All arithmetic uses Rust
checked_*operations - Sufficient balance checks: Both user balance and global vault verified before operations
- SystemAccount validation: All vault PDAs use
SystemAccount<'info>for ownership verification
- All PDAs validated with proper seeds and bumps
- Seeds:
balance,vault,global_vault,session,round,pool,position
- Users deposit SOL to their personal vault PDA (seeds:
["vault", user_pubkey]) - Balance tracked in UserBalance PDA (seeds:
["balance", user_pubkey]) - When playing games, backend verifies on-chain balance before allowing entry
- Winners credited via
credit_winnings, losers debited viatransfer_to_global_vault
| Operation | Signer Required | Can Use Session Key |
|---|---|---|
| Deposit | Wallet | No |
| Withdraw | Wallet | No (critical) |
| Place Bet | Wallet or Session | Yes |
| Claim Winnings | Wallet or Session | Yes |
| Credit Winnings | Authority | N/A |
// Before any game entry:
const onChainBalance = await balanceService.getOnChainBalance(wallet);
const pendingDebits = getTotalPendingDebits(wallet);
const availableBalance = onChainBalance - pendingDebits;
if (availableBalance < entryFee) {
throw new Error('Insufficient balance');
}- Wallet signature required to create session - Attackers cannot create sessions
- Sessions cannot withdraw - Even if session key leaked, funds are safe
- Time-limited - Maximum 7 days validity
- Per-device - Each session has unique keypair
fn verify_session_or_authority(session_token, signer, expected_authority) {
// If signer IS the wallet owner, allow
if signer.key() == expected_authority { return Ok(()); }
// Otherwise, validate session:
// 1. Session authority matches user
// 2. Signer matches session_signer
// 3. Session not expired (clock check)
}- Place bets in Oracle prediction
- Claim winnings (to balance, not wallet)
- Withdraw funds
- Create new sessions
- Transfer funds out of system
- Location: Backend only (
HELIUS_API_KEYin.env) - How it works: Frontend calls
/api/nfts/:walletwhich proxies to Helius - Never: Put API keys in
NEXT_PUBLIC_*variables - they're visible in browser
CMC_API_KEY- CoinMarketCap (backend only)- All private keys (
ESCROW_WALLET_PRIVATE_KEY, etc.) - backend only
Authentication requires wallet signatures. All authenticated endpoints verify that:
- The wallet address is valid (base58 format)
- The signature is valid (signed by the wallet's private key)
- The timestamp is within 5 minutes
- The signature hasn't been used before (replay protection)
Signature verification is ENABLED (REQUIRE_WALLET_SIGNATURES=true)
// web/src/lib/auth.ts
import { createAuthHeaders, authenticatedFetch } from '@/lib/auth';
// Option 1: Get headers manually
const headers = await createAuthHeaders(walletAddress, signMessage);
fetch(url, { headers });
// Option 2: Use helper function
const response = await authenticatedFetch(url, walletAddress, signMessage, options);These files have been updated to use wallet signatures:
web/src/hooks/useProfile.ts- Profile update/delete (via ProfileContext)web/src/contexts/ProfileContext.tsx- Passes signMessage to useProfileweb/src/app/admin/waitlist/page.tsx- Admin dashboardweb/src/app/waitlist/page.tsx- Waitlist signup
- Message:
DegenDome:auth:{timestamp} - Headers required:
x-wallet-address: Wallet public key (base58)x-signature: Signed message (base58)x-timestamp: Unix timestamp (ms)
- Email: Regex validation, max 254 chars, no
+aliases - Wallet addresses: Base58 format, 32-44 chars
- Referral codes: Format
DEGEN[A-HJ-NP-Z2-9]{4} - UTM parameters: Alphanumeric, dash, underscore only
- NFT URLs: Block localhost, internal IPs, validate protocol
- SQL: Parameterized queries, whitelist for sort columns
- Email: Same regex as backend
- Wallet addresses: Base58 format check
- Referral codes: Format validation before API call
globalLimiter: 100 req/min per IPstandardLimiter: 30 req/min (most endpoints)strictLimiter: 5 req/min (write operations)burstLimiter: 60 req/min (price history)
- All
/api/*routes have global limiter - Waitlist join: strict limiter
- Profile updates: standard limiter
- Admin endpoints: standard limiter
- Only allows origins in
ALLOWED_ORIGINSenv var - Default:
https://www.degendome.xyz,https://degendome.xyz - Logs blocked CORS requests
- Allows localhost:3000, localhost:3001, 127.0.0.1
- Disposable email blocking: 500+ domains blocked (
backend/src/utils/disposableEmails.ts) - Email alias blocking: No
+in emails (preventsjohn+1@gmail.comabuse) - IP tracking: Same-IP referrals not credited
- Wallet signature: Required for waitlist signup
- Signature replay protection: 5-minute window, each signature usable once
- Stored in
NEXT_PUBLIC_WHITELISTED_WALLETSenv var - Not hardcoded in source code
# Required
DATABASE_URL= # PostgreSQL connection
HELIUS_API_KEY= # NFT API
CMC_API_KEY= # CoinMarketCap
# Optional
REQUIRE_WALLET_SIGNATURES= # Enable signature auth (default: false)
ADMIN_WALLETS= # Comma-separated admin wallets
ALLOWED_ORIGINS= # CORS origins# Required
NEXT_PUBLIC_BACKEND_URL= # API URL (NO localhost fallback in production)
# Optional
NEXT_PUBLIC_WHITELISTED_WALLETS= # Early access wallets// BAD - visible in browser
const HELIUS_KEY = process.env.NEXT_PUBLIC_HELIUS_API_KEY;
// GOOD - proxy through backend
const response = await fetch(`${BACKEND_URL}/api/nfts/${wallet}`);// BAD
const ADMIN_WALLETS = ['GxjjUm...', 'Cyth...'];
// GOOD
const ADMIN_WALLETS = process.env.ADMIN_WALLETS?.split(',');// BAD - will silently fail or connect to wrong service
const URL = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:3001';
// GOOD - use centralized config that fails clearly
import { BACKEND_URL } from '@/config/api';// BAD - anyone can set this header
const wallet = req.headers['x-wallet-address'];
// GOOD - verify signature (when REQUIRE_WALLET_SIGNATURES=true)
const isValid = verifyAuthSignature(wallet, signature, timestamp);// BAD
const query = `SELECT * FROM users ORDER BY ${sortBy}`;
// GOOD
const validColumns = ['created_at', 'name'];
const safeSortBy = validColumns.includes(sortBy) ? sortBy : 'created_at';- Multi-sig authority: Implement Squads multi-sig for program authority
- Authority key in HSM: Move authority private key to Hardware Security Module
- Rate limiting on settlements: Max credits per hour per user
- CSRF protection: Implement anti-CSRF tokens for state-changing requests
- Database encryption: Encrypt sensitive fields (email, IP addresses)
- Global vault monitoring: Alert when vault balance drops below threshold
- Audit logging: Log all admin/authority actions to database
- Pending transaction reconciliation: Periodic on-chain/off-chain balance sync
- 2FA for admin: Add TOTP for admin dashboard access
- Content Security Policy: Add CSP headers to frontend
- Subresource Integrity: Add SRI hashes for external scripts
- Security headers: Add X-Frame-Options, X-Content-Type-Options
- Authority-only price submission (prevents manipulation)
- Global vault balance verification before payouts
- SystemAccount validation for all vault PDAs
- Session key isolation (cannot withdraw)
- Reentrancy protection on all transfers
- Math overflow protection
- Wallet signature authentication
- PDA balance verification in backend
If you discover a security vulnerability, please report it responsibly.
Last updated: January 2025