CryptoBullsCryptoBulls

How it works

A long-form, mechanical explanation of every moving part. Not marketing - the actual code path.

1. The mechanic in one paragraph

A holder with at least 1,000,000 $BULLS can opt-in to wrap them. Wrapping mints a fresh NFT, transfers the 1,000,000 $BULLS into a vault account, and binds the vault to the NFT's mint address. From that moment, the vault is controlled by whoever holds the NFT - the program enforces it. The NFT is a standard Metaplex Token Metadata NFT, so it lists immediately on Magic Eden and Tensor. When someone buys the NFT, the locked tokens come with it. The buyer can keep the NFT or unwrap it: the program checks they hold the NFT, drains the vault back to them, burns the NFT, and frees the tier slot.

2. The three on-chain accounts

BullBank (singleton)

One per deployment, PDA seeds ["bank"].

  • token_mint - locked at initialize, never changes
  • total_wrapped / total_unwrapped / in_circulation
  • next_tier - counter for fresh tiers (1..1000)
  • free_tiers - stack of recycled tiers (popped before next_tier)
BullAsset (per active bull)

Created on wrap, closed on unwrap. PDA seeds ["bull", tier_index].

  • nft_mint - pubkey of the Metaplex NFT
  • tier_index - public-facing number (CryptoBulls #N)
  • wrapped_at - unix timestamp
Vault token account

An ATA holding exactly 1,000,000 $BULLS, owner = PDA at ["vault", nft_mint].

This is the central trick. The vault's authority is derived from the NFT mint. It can only be signed for by this program, and only when the caller proves ownership of the NFT in the unwrap_bull instruction.

3. Why the vault PDA trick works

Concrete trace:

  1. Time 0. Alice has 4M $BULLS. She runs wrap_bull. Result: 3M loose, 1 NFT (BULL_42), vault at PDA("vault", BULL_42) holding 1M.
  2. Time 1. Alice lists BULL_42 on Tensor for 5 SOL.
  3. Time 2. Bob buys BULL_42. NFT moves to Bob, Alice gets SOL. The vault doesn't move - its address and authority are unchanged.
  4. Time 3. Alice tries unwrap_bull(42). Program checks: does Alice's NFT ATA hold 1 of BULL_42? No. Instruction aborts.
  5. Time 4. Bob calls unwrap_bull(42). Program checks: does Bob hold 1 of BULL_42? Yes. Drains vault to Bob, burns NFT, frees tier.

The tokens "follow the NFT" not because they physically move during a sale - they don't - but because only the NFT holder can drive the program to unlock them. Possession of the NFT is possession of the right to call unwrap_bull. That right is the ownership of the underlying tokens.

4. The three instructions

initialize

One-time call by the protocol deployer. Creates the BullBank, locks the $BULLS mint, sets next_tier = 1.

wrap_bull (7 steps, atomic)
  1. Validate caller balance ≥ 1,000,000 $BULLS
  2. Pop tier (free_tiers stack first, fallback to next_tier)
  3. Initialize fresh NFT mint (decimals=0, freeze authority = vault PDA)
  4. Initialize vault ATA (owner = vault PDA)
  5. Transfer 1,000,000 $BULLS → vault
  6. Mint 1 NFT to caller's ATA
  7. Create Metaplex metadata + master edition (locks supply at 1)
unwrap_bull (4 steps, atomic)
  1. Verify caller's NFT ATA holds 1 of nft_mint
  2. Drain vault → caller (signed by vault PDA)
  3. Burn NFT (mint, ATA, metadata, master edition all closed)
  4. Push tier back to free_tiers, close BullAsset, return rent to caller

5. Off-chain pieces

  • cranker - Node.js indexer + metadata server on a DigitalOcean box. Read-only; not on the wrap/unwrap critical path.
  • renderer - Pure function from sha256(nft_mint) → 24×24 pixel SVG. Locked at wrap time, follows the NFT through transfers.
  • website - Next.js at cryptobulls.fun. Wrap/unwrap UI, gallery, /api/metadata + /api/render endpoints.

6. Trait & rarity model

Every bull's visual is a deterministic projection of sha256(nft_mint_pubkey) into seven trait slots.

Per-item drop rates below are ranges across all categories — each category has its own weight sum, so the exact rate of a Rare body item differs from a Rare accessory item. The tier labels reflect design intent and visual impact, not a single fixed percentage.

TierPer-item rateExamples
Common22–68%brown/black body, ivory horns, normal eyes, pasture/sand bg, "none" for accessory / eyewear / mouth
Uncommon5–18%white/red body, dark/gold horns, closed/angry eyes, sky/sunset bg, bell, gold_chain, cowboy_hat, sunglasses, 3d_glasses, frown, cigarette
Rare2.7–12%golden/cyan/pink body, crimson/silver horns, crying eyes, chart/crimson bg, top_hat, mohawk, tiara, Pump, Phantom, mog, thug_life
Epic1.8–4%zombie body, void/gold/green eyes, void bg, fire_aura, diamond_aura, halo, scar, dubai_hat, lasers, grill
Legendary~1% (1/99 to 1/110)holo body, ski_mask eyes, halo_stars accessory

Mouth and horn categories intentionally cap at Rare — they have no Epic or Legendary tier, so the bull's facial expression and horn color stay readable at a glance.

7. Lifecycle of a single bull

  1. Birth. Wrap creates the NFT mint. Visual is computed from the new mint address - locked from this moment forward.
  2. Travel. The NFT is a regular Metaplex NFT. Transfer it, list it, swap it. The vault stays where it is, accessible only through whoever holds the NFT.
  3. Death. The current holder calls unwrap_bull. Vault drains, NFT burns, tier returns to the free_tiers stack.
  4. Rebirth (optional). A later wrapper claims the same tier number, but a fresh NFT mint generates a new visual. Tier 42 v2 looks nothing like tier 42 v1. Every wrap is a fresh roll.
Wrap your first bull →