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.

speed package

Overview

Package speed is the SPLASH HOURLY ticket draw: a draw every hour instead of every week.

WHY IT IS A SEPARATE REALM AND NOT A PARAMETER ON r/splash/arcade/raffle. The weekly raffle's cadence is woven into eight constants, four build guards and the refund grace window, and every one of them had to be re-derived from first principles here (see "the two clocks" below). A shared realm with a cadence dial would have had to satisfy BOTH sets of constraints simultaneously, which is how a margin gets shaved: the hourly window's pin offset is 2400 rounds and the weekly one's is 230400, and no single guard can assert both. Two realms, each with its own derivation and its own asserted margins, is the honest shape. The mechanics below are the raffle's, deliberately: the ticket index, the Commit/Capture/Resolve three-phase cycle, the FROZEN-TOTAL property, the one-way refund exit, and the pin-at-record-creation rule. Read r/splash/arcade/raffle/AUDIT-2026-09-28.md before changing any of them; this file's job was to re-scale the timing without re-introducing one of its fourteen findings.

========================================================================== V1 IS SELF-CONTAINED: THERE IS NO SPONSORSHIP AND THERE IS NO FundPot. ==========================================================================

The pot is ENTRY MONEY ONLY. Nothing outside this realm can put a coin into a window's pot, and this realm calls no other realm's code except r/gnoland/wugnot (to move money) and r/splash/beacon (to read a round). There is no cross-realm feed in either direction, on purpose, in both directions:

  • NOTHING FEEDS THIS POT. r/splash/arcade/duel's rake goes to the weekly raffle and stays there. Adding a second consumer of that rake would make the duel's settlement depend on two realms' bookkeeping instead of one, and the duel's Collect depending on the raffle's bookkeeping was AUDIT-2026-09-28.md finding 14 — the worst liveness defect of the day, in which any raffle panic trapped a resolved duel's whole pool in both realms. It was severed that evening (Collect books, FlushPot delivers) and this realm does not reinstate the shape.
  • THIS REALM FEEDS NOTHING. No rake, no share, no callback. Its only outbound transfers are wugnot to a winner's wallet, wugnot to a refunding buyer's wallet, and wugnot to opsVaultAddr.

AND THE PROPERTY THAT MAKES THE ABSENCE OF SPONSORSHIP A SECURITY FEATURE RATHER THAN A MISSING ONE — THE SOLE-ENTRANT PROPERTY:

Example
 1WITH NO HOUSE MONEY IN THE POT, A SOLE ENTRANT MERELY WINS BACK THEIR OWN STAKE
 2MINUS THE HOUSE CUT. The weekly raffle still has the sole-entrant attack, and it
 3is a real one there: the duel's rake is sponsored into the live week's pot, so
 4somebody who buys one ticket into a quiet week with a large sponsored balance
 5collects the sponsorship for the price of the fee on that ticket. Here Pot is
 6exactly sum(spendOf) by construction, so a sole entrant's pot IS their own
 7post-cut spend: they pay 100,000 ugnot, and if they are the only entrant they win
 895,000 back. THERE IS NO POSITIVE-EXPECTATION SOLO PLAY, so the attack cannot
 9exist — not mitigated, not made expensive, ABSENT, as an arithmetic consequence
10of the pot's definition.
11
12(A SOLE ENTRANT IS NO LONGER INDIFFERENT BETWEEN Resolve AND Refund, AND THIS
13PARAGRAPH SAID THEY WERE. It read: "The same arithmetic makes a sole entrant
14indifferent between Resolve and Refund: both return exactly their own 95,000."
15Since 2026-09-29 the fee is ESCROWED per window and earned only by a draw
16(AUDIT-2026-09-28.md finding 25), so a refund returns the whole 100,000 and a
17resolve returns 95,000: THE SOLE ENTRANT STRICTLY PREFERS THE DRAW TO FAIL, by
18exactly the fee. The conclusion this paragraph was supporting is unaffected —
19solo play is still never positive-expectation, which is what kills the
20sole-entrant attack — but the incentive it describes is now the general one the
21owner is weighing: with a refund at 100% of stake, every non-winner prefers a
22failed window to a drawn one. That is a product decision and is deliberately NOT
23fixed here; it is recorded in AUDIT-2026-09-28.md so that the next reader of this
24paragraph does not mistake it for a property of this realm's design. The refund
25grace window below is therefore not only a multi-entrant concern any more.)

SO DO NOT ADD FundPot, A SPONSORED FIELD, A ROLLOVER OR A JACKPOT WITHOUT RE-AUDITING THIS PARAGRAPH. Every one of them puts money in the pot that belongs to no entrant, which (a) resurrects the sole-entrant attack, (b) turns the invariant Pot == sum(spendOf) into Pot == sum(spendOf) + Sponsored with everything that follows from it (see the raffle's Draw.Sponsored, Refund's sponsorship transition and SweepDrawlessWeek — three separate mechanisms that exist only because house money belongs to nobody), and (c) makes this realm's liveness someone else's problem again.

========================================================================== THE ONE-HOUR CADENCE, AND THE TWO CLOCKS ==========================================================================

Every duration in the weekly raffle shrinks by roughly 168x here, and THE FAILURE MODE OF GETTING ONE WRONG IS SILENT — it is not a panic, it is a draw whose winning randomness was public on api.drand.sh before entries closed (finding 10). So each constant below is derived in place, and each is asserted by a build guard that obeys the doctrine that finding 11 cost three attempts to learn:

Example
1A GUARD THAT PROTECTS A REAL-WORLD DURATION MUST HAVE SECONDS ON BOTH SIDES,
2SOURCED FROM TWO DIFFERENT CLOCKS, OR IT CANNOT FAIL.

The two clocks available to a realm on this toolchain, neither of which it can measure at runtime (there is no block-timestamp getter):

  • DRAND'S PERIOD: beacon.PeriodSeconds = 3 s, EXACT. A round count multiplied by it is real seconds by definition of the beacon.
  • THE CHAIN'S BLOCK TIME: assumed 86400/blocksPerDay = 3.29998 s, written as the integer 33/10 in guards because 3.3 is not an integer constant. MEASURED ON LIVE gno.land MAINNET ON 2026-09-28: 3.306 s/block over 200,000 blocks and 3.311 s over 20,000. So reality is very slightly SLOWER than this file assumes, by about 0.2%. That is the benign direction for every margin here (a slower chain means the beacon's estimate LAGS drand rather than outrunning it, and lagging is correctable by any relay of drand's head — see speedMaxStaleBlocks), but the guards below are stated against a ±10% band rather than against the measurement, because the measurement is of the past.

Nothing in this realm is a timestamp. A window is a sequence number derived from chain height; no ticket, spend or draw is dated.

Functions

AuditFailure

func AuditFailure() string
Ask AI Claude ChatGPT

AuditFailure re-derives the invariant BY ITERATION and reports the empty string when the books balance, otherwise which check failed and by how much. Never called from a state-changing path — checkInvariant is the O(1) guard for that — and exists so anyone can verify off-chain, and so a test can assert it after every operation.

IT MUST BE ROBUST AGAINST ANYONE WHO CAN SEND THIS REALM TOKENS, which is everyone: wugnot.Transfer needs no permission. So the balance test below is ONE-SIDED — a shortfall is a violation, an unswept surplus is not. See the comment on that check.

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/arcade/speed.AuditFailure()"

Result

AuditInvariant

func AuditInvariant() bool
Ask AI Claude ChatGPT

AuditInvariant is AuditFailure reduced to a bool.

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/arcade/speed.AuditInvariant()"

Result

Buy

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

Buy sells `tickets` tickets in the CURRENT window 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 goes to the ops pot; the remainder — and all integer-division dust, since the cut is floored — joins this window's prize pot and is recorded in the caller's spend for the window.

EVERY CHECK RUNS BEFORE THE PULL, and the window 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 WINDOW'S FIRST BUY PINS THE WINDOW'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. Two consequences a caller can see. First, a sale needs a beacon that has published at least once AND been relayed within speedMaxStaleBlocks, or it is refused with the beacon's own message — a refused sale rather than money accepted into a window that could never be drawn fairly. Second, the buyer who opens a window does not get to pick anything useful: the round is fixed two hours 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 WINDOW IS ALREADY COMMITTED — the last of which is the frozen-total rule, not a convenience check. Once a window's total is frozen, no further ticket may join it.

THAT LAST REFUSAL IS UNREACHABLE THROUGH THE ENTRY POINTS, and is kept deliberately. Commit only accepts a window that is already over, and Buy only ever sells into currentWindow(), so the live window cannot be in a committed state — which is exactly why a commit can no longer shut a window's sales down early (AUDIT-2026-09-28.md finding 2). The check stays as a second lock on the frozen-total rule: if the window'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 (AUDIT-2026-09-28.md finding 8). 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 window'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 — 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. r/splash/exchange states the same distinction about the same word for a different failure mode (a shared send envelope); the property being asked for — "the thing in front of me is a wallet somebody holds a private key for" — is identical.

THERE IS NO SECOND, UNGUARDED ENTRY POINT HERE, and that is a difference from the raffle worth naming: the raffle must leave FundPot realm-callable for the duel's rake, so its asymmetry has to be documented at both ends to stop someone "making it consistent". THIS REALM HAS NO FundPot (see the package comment), so THE GUARD ABOVE IS THE ONLY DOOR MONEY COMES IN BY, and every ugnot in a pot is therefore attributable to a user address that can receive it back.

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/speed" -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/speed" -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
  

Capture

func Capture(cur realm, window int64)
Ask AI Claude ChatGPT

Capture copies a committed window's beacon randomness into the draw.

IT IS A CONVENIENCE AND NOT A SAFETY MECHANISM, and saying so is the point of this comment. In the raffle's pre-drand design its job was "save the randomness before the beacon prunes it"; r/splash/beacon has no retention window and no Prune, drand serves every round for ever from at least three public relays, and Publish is permissionless, so THERE IS NOTHING TO PRESERVE THE VALUE FROM. What it still does:

  • it copies a public value into the draw's own state, so Resolve needs no beacon read later and the settled draw is self-contained;
  • it makes the value visible through CapturedRandomness and the Capture event before the window is settled, which a front end and an auditor can both use.

NOTHING IN THIS REALM'S SAFETY DEPENDS ON ANYONE CALLING IT.

PERMISSIONLESS, deliberately. Its only effect is to store a value that is already public — the round's randomness, readable by anyone the moment it is relayed. Gating it on an operator would buy nothing and would put a liveness dependency for the whole pot onto one key.

WHAT IS STORED IS THE 64-CHARACTER HEX RANDOMNESS — sha256 of the signature, a hash, not the signature itself — and it is NOT re-verified here. The pairing check happened once already inside beacon.Publish on the way in; redoing it would be expensive duplicated work on an input that is already known good, and the beacon has no route by which a published round's randomness can change afterwards.

A SECOND Capture NEVER OVERWRITES THE FIRST, and is a silent no-op rather than a panic: two callers racing to perform a permissionless public good should not make one of them fail.

Capturing before the round is relayed PANICS. There is nothing to preserve yet, and storing anything else — least of all some already-published value — would hand the draw a randomness for a round nobody pinned, which is the exact attack the pin exists to prevent.

COMMIT DOES NOT CAPTURE, AND MUST NOT START. Since the round is pinned at record creation, a window's round may well be relayed by the time it is committed, so a capture there would often succeed — and it would put a beacon read back inside the one function whose job is to choose nothing.

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/speed" -func "Capture" -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/speed" -func "Capture" -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
  

CapturedRandomness

func CapturedRandomness(window int64) string
Ask AI Claude ChatGPT

CapturedRandomness returns what Capture stored for a window, or the empty string if nothing was captured. Kept out of DrawOf so DrawOf's shape stays stable for callers.

Param

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/arcade/speed.CapturedRandomness()"

Result

Commit

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

Commit commits an ALREADY-FINISHED window to its pinned beacon round. Returns that round.

THE WINDOW IS A PARAMETER AND IT MUST BE OVER — window < currentWindow(). Both halves are load-bearing, and each closes a defect the raffle's first version had:

  • IT TAKES A WINDOW, so any past window can be committed at any time, by anyone. A window whose clock ran out before anybody called it would otherwise be unreachable for ever — Resolve and Refund both require Committed — so its whole pot would be lost while heldPots went on counting it and both solvency checks stayed green over money nobody could ever get back. That is AUDIT-2026-09-28.md finding 1, and at an hourly cadence it would happen 24 times a day rather than once a week: MOST WINDOWS OF A QUIET DAY WILL BE COMMITTED LATE BY SOMEBODY WHO WANTS THEIR MONEY, and that path has to work.
  • THE LIVE WINDOW CAN NEVER BE COMMITTED, so a commit can no longer cut a window's sales short. Otherwise anyone could Buy(1) and immediately Commit: that makes them the sole owner of a FrozenTotal of 1 — the guaranteed winner — and, because Buy refuses a committed window, shuts the window's sales for the rest of its hour. That is AUDIT-2026-09-28.md finding 2. Note that the PRIZE half of that attack is already dead here by the sole-entrant property (winning a one-ticket window returns the attacker's own 95,000 ugnot out of the 100,000 they staked), so what remains is pure griefing — and refusing the live window closes it at the root anyway, rather than with a minimum-tickets or minimum-elapsed heuristic. THIS USED TO SAY THE GRIEFER'S WIN "costs them the 5,000 cut", AND IT COSTS THEM NOTHING NOW: since finding 25 the fee is escrowed and earned only by a draw, so the griefer who DOES NOT resolve pays nothing at all and reclaims the whole 100,000 through Refund. The reason the attack is dead is the refusal below, not a 5,000 ugnot price on it. AUDIT-2026-09-28.md finding 29.

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 window is up.

It freezes two numbers and CHOOSES NOTHING:

  • FrozenTotal = the window's final ticket total. The window is over and Buy only ever sells into currentWindow(), so this total can no longer grow BY CONSTRUCTION; it is still assigned explicitly, because Resolve reads this field and not the live total.
  • CommittedAt, the height the refund grace window counts from.
  • AND IT USES Draw.Round EXACTLY AS PINNED, which was written when this window's record was created and is not this function's business.

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 draw into a purchase.

THERE IS DELIBERATELY NO "THE ROUND MUST STILL BE IN THE FUTURE" CHECK HERE. A reader who expects one has the pin backwards. A window is committed at least an hour after its round was pinned, so by now the pinned round MAY LEGITIMATELY ALREADY BE SIGNED AND RELAYED — that is the ordinary case for a window whose record opened early in its hour, and it is harmless precisely because nobody CHOSE it. 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 int64 wrap case lives where the arithmetic is, at maxPinnableLatest.

WHY FREEZING THE TOTAL ALSO FREEZES WHO OWNS UNIT k — the part r/splash/game 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 for ever) 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.

Panics if the window is not over yet, if it is already committed, or if it has NO TICKETS AT ALL. The last is not a formality: there is no rollover, so a drawless window must never be allowed to exist — there is no unit to draw and no owner to pay. It reads no beacon state at all; the "beacon must exist and be fresh" refusal lives at the pin, which is to say in Buy, where a sale is refused rather than accepted into a window 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/speed" -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/speed" -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
  

CurrentWindow

func CurrentWindow() int64
Ask AI Claude ChatGPT

CurrentWindow is the window tickets are being sold into right now.

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/arcade/speed.CurrentWindow()"

Result

CurrentWindowEndHeight

func CurrentWindowEndHeight() int64
Ask AI Claude ChatGPT

CurrentWindowEndHeight is the chain height at which the LIVE window ends: the one number a countdown wants. WindowEndHeight(CurrentWindow()), read in a single call.

IT IS ONE CALL ON PURPOSE, and it matters more here than in the raffle because this realm's window is an HOUR rather than a week. Composing the two exported reads off chain means two queries at two different chain heights, and a window that turns over between them yields a boundary for a window that has already ended — a countdown that jumps backwards by a whole hour exactly at the moment it matters most. Read here, both halves see one height.

A BLOCK HEIGHT, NOT A TIME, for the reasons on WindowEndHeight.

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/arcade/speed.CurrentWindowEndHeight()"

Result

DrawOf

func DrawOf(window int64) (bool, int64, int64, int64, bool, address, bool, int64)
Ask AI Claude ChatGPT

DrawOf reports a window's record: committed, frozen ticket total, beacon round, pot, resolved, winner, abandoned, committed-at-height. All zero for a window that never sold a ticket.

ABANDONED IS PART OF THIS TUPLE rather than a separate read because it is not a detail: it is the difference between a window that is waiting for its randomness and a window that is paying its buyers back and will never have a winner. A front end that showed "unresolved" for both would be lying to a buyer about whether to expect a draw or to go and claim a refund.

COMMITTED-AT IS THE LAST ELEMENT, and it is here so a front end can compute the one number a buyer of a stuck window actually wants: CommittedAt + refundGraceBlocks, the height at which their money becomes reclaimable whatever the beacon does. RefundOpensAt does that arithmetic; this returns the raw height so a caller can also show how long the window has been waiting. Zero on an uncommitted window, which is exactly the "no refund clock is running yet" case.

THERE IS NO SPONSORED ELEMENT, unlike the raffle's nine-tuple: this realm's pot is entry money only, so "pot minus sponsored" — the number a raffle buyer needs to know which part of the pot is a claim of theirs — is just the pot.

Param

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/arcade/speed.DrawOf()"

Result

EscrowedCut

func EscrowedCut(window int64) int64
Ask AI Claude ChatGPT

EscrowedCut is the house fee held against one window and NOT YET EARNED, in ugnot: it becomes ops revenue if that window resolves and goes back to the buyers if it does not. Zero for a window that has resolved, been fully refunded, or never sold a ticket.

Param

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/arcade/speed.EscrowedCut()"

Result

HeldCuts

func HeldCuts() int64
Ask AI Claude ChatGPT

HeldCuts is the total unearned house fee this realm holds across every window, in ugnot — the aggregate of EscrowedCut and the third term in the solvency equality.

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/arcade/speed.HeldCuts()"

Result

MaxStaleBlocks

func MaxStaleBlocks() int64
Ask AI Claude ChatGPT

MaxStaleBlocks is speedMaxStaleBlocks, EXPORTED so a front end can tell a player why a Buy was refused — the beacon needs a relay — rather than showing them a panic. beacon.StaleBlocks() is the number to compare it against.

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/arcade/speed.MaxStaleBlocks()"

Result

OpsPot

func OpsPot() int64
Ask AI Claude ChatGPT

OpsPot is the house's EARNED revenue this realm holds, in ugnot: the fees of windows that actually drew, plus absorbed donations, plus any swept ticketless window's pot.

IT IS NOT "THE ACCUMULATED HOUSE CUT" ANY MORE, which is what this comment said until 2026-09-29. A live window's fee is NOT in here — it is escrowed in that window and is still refundable. Read EscrowedCut and HeldCuts, and finding 25.

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/arcade/speed.OpsPot()"

Result

PinOffsetRounds

func PinOffsetRounds() int64
Ask AI Claude ChatGPT

PinOffsetRounds is pinOffsetRounds, EXPORTED so a front end (or an auditor) can check a window's pinned round against the anchor in its PinRound event without hardcoding the offset. Read pinOffsetRounds for what it protects.

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/arcade/speed.PinOffsetRounds()"

Result

PotSize

func PotSize() int64
Ask AI Claude ChatGPT

PotSize is the CURRENT window's prize pot in ugnot: ticket sales after the house cut, and nothing else. It is the whole amount the winner takes.

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/arcade/speed.PotSize()"

Result

Refund

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

Refund returns the caller's own ticket money for a window nobody settled. PERMISSIONLESS, and self-service only: a caller can only ever reclaim their own record.

WHAT IT IS FOR IS LIVENESS, NOT LOSS. A window's pot must not be hostage to whether anybody spends ~17.0M gas relaying one drand round, and at an hourly cadence with pots of a few tenths of a GNOT that is not an exotic worry — IT IS THE EXPECTED OUTCOME OF A QUIET HOUR. That is the only failure left, it is unbounded in time, so the exit is A CLOCK: refunds open refundGraceBlocks (twelve hours) after Commit and not before.

THE PRECONDITION IS THEREFORE NOT THE COMPLEMENT OF Resolve's — the two 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 window: the first refund that pays out sets Abandoned, after which Resolve and Capture refuse that window for ever and every later refund proceeds without consulting the beacon again. Read Draw.Abandoned for the theft this prevents — it is AUDIT-2026-09-28.md finding 3 and it carries more weight since the drand migration, not less, because a pinned round can be relayed by anyone at any time for ever.

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 + Cut == sum(spendOf) for an unresolved window — WITH NO SPONSORSHIP TERM TO GET IN THE WAY, which is the whole simplification this realm buys by having no FundPot — the refunds can never total more than the realm holds against this window.

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 against the chance of an unrelayed 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 window (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. A refunded window is a window that did not run, and 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. AUDIT-2026-09-28.md finding 25.

A WINDOW THAT WAS NEVER COMMITTED IS NOT REFUNDABLE HERE, and needs no separate path — but only because Commit TAKES THE WINDOW AS AN ARGUMENT and any window that is over can be committed by anyone at any time. So a buyer holding a stranded window's tickets names that window to Commit themselves, and then either Resolves it (the normal outcome, once somebody relays the round) or refunds it here twelve hours later. Before Commit took that argument the equivalent sentence in the raffle was simply FALSE, and that was AUDIT-2026-09-28.md finding 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/speed" -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/speed" -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
  

RefundGraceBlocks

func RefundGraceBlocks() int64
Ask AI Claude ChatGPT

RefundGraceBlocks is refundGraceBlocks, EXPORTED so a front end can state the window without hardcoding it. Read refundGraceBlocks for what it means and how it was derived.

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/arcade/speed.RefundGraceBlocks()"

Result

RefundOpensAt

func RefundOpensAt(window int64) int64
Ask AI Claude ChatGPT

RefundOpensAt is the chain height from which Refund will pay this window's buyers REGARDLESS OF THE BEACON: CommittedAt + refundGraceBlocks.

ZERO MEANS "THERE IS NO FUTURE HEIGHT TO WAIT FOR", which covers four unrelated states and a caller must not read any of them as "refundable now": no record for the window, not committed yet (nothing is locked up), already resolved (the pot has gone to its winner), or already abandoned (refunds are open this instant and the height is behind us). Render draws the same distinction, as "n/a".

Param

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/arcade/speed.RefundOpensAt()"

Result

Render

func Render(path string) string
Ask AI Claude ChatGPT

Render is the human-readable view. It ITERATES THE avl.Tree in key order, which is window order because windowKey is fixed-width and zero-padded, so the output is byte-identical across runs. A Go map here would randomise the order and make the rendered state unreproducible.

IT LISTS EVERY RECORDED WINDOW, and at 24 a day that table grows without bound — which is the same unbounded growth r/splash/arcade/raffle's Render has at 52 a year, reached ~168x faster. It is left unbounded here deliberately rather than silently truncated: Windows/PageLimit is the paged reader for a front end, and a Render that quietly stopped at N would be a page that lies about the realm's state. If gnoweb's response size ever becomes the binding constraint, the right change is an explicit "showing the last N of M" line, not a silent cap.

Param

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/arcade/speed.Render()"

Result

Resolve

func Resolve(cur realm, window int64)
Ask AI Claude ChatGPT

Resolve picks the window's winner and PAYS THEIR WALLET. PERMISSIONLESS.

The whole pot leaves this realm in a single wugnot.Transfer to the winner's address. No internal balance is credited, there is nothing left to claim afterwards, and no buyer's money is held past the window they bought into.

winner = ownerOfUnit(randomness mod FrozenTotal). FrozenTotal, NOT the live ticket total: recomputing it here would reopen the buy-the-winning-unit attack described on Commit.

The pot pays out UNTAXED. The house cut was already taken on the way in, at sale time; taxing the payout as well would be charging twice for the same ticket.

RANDOMNESS SOURCE: a captured value if there is one, otherwise the beacon directly — so the happy path needs no extra transaction.

THERE IS ONE FAILURE MESSAGE AND THERE CAN ONLY BE ONE. "Not relayed yet" is always a wait-and-retry, never "this draw is dead": no round is ever unobtainable, so anybody who sees that message can make it stop being true by relaying the round themselves for ~17.0M gas. A realm that offered a second, terminal message would be steering buyers into refunds they do not need.

THERE IS NO HEIGHT GATE HERE AND THERE MUST NEVER BE ONE. refundGraceBlocks does not appear in this function and CommittedAt is not read: the grace window is a FLOOR ON BUYER ACCESS, NOT A DEADLINE ON THE DRAW. It only ever opens Refund; it never closes Resolve. An honest relay that arrives a day after Commit, before anybody has refunded, should still pay the winner it always would have, over exactly the FrozenTotal it was frozen with. Adding a height deadline here would manufacture a second class of dead window — past the grace, relayed, and resolvable by nobody — which is the very shape of defect the grace window exists to remove.

WHAT DOES CLOSE THIS FUNCTION IS Abandoned, and that is enough. Once the grace window has opened, Resolve and Refund are both available and the first one called wins: a refund sets Abandoned and this function refuses for ever, a resolve pays the winner and Refund's Resolved check refuses for ever. NEITHER ORDERING CAN PAY THE SAME UGNOT TWICE, which is the property that matters. The fairness cost of that race — a buyer who can see they did not win refunds instead, annulling the draw — is AUDIT-2026-09-28.md finding 7's recorded residual, and it is CHEAPER HERE THAN THERE because the window that produces it is twelve hours rather than three weeks. It is derived and discussed at refundGraceBlocks rather than buried here.

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/speed" -func "Resolve" -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/speed" -func "Resolve" -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
  

SpendOf

func SpendOf(addr address) int64
Ask AI Claude ChatGPT

SpendOf is addr's refundable contribution to the CURRENT window's pot in ugnot: what they paid MINUS the house cut, which is what Refund would return. It is not the gross ticket price they paid.

Param

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/arcade/speed.SpendOf()"

Result

SpendOfWindow

func SpendOfWindow(window int64, addr address) int64
Ask AI Claude ChatGPT

SpendOfWindow is addr's remaining refundable contribution to a given window's pot. Zero once they have been refunded.

Params

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/arcade/speed.SpendOfWindow(,)"

Result

SweepDrawlessWindow

func SweepDrawlessWindow(cur realm, window int64) int64
Ask AI Claude ChatGPT

SweepDrawlessWindow moves a FINISHED, TICKETLESS window's pot to the ops pot, so that money in such a window can leave. PERMISSIONLESS, like Commit. Returns the amount moved.

READ THIS WHOLE COMMENT BEFORE DELETING THE FUNCTION AS DEAD CODE, because in v1 IT IS UNREACHABLE THROUGH EVERY ENTRY POINT and that is deliberate.

WHY IT IS UNREACHABLE HERE. A Draw record comes into existence in exactly one place (getOrCreateDraw) with exactly one caller (Buy), and Buy appends a positive ticket count in the same transaction; a panic anywhere after the record is created reverts the whole transaction, record included. So there is NO SEQUENCE OF CALLS that leaves a window with a record, a zero ticket total and a positive pot. That argument is asserted by the suite (a ticketless window cannot be produced through Buy) and the function's own preconditions are exercised by installing the state directly.

WHY IT EXISTS ANYWAY. It is the shape of AUDIT-2026-09-28.md's fifth fix, and the defect it exists against is the single most common one in that document — SIX of the fourteen findings are "money is in the realm and no path can ever get it out", and four of those six were found by an engineer implementing something else, not by an audit. In the raffle the state is REACHABLE: FundPot credits the live week and creates its record, so a week in which the duel's rake arrived but nobody bought a ticket ends holding money in a Draw that Commit rightly refuses (no tickets), that Resolve and Refund cannot touch (both require Committed) and that FundPot will never revisit (it only funds the live week). ADDING ANY OF THE THINGS THE PACKAGE COMMENT WARNS AGAINST — sponsorship, a rake feed, a jackpot, a rollover — MAKES IT REACHABLE HERE TOO, and the day that happens this exit should already be in place and tested rather than being the thing somebody forgot.

WHERE THE MONEY GOES, AND WHY IT IS NOT "ROLLED FORWARD" INTO THE LIVE WINDOW'S POT.

THE PARAGRAPH THAT STOOD HERE RECOMMENDED A DESIGN TWO AUDITS FOUND EXPLOITABLE, AND THAT IS WHY IT IS QUOTED RATHER THAN QUIETLY REPLACED — AUDIT-2026-09-28.md finding 36. It read:

Example
1"The raffle rolls a drawless week's sponsorship into the live week's pot, AND IT IS
2RIGHT TO: that money is the PLAYERS' side of the duel rake, so handing it to the house
3would take the players' share away from players, and the raffle has a Sponsored field
4that lets the destination week's pot exceed its spend records without breaking
5anything."

EVERY CLAUSE OF IT IS NOW FALSE, AND THE "AND IT IS RIGHT TO" WAS THE DEFECT. The raffle does not roll a dead week's sponsorship anywhere: rolling it into the live week was AUDIT-2026-09-28.md finding 28 (a destination evaluated at call time is a destination the permissionless caller chooses, by choosing when to fire), and the register that replaced it for one day was finding 31 (the caller who opens the destination week is the caller who calls Buy, so they could be its only entrant and capture the whole amount for the price of the house cut). A dead week's sponsorship now becomes a PER-UNIT CLAIM ON THE WEEK IT IS ALREADY IN, payable to the holders of that week's EARNED entries — read the raffle's retireSponsorship and ClaimDeadWeek. There is no destination at all.

SO A READER WHO PORTS SPONSORSHIP INTO THIS FILE MUST NOT PORT THE SENTENCE ABOVE. The half of it that was sound — this money is the PLAYERS' share and must not end up with the house — is satisfied by giving it back to the players who earned it, in place, and never by moving it to a week somebody can pick. This comment is where an engineer adding a rake feed here would look first, which is exactly why a recommendation for a broken design sitting in it is a defect and not untidiness.

NEITHER HALF OF THE ORIGINAL APPLIED HERE IN ANY CASE.

  • This realm has no sponsorship and no rake, so there is no players' share to protect: any money in a ticketless window is unattributable by construction — exactly like a stranger's unsolicited transfer, which sweepDonations also sends to opsPot.
  • Crediting the live window's Pot would BREAK Pot == sum(spendOf) for that window, which is the identity that makes Refund unable to overdraw and makes a sole entrant's prize exactly their own stake. AuditFailure would then report the live window as short, correctly, and the only way to "fix" it would be to add the Sponsored field and the three mechanisms that come with it — porting the complexity the package comment says not to port, to launder money that cannot exist.

SO opsPot — AND SINCE 2026-09-29 THE TWO REALMS AGREE ON THIS SHAPE RATHER THAN DIFFERING, which is the opposite of what this paragraph used to say. It read: "SO opsPot, WHICH IS A DELIBERATE DEVIATION FROM THE WEEKLY REALM'S CHOICE AND IS NAMED AS ONE ... the routing question ('where does it belong') is answered differently because the money has a different origin." The raffle's own no-claimant branch now sends a dead week's sponsorship to opsPot for exactly the reason given here — there is provably no address holding a unit of that week, so there is nobody to wrong — and it only pays holders when holders exist. Read the raffle's retireSponsorship: the branch is on its EARNED unit total and nothing else. The essential property of the fifth fix is preserved in full either way: money in the realm has a path out, here through WithdrawOps.

Panics if the window is not over yet (a live window may still sell its first ticket, which would make it a real draw whose buyers are entitled to everything in its pot), if there is no record for it, if it sold ANY tickets, if it holds nothing, or — defensively — if it is committed, resolved or abandoned.

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/speed" -func "SweepDrawlessWindow" -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/speed" -func "SweepDrawlessWindow" -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
  

TicketTotal

func TicketTotal() int64
Ask AI Claude ChatGPT

TicketTotal is the CURRENT window's ticket units sold.

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/arcade/speed.TicketTotal()"

Result

TicketTotalOfWindow

func TicketTotalOfWindow(window int64) int64
Ask AI Claude ChatGPT

TicketTotalOfWindow is a given window's ticket units sold: the LIVE total, not FrozenTotal, so it is readable before a commit and is zero for a window that never sold a ticket. DrawOf reports FrozenTotal, which is zero on any uncommitted window, so a caller cannot use it to tell "sold nothing" from "sold plenty and was never committed"; this read is that distinction.

Param

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/arcade/speed.TicketTotalOfWindow()"

Result

TicketsOf

func TicketsOf(addr address) int64
Ask AI Claude ChatGPT

TicketsOf is addr's ticket units in the CURRENT window.

Param

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/arcade/speed.TicketsOf()"

Result

TicketsOfWindow

func TicketsOfWindow(window int64, addr address) int64
Ask AI Claude ChatGPT

TicketsOfWindow is addr's ticket units in a given window, so a front end can still show a buyer their past windows once the clock has moved on.

Params

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/arcade/speed.TicketsOfWindow(,)"

Result

WindowBlocks

func WindowBlocks() int64
Ask AI Claude ChatGPT

WindowBlocks is windowBlocks, EXPORTED so a front end can show how long a window lasts and compute when the current one ends without hardcoding the cadence.

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/arcade/speed.WindowBlocks()"

Result

WindowCount

func WindowCount() int64
Ask AI Claude ChatGPT

WindowCount is the number of windows that have a record: the windows that sold at least one ticket. O(1); avl keeps its size.

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/arcade/speed.WindowCount()"

Result

WindowEndHeight

func WindowEndHeight(window int64) int64
Ask AI Claude ChatGPT

WindowEndHeight is windowEndHeight, EXPORTED: the chain height at which a given window stops being the live one. A window is live for heights [WindowEndHeight(window-1), WindowEndHeight(window)) — the returned height is the FIRST height of window+1, NOT the last height of window, so at exactly this height CurrentWindow already reads window+1. A countdown that showed "1 block left" here would be off by one.

IT IS A BLOCK HEIGHT AND DELIBERATELY NOT A TIME. A window is windowBlocks = blocksPerDay/24 = 1090 blocks, and blocksPerDay = 26182 encodes an ASSUMED 86400/26182 = 3.29998 s per block (read blocksPerDay and windowBlocks for how much that assumption is allowed to be wrong). The chain does not actually run at that rate: 3.306 s measured on gno.land mainnet and 3.376 s on onyx-1. So a caller wanting a wall-clock countdown must multiply by a block time IT chooses and own the error; this realm will not return seconds or "time remaining", because that would bake one clock's rounding into every front end that reads it and make the contract the source of a number it cannot know. Nothing on chain converts blocks to seconds and neither does this.

WHY THIS AND NOT AN EXPORTED launchHeight: WindowBlocks is already exported, so LaunchHeight() + WindowBlocks() would indeed let a caller derive the boundary — but it would leave the +1 above to be re-implemented off chain in every reader, and the raffle cannot offer the same pair at all (both raffleWeekDays and blocksPerDay are unexported there). One read with one name in both realms beats two constants and a formula the caller has to get right twice. This read also subsumes the constant: launchHeight is WindowEndHeight(-1).

NEGATIVE OR FAR-FUTURE window IS NOT REFUSED and returns the arithmetic for it, exactly as windowEndHeight does — this is a pure function of window and launchHeight with no record lookup, so there is no "no such window" state to report.

Param

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/arcade/speed.WindowEndHeight()"

Result

Windows

func Windows(offset, limit int64) []int64
Ask AI Claude ChatGPT

Windows is a window over the recorded window numbers in ASCENDING ORDER: it skips `offset` of them and holds at most min(limit, PageLimit). Empty past the end. Negative arguments are a caller error and panic.

Ascending order is window order, not merely key order, because windowKey is fixed-width and zero-padded (see keyWidth) — the same property Render relies on.

A CALLER FINDS THE WINDOWS THAT STILL NEED SOMETHING DOING by walking this and keeping those where window < CurrentWindow() and DrawOf reports not resolved: those need a Commit if not committed, a Resolve once their round is relayed, and a Refund past RefundOpensAt if nobody relays it.

Params

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/arcade/speed.Windows(,)"

Result

WithdrawOps

func WithdrawOps(cur realm) int64
Ask AI Claude ChatGPT

WithdrawOps pushes the whole ops pot to opsVaultAddr and returns the amount moved. PERMISSIONLESS.

THE DEFECT IT CLOSES IS AUDIT-2026-09-28.md FINDING 6, and it is in this file from the start rather than added later: without it opsPot only ever increases — the 5% cut on every ticket (the product's entire revenue), every swept donation, and anything SweepDrawlessWindow ever moves — and all of it would be permanently unreachable by every path in the realm while checkInvariant and AuditFailure both stayed green, because both count opsPot as money the realm holds and neither has an opinion about whether anybody can reach it.

PERMISSIONLESS, AND THAT IS SAFE PRECISELY BECAUSE THE DESTINATION IS FIXED AT COMPILE TIME. A reader will look for a caller check here and there is none, on purpose: opsVaultAddr is a const, so the caller does not choose where the money goes, cannot name themselves, and cannot name a window's pot. All they can do is push the house's money to the house — and paying the gas to do somebody else's bookkeeping is a favour, not an attack. Gating it on an operator key would buy nothing and would put a liveness dependency for the realm's revenue onto one key, exactly as it would for Commit, Capture and Resolve. The property that makes it sound is the one tools/speed_addr_test.go exists to keep true.

IT NEVER REACHES A PLAYER'S MONEY. No Draw is touched: not Pot, not spendOf, not a ticket. The only book that moves is opsPot, which by construction holds nothing any buyer can claim — the fee in it was EARNED BY A WINDOW THAT ACTUALLY DREW (Resolve releases that window's escrow; a window that fails refunds it with the gross) and a donation belongs to whoever chose to give it away.

THE CITATION THAT USED TO STAND HERE IS RETIRED: this paragraph said "the cut was taken at sale time (see Refund's 'THE HOUSE CUT IS NOT REFUNDED')", and neither half survives — finding 25 stopped the fee being taken at sale time and deleted that section of Refund. A citation that outlives the thing it cites is how a reader ends up trusting a rule the code stopped following. AUDIT-2026-09-28.md finding 29.

sweepDonations RUNS FIRST, as in every other mutator, so a donation that arrived since the last state change is included in this withdrawal rather than left behind.

BOOKS FIRST, TRANSFER SECOND, exactly as Resolve and Refund do it: opsPot is zeroed BEFORE the wugnot leaves, so a re-entrant call — which wugnot's Transfer cannot cause today, having no hook or callback, but a future GRC20 might — finds an empty pot and panics rather than a balance it can take twice.

THE INVARIANT IS EXACT ACROSS IT, not merely preserved one-sidedly: checkInvariant asserts heldPots + opsPot == balance, and this call reduces both sides by the same amount. heldPots is deliberately untouched.

Panics if there is nothing to withdraw.

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/speed" -func "WithdrawOps" -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/speed" -func "WithdrawOps" -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