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

Refund

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

Refund returns the caller's own ticket money for a week whose draw can never be resolved. PERMISSIONLESS, and self-service only: a caller can only ever reclaim their own record.

THIS IS THE SAFETY VALVE r/splash/game COULD NOT HAVE. There, the pot was accrued protocol fees, nobody had staked anything, and an unresolved draw could simply wait forever with no one out of pocket — so a void path would have been the only route by which a prize could move without a winner, and was deliberately omitted. Here the pot is the buyers' own money, and without this function a week nobody settles would lose every ticket bought into it.

ITS JUSTIFICATION CHANGED COMPLETELY IN THE DRAND MIGRATION, AND THE OLD ONE IS WRITTEN OUT HERE BECAUSE A READER WILL COME LOOKING FOR IT. It used to be: "the committed beacon round was published and PRUNED with nobody calling Capture, so no winner can ever be named". That was a claim about a beacon that forgot things. r/splash/beacon does not forget, drand's API serves every round for ever, and Publish is permissionless — so no draw is ever unresolvable, and the precondition this function was built on no longer exists (see resolvable()'s removal, above).

WHAT IT IS FOR NOW IS LIVENESS, NOT LOSS. A week's pot must not be hostage to whether anybody spends ~17.0M gas relaying one drand round. That is the only failure left, and it is unbounded in time, so the exit is a CLOCK: refunds open refundGraceBlocks (21 days) after Commit and not before.

THE PRECONDITION IS THEREFORE NO LONGER THE COMPLEMENT OF Resolve's — the two now OVERLAP for ever once the window opens, and that is fine and is handled by Abandoned. There is no state in which Resolve is impossible, so a refund gate that waited for one would never open at all.

AND THE EXIT IS ONE-WAY, WHICHEVER WAY IT GOES. Refund and Resolve are permanently mutually exclusive per week: the first refund that pays out sets Abandoned, after which Resolve and Capture refuse that week forever and every later refund proceeds without consulting the beacon again. THE ONE-WAY EXIT MATTERS MORE NOW, NOT LESS: it used to protect against a round being published late or re-published after a prune, and it now carries the whole burden, because a pinned round can be relayed by anyone at any time for ever — so a week in refund would otherwise be re-resolvable indefinitely. Without it, a week could take refunds from some buyers and then be resolved over the original frozen ticket set, paying the remaining pot — those other buyers' money — to a winner who may already have been refunded.

AND THE ESCROWED FEE FOLLOWS THE SAME PRINCIPLE AS THE SPONSORSHIP, BY A DIFFERENT ROUTE — BUT THE SPONSORSHIP DOES NOT ROLL ANYWHERE AND HAS NOT SINCE 2026-09-29. The retired sentence, ~240 lines above a transition that does nothing of the kind:

Example
1"An abandoned week's Sponsored ROLLS FORWARD TO THE LIVE WEEK (finding 16), because it
2belongs to no buyer and there is nobody to hand it back to."

BOTH HALVES WERE WRONG BY THE TIME IT WAS WRITTEN. Rolling forward to the live week was AUDIT-2026-09-28.md finding 28 — the live week is whichever week the permissionless caller chose to fire in, so the caller picked the destination by picking the moment — and the premise "there is nobody to hand it back to" is false whenever a duel was played in the week: THE EARNED-ENTRY HOLDERS ARE EXACTLY THE PEOPLE WHOSE RAKE IT IS (findings 31 and 34). The sponsorship becomes a per-unit claim on THIS week, payable to the holders of its EARNED entries, and it never changes weeks.

SO THE TWO AMOUNTS NOW ANSWER THE SAME QUESTION THE SAME WAY, AND THE PARALLEL IS TIGHTER THAN IT WAS. An abandoned week's Cut does not roll anywhere: it belongs to specific buyers, so it goes BACK to them, one gross refund at a time, and the abandonment transition below deliberately does not touch d.Cut at all. The sponsorship likewise goes back to the people it came from, in place. We did not run a draw, so we keep neither — and the only money that reaches the house is a sponsorship with no player behind it at all.

A BUYER WHO NEVER CALLS THIS LEAVES THEIR OWN GROSS SITTING IN THE WEEK, fee part included, and that is not a new stranded-money path: there is no deadline on a refund, Abandoned is one-way so nothing else can take it, and the only address that can ever reclaim it is theirs. It is the same "unclaimed, not unreachable" state the prize half has always had.

AN EARNED ENTRY RECLAIMS NOTHING *HERE*, AND THAT IS CORRECT RATHER THAN A GAP — BUT IT RECLAIMS EVERYTHING THROUGH ClaimDeadWeek. Finding 15 lets duel play earn ticket weight with no spendOf record (FundPotWithEntries), and this function pays spendOf and nothing else — so a holder of only earned entries is refused with "nothing to reclaim for that week". They paid this realm no ticket money to get back: what they paid was the duel's rake, and the abandonment transition below turns that rake into THEIR claim on this same week (AUDIT-2026-09-28.md findings 31 and 34).

THE TWO SENTENCES THAT FOLLOWED WERE BOTH FALSE AND ARE KEPT BECAUSE EACH WAS LOAD-BEARING FOR A DIFFERENT READER:

Example
1"A week whose entries are ALL earned therefore refunds nobody and MOVES ITS WHOLE POT TO
2opsPot ON THE FIRST REFUND ATTEMPT — except that there can be no first refund attempt,
3because no caller has a spend record, so such a week simply stays unrefundable and
4RESOLVE REMAINS ITS ONLY EXIT."

The first clause contradicted itself in its own sentence (a refund attempt that cannot happen cannot move anything) and named a destination that is deleted. The second was true when it was written and was FIXED BY finding 17 in the same audit: a pure-earned week has a SECOND exit, SweepDrawlessWeek's shape 2, gated on the same grace window this function uses, and that exit retires the sponsorship to the earned holders. Leaving "Resolve remains its only exit" standing is how a reader concludes that the stranding finding 17 closed is still open — or, worse, re-derives finding 17 as a new idea and implements it a second time.

WHAT IS TRUE: such a week refunds nobody through this function, its draw is Resolve, and if no draw happens for the whole grace window then SweepDrawlessWeek marks it abandoned and its sponsorship becomes its earned holders' claim. It strands nothing by either route.

EACH BUYER RECLAIMS EXACTLY THEIR OWN RECORDED SPEND, ONCE. The record is zeroed before the transfer, so a second call finds nothing and panics; and because Pot == sum(spends) + Sponsored for an unresolved week — with the sponsorship term taken out of the pot the moment the week is abandoned, see below — the refunds can never total more than the pot holds.

SPONSORSHIP IS NOT REFUNDABLE AND IS NOT LEFT BEHIND. House money added by FundPot (the duel's rake, for one) belongs to no buyer, so nobody can claim it here; the first refund that pays out ROLLS IT FORWARD INTO THE LIVE WEEK'S POT — exactly as SweepDrawlessWeek does for a week nobody entered — instead of leaving it stranded in a pot whose every claim has been zeroed. It went to opsPot until 2026-09-29, and the two paths disagreeing about the same money was AUDIT-2026-09-28.md finding 16. The code and the reasoning are at the transition below.

THE HOUSE FEE *IS* REFUNDED, SINCE 2026-09-29, AND THE PARAGRAPH THAT USED TO BE HERE SAID THE OPPOSITE. It read: "What a buyer gets back is their post-cut contribution — the part that was ever in the prize pot — not the gross ticket price. The 5% was taken at sale time and is already in the ops pot; clawing it back would mean the ops pot could go negative, or that this realm had to hold every cut in escrow for a week against the small chance of a pruned round." IT IS KEPT VERBATIM BECAUSE IT NAMES THE COST OF THE FIX AND THE FIX PAID IT: the realm now DOES hold every fee in escrow per week (Draw.Cut), which is the second option that sentence dismissed, and the ops pot cannot go negative because nothing credits it until a draw succeeds.

WHAT A BUYER GETS BACK IS THEREFORE THE WHOLE TICKET PRICE. The owner's rule is that WE EARN WHEN THE GAME RUNS, NOT WHEN IT FAILS, and a refunded week is a week that did not run: the fee was a charge for a draw that never happened. This is a REDUCTION IN HOUSE REVENUE in the failure case, which needs no permission and is the safe direction — the same shape as finding 16 sending an abandoned week's sponsorship back to players rather than to the house.

THE SPLIT IS IN THE BODY, and it is where the only rounding risk in the change lives; read it there. AUDIT-2026-09-28.md finding 25.

A WEEK THAT WAS NEVER COMMITTED IS NOT REFUNDABLE HERE, and needs no separate path — but only because Commit TAKES THE WEEK AS AN ARGUMENT and any week that is over can be committed by anyone at any time. So a buyer holding a stranded week's tickets names that week to Commit themselves, and then either Resolves it (the normal outcome) or refunds it here if its round was lost. Before Commit took that argument this paragraph was simply false: a caller could not name the week, Commit could only act on the live one, and a week the clock had passed was unreachable by every path in this file — committed never, resolved never, refunded never, and counted in heldPots forever.

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 "Refund" -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 "Refund" -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