Appearance
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
Four programs, phased grant-first.
- P1
numero-token—SKUSDC(test-USDC SPL mint + faucet) +TSKPK, a 1:1 USDC-backed wrapper. - P2
numero-core(the demonstrable "play on Solana" milestone, lands first) — cage-gateddeposit, ed25519-receiptsettle+ rake,withdraw,collect_rake;Table/Position/vault PDAs; the keccak position chain; conservation. - P3
numero-custody— escape / idle-kick → FREE per-user claims, signedredeem, dispute dual-sigrelease_disputed. - P4
numero-betting— prediction-market pools (prediction markets are in development), cage-gateddeposit, pool-capped single-useclaim, cage-gatedwithdraw(reclaim unbet funds), andfreeze → custody.
- P1
The cage is the mandatory on-chain submitter. The user authorizes via an SPL
approve/delegate (mirroring the EVMdepositFor/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.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.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) andfreeze(betting → custody). Each seam is one atomic instruction — the source program exposes a cage-gatedrelease_*_to_custody_vault, and custody invokes it then writes the FREE claim, so the debit + the claim-write can't drift.Conservation triad. No core / custody / betting payout exceeds the vault / pool — the Solana analog of the EVM
debitPoolunderflow revert + the pool-capped claim.Two-token model mirrors the EVM
SKPKUSDC+TSKPK. Off-chain everything speaks integer chips; the vault holdsTSKPK.
Notable implementation decisions (made during the build)
- Custody entries keyed by a cage-assigned
entry_id(the Solana analog of EVM's globalnextEntryId), not(table_id, user_id). This lets core table escapes AND betting market freezes share ONE redeemable namespace without colliding — a tournament'stable_idcan numerically equal its bettingmarket_id. The redeem / release receipts bindentry_id(matches the EVMCustodyClaimReceipt). - Betting
withdrawreclaims only unbet funds — a cage-gated, pool-capped admin withdrawal exactly like a cash-game table'snumero-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
redeemedflag; a betting claim is single-use via aclaim_markerPDA whoseinitfails 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::settleis 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 swappedrecipient. 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 hardcodedINIT_AUTHORITYsigner, blocking init front-running / table squatting. Chosen over an upgrade-authority/ProgramDatagate becauseanchor testdeploys non-upgradeable; the constant is a documented pre-mainnet swap to the production deploy key. - F3 — deposit sources are owner-bound (
token::authorityonuser_tskpk/bettor_tskpk). - F4 —
init_custodyrequiressigner != arbiterso 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.