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/v2 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

ClaimSponsorRefund

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

ClaimDeadWeek pays the caller their pro-rata share of the sponsorship that a DEAD week's draw never got to pay out, and returns the amount. PERMISSIONLESS, self-service only — a caller can only ever claim their own share — and it is the EXIT for the claim term retireSponsorship creates. AUDIT-2026-09-28.md finding 31.

WHY IT IS A NEW ENTRY POINT AT ALL, WHICH IS THE ONE QUESTION WORTH ANSWERING HERE. The cheaper design is to fold each holder's share into their spendOf and let the existing, already-audited Refund be the exit — no new function, no new term, no new tree. THAT DOES NOT WORK, AND THE ARITHMETIC IS WHY RATHER THAN A JUDGEMENT CALL:

Example
 1Refund re-derives the house fee it must give back as
 2    cutPart = floor(gross * HouseCutBps / bpsDenom)
 3and decrements d.Cut by it. That derivation is EXACT — see cutPerTicket's build guard —
 4only because a spendOf is always a whole number of ticket prices, so its floor is
 5precisely the sum of the cuts those purchases booked.
 6
 7A sponsorship share booked NO CUT. Add a share s to a buyer whose gross was
 8g = n * TicketPrice and Refund derives floor((g+s)/20) = n*500_000 + floor(s/20), while
 9d.Cut only ever received n*500_000. d.Cut goes NEGATIVE by floor(s/20) and Refund's own
10"refund would overdraw the week's escrowed fee" panic fires — so the buyer cannot
11reclaim their own ticket money at all. Worse in the pure-earned case: a holder with no
12purchase gets gross s against a week whose Cut is 0, and floor(s/20) is stolen from
13nothing. THE FIX FOR A FUND-STRANDING DEFECT WOULD HAVE CREATED ONE.
14
15The identity itself survives — Refund removes `amount` from both sides whatever the
16rounding — which is exactly what makes this dangerous: checkInvariant and AuditFailure
17stay green right up to the panic. It is the Cut term, not the identity, that breaks.

SO THE CLAIM IS A SEPARATE TERM WITH ITS OWN EXIT, AND IT CARRIES NO FEE BY CONSTRUCTION: sponsorship never paid a house cut on the way in and this pays none on the way out. The term appears in checkInvariant (heldClaims) and in AuditFailure (per week, and in the aggregate, and re-derived against claimedOf by deadWeekClaimFailure), which is the price of not reusing Refund and is paid in full.

IT HAS NO CLOCK, NO BEACON READ AND NO REFUSABLE DEPENDENCY, deliberately. By the time a claim exists the week is already abandoned — every gate that had to be waited out (Refund's grace window, or SweepDrawlessWeek's) was waited out before retireSponsorship ran — so a second wait would only be a second thing that could go wrong on a payout path. Compare ReclaimStrandedCarry, which this replaces: that needed a beacon test AND a three-week long stop because it was trying to find a destination. There is nothing to find.

IT IS O(log N) AND ALL-OR-NOTHING PER HOLDER. The share is derived from the rate and the holder's unit weight at call time rather than stored per holder at abandonment, which is what keeps the abandonment transition constant-time (read retireSponsorship). The weight cannot have changed since: an abandoned week is always a past week, and Buy and FundPotWithEntries only ever touch currentWeek(). One claimedOf entry therefore settles a holder for ever, and a second call finds it and refuses.

A REFUNDED BUYER MAY STILL CLAIM, AND THAT IS CORRECT, NOT A LOOPHOLE. Refund returns the gross they paid for their tickets; this returns their share of money that was never theirs to pay and never anybody's to win. The two are different money with different owners, and the week's identity keeps them apart — spendOf on one side, Claims on the other. A holder with earned entries and no purchase has a claim here and nothing to refund, which is the whole point of finding 15's players being paid.

BOOKS FIRST, TRANSFER SECOND, as in Resolve, Refund and WithdrawOps.

Panics if there is no record for the week, if that week never distributed anything, if the caller holds no units in it, or if the caller has already claimed. ClaimSponsorRefund returns a sponsor's payment from a week that died without drawing. It pays the GROSS — the house's 5% included — because the fee was only ever earned by a draw actually running, and this draw never ran.

THE OWNER'S RULING, 2026-09-30: a sponsor's money is "either returned to sponsor or rolls over until a draw happens". Rolling forward is what AUDIT-2026-09-28.md findings 28 and 31 removed — the caller chose which week it landed in by choosing when to call, and could aim it at a draw they already dominated. So it is returned, and it is PULLED rather than pushed: paying up to maxSponsorsPerWeek addresses inside retireSponsorship would put those transfers on the critical path of a buyer's own Refund, where one failing recipient could block the week.

IT DOES NOT COMPETE WITH ClaimDeadWeek. That pays earned-entry holders out of the duel's OWNERLESS rake; this pays named sponsors out of money that never entered that pool, because retireSponsorship carves it out first. The two draw on different halves of the same pot and neither can reach the other's.

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/v2" -func "ClaimSponsorRefund" -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/v2" -func "ClaimSponsorRefund" -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