Buy
Ask AI
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.
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 "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" -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