Commit
Ask AI
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.
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