On-chain lottery on Solana using MagicBlock VRF for verifiable randomness.
An admin creates a pool with a prize and N tickets. Players buy tickets — the price doubles after each purchase. Every ticket triggers a VRF draw: probability of winning is 1 / tickets_remaining. If someone wins, they receive the entire prize pool.
Devnet only. Built with Anchor 0.32.1.
ID: 9As38cKdZYeMStQjfDdWgTUKTBLpxb4XUhPd6FtJhtbQ
| Instruction | Who | What |
|---|---|---|
initialize |
super_admin | Creates Config PDA (one-time) |
add_admin |
super_admin | Adds an admin |
remove_admin |
super_admin | Removes an admin |
create_pool |
admin / super_admin | Creates a pool + deposits prize SOL |
buy_ticket |
anyone | Buys a ticket, triggers VRF |
resolve_ticket |
VRF callback | Resolves the draw (auto, not called manually) |
cancel_pool |
pool creator | Cancels if no tickets sold |
Config PDA ["config"]
├── super_admin, admins (max 10), pool_count
Pool PDA ["pool", pool_id]
├── creator, prize_pool, ticket_price, total_tickets, ticket_left
├── status: Open | PendingVrf | Settled | Cancelled
├── last_buyer, winner, created_at, closed_at
PoolCreated, TicketBought, TicketResolved, PoolCancelled — emitted on every state change.
buy_ticket→ pool status becomesPendingVrf- MagicBlock oracle resolves randomness off-chain (~5-30s)
resolve_ticketcallback fires automatically- Pool returns to
Open(lost) or becomesSettled(won)
TypeScript SDK in sdk/ — PDA helpers, fetch functions, instruction builders. Ready to use with @coral-xyz/anchor.
import { createProgram, fetchAllPools, buyTicket, createPool } from "./sdk";Small vibe-coded Next.js app in app/ to test the game flow. Wallet adapter + Tailwind. Nothing fancy, just enough to interact with the program on devnet.
anchor build
anchor deploy --provider.cluster devnetThis is a small devnet program, not production-ready. Things that would need to be addressed for a real deployment:
- VRF timeout recovery — If the MagicBlock oracle never callbacks, the pool stays in
PendingVrfforever and funds are locked. Aforce_reopeninstruction with a timeout check would fix this. - Ticket price escalation — Price doubles every purchase (
saturating_mul). With 50 tickets max, the price becomes astronomically high well before all tickets are sold. Amax_ticket_pricecap or pool expiry would help. - Super admin transfer — No way to transfer the
super_adminrole. If the key is lost, the admin system is bricked. - No fee mechanism — The protocol takes no cut on ticket sales or prize payouts. A production version would need a fee split (e.g. % of each ticket going to a fee vault) to be sustainable.
- Pool rent accumulation — Settled/Cancelled pools stay on-chain forever (~0.002 SOL each). A
close_poolinstruction would reclaim rent, but we keep them for history. - No re-buy protection — Same wallet can buy all tickets in a pool.
- Hardcoded devnet constants — Oracle queue, VRF program addresses are devnet-only.