Commit
Ask AI
Commit commits an ALREADY-FINISHED week to a future beacon round. Returns that round.
THE WEEK IS A PARAMETER AND IT MUST BE OVER — week < currentWeek(). Both halves of that sentence are load-bearing, and each fixes a defect the first version of this function had:
-
IT TAKES A WEEK, so any past week can be committed at any time, by anyone. Before, Commit could only ever act on currentWeek(), which meant a week whose clock ran out before anybody called it could never be committed — and because Resolve and Refund both require Committed, that week's whole pot was unreachable forever while heldPots went on counting it, so the invariant and the audit both stayed green over money nobody could ever get back. A stranded week is now always recoverable: commit it, then resolve it, or refund it if its round was lost.
-
THE LIVE WEEK CAN NEVER BE COMMITTED, so a commit can no longer cut a week's sales short. Before, anyone could Buy(1) and immediately Commit: that made them the sole owner of a FrozenTotal of 1 — the guaranteed winner — and, because Buy refuses a committed week, shut the week's sales for its remaining days. It cost the attacker the 5% cut on one ticket and was repeatable every week with no admin and no pause to stop it; the front-running variant landed a commit just ahead of somebody's large Buy and made their purchase panic. Refusing week >= currentWeek() closes all of that at the root rather than with a minimum-tickets or minimum-elapsed heuristic: the week being sold into is simply not a committable week.
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 week is up.
It freezes two numbers, and CHOOSES NOTHING:
- FrozenTotal = the week's final ticket total. The week is over and Buy only ever sells into currentWeek(), so this total can no longer grow BY CONSTRUCTION; it is still assigned explicitly, because Resolve reads this field and not the live total.
- Pot, which stops growing for the same reason (and Buy also refuses a committed week, which is now a second, unreachable-by-design lock).
- AND IT USES Draw.Round EXACTLY AS PINNED, which was written when this week's record was created and is not this function's business. See below.
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 raffle into a purchase.
THIS FUNCTION NO LONGER BINDS THE ROUND, AND THAT IS THE 2026-09-28 FIX. It used to set Round = beacon.Latest() + 1 here, which looked like the conservative choice — the round is unpublished at commit time, so its randomness "does not exist yet" — and was in fact the operator's cheapest attack on the product: the beacon's key holder knows every round's randomness in advance, and Commit ran AFTER the week's ticket set was frozen and public, so they could enumerate candidate rounds, Publish until Latest()+1 named them, and commit. The round is now pinned at record creation instead; read pinOffsetRounds for the full account, including what remains open.
THERE IS DELIBERATELY NO "THE ROUND MUST STILL BE IN THE FUTURE" CHECK HERE, and the old `round <= latest` wrap check is gone with it. A reader who expects one has the fix backwards. A week is committed at least a week after its round was pinned, so by now the pinned round MAY LEGITIMATELY ALREADY BE PUBLISHED — that is the ordinary case for a week whose record opened early, and it is harmless precisely because nobody chose it: the round was fixed before a single ticket of that week had been sold. 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 wrap case the old check guarded is now handled where the arithmetic happens, at maxPinnableLatest.
WHY FREEZING THE TOTAL ALSO FREEZES WHO OWNS UNIT k — the part the old realm 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 forever) 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 and its fix is what this realm inherits. Buy's refusal after commit closes the same door a second time.
A WEEK'S TICKET TOTAL MAY BE ENTIRELY EARNED AND THIS FUNCTION DOES NOT CARE. Since finding 15 a week can reach this point with tickets and NO spend records at all — every entry credited to a duel player through FundPotWithEntries, not one ticket bought. total > 0 is satisfied, the week commits, and Resolve pays the winner a pot made purely of the rake those players paid. That is the intent, and it is why the zero-ticket refusal below is a check on the TICKET INDEX and not on the spend records: a drawless week is one nobody entered, not one nobody paid this realm directly.
Panics if the week is not over yet, if it is already committed, or if it has no tickets at all (there is NO ROLLOVER, so a drawless week must never be allowed to exist). It no longer reads the beacon at all: the "no round has ever been published" refusal moved to the pin, which is to say to Buy and FundPot, where a sale is refused rather than accepted into a week 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/raffle" -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/raffle" -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