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

Buy

func Buy(cur realm, tickets int64)
Ask AI Claude ChatGPT

Buy sells `tickets` tickets in the CURRENT week to the caller.

Cost is tickets * TicketPrice, pulled from the caller with the GRC20 allowance path, so the caller wraps native ugnot with wugnot.Deposit and approves this realm first. HouseCutBps of the cost is ESCROWED AGAINST THIS WEEK in Draw.Cut; the remainder — and all integer-division dust, since the cut is floored — joins this week's prize pot.

IT SAID "goes to the ops pot" UNTIL NOW AND THAT HAS BEEN FALSE SINCE 2026-09-29. A sale banks nothing for the house: the fee is unearned until the week actually draws, Resolve is what moves it to opsPot, and a week that fails hands it back as part of each buyer's gross refund. The distinction is the whole of finding 25 — "we earn when the game runs, not when it fails" — and a comment that says the money is already the house's describes a realm that cannot refund in full. AUDIT-2026-09-28.md finding 29.

EVERY CHECK RUNS BEFORE THE PULL, and the week record is created only after it, so a failed Buy moves nothing and creates nothing.

Each purchase APPENDS its own ticket index rather than growing an existing one. That is not a style choice: it is the property Commit depends on, and the reason is spelled out there.

THE WEEK'S FIRST BUY PINS THE WEEK'S BEACON ROUND, as a side effect of creating the record (getOrCreateDraw). That is deliberate and is the only moment the round is ever chosen: read pinOffsetRounds. Two consequences a caller can see. First, a sale needs the beacon to have published at least one round ever, or it is refused with the beacon's own "no round published yet" — which is a refused sale rather than money accepted into a week that could never be drawn. Second, the buyer who opens a week does not get to pick anything useful: the round is fixed a week and more ahead of the moment the ticket set closes, so at the instant it is pinned there is no ticket set for it to be advantageous against.

Panics if tickets is not positive, if the cost overflows int64, if the allowance is short, or if THIS WEEK IS ALREADY COMMITTED — the last of which is the frozen-total rule, not a convenience check. Once a week's total is frozen, no further ticket may join it.

THAT LAST REFUSAL IS NOW UNREACHABLE, and is kept deliberately. Commit only accepts a week that is already over, and Buy only ever sells into currentWeek(), so the live week cannot be in a committed state — which is exactly why a commit can no longer shut a week's sales down early. The check stays as a second lock on the frozen-total rule: if the week's identity ever stops being "the one the clock is on", this is the line that still refuses to grow a frozen ticket set.

ONLY A USER MAY BUY, AND THE REASON IS THE PAYOUT, NOT THE PURCHASE. A ticket is a claim on two transfers this realm will later push OUT: Resolve's whole pot to the winner's address, and Refund's spend back to the buyer's. Both go to cur.Previous().Address(), which for a realm caller is that realm's package-derived address — an address whose only "key" is its package path, so nothing can move wugnot out of it unless that realm happens to contain code that does. A realm with no such code that won a draw would have the entire week's pot transferred into an address it can never spend from: money that has left this realm's books (heldPots drops, the invariant stays green) and is unreachable by anybody, for ever. This realm cannot tell which realms those are and must not try, so it refuses them all at the door instead of stranding a pot at settlement time.

cur.Previous().IsUserCall() IS THE CHECK, COPIED FROM r/splash/exchange:378 — AND IsUser() IS NOT. IsUser() also accepts a `maketx run` ephemeral realm, which is code executing under a user's signature and NOT an EOA: its address is still package-derived, so it can win a pot and then cease to exist with the money in it. The exchange states the same distinction for the same word, in the context of a send envelope rather than a payout; the failure mode differs but the property being asked for — "the thing in front of me is a wallet somebody holds a private key for" — is identical.

FundPot IS DELIBERATELY NOT GUARDED THIS WAY, AND MUST NOT BE "MADE CONSISTENT". r/splash/arcade/duel calls it from realm code — from FlushPot, once per batch of collected duels; it was from Collect itself until AUDIT-2026-09-28.md finding 14 — so adding this check there would break the arcade's rake, the thing that makes the duel and the raffle one product. It is safe there precisely because sponsorship buys nothing: no ticket unit, no winning chance, no refundable claim (see FundPot and Draw.Sponsored), so a realm that funds a pot can never be the address a payout is pushed to, and the defect above has no way to arise. The asymmetry is the point: the guard belongs on the entry point that creates a claim on an outbound transfer, not on the one that only ever pays in.

AND FundPotWithEntries DOES CREATE SUCH A CLAIM, WHICH IS WHY IT IS THE ONE GUARDED FUNCTION IN THIS FILE — WITH A DIFFERENT GUARD, FOR A DIFFERENT REASON. It appends ticket weight for addresses the CALLER names, so this finding's defect would arise there twice over if it were open: any caller could credit a realm address that can never spend a pot, and could do it for somebody else's benefit. IsUserCall cannot be the check there, because the only legitimate caller IS a realm. So that function admits exactly one package path, and the finding-8 property — every holder of an earned entry is an address a payout can actually reach — is enforced where it is knowable: r/splash/arcade/duel records whether each party's own Post or Accept was a direct user call and credits only those. A REALM CANNOT PLAY A DUEL AT ALL SINCE 2026-09-30, and the sentence that stood here — "a realm may still play duels; it simply earns no raffle entries by doing so" — is corrected rather than deleted because the reason it stopped being true is this finding travelling one realm further. r/splash/arcade/duel found that leaving SETTLEMENT open to realms had the same defect in its own escrow: its Collect is winner-only, so a winning realm with no code calling it stranded both stakes with no exit. Post and Accept there now require a direct user call. The duel still records each party's flag and still credits entries only to eligible parties — nothing this realm relies on has weakened, and the property below is now enforced twice over. AUDIT-duel-2026-09-27.md addendum 2, finding D-1.

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/v3" -func "Buy" -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/v3" -func "Buy" -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