Skip to content

Solana Programs — the on-chain layer

Rebuild the live EVM on-chain surface on Solana as four separate Anchor programs at complete parity, behind the same cage interface so the off-chain solana-bank is a drop-in swap (the game-server stays chain-unaware; the chain is selected by ADMIN_SERVER_URL). Phased grant-first. Built + verified on solana-test-validator (16/16); devnet deployment of the .so is the one deferred operational step.

For the full instruction interface + account model, see the guide: Solana Programs (on-chain).

Decision

  1. Four programs, phased grant-first.

    • P1 numero-tokenSKUSDC (test-USDC SPL mint + faucet) + TSKPK, a 1:1 USDC-backed wrapper.
    • P2 numero-core (the demonstrable "play on Solana" milestone, lands first) — cage-gated deposit, ed25519-receipt settle + rake, withdraw, collect_rake; Table/Position/vault PDAs; the keccak position chain; conservation.
    • P3 numero-custody — escape / idle-kick → FREE per-user claims, signed redeem, dispute dual-sig release_disputed.
    • P4 numero-betting — prediction-market pools (prediction markets are in development), cage-gated deposit, pool-capped single-use claim, cage-gated withdraw (reclaim unbet funds), and freeze → custody.
  2. The cage is the mandatory on-chain submitter. The user authorizes via an SPL approve/delegate (mirroring the EVM depositFor/withdrawFor + ERC-2612 permit); the cage enforces buy-in limits + wallet-linkage off-chain. There are no user-signed fund movements that bypass the gate.

  3. Settlement receipts via the Ed25519 native program + instructions-sysvar introspection. Solana has no ecrecover, and verifying ed25519 in-program (pure BPF) is compute-prohibitive. settle (and the custody / betting signed-payout paths) read the sibling Ed25519 verify instruction and bind signer == the platform signer AND message == the receipt the program reconstructs — a valid signature over a different message, or by a different signer, cannot authorize a payout.

  4. Separate programs, narrow CPI seams. Custody and betting own their own state + vaults and touch the others only at fund-handoff seams: escape (core → custody) and freeze (betting → custody). Each seam is one atomic instruction — the source program exposes a cage-gated release_*_to_custody_vault, and custody invokes it then writes the FREE claim, so the debit + the claim-write can't drift.

  5. Conservation triad. No core / custody / betting payout exceeds the vault / pool — the Solana analog of the EVM debitPool underflow revert + the pool-capped claim.

  6. Two-token model mirrors the EVM SKPKUSDC + TSKPK. Off-chain everything speaks integer chips; the vault holds TSKPK.

Notable implementation decisions (made during the build)

  • Custody entries keyed by a cage-assigned entry_id (the Solana analog of EVM's global nextEntryId), not (table_id, user_id). This lets core table escapes AND betting market freezes share ONE redeemable namespace without colliding — a tournament's table_id can numerically equal its betting market_id. The redeem / release receipts bind entry_id (matches the EVM CustodyClaimReceipt).
  • Betting withdraw reclaims only unbet funds — a cage-gated, pool-capped admin withdrawal exactly like a cash-game table's numero-core::withdraw. Betting stays entry-only: staked funds back outcome shares and are locked until settlement (no position cash-out — conservation depends on the pool staying whole). The cage authorizes the reclaimable amount off-chain.
  • Single-use guards are PDAs. A custody entry redeems once via its redeemed flag; a betting claim is single-use via a claim_marker PDA whose init fails on replay.

Later hardening pass

A follow-up hardening pass surfaced four code-confirmed gaps the happy-path tests never exercised; all were fixed + regression-tested:

  • F1 — signature-gated payouts bind their destination. core::settle is authorized by the cage's ed25519 receipt (anyone may submit it), so the payout destination is bound to the receipt identity (token::authority = recipient_owner) — a captured receipt can't be re-submitted with a swapped recipient. The general rule: when a signature is the authorization, the signed message must pin the destination (custody redeem/release + betting claim already bind the destination token account directly).
  • F2 — initialization is deploy-gated. Every init_* requires a hardcoded INIT_AUTHORITY signer, blocking init front-running / table squatting. Chosen over an upgrade-authority/ProgramData gate because anchor test deploys non-upgradeable; the constant is a documented pre-mainnet swap to the production deploy key.
  • F3 — deposit sources are owner-bound (token::authority on user_tskpk / bettor_tskpk).
  • F4 — init_custody requires signer != arbiter so the dispute dual-sig can't collapse to one key.

Parity note (Solana ≠ EVM at hand-off)

The EVM diamond commingles every table's balance, so escape / freeze are pure internal-ledger moves (no ERC-20 transfer at hand-off). Solana has no shared storage — each vault is its own SPL token account — so the faithful analog moves TSKPK source-vault → custody-vault at hand-off. The funds never leave the protocol (they sit in the custody vault until redeemed); the EVM "no token move at escape" sub-clause does not translate, while every meaningful invariant (redeemable claims, debit conservation, redeemed-once, dispute dual-sig) holds identically.

Alternatives rejected

One monolithic program (couples independent failure domains); a raw-USDC vault (the chip wrapper mirrors the EVM two-token model); user-signed deposits with no cage gate (bypasses buy-in limits + linkage); in-program ed25519 verification (CU-prohibitive); porting the dead EVM surface (Poker/Numero/Transfer/ Fairness facets, per-player balances, emergencyWithdraw, tableEscape — 0 live callers; pruned separately); a secondary market / position cash-out for betting (breaks pool conservation — entry-only by design, only unbet funds are reclaimable).

Toolchain

Anchor 0.31.1 + Agave 4.0; build --no-idl (the idl-build step breaks on seed expressions under rustc 1.90) with raw @solana/web3.js integration tests; single upgrade authority (mirrors the diamond owner), a Squads multisig later.