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
- When the raffle is created the server makes a secret 32-byte
seedand publishesseedCommit = SHA-256(seed). The prize, number of winners, closing time and entry conditions are locked. - 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. - 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 announcesdrawSlot = current finalized slot + 150, a Solana block about a minute in the future that nobody can know yet. - 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;entriesHashfrom hex,blockhashfrom base58.- Random numbers: block
i(i = 0, 1, 2 …) of the stream isSHA-256(drawSeed || i),ias a 4-byte big-endian integer; each block is read as eight unsigned 32-bit big-endian integers.nextInt(n)takes the next valuev, skips it ifv >= 2^32 - (2^32 mod n), and returnsv mod n. - Winners: for
i = 0 … k-1swap positioniof the sorted list with positioni + nextInt(n - i); the firstkwallets are the winners in rank order (k= number of prizes, or everyone if fewer entered). - The
seedis 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), andentriesHashis the SHA-256 of those lines joined with\n. - Winners: for each rank,
r = nextInt(remaining)whereremainingis 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 exceedsrwins, 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
| Who | Tries to | Why it fails |
|---|---|---|
| ProofDraw | swap the seed after seeing entries | seedCommit was public from the start |
| ProofDraw | add fake entries just before closing | the deciding block does not exist yet, so the effect cannot be computed; fake entries would also need real Seeker tokens and signatures |
| Host | change prize, winners or closing time | locked when the raffle opens |
| Farmers | enter many times | Seeker 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.