ProofDraw

ProofDraw: how a draw is decided, and how to check it yourself

ProofDraw runs giveaways where one Seeker gets one entry and anyone can re-run the draw. Nobody, the host and ProofDraw included, can know the winners before entries close, and nobody can change them afterwards.

The four steps

  1. When the raffle is created the server makes a secret 32-byte seed and publishes seedCommit = SHA-256(seed). The prize, number of winners, closing time and entry conditions are locked.
  2. While it is open every entry is the text I enter ProofDraw raffle #<id> signed (Ed25519) by the entrant's wallet. In a Seeker raffle entries need a Seeker Genesis Token, one entry per Seeker. In a raffle **open to any wallet** it is one entry per wallet, and the host must have set at least one holding condition (a token, an NFT of a collection, some SOL), which is what makes extra wallets cost something. Conditions apply to both kinds. Signed messages and signatures are published, and the page says which kind a raffle is.
  3. At closing no more entries are taken. Every entry is checked again (Genesis Token and conditions); an entry that no longer qualifies is removed and the reason is published. Then the list is frozen: wallets sorted in plain text order, joined with \n (no trailing newline), entriesHash = SHA-256(list). The server also announces drawSlot = current finalized slot + 150, a Solana block about a minute in the future that nobody can know yet.
  4. The draw uses the first block produced at or after drawSlot (slots without a block are skipped), once it is finalized:
    • drawSeed = SHA-256(seed || entriesHash || blockhash): 32 + 32 + 32 bytes; entriesHash from hex, blockhash from base58.
    • Random numbers: block i (i = 0, 1, 2 …) of the stream is SHA-256(drawSeed || i), i as a 4-byte big-endian integer; each block is read as eight unsigned 32-bit big-endian integers. nextInt(n) takes the next value v, skips it if v >= 2^32 - (2^32 mod n), and returns v mod n.
    • Winners: for i = 0 … k-1 swap position i of the sorted list with position i + nextInt(n - i); the first k wallets are the winners in rank order (k = number of prizes, or everyone if fewer entered).
    • The seed is published with the result.

Version 2: holder-snapshot raffles (tickets by holdings)

A holder-snapshot raffle has no sign-up: at closing, every wallet holding the token or NFT collection the host named is entered automatically, with tickets = min(maxTickets, floor(holdings / perTicket)), and wallets with no full ticket are left out. The holdings are read from the chain in pages while the raffle is "closing" and published with the list, so anyone can spot-check them. Everything else is as above, with two changes, marked version: 2 in the proof:

  • The frozen list's lines are wallet:tickets (wallets sorted as before, tickets as decimal integers), and entriesHash is the SHA-256 of those lines joined with \n.
  • Winners: for each rank, r = nextInt(remaining) where remaining is the number of tickets held by wallets that have not won yet; walk the sorted list skipping winners, adding each wallet's tickets to a running total; the first wallet whose running total exceeds r wins, and its tickets leave the pool. A wallet can win once.

Version 1 proofs are checked as version 1 forever; the checker reads version from the proof.

The result must be published within 31 minutes of closing (the deciding block is about a minute after closing, so 30 minutes after the block). If it is not, the raffle is marked "late" in public, and is still drawn with the same block as soon as possible; the late mark stays.

A host can cancel a raffle only while it is open; the cancellation and its reason are public.

Why nobody can cheat

WhoTries toWhy it fails
ProofDrawswap the seed after seeing entriesseedCommit was public from the start
ProofDrawadd fake entries just before closingthe deciding block does not exist yet, so the effect cannot be computed; fake entries would also need real Seeker tokens and signatures
Hostchange prize, winners or closing timelocked when the raffle opens
Farmersenter many timesSeeker raffle: one Genesis Token per Seeker, one entry per token. Open raffle: every wallet must hold what the host asked for, so each extra entry costs that much

Check it yourself

Every raffle publishes a proof at https://draw.2cstudio.app/raffles/<id>/proof, and its page (https://draw.2cstudio.app/r/<id>) re-runs the draw in your browser at the press of a button. To check without trusting the page, with Node.js 20+:

curl -O https://draw.2cstudio.app/verify.mjs
node verify.mjs https://draw.2cstudio.app/raffles/12/proof --rpc https://api.mainnet-beta.solana.com

The script has no dependencies and checks the seed, every entry's signature, the entry list, the deciding block (against Solana, with --rpc) and the winners. The browser check runs the same code. The proof also lists the entries removed at closing and why.

Check a draw now: open any result page and press "Re-run the draw in this browser", or run the standalone script:

curl -O https://draw.2cstudio.app/verify.mjs
node verify.mjs https://draw.2cstudio.app/raffles/12/proof --rpc https://api.mainnet-beta.solana.com

The script needs Node.js 20+ and nothing else. The browser check runs the same code.