Verify it on chain
Where everything lives, what the website reads in one call, and how to recompute a draw yourself.
Programs
Section titled “Programs”- raffle-hook: to be published at launch. The transfer hook, the table, rounds, draws and prizes. Deployed with no upgrade authority.
- raffle-pot: to be published at launch. Holds the pot and collects curve fees. Deployed with no upgrade authority.
- Meteora DBC:
dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN. Meteora’s curve program. Meteora can upgrade it. - Token-2022:
TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb. The token program.
solana program show <address> prints a program’s upgrade authority. Both raffle programs are deployed final, with none, so nobody can change them.
Accounts
Section titled “Accounts”- Mint: to be published. Token-2022, 6 decimals, 1 trillion supply, immutable metadata. Its transfer hook extension names raffle-hook. Its hook authority is Meteora DBC’s pool authority,
FhVo3mqL8PW5pH5U2CN4XE33DokiyZnUwuGpH2hmHLuM, as for every DBC hook token. - Table: PDA
["table"]of raffle-hook. 92,624 bytes: the status block, 1,024 entries, the ticket trees. - Account list: PDA
["extra-account-metas", mint]of raffle-hook. 86 bytes, with exactly two fixed entries: the table as a PDA entry with the literal seed"table", writable, then the raffle-pot program id as a literal, read-only. The hook and the mint are ground so that both PDAs, the table and this list, have bump 255. - Hook signer: PDA
["auth"]of raffle-hook. The only signer raffle-pot’s pay instruction accepts. - Pot: PDA
["pot"]of raffle-pot. The curve config’s fee claimer, and the owner of the pot’s USDC account. - Pot USDC account: to be published. The pot’s balance.
- DBC config: to be published. A flat 2% fee, creator share 16%, the pot PDA as fee claimer, USDC as quote, completion at $100B FDV.
- DBC pool: to be published. The curve itself: reserves, price, and fees waiting to be collected.
- USDC:
EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v. The quote token.
What to look for:
- Both raffle programs have no upgrade authority.
- The mint’s transfer hook names raffle-hook.
- The account list holds only fixed addresses, with the table as the one writable entry.
- The DBC config’s fee claimer is the pot PDA, not a wallet.
- Money leaves the pot’s USDC account only as prizes and bounties.
The status block, in one call
Section titled “The status block, in one call”The website stays live by polling a single public RPC call. It reads the start of three accounts: the table, the pot’s USDC account and the DBC pool.
getMultipleAccounts([table, potUsdc, dbcPool], { encoding: 'base64', commitment: 'confirmed', dataSlice: { offset: 0, length: 6608 }, // the whole status block, trade ring included})What that one reply holds:
- From the table’s status block: the open round, its counted swaps and the current floor, whether a draw is pending and its target slot, the number of holders in the table, the jackpot, running totals, and the last 8 draws with their winners, amounts and frozen tickets.
- From the pot’s USDC account: the pot’s balance.
- From the DBC pool: fees waiting to be collected, the curve’s reserves and its price.
The status block also holds a ring of the last 128 transfers the hook recorded, so the website shows trades as they happen without a second call.
All numbers are little-endian. Token amounts and USDC both use 6 decimals. The current layout starts with the magic RAFTBL06, with its layout version, 6, at byte 10. The full byte layout is published with the addresses.
Check a draw
Section titled “Check a draw”Two time limits matter:
- SlotHashes keeps a slot’s hash for only 512 slots, about 3.4 minutes.
- The frozen tickets stay in the table only until the next round closes. Rounds aim at about an hour, but with no minimum age the next one can close within seconds.
So read the table while the draw is pending, then compare with the draw record once it is revealed. The offsets below are from layout 6.
- Poll the status block. When flag bit 0 at byte 8 turns on, note the target slot (u64 at byte 40) and the round (u32 at byte 48).
- Once the chain is past the target slot, read the SlotHashes sysvar,
SysvarS1otHashes111111111111111111111111111. Its data is a u64 count, then (slot u64, hash 32 bytes) pairs, newest first. Take the target slot’s pair. If the target was skipped, take the oldest pair after it. - Compute the seed:
sha256("raffle-draw" || hash || slot as u64 LE || round as u32 LE || raffle-hook program id). - Read the whole table account, 92,624 bytes. Four trees of 1,024 u64 follow the entries, at byte 59,856: live balances, frozen balances, then two deficit trees. The frozen deficit tree is the one byte 9 doesn’t name. Frozen tickets are frozen balances minus the frozen deficit, node by node.
- Pick the three prize winners as described on The draw.
- After the reveal, compare with the draw record: the seed, the seed slot, the frozen total, and each winner’s token account and frozen tickets should all match.
Reference code
Section titled “Reference code”This follows the program’s own lookup, and was tested against the program’s table code.
// Node 18+. Recomputes a draw's prize winners from public data: the table account's bytes,// the SlotHashes entry for the seed slot, and the raffle-hook program id.import { createHash } from 'node:crypto';
const CAP = 1024;const HEADER = 6608; // the status block's length in layout 6 (magic RAFTBL06); the entries follow itconst TREES = HEADER + 52 * CAP; // the four trees follow the entries, at byte 59,856
const sha256 = (...parts) => createHash('sha256').update(Buffer.concat(parts)).digest();const u64le = (n) => { const b = Buffer.alloc(8); b.writeBigUInt64LE(BigInt(n)); return b; };const u32le = (n) => { const b = Buffer.alloc(4); b.writeUInt32LE(n); return b; };
// slotHash: 32 bytes from SlotHashes. seedSlot: the slot they belong to.// round: status block offset 48. hookId: the program id as 32 bytes.export function seedOf(slotHash, seedSlot, round, hookId) { return sha256(Buffer.from('raffle-draw'), slotHash, u64le(seedSlot), u32le(round), hookId);}
// Frozen tickets, node by node: frozen balances minus the frozen deficits, mod 2^64.// Byte 9 names the live deficit tree; the frozen one is the other.function frozenTree(table) { const b = TREES + 1 * CAP * 8; const d = TREES + (2 + ((table[9] & 1) ^ 1)) * CAP * 8; return Array.from({ length: CAP }, (_, j) => BigInt.asUintN(64, table.readBigUInt64LE(b + 8 * j) - table.readBigUInt64LE(d + 8 * j)));}
// The smallest entry index whose running total of frozen tickets is above u.function pick(tree, u) { let pos = 0; for (let step = CAP; step > 0; step >>= 1) { if (pos + step <= CAP && tree[pos + step - 1] <= u) { pos += step; u -= tree[pos - 1]; } } return pos;}
// The three prize winners. The jackpot roll needs the program's fixed-point exp, so it is left out here.export function prizeWinners(table, seed) { const tree = frozenTree(table); const total = tree[CAP - 1]; if (total === 0n) return []; return [0, 1, 2].map((k) => { const h = sha256(seed, Buffer.from([k])); const u = (h.readBigUInt64LE(0) | (h.readBigUInt64LE(8) << 64n)) % total; const i = pick(tree, u); return { entry: i, tokenAccount: table.subarray(HEADER + 52 * i, HEADER + 32 + 52 * i) }; });}Other checks
Section titled “Other checks”- A draw’s prizes add up to the free pot times its payout share at that reveal, or the whole free pot under the dust rule, plus the jackpot when it is won or swept.
- Each delivered prize shows up in the reveal transaction as a USDC transfer to an account owned by the winning wallet, and the transaction carries the draw’s memo.
- The pot’s USDC balance is never below the jackpot.
Pot Draw runs on public programs on Solana. Addresses are published at launch. Nothing here is financial advice or a promise of returns.