Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

raffle package

Overview

Package raffle is the SPLASH weekly ticket raffle.

WHAT THIS REPLACES, AND WHY IT IS A SEPARATE REALM.

r/splash/game is a permanent-deposit yield product: you deposit GNOT, you can never withdraw, your position size IS your ticket count, and winning the weekly draw ADDS the pot to your locked position rather than paying your wallet. Its raffle had to live inside that realm because the draw mutated three pieces of that realm's state atomically (see the long comment at the top of r/splash/game/raffle.gno).

This realm sells TICKETS, not positions. A ticket costs TicketPrice and buys exactly unitsPerTicket units of one week's draw — one ticket was one unit until 2026-09-29, and EntryUnitPrice says why the two numbers had to come apart; the pot pays out to the winner's WALLET; and nothing a buyer paid is held beyond the week they bought into. Because there is no position, no yield index and no permanent deposit, the draw needs nothing from r/splash/game and this realm holds no authority over it, over r/splash/beacon, or over r/gnoland/wugnot.

THERE IS EXACTLY ONE PACKAGE-PATH GUARD IN THIS FILE, AND UNTIL 2026-09-29 THERE WERE NONE. The sentence that used to be here — "there is no package-path guard anywhere in this file and nothing for one to protect" — is no longer true and is not quietly deleted. FundPotWithEntries admits r/splash/arcade/duel and nobody else, because duel play now EARNS entries in the live week's draw (AUDIT-2026-09-28.md finding 15). Read that function in full before touching it: it is the only privileged door in this product, it is the shape r/splash/game declined to add five times, and what keeps it small is that everything it can do is bounded by the money delivered through it in the same call. Every OTHER entry point here is still permissionless and can still only do the one thing the week's state machine allows next.

THE MECHANICS ARE r/splash/game/raffle.gno's, DELIBERATELY UNCHANGED: the week number derived from the global clock, the Commit/Capture/Resolve three-phase cycle, and above all the FROZEN-TOTAL property. Read Commit below before changing anything: freezing the ticket total is what stops a buyer from waiting for the randomness to publish and then buying exactly the winning ticket index, and that attack was live in an earlier version of the old realm.

ONE MECHANIC IS NOT THE OLD REALM'S ANY MORE: WHICH ROUND DECIDES A WEEK. The old realm — and this one until 2026-09-28 — bound Round = beacon.Latest() + 1 inside Commit, which handed the round choice to whoever called Commit, AFTER the week's ticket set was already frozen and public. The publisher's key can sign any round at will and beacon.Publish accepts rounds in any order, so that was a deterministic, risk-free win for the key holder. The round is now PINNED WHEN THE WEEK'S RECORD IS CREATED — before the first ticket of that week exists — and is never written again. See getOrCreateDraw and pinOffsetRounds.

THAT DOES NOT MAKE THE DRAW TRUSTLESS, and nothing in this file should be read as claiming it does. The beacon's key holder still knows every round's randomness in advance, so they can still rig a week by choosing the MODULUS — buying last, just before the week closes, to move FrozenTotal and the unit ranges. Pinning removes the round-selection freedom, which was the cheap, deniable and exact version of the attack; the residual one leaves an on-chain trace as a suspicious late purchase. THE ARCHITECTURAL PROBLEM REMAINS OPEN and is the owner's decision: closing it needs an externally-controlled beacon or a VRF. See AUDIT-2026-09-28.md findings 4 and 10.

THE ARCADE FEEDS THIS POT, AND SINCE 2026-09-29 IT ALSO FILLS THE DRAW. r/splash/arcade/duel sends its rake here, so every coin flip makes this week's prize bigger — that is what makes the duel and the raffle one product instead of two contracts. The rake itself still buys no tickets and no winning chance: it is tracked separately as Draw.Sponsored, because the refund safety property is the identity Pot == sum(spends) + Sponsored and house money belongs to no buyer. Read Draw.Sponsored and FundPot together before changing either.

WHAT CHANGED IS THAT THE PLAYERS WHO PAID THAT RAKE NOW ARRIVE WITH IT. Until 2026-09-29 the money came and the entrants did not, so a week of busy coin flipping and no ticket sales could be won outright by one cheap ticket bought in its final block — the whole pot, to somebody who had contributed none of it. FundPotWithEntries is the fix: the duel delivers the same rake and, in the same call, the two players of each settled duel are credited entries worth EXACTLY what that rake would have bought at EntryUnitPrice. The money and the entrants now arrive together. Earned entries carry ticket weight and NO spendOf record, which is consistent with every identity in this file and is spelled out at FundPotWithEntries. AUDIT-2026-09-28.md finding 15.

WHAT IS NEW, and could not have existed in the old design: Refund. There, the pot was accrued protocol fees and nobody's stake was ever at risk, so an unresolvable draw could simply wait forever. Here the pot is buyers' own money, so a week nobody settles must have a way back out. SINCE THE DRAND MIGRATION THAT IS A LIVENESS EXIT AND NOT A LOSS EXIT — no round is ever unobtainable, so what Refund guards against is nobody bothering to relay one. See Refund.

Function

Commit

func Commit(cur realm, week int64) int64
Ask AI Claude ChatGPT

Commit commits an ALREADY-FINISHED week to a future beacon round. Returns that round.

THE WEEK IS A PARAMETER AND IT MUST BE OVER — week < currentWeek(). Both halves of that sentence are load-bearing, and each fixes a defect the first version of this function had:

  • IT TAKES A WEEK, so any past week can be committed at any time, by anyone. Before, Commit could only ever act on currentWeek(), which meant a week whose clock ran out before anybody called it could never be committed — and because Resolve and Refund both require Committed, that week's whole pot was unreachable forever while heldPots went on counting it, so the invariant and the audit both stayed green over money nobody could ever get back. A stranded week is now always recoverable: commit it, then resolve it, or refund it if its round was lost.

  • THE LIVE WEEK CAN NEVER BE COMMITTED, so a commit can no longer cut a week's sales short. Before, anyone could Buy(1) and immediately Commit: that made them the sole owner of a FrozenTotal of 1 — the guaranteed winner — and, because Buy refuses a committed week, shut the week's sales for its remaining days. It cost the attacker the 5% cut on one ticket and was repeatable every week with no admin and no pause to stop it; the front-running variant landed a commit just ahead of somebody's large Buy and made their purchase panic. Refusing week >= currentWeek() closes all of that at the root rather than with a minimum-tickets or minimum-elapsed heuristic: the week being sold into is simply not a committable week.

PERMISSIONLESS: there is no keeper and no admin. Anyone who wants the draw to happen — every ticket holder, for a start — can call it, once the week is up.

It freezes two numbers, and CHOOSES NOTHING:

  • FrozenTotal = the week's final ticket total. The week is over and Buy only ever sells into currentWeek(), so this total can no longer grow BY CONSTRUCTION; it is still assigned explicitly, because Resolve reads this field and not the live total.
  • Pot, which stops growing for the same reason (and Buy also refuses a committed week, which is now a second, unreachable-by-design lock).
  • AND IT USES Draw.Round EXACTLY AS PINNED, which was written when this week's record was created and is not this function's business. See below.

THE SECURITY RULE, AND THE ONLY REASON THIS IS THREE PHASES: the ticket set must be frozen BEFORE the randomness is knowable. If the total were read at resolve time, anybody who saw the published round could compute the winning unit and then buy precisely enough tickets to own it — which turns a raffle into a purchase.

THIS FUNCTION NO LONGER BINDS THE ROUND, AND THAT IS THE 2026-09-28 FIX. It used to set Round = beacon.Latest() + 1 here, which looked like the conservative choice — the round is unpublished at commit time, so its randomness "does not exist yet" — and was in fact the operator's cheapest attack on the product: the beacon's key holder knows every round's randomness in advance, and Commit ran AFTER the week's ticket set was frozen and public, so they could enumerate candidate rounds, Publish until Latest()+1 named them, and commit. The round is now pinned at record creation instead; read pinOffsetRounds for the full account, including what remains open.

THERE IS DELIBERATELY NO "THE ROUND MUST STILL BE IN THE FUTURE" CHECK HERE, and the old `round <= latest` wrap check is gone with it. A reader who expects one has the fix backwards. A week is committed at least a week after its round was pinned, so by now the pinned round MAY LEGITIMATELY ALREADY BE PUBLISHED — that is the ordinary case for a week whose record opened early, and it is harmless precisely because nobody chose it: the round was fixed before a single ticket of that week had been sold. Requiring it to be unpublished would refuse the honest case and, worse, would reintroduce round selection through the back door, since the only way to satisfy such a check would be to recompute the round here. The wrap case the old check guarded is now handled where the arithmetic happens, at maxPinnableLatest.

WHY FREEZING THE TOTAL ALSO FREEZES WHO OWNS UNIT k — the part the old realm got wrong first time, and the reason Buy appends:

  • Every purchase APPENDS A NEW ticket index (appendTickets). No index's value is ever increased afterwards, and fenwick.Add is not called from this realm at all.
  • An appended index therefore starts at a prefix equal to the whole total at the moment it was appended, so anything appended after Commit lies at or beyond FrozenTotal — outside the frozen range by construction, not by convention.
  • And because no earlier index's value changes, no earlier prefix moves: the owner of every unit below FrozenTotal is fixed from the instant that unit was sold.

WITH ONE INDEX PER BUYER INSTEAD, a further purchase would grow that buyer's existing value, widening their range IN PLACE, from its start, inside the frozen range, and pushing every later buyer along. The earliest buyer (prefix 0 forever) could then wait for the round to publish, compute k = randomness mod FrozenTotal, buy until their own value exceeded k, and call Resolve to take the pot: a free option on the whole pot. That exploit was real in r/splash/game and its fix is what this realm inherits. Buy's refusal after commit closes the same door a second time.

A WEEK'S TICKET TOTAL MAY BE ENTIRELY EARNED AND THIS FUNCTION DOES NOT CARE. Since finding 15 a week can reach this point with tickets and NO spend records at all — every entry credited to a duel player through FundPotWithEntries, not one ticket bought. total > 0 is satisfied, the week commits, and Resolve pays the winner a pot made purely of the rake those players paid. That is the intent, and it is why the zero-ticket refusal below is a check on the TICKET INDEX and not on the spend records: a drawless week is one nobody entered, not one nobody paid this realm directly.

Panics if the week is not over yet, if it is already committed, or if it has no tickets at all (there is NO ROLLOVER, so a drawless week must never be allowed to exist). It no longer reads the beacon at all: the "no round has ever been published" refusal moved to the pin, which is to say to Buy and FundPot, where a sale is refused rather than accepted into a week that could never be drawn.

Param

Command

# WARNING: This command is running in an INSECURE mode.
# It is strongly recommended to use a hardware device for signing
# and avoid trusting any computer connected to the internet,
# as your private keys could be exposed.

gnokey maketx call -pkgpath "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/arcade/raffle" -func "Commit" -args $'' -gas-fee 1000000ugnot -gas-wanted 1_000_000_000 -send "" -chainid "onyx-1" -remote "https://rpc.onyx.testnets.gno.land" ADDRESSgnokey query -remote "https://rpc.onyx.testnets.gno.land" auth/accounts/ADDRESS
gnokey maketx call -pkgpath "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/arcade/raffle" -func "Commit" -args $'' -gas-fee 1000000ugnot -gas-wanted 1_000_000_000 -send "" -broadcast=false ADDRESS > call.tx
gnokey sign -tx-path call.tx -chainid "onyx-1" -account-number ACCOUNTNUMBER -account-sequence SEQUENCENUMBER ADDRESS
gnokey broadcast -remote "https://rpc.onyx.testnets.gno.land" call.tx