-
Notifications
You must be signed in to change notification settings - Fork 537
feat(gacha): add provably-fair gacha (pack-pull) example #664
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+24,535
−0
Merged
Changes from 25 commits
Commits
Show all changes
29 commits
Select commit
Hold shift + click to select a range
76501f0
init draft
amilz 7dd211c
feat(gacha): rework program around cc-vrf registry commit-reveal
amilz e908626
test(gacha): rework LiteSVM integration suite
amilz 9ef21d1
test(gacha): add Light-stack suite exercising the real cc-vrf settle …
amilz d2ab2ff
docs(gacha): trust-model docs, READMEs, and CI wiring
amilz 928ca9d
chore: cleanup
amilz 124a4da
feat(gacha): frontend wip
amilz 345b805
feat(gacha): devnet e2e working
amilz 10080e7
chore: migrate scripts to plugins
amilz d54cb1b
chore: leverage gacha plugin
amilz 801b58e
chore: leverage gacha plugin for webapp
amilz 1b729a9
feat(gacha-webapp): simd-allstars
amilz 5f02c9b
feat(gacha-webapp): improve reveal
amilz f9f0701
wip
amilz 53523b7
chore: fix flow
amilz 5156643
chore: cleanup
amilz 7fb099d
chore: update weights
amilz 9c7d14d
chore: remove web3js
amilz d946fca
chore: add gacha reveal modal
amilz 4b49b09
chore: nit cleanup
amilz a70e396
chore: cleanup unused deps
amilz 782d697
chore: docs
amilz dd1f2e2
Merge branch 'main' into feat/add-gacha-pino
amilz ed15bc7
ci: register gacha crates in workspace-ignore
amilz 3fa12a5
style: prettier format gacha example files
amilz 4b416ae
chore: bump rpc-websocket for vercel deployment
amilz c76e2d6
fix(gacha): regenerate IDL to codama format
amilz 40922c5
ci: prettier-ignore generated gacha IDL
amilz 9b391a6
refactor(gacha): apply PR review feedback
amilz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| # Gacha | ||
|
|
||
| A provably-fair **gacha** (loot-box / pack-pull) game — the on-chain mechanic behind | ||
| Solana RWA pack platforms . An admin configures a | ||
| pool of fixed-weight reward tiers and a fixed entry fee; buyers open pulls that are | ||
| revealed with a verifiable random function (RFC 9381 ECVRF) anchored in Collector | ||
| Crypt's deployed [`cc-vrf`](https://vrf.collectorcrypt.com) registry by CPI, and each | ||
| prize is minted as a Token-2022 NFT carrying its `rarity` in the token metadata. | ||
|
|
||
| The VRF input binds buyer-supplied entropy (`SHA-256(pull || client_seed)`), so no | ||
| one — including the operator — can predict an outcome before the buy lands, and | ||
| every reveal is publicly verifiable off-chain. Unsettled pulls are refundable after | ||
| a deadline. | ||
|
|
||
| | Framework | Path | | ||
| | --------- | ---------------------------- | | ||
| | Pinocchio | [`./pinocchio`](./pinocchio) | | ||
|
|
||
| The Pinocchio example is a self-contained nested workspace: it pins its own toolchain | ||
| and dependencies and builds/tests via its own `justfile`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| **/target | ||
| .idea | ||
| **/node_modules | ||
| dist | ||
| **/.next/ | ||
| **/*.tsbuildinfo | ||
| bun.lockb | ||
|
|
||
| **/generated/ | ||
|
|
||
| # Solana test validator ledger data | ||
| test-ledger/ | ||
| **/test-ledger/ | ||
| .validator-ledger | ||
|
|
||
| # Log files | ||
| *.log | ||
| **/cu_report.md | ||
|
|
||
| # Local config (contains generated mint addresses) | ||
| config.json | ||
|
|
||
| # Environment secrets | ||
| .env | ||
| .env.local | ||
| **/.env.local | ||
|
|
||
| keys/ | ||
|
|
||
| # Dumped mainnet program fixtures | ||
| tests/fixtures/ | ||
|
|
||
| # TypeDoc generated API docs | ||
| clients/typescript/docs/ | ||
|
|
||
| .claude/ | ||
|
|
||
| # Editor / tool config | ||
| .emdash.json | ||
| .vscode/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| 24.13.0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| # IDL files | ||
| idl/ | ||
|
|
||
| # Generated files | ||
| clients/typescript/src/generated/ | ||
| webapp/next-env.d.ts | ||
|
|
||
| # Build outputs | ||
| dist/ | ||
| build/ | ||
| target/ | ||
| webapp/.next/ | ||
|
|
||
| # Dependencies | ||
| node_modules/ | ||
| package.json | ||
| pnpm-lock.yaml | ||
|
|
||
| # TypeDoc generated API docs | ||
| clients/typescript/docs/ | ||
|
|
||
| # Editor / tool config | ||
| .emdash.json | ||
| .vscode/ | ||
| .remember/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| "@solana/prettier-config-solana" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,181 @@ | ||
| # CLAUDE.md | ||
|
|
||
| This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. | ||
|
|
||
| ## What this is | ||
|
|
||
| A provably-fair **gacha** (loot-box / pack-pull) game on Solana, modeled on how | ||
| platforms like Collector Crypt and Phygitals actually work — and wired into | ||
| Collector Crypt's real [`cc-vrf`](https://vrf.collectorcrypt.com) registry | ||
| program by CPI. An admin configures a pool of fixed-weight reward tiers and a | ||
| fixed entry fee, and registers an off-chain VRF operator. A buyer pays the fee | ||
| to open a pull, committing buyer-supplied entropy into the VRF input. The | ||
| operator reveals the ECVRF output (`beta`); the program anchors the proof in the | ||
| cc-vrf registry, expands `beta` into a weighted tier, and the prize — a | ||
| Token-2022 NFT whose metadata carries a `rarity` field — is minted to the buyer. | ||
|
|
||
| ## Randomness model (important) | ||
|
|
||
| RFC 9381 `ECVRF-EDWARDS25519-SHA512-TAI`, following Collector Crypt's cc-vrf. | ||
| **Solana cannot verify an ECVRF proof on-chain** (no precompile), so the trust | ||
| model is _detection, not prevention_: on-chain the program accepts the | ||
| registered operator's signed `beta`; off-chain anyone can prove cheating. The | ||
| design closes every gap that detection alone leaves open: | ||
|
|
||
| 1. **Fixed ≠ unpredictable.** `alpha = SHA-256(pull_address || client_seed)` | ||
| where `client_seed` is 32 random bytes chosen by the buyer at commit. An | ||
| alpha that is merely _fixed_ (say, the pull address alone) is worthless | ||
| against the operator: `beta = VRF(operator_key, alpha)` is deterministic, so | ||
| a predictable alpha lets the operator precompute every outcome before anyone | ||
| buys. Buyer entropy is what makes the outcome unknowable at commit time. | ||
| 2. **Fixed weights ⇒ order-independence.** Tier odds never change after init, | ||
| so a pull's outcome depends only on its `beta` — not on supply counters or | ||
| the order in which the operator settles. (Selection against a mutable | ||
| remaining-supply table would let an operator who knows every pending `beta` | ||
| route rare tiers to favored wallets purely by choosing settle order, and | ||
| per-pull proof verification would never catch it.) | ||
| 3. **One reveal per pull, enforced by cc-vrf.** `settle_pull` CPIs cc-vrf's | ||
| `commit_proof_with_beta`, whose Light Protocol compressed account derives | ||
| from `(authority, memo_hash = SHA-256(pull_address))` — a second commit for | ||
| the same pull fails at the Light system program. The commit also proves (via | ||
| validity proof against the registry) that the operator's authority record is | ||
| **frozen**, unrevoked, and keyed by exactly `pool.operator`. | ||
| 4. **Liveness has an escape hatch.** An operator can withhold a reveal (e.g. | ||
| after privately computing an unfavorable `beta`), but `refund_pull` returns | ||
| the buyer's entry fee and rent after `settle_deadline_slots`, and | ||
| `withdraw_fees` can never touch pending buyers' escrow (the vault reserves | ||
| `pending_pulls × entry_fee`). Withholding delays; it never steals. | ||
| 5. **Verification story.** From the emitted events anyone can: recompute | ||
| `alpha` from `(pull, client_seed)`, verify the proof with | ||
| `@collectorcrypt/ecvrf`, reproduce the tier with `selectTier`, and check the | ||
| registry commit. The operator's 32-byte Ed25519 seed is **both** its Solana | ||
| signing key and its ECVRF key, so `pool.operator` equals the ECVRF public key. | ||
|
|
||
| Comparison: oracle VRFs (Switchboard On-Demand, MagicBlock VRF, ORAO) verify the | ||
| randomness proof **on-chain** at the cost of oracle fees, extra latency, and an | ||
| oracle-network liveness dependency. The cc-vrf pattern is cheaper and | ||
| self-operated, but trust is detection-based. Trust notes: cc-vrf's upgrade | ||
| authority was **not** renounced as of 2026-07-29 (contradicting its docs), and | ||
| its repo declares MIT but commits no LICENSE file. | ||
|
|
||
| ## Required Versions | ||
|
|
||
| - **Rust**: See `rust-toolchain.toml` | ||
| - **Node.js**: See `.nvmrc` | ||
| - **pnpm**: See `package.json` `packageManager` field | ||
| - **Light CLI**: pinned in `justfile` (`zk_cli_version`), installed by `just setup` | ||
|
|
||
| ## Build Commands | ||
|
|
||
| ```bash | ||
| just build # program .so → IDL → TS client → dist | ||
| just generate-idl # Generate IDL via Codama (cargo build with build.rs) | ||
| just generate-clients # Generate TypeScript + Rust clients from IDL | ||
| just build-program # Build .so binary only (cargo build-sbf) | ||
| just test # unit + integration + light + client tests | ||
| just unit-test # Rust host unit tests (selection + alpha derivation) | ||
| just integration-test # LiteSVM integration tests (builds the .so first) | ||
| just light-test # Light-stack tests: real settle -> cc-vrf CPI w/ proofs | ||
| just dump-cc-vrf # Fetch the mainnet cc-vrf binary for light-test | ||
| just client-test # TypeScript client tests (parity + ECVRF + forged-reveal) | ||
| just demo # Off-chain operator/verifier demo (no RPC) | ||
| just fmt # cargo fmt + prettier | ||
| just check # fmt-check + lint-check | ||
| ``` | ||
|
|
||
| ## Architecture | ||
|
|
||
| Solana program using **Pinocchio** (lightweight `no_std` framework) with **Codama** | ||
| for IDL-driven client generation. | ||
|
|
||
| ### Client generation pipeline | ||
|
|
||
| ``` | ||
| Rust code with #[codama(...)] attributes | ||
| ↓ | ||
| program/build.rs → idl/gacha.json | ||
| ↓ | ||
| scripts/generate-clients.ts | ||
| ↓ | ||
| clients/{typescript,rust}/src/generated/ (gitignored; re-exported from src/index.ts / lib.rs) | ||
| ``` | ||
|
|
||
| ### Program | ||
|
|
||
| - `program/src/lib.rs` — declares the program ID, wires modules | ||
| - `program/src/gacha.rs` — pure logic: `select_tier`, `derive_alpha`, prize constants (host unit-tested) | ||
| - `program/src/ccvrf.rs` — hand-built Anchor CPI to cc-vrf `commit_proof_with_beta` (program IDs, wire layout, account order) | ||
| - `program/src/instructions/` — `init_pool`, `buy_pull`, `settle_pull`, `refund_pull`, `withdraw_fees`, `claim_prize`, `emit_event` (self-CPI target) + `helpers/` | ||
| - `program/src/state/` — `Pool`, `Pull` PDA structs + `Vault` / `PrizeMint` markers + `common.rs` (discriminator, pull status, PDA derivation) | ||
| - `program/src/event_engine.rs` — Anchor-compatible self-CPI event emission | ||
| - `program/src/events/` — one event struct per state-changing instruction | ||
| - `program/src/errors.rs` — error codes (100s generic / 200s pool / 300s pull / 400s settle+claim / 500s vault / 600s event) | ||
| - `program/src/tests.rs` — host unit tests for the pure logic | ||
|
|
||
| ### Accounts | ||
|
|
||
| - **Pool** — PDA `["pool", admin]`. One machine per admin: `operator` (+ its cc-vrf | ||
| `authority_label`), `entry_fee`, `settle_deadline_slots`, `tier_count`, fixed | ||
| `weights`, monotonic `pulls_count`, and `pending_pulls` (open refund liabilities). | ||
| - **Pull** — PDA `["pull", pool, buyer, index_le]`. One per pull: `client_seed`, | ||
| `alpha` (= `SHA-256(pull || client_seed)`), `beta` (set on reveal), | ||
| `tier_selected`, `status` (Pending → Settled → Claimed), `requested_slot`, | ||
| `settled_slot`. Closed on refund. | ||
| - **Vault** — program-owned, zero-data PDA `["vault", admin]` that escrows entry | ||
| fees; invariant: balance ≥ rent floor + `pending_pulls × entry_fee`. | ||
| - **PrizeMint** — Token-2022 mint PDA `["mint", pull]`, created at claim: | ||
| decimals 0, supply 1, mint authority discarded, `MetadataPointer` pointing at | ||
| itself, `TokenMetadata` with `additional_metadata: [("rarity", <tier label>)]`. | ||
|
|
||
| ### Lifecycle | ||
|
|
||
| `init_pool` (admin sets tiers, fee, deadline, operator + cc-vrf label) → | ||
| `buy_pull` (buyer pays fee + pull rent, supplies `client_seed`, status `Pending`) → | ||
| either `settle_pull` (operator: cc-vrf commit CPI + tier selection, status `Settled`) | ||
| or, past the deadline, `refund_pull` (buyer: fee + rent back, pull closed) → | ||
| `claim_prize` (anyone: mints the prize NFT to the buyer, status `Claimed`). | ||
| `withdraw_fees` (admin) drains settled revenue only. The 80-byte proof is never | ||
| stored on-chain — it is hashed into the cc-vrf commit and emitted in | ||
| `PullSettledEvent` for off-chain verification. | ||
|
|
||
| `settle_pull` and `claim_prize` are separate instructions because a settle | ||
| carries ~10 Light passthrough accounts + a 129-byte validity proof, and adding | ||
| the mint CPI stack would exceed the 1232-byte transaction limit. (A production | ||
| client could recombine them with an address lookup table.) | ||
|
|
||
| ### Testing layers | ||
|
|
||
| - `tests/integration-tests` — LiteSVM 0.12: everything that does not need a live | ||
| cc-vrf (init/buy/refund/withdraw, settle/claim negatives, claim decode via a | ||
| fabricated settled pull with `set_account`). | ||
| - `tests/light-integration-tests` — `light-program-test`: the real | ||
| `settle_pull` → cc-vrf → Light CPI chain with genuine validity proofs (local | ||
| gnark prover, prepared by `just light-bootstrap`; binary + keys cached in | ||
| `~/.config/light`). Runs against the mainnet-dumped `tests/fixtures/cc_vrf.so`. | ||
| Single-threaded (shared prover port). | ||
|
|
||
| ## Conventions | ||
|
|
||
| - **Pinocchio, not Anchor**: use `pinocchio::AccountView`, `Address`, `ProgramResult`. | ||
| - **Packed state**: `#[repr(C, packed)]`, byte-0 discriminator, zero-copy `transmute`. | ||
| Never take a reference to a packed field whose type has alignment > 1 (u32/u64 | ||
| arrays) — copy the field into a local first. | ||
| - **Foreign CPIs are hand-serialized**: Anchor programs (cc-vrf) take an 8-byte | ||
| `sha256("global:<name>")` discriminator + borsh args; SPL interface programs | ||
| (token-metadata) take an 8-byte `SplDiscriminate` hash + borsh args. Program | ||
| IDs and discriminators are constants next to the builder that uses them. | ||
| - **No `mod.rs` business logic**: module declarations and re-exports only. | ||
| - **No code comments** for logic — prefer clear names; use `///` doc comments. | ||
| - **Codama attributes drive IDL**: array field types must use a **literal** size | ||
| (`[u32; 8]`, not `[u32; MAX_TIERS]`), and Codama cannot express arrays of custom | ||
| structs — hence primitive tier arrays. `just generate-idl && git diff` catches drift. | ||
| - **Cross-language parity**: `select_tier`/`selectTier` and | ||
| `derive_alpha`/`pullAlpha` must stay byte-for-byte identical; both pairs are | ||
| pinned by shared fixtures in their respective test suites. | ||
|
|
||
| When extending: keep `#[codama(...)]` attributes in sync, emit an event per new | ||
| instruction, and add an integration test per instruction in `tests/integration-tests/`. | ||
|
|
||
| ## Program ID | ||
|
|
||
| `Bv65bJKK9kwTERuyHdCrXqWf2gKBFwkTx2rnscXaBZsS` (keypair in `keys/`, gitignored) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| [workspace] | ||
| members = [ | ||
| "clients/rust", | ||
| "program", | ||
| "tests/integration-tests", | ||
| ] | ||
| resolver = "2" | ||
|
|
||
| [workspace.package] | ||
| version = "0.1.0" | ||
| edition = "2021" | ||
| license = "MIT" | ||
| repository = "https://github.com/solana-foundation/program-examples" | ||
|
|
||
| [workspace.metadata.cli] | ||
| solana = "3.1.10" | ||
|
|
||
| [workspace.lints.rust] | ||
| unused_imports = "deny" | ||
| dead_code = "warn" | ||
| unused_variables = "warn" | ||
| deprecated = "warn" | ||
| unexpected_cfgs = { level = "warn", check-cfg = [ | ||
| 'cfg(target_os, values("solana"))', | ||
| ] } | ||
|
|
||
| [workspace.dependencies] | ||
| codama = "=0.9.3" | ||
| const-crypto = "0.3" | ||
| pinocchio = { version = "0.11.1", features = ["cpi", "copy"] } | ||
| pinocchio-associated-token-account = "0.4.0" | ||
| pinocchio-system = "0.6.1" | ||
| pinocchio-token-2022 = "0.3.1" | ||
| serde_json = "1" | ||
| solana-address = { version = "2", features = ["curve25519"] } | ||
| solana-sha256-hasher = { version = "3", default-features = false, features = ["sha2"] } | ||
| solana-security-txt = "1.1.3" | ||
| thiserror = { version = "2", default-features = false } | ||
|
|
||
| [profile.release] | ||
| overflow-checks = true | ||
| opt-level = 3 | ||
| lto = true | ||
| codegen-units = 1 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| MIT License | ||
|
|
||
| Copyright (c) 2026 Solana Foundation | ||
|
|
||
| Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: | ||
|
|
||
| The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. | ||
|
|
||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.