Skip to content

oxy-Op/anigacha

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Pinocchio VRF NFT Lottery

Native Solana NFT lottery built with Pinocchio, Switchboard randomness, Metaplex Core custody, zero-copy account layouts, and indexer-friendly self-CPI events.

Anigacha devnet UI

The devnet UI and backend live in apps/frontend and apps/backend. The frontend expects a backend API URL through VITE_API_BASE; the backend expects RPC, keypair, lottery, collection, and worker settings through environment variables.

This repository is intentionally close to the Solana runtime model. It does not use Anchor account macros, Anchor serialization, or generated CPI wrappers. The point is to make account validation, PDA authority, byte layout, randomness reduction, asset custody, and event emission visible in the code.

What this demonstrates: native Pinocchio program engineering, manual account validation, zero-copy chunked state, async VRF commit/reveal, grind-resistant economic design, and an indexer-friendly event ABI.

Highlights

  • Native Pinocchio instruction processors using explicit AccountView account order and manual account validation.
  • Fixed-size #[repr(C)] zero-copy accounts with bytemuck::Pod, stored PDA bumps, version bytes, explicit padding, and reserved bytes.
  • Chunked NFT pool accounts: CHUNK_CAPACITY = 250, MAX_CHUNKS = 250, for up to 62,500 NFT slots without one giant serialized vector.
  • On-demand chunk creation during DepositNfts, so setup only pays rent for chunks that are actually used.
  • O(1) prize removal with tail-swap: reveal moves the last active NFT into the drawn slot and decrements the active count.
  • Switchboard On-Demand commit/reveal flow for randomness, with timeout-based permissionless refunds.
  • FIFO settlement fixed at commit time, preventing keepers from reordering known random outcomes.
  • Metaplex Core transfer CPI helpers for NFT custody and prize delivery.
  • Self-CPI events for initialization, deposits, activation, play commits, reveals, and refunds.
  • Hand-written Solana Kit TypeScript SDK plus Rust unit tests, LiteSVM tests, and opt-in devnet integration tests.

Program Flow

  1. InitLottery creates the lottery PDA and stores admin, price, timeout, status, and bump.
  2. DepositNfts transfers one bounded batch of Metaplex Core assets into lottery custody and creates the next chunk PDA when needed.
  3. ActivateLottery moves the lottery from setup to playable after at least one NFT has been deposited.
  4. Play escrows the SOL price in a per-play PDA, reserves one available pool slot with pending_plays, assigns a monotonic settlement sequence, and commits the player's Switchboard randomness account.
  5. Reveal runs after a Switchboard reveal instruction in the same transaction, requires the next committed sequence, reduces the random bytes to an active NFT index, transfers the prize to the player, sends the price to admin, advances settlement, decrements total_nfts and pending_plays, and closes the Play PDA.
  6. RefundExpired is permissionless after timeout only when the randomness account never settled. A crank signs the transaction, but the Play PDA can only close back to the player stored in the Play account, and the reservation is released by decrementing pending_plays.

The current payment path is native SOL. The account layouts reserve space for future compatible fields, but token payments and broader admin controls are not implemented in this version.

Security Considerations

Selective-Reveal / Refund Grind Attack

Threat: if the outcome were computable at commit time, or if refund could escape an unfavorable settled result, a player could avoid bad draws: refund losses, reveal wins, and free-roll the lottery.

Defense: the outcome does not exist at Play commit time. Switchboard On-Demand randomness is committed first and only becomes available after the Switchboard reveal path settles the randomness account, so result-grinding is impossible at the primitive level.

Defense: RefundExpired is legal only when the Switchboard randomness account proves it never settled and the timeout elapsed. A settled-but-unfavorable outcome cannot escape through refund; the only legal settlement path is Reveal.

Defense: Reveal is permissionless. Once randomness settles, any keeper can include the Switchboard reveal instruction and the lottery Reveal instruction, forcing prize transfer and play settlement. The player cannot hold out on revealing a loss.

Conclusion: the player has no profitable deviation. Settled randomness must reveal and anyone can force it; never-settled randomness plus timeout can refund.

Custody Is Verified, Not Trusted

Threat: a caller could pass arbitrary chunk, asset, collection, or PDA accounts to trick the program into transferring the wrong NFT or accepting fake state.

Defense: deposit and reveal validate PDA derivations, stored bumps, chunk indices, program ownership, Metaplex Core asset ownership, and collection membership on-chain. Caller-supplied accounts are treated as claims to verify, not as trusted facts.

Minimal Admin Authority

Threat: admin keys are operationally sensitive, and broad admin withdrawal controls can become custody risk.

Defense: price, timeout, admin, and payment mint are fixed at initialization. The admin cannot pull NFTs from an active pool or funds owed to committed plays. Payment reaches admin only through successful Reveal; refund closes back to the stored player. No admin cleanup path exists in this version.

Modulo Bias In Randomness Reduction

Threat: mapping 32 random bytes into [0, active_count) with modulo has tiny bias unless active_count divides 2^256 exactly.

Defense: the program reduces the full 32-byte Switchboard value as a big-endian integer modulo active_count. With a maximum pool size of 62,500, the bias is bounded by one extra preimage out of a 256-bit domain, which is negligible for this lottery size. The implementation does not use rejection sampling.

Tail-Swap Instead Of Drawn Bitmap

Threat: a drawn-bitmap design can require repeated sampling when randomness lands on already-drawn slots, and scanning or retrying can increase CU and reveal complexity as the pool drains.

Defense: reveal uses tail-swap. The active pool remains densely packed in [0, total_nfts), removal is O(1), and the next draw never has to reject an already-drawn slot. This is a deliberate CU and state-size tradeoff; deposit order is not preserved.

Architecture

The deeper design notes live in docs/architecture.md, and the security notes are mirrored in docs/security.md:

  • account layouts and why padding is explicit,
  • chunk indexing and tail-swap removal,
  • Switchboard randomness handling,
  • self-CPI event format,
  • compute-unit rationale,
  • what is verified locally versus on devnet.

IDL notes are in docs/idl.md, and the current devnet test deployment is recorded in docs/devnet-deployment.md.

Events

Events are emitted as self-CPIs instead of plain logs. The inner instruction data is:

[event instruction discriminator, event version, event kind, fixed POD payload]

The event self-CPI requires an event authority PDA derived from:

["__event_authority"]

The event processor validates that PDA signer and then returns without mutating state. This gives indexers durable inner instruction data while preventing normal users from spoofing valid program events.

Testing

Fast Rust checks:

cargo fmt -- --check
cargo check
cargo test

The Rust tests cover instruction parsing, PDA derivation, byte layout sizes and offsets, event payload layout, randomness reduction, and the local Switchboard randomness parser.

LiteSVM runtime tests are ignored by default because they need a compiled SBF artifact:

cargo build-sbf --features bpf-entrypoint
cargo test --test litesvm_lottery_lifecycle -- --ignored --nocapture

Local Deposit/Play/refund LiteSVM tests use the local-testing feature to avoid live Metaplex Core and Switchboard dependencies:

cargo build-sbf --features bpf-entrypoint,local-testing
cargo test --test litesvm_lottery_lifecycle -- --ignored --nocapture

Do not deploy a local-testing build. It is only for local runtime tests.

The TypeScript SDK checks:

cd sdk
npm install
npm run typecheck
npm test
npm run build

The devnet integration harness is opt-in:

cd sdk
npm run test:devnet

It mints or reuses a small Metaplex Core fixture, initializes/deposits/activates when needed, then exercises failure cases plus live play/refund and play/reveal paths using devnet Switchboard and Metaplex Core.

For reveal, the SDK/backend must know the random value before constructing the lottery Reveal instruction, because the transaction account list must include the correct draw chunk, tail chunk, and selected asset. The devnet flow does this atomically by submitting [Switchboard reveal ix, lottery Reveal ix]; the program still recomputes and validates the result on-chain.

Generated devnet fixture state can contain local signer secrets. It is ignored by git and should not be committed.

SDK

The sdk/ package is a small hand-written Solana Kit SDK. It mirrors the native ABI directly:

  • one-byte instruction discriminators,
  • little-endian integer args,
  • explicit PDA helpers,
  • account metas in processor order,
  • fixed-layout account decoders,
  • self-CPI event parsing.

The SDK is intentionally not pretending this is an Anchor program. The Anchor-style IDL in idl/ exists for explorer compatibility and Codama conversion.

IDL

IDL artifacts live in idl/:

  • pinocchio_vrf_nft_lottery.anchor.json is the Solscan-facing Anchor-style ABI description.
  • pinocchio_vrf_nft_lottery.codama.json is generated from that source with Codama.

Regenerate the Codama artifact with:

cd sdk
npm run codama:convert

Native-program differences:

  • instruction discriminators are explicit one-byte values,
  • program-owned accounts do not use Anchor's 8-byte account discriminator,
  • padding and reserved bytes are part of the public binary layout,
  • self-CPI events use instruction discriminator 6, not Anchor event logs.

Compute Units

CU matters here because it changes what can fit into one transaction, how easily instructions compose, how much priority fee a sponsored transaction needs under load, and how many play/reveal/refund transactions a keeper can settle per slot.

This program keeps hot paths bounded:

  • fixed account sizes instead of realloc,
  • stored PDA bumps instead of repeated canonical bump search on validation paths,
  • zero-copy account loading instead of full Borsh deserialization,
  • chunked NFT state instead of scanning or deserializing one large vector,
  • O(1) draw removal through tail-swap.

Measured CU values:

Instruction CU Measurement source
InitLottery 4,857 LiteSVM, cargo test --test litesvm_lottery_lifecycle -- --ignored --nocapture
DepositNfts 8,760 LiteSVM local-testing path, first chunk creation plus one bounded deposit batch
Play 7,390 LiteSVM local-testing path, SOL escrow, sequence assignment, and play PDA creation; live Switchboard commit CPI disabled by feature
Reveal 11,764 Pre-FIFO devnet tx 3315cP...UBaqk; the current FIFO build has not been remeasured on devnet
RefundExpired 3,843 LiteSVM local-testing path, ordered timeout refund and play PDA close

No naive full-deserialize contrast row is included because this repository does not contain a measured naive implementation. The table above is measured only.

Status

Implemented and tested:

  • native SOL payment flow,
  • chunked Metaplex Core NFT custody,
  • Switchboard commit/reveal integration,
  • permissionless timeout refund,
  • self-CPI event ABI,
  • Rust unit tests,
  • LiteSVM tests,
  • TypeScript SDK tests,
  • devnet integration tests.

Known limits:

  • only native SOL payments are implemented,
  • external CPI layouts are hand-maintained and should be rechecked when upstream packages change,
  • the public IDL is manually maintained for explorer compatibility,
  • monitoring and mainnet deployment are outside this repository.

About

Native Solana NFT lottery built with Pinocchio, Switchboard randomness, VRF, Metaplex Core custody, zero-copy account layouts, and indexer-friendly pinocchio self-CPI events.

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Contributors