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.

duel state

Back to all declarations

rakeBps

untyped bigint

Value

(300 <untyped> bigint)

bpsDenom

untyped bigint

Value

(10000 <untyped> bigint)

rakePotShareBps

untyped bigint

Value

(6000 <untyped> bigint)

rakeOpsShareBps

untyped bigint

Value

(4000 <untyped> bigint)

rung0

untyped bigint

// 10 GNOT

Value

(10000000 <untyped> bigint)

rung1

untyped bigint

// 50 GNOT

Value

(50000000 <untyped> bigint)

rung2

untyped bigint

// 200 GNOT

Value

(200000000 <untyped> bigint)

rung3

untyped bigint

// 1,000 GNOT

Value

(1000000000 <untyped> bigint)

maxStake

untyped bigint

rungs is the stake ladder, in ugnot (GNOT has 6 decimals): 10 / 50 / 200 / 1,000 GNOT. FIXED AT DEPLOY, NEVER MUTABLE — there is no function anywhere in this file that writes to it (SPEC-03 §3/§4). WHY THESE NUMBERS. The previous ladder (100 / 500 / 2,500 / 10,000) was chosen for a SPLASH token priced quite differently; read in GNOT at roughly $0.08 it ran $8 to $785 a side, which is a casino. This ladder is roughly $0.79 / $3.93 / $15.70 / $78.50 a side — "go on then" to "I mean it" — which is an arcade. SPEC-03 §9 records the exact rung values as a free dial.

Value

(1000000000 <untyped> bigint)

maxStakeHeadroom

untyped bigint

rungs is the stake ladder, in ugnot (GNOT has 6 decimals): 10 / 50 / 200 / 1,000 GNOT. FIXED AT DEPLOY, NEVER MUTABLE — there is no function anywhere in this file that writes to it (SPEC-03 §3/§4). WHY THESE NUMBERS. The previous ladder (100 / 500 / 2,500 / 10,000) was chosen for a SPLASH token priced quite differently; read in GNOT at roughly $0.08 it ran $8 to $785 a side, which is a casino. This ladder is roughly $0.79 / $3.93 / $15.70 / $78.50 a side — "go on then" to "I mean it" — which is an arcade. SPEC-03 §9 records the exact rung values as a free dial.

Value

(230584300921369 <untyped> bigint)

blocksPerDay

untyped bigint

\--- the reveal window (SPEC-03 §6, re-shaped 2026-09-30) ------------------ ONE CLOCK, AND BOTH ENDS OF IT ARE CHAIN HEIGHT. This block used to hold FOUR coupled numbers — voidGraceBlocks, duelBindOffsetRounds, duelMaxStaleBlocks and minVoidGraceMultiple — three of which existed only to keep a drand round binding honest against a beacon estimate. The beacon is gone from this realm, so they are gone with it: duelBindOffsetRounds DELETED. Nothing binds a round any more. duelMaxStaleBlocks DELETED. Accept reads no beacon, so it has no staleness to bound. minVoidGraceMultiple DELETED. It bounded the grace against the wait for a bound round. voidGraceBlocks RENAMED revealWindowBlocks, same 24 hours, different meaning. AND THE TWO BUILD GUARDS THAT STOOD HERE ARE DELETED RATHER THAN RESCALED, WHICH IS REQUIRED BY THIS PROJECT'S FINDING 11 DOCTRINE AND NOT MERELY TIDY. They were var _ [duelBindOffsetRounds*beacon.PeriodSeconds/2 - duelMaxStaleBlocks*33/100 - 1]struct{} var _ [voidGraceBlocks*33/10/(duelBindOffsetRounds*beacon.PeriodSeconds) - minVoidGraceMultiple]struct{} and BOTH were legitimate because each had SECONDS ON BOTH SIDES FROM DIFFERENT CLOCKS: the chain's block time on one side, drand's period on the other. That is exactly what made them able to fail. With drand's clock removed there is only one clock left, so any guard written between two windows here would have the block time on both sides, it would cancel, and the guard COULD NOT FAIL — which AUDIT-2026-09-28.md finding 11 says is worse than no guard at all, because it looks like protection. So there is DELIBERATELY NO BUILD GUARD ON revealWindowBlocks, and the relation it would have asserted is stated in prose below instead, exactly as pendingGraceBlocks already does for its own window.

Value

(26182 <untyped> bigint)

revealWindowBlocks

untyped bigint

\--- the reveal window (SPEC-03 §6, re-shaped 2026-09-30) ------------------ ONE CLOCK, AND BOTH ENDS OF IT ARE CHAIN HEIGHT. This block used to hold FOUR coupled numbers — voidGraceBlocks, duelBindOffsetRounds, duelMaxStaleBlocks and minVoidGraceMultiple — three of which existed only to keep a drand round binding honest against a beacon estimate. The beacon is gone from this realm, so they are gone with it: duelBindOffsetRounds DELETED. Nothing binds a round any more. duelMaxStaleBlocks DELETED. Accept reads no beacon, so it has no staleness to bound. minVoidGraceMultiple DELETED. It bounded the grace against the wait for a bound round. voidGraceBlocks RENAMED revealWindowBlocks, same 24 hours, different meaning. AND THE TWO BUILD GUARDS THAT STOOD HERE ARE DELETED RATHER THAN RESCALED, WHICH IS REQUIRED BY THIS PROJECT'S FINDING 11 DOCTRINE AND NOT MERELY TIDY. They were var _ [duelBindOffsetRounds*beacon.PeriodSeconds/2 - duelMaxStaleBlocks*33/100 - 1]struct{} var _ [voidGraceBlocks*33/10/(duelBindOffsetRounds*beacon.PeriodSeconds) - minVoidGraceMultiple]struct{} and BOTH were legitimate because each had SECONDS ON BOTH SIDES FROM DIFFERENT CLOCKS: the chain's block time on one side, drand's period on the other. That is exactly what made them able to fail. With drand's clock removed there is only one clock left, so any guard written between two windows here would have the block time on both sides, it would cancel, and the guard COULD NOT FAIL — which AUDIT-2026-09-28.md finding 11 says is worse than no guard at all, because it looks like protection. So there is DELIBERATELY NO BUILD GUARD ON revealWindowBlocks, and the relation it would have asserted is stated in prose below instead, exactly as pendingGraceBlocks already does for its own window.

Value

(26182 <untyped> bigint)

raffleRealmAddr

.uverse.address

Value

<gnolang.StringValue>

opsVaultAddr

.uverse.address

Value

<gnolang.StringValue>

maxLabelLen

untyped bigint

maxLabelLen bounds an offer's label, in BYTES (len of the string, not runes). 64 is room for "loser buys lunch, and the dessert" with change, and small enough that the storage deposit a label costs — 100 ugnot/byte until the record is deleted — is a rounding error on the smallest stake.

Value

(64 <untyped> bigint)

Offer

type

Offer is a standing stake in the lobby.

Value

duel.Offer

Duel

type

Duel is an accepted offer waiting to be resolved and collected.

Value

duel.Duel

nextOfferID

int64

Value

3

nextDuelID

int64

Value

3

escrowed

int64

Value

120000000

pendingPot

int64

Value

0

pendingSince

int64

Value

0

pendingEntryUnits

int64

Value

0

maxPendingEntryHolders

untyped bigint

maxPendingEntryHolders bounds the batch FlushPot builds, and therefore bounds FlushPot's gas. IT EXISTS BECAUSE THE ONE THING THAT MUST NEVER HAPPEN IS AN UNDELIVERABLE BATCH. Every distinct player who collects between two flushes adds a holder, and FlushPot pays to append each one in the raffle. With no bound, a long spell of the raffle refusing every flush — see FundPot's refusals — could grow the batch until delivering it cost more than a block, at which point the booked rake's primary exit would be gas-bricked and only SweepStuckRake's last resort would be left. That is the "money in, no way out" shape six of this product's findings have had, so it is bounded rather than hoped about. WHAT HAPPENS AT THE BOUND IS A DROPPED ENTRY, NEVER A FAILED Collect. A winner must always be able to collect (finding 14), so Collect cannot start refusing because a queue is full. A player who would be the (n+1)th distinct holder therefore earns nothing for that duel and an event says so. It costs any caller ONE FLUSH to prevent that for everybody, permanently — and in normal operation the batch is emptied by the next flush, which anybody may make at any height and which the players themselves have every reason to make. WHAT THAT FLUSH COSTS IS A RANGE AND NOT A FIGURE, corrected 2026-09-30 (AUDIT-duel-2026-09-27.md addendum 2, finding D-4). This paragraph said "~8.6M gas", which is A TWO-HOLDER FIGURE — and this paragraph is about the moment the batch is FULL, which is the one regime in which that number is more than thirty times too small. The two ends of the range, each with its provenance, because they are not both reproducible the same way: TWO HOLDERS: 10,378,157 gas, 0.35% of a 3e9 block. MEASURED 2026-09-30 on /home/johnny/go/bin/gno as GasCollectAndFlush minus GasCollectSetupAndCollect in zz_gas_test.gno, stable across repeated runs. NOTE THAT FILE'S TABLE STILL RECORDS 8,638,590 FOR THE SAME DIFFERENCE AND NO LONGER REPRODUCES — it is stale by ~1.7M and is owed a re-measurement, which is finding D-6 and is NOT fixed here. A FULL 128-HOLDER BATCH against a live week grown to 100,096 ticket indices: 351,725,503 gas, 11.7% of a block. AN OUT-OF-SUITE FIGURE BY CONSTRUCTION, and it must be read as one: seeding 100,096 indices takes ~7 minutes of wall clock, so it was measured with throwaway probes in a scratch tree and CANNOT be reproduced by running this package. zz_gas_test.gno records the method. The in-suite TestGasFlushAFullBatch exercises the same 128 holders against a far smaller index. THE ARGUMENT IS UNCHANGED AND THE HONEST VERSION IS STILL CHEAP: even at the full-batch end it is one transaction, well inside a block, available to anybody at any height. But a reader deciding whether the mitigation is realistic must be given the number that applies in the state the mitigation is for, not the cheapest one in the file. THE FIGURE SAID "~1.1M" UNTIL 2026-09-29 AND WAS STALE BY FINDING 15, which put two fenwick appends and their events inside every FlushPot. The ARGUMENT is unchanged — preventing a dropped entry for everybody costs one caller one affordable transaction — but this number has now been wrong three times running, in three different ways, which is itself the lesson: A MEASURED FIGURE IN A COMMENT DECAYS, and the only ones worth quoting are the ones a reader can re-derive from a named test. AUDIT-2026-09-28.md finding 19; AUDIT-duel-2026-09-27.md addendum 2, findings D-4 and D-6. 128 IS PICKED, NOT DERIVED, AND WHAT MAKES IT SAFE IS THAT BOTH SIDES OF IT ARE MEASURED: zz\_gas\_test.gno pins the cost of a full batch's delivery, so a future edit that raises this constant has to re-measure rather than reason. It is far above the number of distinct players a quiet arcade sees between two flushes and far below anything that threatens a block. AND THE BOUND WAS RE-MEASURED IN THE REGIME IT PROTECTS, WHICH IS THE POINT OF FINDING 19. This constant caps the HOLDER dimension at 128 and says nothing about the other dimension the cost depends on: THE SIZE OF THE RAFFLE WEEK'S TICKET INDEX, which grows on its own with no code edit at all. The earlier full-batch figure was taken against a week grown to only a few hundred entries. Measured against a live week seeded to 100,096 indices on this toolchain, a full 128-holder FlushPot costs 351,725,503 gas — 11.7 % of a 3e9 block, against 8.8 % at a few hundred. The tree-dependent half grew by about a third rather than doubling, the bound holds with more than eight times the headroom it needs, and 128 IS THEREFORE KEPT. The measurement, including how the index was seeded, is in zz\_gas\_test.gno.

Value

(128 <untyped> bigint)

realmSelf

.uverse.address

realmSelf is this realm's own address, resolved ONCE at package initialisation, for the same reason game.gno does it: under the test harness unsafe.CurrentRealm() follows the origin caller in a non-crossing frame, and a realm's own address is a constant for its whole life.

Value

<gnolang.StringValue>

idKeyWidth

untyped bigint

idKeyWidth is enough decimal digits for any non-negative int64, so that avl's lexicographic key order is numeric ID order and Render lists records as 1, 2, ... 10 rather than 1, 10, 2 — the same convention as r/splash/beacon's roundKey and r/splash/game's ticketIndexKey.

Value

(19 <untyped> bigint)

idZeroPad

untyped string

Value

"0000000000000000000"

getOffer

func(id int64) *duel.Offer

getOffer returns the offer, or nil. avl's Get returns a single value, so existence is a separate Has.

Open
OID
0a537e…a358:28
getOffer details

Inspect func

getDuel

func(id int64) *duel.Duel
Open
OID
0a537e…a358:29
getDuel details

Inspect func

stakeOf

func(rung int64) int64

stakeOf is the only place the ladder is read. Panics on any index outside it, including negative ones.

Open
OID
0a537e…a358:30
stakeOf details

Inspect func

mulDiv

func(a uint64, b uint64, d uint64) uint64

mulDiv returns floor(a\*b/d) via a 128-bit intermediate product, exactly as r/splash/game and r/splash/exchange do it. Raw a\*b/d on token-scale int64s wraps silently on the GnoVM (tools/muldiv\_probe.gno).

Open
OID
0a537e…a358:31
mulDiv details

Inspect func

poolOf

func(stake int64) int64

poolOf is 2 x stake, the whole pot of a duel, with the input bounded first (SPEC-v1.md §7.9; commit 3622284's lesson).

Open
OID
0a537e…a358:32
poolOf details

Inspect func

rakeOf

func(pool int64) int64

rakeOf is the rake on a pool: floor(pool x 300 / 10000).

Open
OID
0a537e…a358:33
rakeOf details

Inspect func

splitRake

func(rake int64) (potShare int64, opsShare int64)

splitRake divides a rake 60 / 40 into the raffle pot share and the ops share. Ops is floored; THE POT TAKES THE REMAINDER AND ALL THE DUST, so potShare + opsShare == rake exactly, always (BIBLE.md §2.7). THE ARITHMETIC IS UNCHANGED FROM THE OLD THREE-WAY SPLIT, and that is the point. There, ops was floor(rake x 2000 / 10000) and prizes was the remainder after yield and ops; so yield + prizes was already exactly rake - ops, dust included. Folding yield into the pot share therefore moves no money between the house and the players — it only sends one number where two used to go. Both checks below are REAL guards, not tautologies (see r/splash/game's Claim for the history). The compile-time array above makes the negative case unreachable today; they stay here so a runtime reader sees the property asserted where the money moves, and so that a pot share quietly shorted below its floor share would fail loudly rather than dribble into ops.

Open
OID
0a537e…a358:34
splitRake details

Inspect func

rakeEntryUnits

func(rake int64) int64

rakeEntryUnits is how many raffle entry units a rake of \`rake\` ugnot earns: EXACTLY WHAT THAT MONEY WOULD HAVE BOUGHT AT raffle.EntryUnitPrice. IT WAS raffle.TicketPrice UNTIL 2026-09-29 AND THAT IS AUDIT-2026-09-28.md FINDING 22 — the general lesson being that TWO INDEPENDENT PRODUCT PARAMETERS SHARING ONE CONSTANT WILL EVENTUALLY BE GIVEN VALUES THAT CANCEL EACH OTHER OUT. The raffle's ticket price went to 10 GNOT, and against a 10 GNOT divisor this ladder's rakes — 0.6 / 3 / 12 / 60 GNOT — earn 0 / 0 / 1 / 6 entries. THREE OF THE FOUR RUNGS WOULD HAVE EARNED NOTHING, silently, with the build green and every existing test that asserted the RATE rather than a count still passing. The entry unit is now its own constant at 0.25 GNOT, one ticket is 40 of them, and the rungs earn 2 / 12 / 48 / 240. TestFix22\_EveryRungEarnsAtLeastOneEntryAndTheCountsAreExact is the regression. THIS FUNCTION IS THE SECURITY PROPERTY OF THE WHOLE FEATURE AND THE REASON IT IS PRICED OFF THE RAKE AND NEVER OFF THE STAKE. A POSTER MAY ADDRESS AN OFFER TO ONE OPPONENT (see Post), so two accounts under one hand can flip each other for as long as they like and the ONLY real cost of doing so is the rake — the stakes go round in a circle. Weight entries by the stake or the pool and that is free money: a rung-0 duel is a 20 GNOT pool on a 0.6 GNOT rake, so pool-weighting would pay 80 units of weight (20 GNOT / EntryUnitPrice) for 2 units' worth of cost. ENTRIES AT A THIRTY-THIRD OF FACE VALUE. Nobody would buy a ticket again and one funded account would own every draw, for ever, on a contract with no owner and no pause. PRICED OFF THE RAKE, FLIPPING IS NEVER CHEAPER THAN BUYING AND SELF-DEALING GAINS NOTHING. The self-dealer's net cost of a rung-1 flip is the 3000000 ugnot rake and it earns 12 units: 250000 ugnot each, which is raffle.EntryUnitPrice to the ugnot. It is in fact weakly WORSE than buying, on two counts since 2026-09-29: only 60% of a rake reaches the pot against 95% of a ticket price — the same price per unit, into a smaller prize — and at rung 0 the floor discards 0.4 of a unit, so that rung pays 300000 a unit, 1.2x face value. BOTH ERRORS RUN THE SAFE WAY. duel_test.gno asserts that rate against raffle.EntryUnitPrice rather than against a hardcoded 2, and r/splash/arcade/raffle enforces the same ceiling at its door from the money it actually receives (raffle.ArcadeEntryUnitsBound), so neither side has to trust the other's arithmetic. THE FLOOR IS DELIBERATE: a rake that cannot buy one whole entry earns none. On today's ladder rungs 1, 2 and 3 are exact multiples and RUNG 0 IS NOT — 600000 / 250000 is 2.4, floored to 2 — so the floor now discards 0.4 of a unit at the bottom rung where it used to discard nothing at any rung. That is a fractional unit of weight the flipper paid for and does not get; it is the safe direction (it can only ever make flipping dearer than buying, never cheaper) and TestExtra\_LadderAndRakeConstants pins the exact counts. AND MIND WHAT THE FLOOR DOES TO splitEntries AT RUNG 0 — IT ONCE MADE THAT RUNG DEGENERATE AND THE OWNER HAS HAD IT FIXED. Two units split one-third / two-thirds is 0 to the winner and 2 to the loser, so a rung-0 winner used to earn NO ENTRIES AT ALL. THE CAUSE WAS THIS FLOOR MEETING A VERY SMALL TOTAL, NOT THE CONSOLATION WEIGHTING: a third of 2 is 0 however you weight it. Since 2026-09-29 splitEntries guarantees each player one entry whenever the total is at least 2, so rung 0 is 1 / 1 and every other rung is untouched. The TOTAL this function returns did not move by a unit at any rung, which is the only quantity an attacker could care about. Read splitEntries for the guarantee and its three boundaries. raffle.EntryUnitPrice IS A CONSTANT OF ANOTHER REALM AND THAT IS ON PURPOSE. It is a compile-time const, not a call, so reading it adds no runtime dependency ON THE RAFFLE to any path in this file — which matters most in checkInvariant, which runs at the end of Collect and must never be able to fail for a reason IN THE RAFFLE (finding 14). "in the raffle" and not "in another realm", corrected 2026-09-30: checkInvariant does read another realm, namely r/gnoland/wugnot's ledger for the escrow balance, and it always has. See checkInvariant, where that is now stated exactly. ============================================================================ DISCLOSURE: WHAT AN EARNED ENTRY IS SETTLED BY, AND THE ONE KNOWN OPEN RISK IN IT ============================================================================ READ THIS IF YOU PLAY THE DUEL. A coin flip here is decided by the two players' own commit-reveal (posterWins), with the caveats in this file's doc comment and NO DEPENDENCE ON A BEACON AT ALL since 2026-09-30. But the ENTRIES this function prices are entries in r/splash/arcade/raffle's WEEKLY DRAW, and that draw IS settled by a beacon round, and it has a known, open, recorded weakness of its own. It is disclosed here AND THE SENTENCE THIS REPLACES SAID THE OPPOSITE OF THE TRUTH, WHICH IS WHY IT IS QUOTED RATHER THAN DELETED: "a coin flip here is decided by a beacon round bound when the duel is accepted, AND THAT PART IS FINE". It was not fine. The binding's freshness signal was forgeable for about a tenth of a US cent, which is why the coin was replaced and why this disclosure now has a matching pair of caveats at the top of the file. A reassurance in a comment is worth exactly as much as the test behind it. because a duel player never has to read the raffle's source, and since finding 15 duel players are the LARGEST CLAIMANT CLASS in that weekly pot — people who opted into a coin flip, not a raffle. THE RISK, IN ONE SENTENCE: anybody who can learn a week's winning randomness BEFORE that week's ticket sales close can buy last, size the buy so that randomness mod FrozenTotal lands on a unit they own, and take the pot — a pot that since finding 15 is mostly duel rake. This is AUDIT-2026-09-28.md finding 4, narrowed by finding 10 and recorded OPEN there. WHY EARNED ENTRIES DO NOT MAKE IT BETTER OR WORSE, BUT CHANGE WHO PAYS FOR IT. Earned entries raise a week's ticket total, and the attack's cost is set by the attacker's own last purchase against that total — so a bigger total costs them more tickets, but they were always going to buy the number of tickets the arithmetic needed. IT DOES NOT RAISE THE ATTACK'S COST IN ANY MEANINGFUL SENSE. What it changes is the loser: before, the pot was buyers' money and a rigged draw robbed raffle buyers; now the pot is mostly rake and a rigged draw robs duel players. THE FORM THE RISK TAKES TODAY, STATED ACCURATELY RATHER THAN AS IT WAS FIRST WRITTEN. Finding 4 was written when r/splash/beacon verified a key THIS PROJECT HELD, which made the operator able to compute any future round's randomness at will. THAT IS NO LONGER THE CASE and the entry in the audit file predates the change: the beacon now verifies drand evmnet under a DKG threshold group key that nobody here holds, so no single party can sign a future round and nobody can read a future round's randomness. The finding is nevertheless still recorded OPEN, and it is open for a narrower reason that is worth stating plainly: THE RAFFLE PINS ITS WEEK'S ROUND AGAINST AN ESTIMATE OF DRAND'S CURRENT ROUND, DERIVED FROM CHAIN HEIGHT (r/splash/beacon's EstimatedLatestRound). If this chain's blocks run slower than the 3.29998 s the estimate assumes, or if nobody has relayed a round recently, the pinned round drifts EARLIER relative to real time — and a round whose time has come is PUBLIC on api.drand.sh, readable by anybody, for free. Drift far enough and the week's winning randomness is readable before its sales close, which is exactly the last-buyer attack above with no key and no privilege required. IT IS BOUNDED, AND THE BOUND IS MEASURED RATHER THAN ASSUMED: beacon's maxEstimateStaleBlocks refuses to open a week against an anchor more than one day of blocks old, and AUDIT-2026-09-28.md finding 13 computes the remaining margin as ~24 hours at nominal block time and ~7.2 hours at the slow edge of the stated ±10 % band, against ~2.4 hours of admitted estimation error. So it holds today at the measured constants. WHAT IT RESTS ON IS blocksPerDay = 26182 BEING RIGHT ABOUT THE REAL CHAIN, which finding 12 records as a DEPLOYMENT-BLOCKING MEASUREMENT the owner must make. NOTHING IN THIS REALM CLOSES IT AND NOTHING HERE SHOULD BE READ AS TRYING TO. It is the raffle's draw, it is the owner's decision (an externally-timed pin, or a VRF), and the only thing this comment does is make sure a duel player or a reviewer meets it here — because the money at risk is theirs. See Collect's doc for the pointer and AUDIT-2026-09-28.md findings 4, 10, 12, 13 and 20 for the full record.

Open
OID
0a537e…a358:35
rakeEntryUnits details

Inspect func

splitEntries

func(total int64) (winnerUnits int64, loserUnits int64)

splitEntries divides a duel's earned entries between its winner and its loser: EACH PLAYER IS GUARANTEED ONE ENTRY WHENEVER THE TOTAL CAN PAY FOR TWO, AND THE REMAINDER IS WEIGHTED TWO THIRDS TO THE LOSER. THE FLOOR OF ONE IS THE OWNER'S 2026-09-29 CHANGE AND IT IS THE ONLY THING THAT MOVED. Before it, the rule was the bare floored third — winner = total/3, loser the rest — and at the bottom rung that produced a DEGENERATE result: a 10 GNOT flip earns 2 units (600000 ugnot of rake over a 250000 ugnot entry unit, floored), a third of 2 floors to 0, AND THE WINNER OF A DUEL EARNED NOTHING. The weighting was never wrong; a floored share of a very small total is. So the small total is handled where it arises instead of by reweighting anything: one unit each first, then the old arithmetic on what is left. THE CONSOLATION WEIGHTING IS A PRODUCT DECISION AND IT COSTS NOTHING, which is why it is safe, and the same sentence is why the floor is safe. The TOTAL is fixed by rakeEntryUnits and is the only quantity an attacker could care about; how it is divided between two parties who may be the same person creates no edge whatever. A self-dealer holding both accounts gets the same total either way, which is exactly what the self-dealing test asserts. NOTHING BELOW RAISES A TOTAL BY ONE UNIT, so the money-backed ceiling this realm checks in maxEntryUnitsFor and the raffle checks again in ArcadeEntryUnitsBound is untouched at every rung. THE NUMBERS IT PRODUCES on today's ladder. ONLY RUNG 0 MOVED: rung 0: rake 600000 -> 2 units -> 1 winner / 1 loser <- WAS 0 / 2 rung 1: rake 3000000 -> 12 units -> 4 winner / 8 loser rung 2: rake 12000000 -> 48 units -> 16 winner / 32 loser rung 3: rake 60000000 -> 240 units -> 80 winner / 160 loser Rungs 1, 2 and 3 are unchanged because their floored third is already at least one, so the floor never binds: max(1, total/3) == total/3 for every total >= 3. The floor can therefore only ever be visible at total 2, which is the one place the defect was. TestFix26\_EachPlayerIsGuaranteedAnEntryAndTheRungCountsAreExact pins every row of the table above and pins the totals as unchanged. THE THREE BOUNDARIES, CHOSEN EXPLICITLY RATHER THAN FALLING OUT OF THE ARITHMETIC: total 0 -> 0 / 0. NEITHER PLAYER CAN GET ANYTHING AND THAT MUST NOT PANIC. It is what a sub-unit rake produces — a rake under raffle.EntryUnitPrice floors to no units at all — and it is reachable from Collect's `potShare > 0` branch on any ladder whose bottom rung is small enough. There is nothing to guarantee here: a guarantee of one entry each out of a total of zero would have to invent the entries, which is exactly the ceiling violation this whole feature is priced to prevent. total 1 -> 0 / 1, THE LOSER TAKES IT. One unit cannot be split two ways and something must decide; the loser gets it for consistency with the consolation intent that shapes the whole function, which is the same reason the remainder goes to the loser at every other total. Unreachable on today's ladder (the smallest rung earns 2) and therefore stated and tested rather than left to be discovered by a future rung. total >= 2 -> at least 1 each. This is the guarantee. THE WINNER'S SHARE IS THE FLOORED ONE AND THE LOSER TAKES THE REMAINDER, in the same spirit as splitRake giving the pot the dust: the asymmetry must never round against the party it is meant to favour, and the two shares must sum to the total EXACTLY or the ceiling the raffle enforces would be met by a total this realm did not intend. So loser >= winner always, and winner + loser == total for every total including 0, 1 and 2. ONE PREVIOUSLY TRUE CLAIM IS NOW FALSE AND IS RETIRED RATHER THAN QUIETLY DROPPED. This comment used to say "strictly greater for any total >= 1". AT TOTAL 2 THE TWO SHARES ARE NOW EQUAL, 1 and 1, which is the whole point of the change: a two-unit pot split "strictly in the loser's favour" is 0 and 2, and 0 is the bug. The invariant that survives — and that the raffle's ceiling and this realm's own checkInvariant actually rest on — is loser >= winner, which the panic below enforces. Strictness still holds for every total >= 3.

Open
OID
0a537e…a358:36
splitEntries details

Inspect func

maxEntryUnitsFor

func(potShare int64) int64

maxEntryUnitsFor is the most booked entry units a booked pot share of \`potShare\` ugnot may ever carry — the SAME ceiling r/splash/arcade/raffle applies at its door, computed here from THIS realm's own constants so that checkInvariant can assert it without calling another realm. potShare >= rake \* rakePotShareBps / bpsDenom, so rake \<= potShare \* bpsDenom / rakePotShareBps, and an entry costs raffle.EntryUnitPrice of rake. It is an inequality and not an equality because a party who played from realm code earns nothing (see Collect) and because the floor in rakeEntryUnits can discard a remainder — which since 2026-09-29 it actually does at rung 0 — so this realm may always book FEWER entries than the ceiling, never more. BOTH OF ITS TWO CONSTANTS MOVED ON 2026-09-29 and both had to: the gross-up is now 10000/6000 (finding 24's rake split) and the divisor is now the ENTRY UNIT and not the ticket price (finding 22). Getting either wrong in isolation is visible — too small a ceiling refuses honest batches here in checkInvariant, too large a one lets the raffle's own door refuse them — and TestFix15\_TheRafflesEntryBoundIsExactlyThisRealmsRakeEquivalent pins this function against the raffle's copy to the entry, at every rung.

Open
OID
0a537e…a358:37
maxEntryUnitsFor details

Inspect func

hexLen32

untyped bigint

hexLen32 is how long a 32-byte value is in hex: 64 characters. The secret, the nonce and the commitment hash are all exactly this long and all lowercase hex, checked at BOTH ENDS — checkCommitment and checkNonce at the doors, checkSecret again at Reveal. IT IS THE SAME 64 randomnessHexLen USED TO BE, AND THE NAME CHANGED ON PURPOSE. That constant was "the length of a beacon randomness string (as in raffle.gno)", and this realm reads no beacon; a length named after a thing it no longer touches is how a stale premise survives a change. The number is unchanged because sha256 is unchanged.

Value

(64 <untyped> bigint)

commitTag

untyped string

THE TWO DOMAIN TAGS, AND THEY MUST DIFFER. commitTag seals the poster's secret; outcomeTag turns it plus the taker's nonce into a coin. If one string could be hashed under both tags then a commitment would BE an outcome — a poster could publish, as their commitment, the very digest whose parity they want, and the two uses of sha256 in this file would stop being independent functions. EQUAL LENGTH AND DIFFERENT CONTENT, deliberately: equal so that neither tag can ever be a prefix of the other followed by the start of a field, different so that domain separation is real. Both are compile-time constants of this file and are reachable by no caller, so nothing an attacker supplies can move the boundary between tag and payload. THE EQUAL LENGTH IS A BUILD GUARD AND THE DIFFERENCE IS AN init GUARD, and the split is forced rather than chosen: an array length must be an integer constant expression, and a string comparison is not one on this toolchain. So the length relation — the one that IS integer arithmetic over two independent constants — fails the BUILD, and the inequality fails the DEPLOYMENT, which this file already establishes at the ladder as the next best thing (see init). Both can fail: neither constant cancels, and they were chosen independently.

Value

"splash/duel/v1/commit:"

outcomeTag

untyped string

THE TWO DOMAIN TAGS, AND THEY MUST DIFFER. commitTag seals the poster's secret; outcomeTag turns it plus the taker's nonce into a coin. If one string could be hashed under both tags then a commitment would BE an outcome — a poster could publish, as their commitment, the very digest whose parity they want, and the two uses of sha256 in this file would stop being independent functions. EQUAL LENGTH AND DIFFERENT CONTENT, deliberately: equal so that neither tag can ever be a prefix of the other followed by the start of a field, different so that domain separation is real. Both are compile-time constants of this file and are reachable by no caller, so nothing an attacker supplies can move the boundary between tag and payload. THE EQUAL LENGTH IS A BUILD GUARD AND THE DIFFERENCE IS AN init GUARD, and the split is forced rather than chosen: an array length must be an integer constant expression, and a string comparison is not one on this toolchain. So the length relation — the one that IS integer arithmetic over two independent constants — fails the BUILD, and the inequality fails the DEPLOYMENT, which this file already establishes at the ladder as the next best thing (see init). Both can fail: neither constant cancels, and they were chosen independently.

Value

"splash/duel/v1/result:"

isLowerHex32

func(s string) bool

isLowerHex32 reports whether s is exactly 64 characters of LOWERCASE hex. LOWERCASE IS REQUIRED AND IS NOT FUSSINESS: it makes the encoding of a 32-byte value CANONICAL, so one secret has one spelling. Admitting both cases would mean a secret has up to 2^64 spellings, each hashing to a different commitment — harmless for the commitment (any one of them is a fine commitment) but it turns "the length is checked, so the opening is unique" into an argument a reader has to redo. One spelling, one check.

Open
OID
0a537e…a358:38
isLowerHex32 details

Inspect func

checkCommitment

func(commitHash string)

checkCommitment validates a commitHash at Post: 64 lowercase hex characters, no exceptions and no empty string. "" is reserved for LEGACY records (Offer.CommitHash) and must never be creatable, or a poster could post a duel that nobody can ever reveal and that Forfeit deliberately refuses to settle.

Open
OID
0a537e…a358:39
checkCommitment details

Inspect func

checkNonce

func(nonce string)

checkNonce validates the taker's nonce at Accept: 64 lowercase hex characters, i.e. exactly 32 bytes. Checked HERE and not only at settlement because the nonce is half the coin: a short or empty one would shrink the taker's contribution to the outcome, and a zero-length nonce would make every duel a function of the secret alone — which the poster knows.

Open
OID
0a537e…a358:40
checkNonce details

Inspect func

checkSecret

func(secret string)

checkSecret validates a revealed secret: 64 lowercase hex characters. THIS IS THE OTHER END OF THE LENGTH CHECK AND IT IS THE LOAD-BEARING ONE — REQUIREMENT 1 OF THIS CHANGE, AND THE ONE THAT KILLED AN EARLIER PROPOSAL OUTRIGHT. The rejected shape was commitHash = H(secret ‖ salt) with both fields variable length. Two variable-length fields concatenated have NO UNIQUE SPLIT: a preimage of length L admits L+1 different (secret, salt) readings, every one of which passes the hash check, and the poster simply announces whichever reading wins. Not a bias — CERTAINTY, for free, with no grinding and no collision. ONE FIELD, FIXED LENGTH, ENFORCED AT BOTH ENDS is what removes it: the preimage of commitTag ‖ secret is commitTag's 22 known bytes followed by exactly 64 more, so there is exactly one place it can be cut and exactly one secret that opens a commitment. posterWins' own comment has warned about this since the beacon version ("without it the split point would be attackable") and it is now enforced rather than remarked on.

Open
OID
0a537e…a358:41
checkSecret details

Inspect func

commitmentOf

func(secret string) string

commitmentOf is H(commitTag ‖ secret), hex-encoded lowercase: what a poster publishes at Post and what Reveal recomputes. The secret is validated first, so this function is never given a variable-length preimage (checkSecret).

Open
OID
0a537e…a358:42
commitmentOf details

Inspect func

posterWins

func(secret string, nonce string, duelID int64) bool

posterWins is the whole winner rule, in one place (SPEC-03 §5): h = sha256(outcomeTag ‖ secret ‖ nonce ‖ duelID) winner = Poster if h is even else Taker "h is even" is the parity of h read as a big-endian integer, i.e. of its last byte — unchanged from the beacon version, which read exactly the same bit of exactly the same hash function. WHAT CHANGED IS ONLY WHERE THE UNPREDICTABILITY COMES FROM: it was one value published by a third party (a drand round) and is now TWO values contributed by the two people with money on the table. WHY NEITHER PARTY CAN CHOOSE THE ANSWER, which is the entire security of this realm: THE POSTER CANNOT GRIND. They fix secret at Post, when `nonce` does not exist and `duelID` has not been assigned. Every candidate secret they might try is a commitment to an outcome they cannot evaluate, because two of the three inputs are still in the future. Grinding requires being able to score a candidate, and there is nothing to score against. THE TAKER CANNOT STEER. They choose `nonce` freely, with `duelID` and `commitHash` both known — and without `secret`. Knowing H(commitTag ‖ secret) tells them nothing about H(outcomeTag ‖ secret ‖ nonce ‖ duelID) short of inverting sha256, so every nonce they could pick is a coin whose face they cannot see. Choosing among coins you cannot evaluate is not choosing. AND NEITHER CAN CHANGE THEIR MIND. The poster is bound by collision resistance (one secret opens one commitment — checkSecret) and the taker's nonce is written into the record at Accept, before any secret exists on chain. MIXING IN THE DUEL ID IS STILL MANDATORY (SPEC-v1.md §7.2), for a reason that survived the change of source: a poster may stand several offers at once, and without the ID two duels that happened to share a secret and a nonce would share an outcome. The concatenation is UNAMBIGUOUS because secret and nonce are each exactly hexLen32 bytes and the only variable-length field, the decimal ID, is LAST — nothing follows it, so nothing can be shifted into it. Factored out, as before, so the tests can derive a duel's outcome independently of Reveal — and so that the poster-cannot-grind and taker-cannot-steer tests can drive the function directly over thousands of candidates instead of asserting a property in a comment.

Open
OID
0a537e…a358:43
posterWins details

Inspect func

commitKey

func(poster .uverse.address, commitHash string) string

commitKey is liveCommitments' key: the poster and their commitment, joined by a separator that cannot occur in either half. An address is bech32 (no "|") and a commitment is lowercase hex (no "|"), so the two halves cannot be confused for one another whatever the lengths — the same unique-split discipline as the hash preimages above, applied to a tree key.

Open
OID
0a537e…a358:44
commitKey details

Inspect func

requireFreshCommitment

func(poster .uverse.address, commitHash string)

requireFreshCommitment refuses a (poster, commitHash) pair that a live record already holds. CHECKED BEFORE THE STAKE IS PULLED, so a refused Post moves nothing.

Open
OID
0a537e…a358:45
requireFreshCommitment details

Inspect func

holdCommitment

func(poster .uverse.address, commitHash string)

holdCommitment records the pair. WRITTEN AFTER THE PULL, BESIDE THE RECORD IT BELONGS TO, and the split from the check above is deliberate: a write made before a pull that then fails is discarded on chain (the panic aborts the transaction) but is KEPT under \`gno test\`, where uassert's Aborts\* helpers catch the abort with revive() and "state mutations remain even after revive()" — see duel\_test.gno's F2 note. So a check-then-write pair straddling the pull would leave this suite with a held commitment for an offer that does not exist, and AuditFailure would rightly refuse the books. Checking early and writing late gives the same refusal with no window in which the index can disagree with the records.

Open
OID
0a537e…a358:46
holdCommitment details

Inspect func

releaseCommitment

func(poster .uverse.address, commitHash string)

releaseCommitment frees a pair when its record dies. Silent on a pair that is not held: a legacy record never held one, and a release that finds nothing must not be able to stop an exit (see liveCommitments on why a leak and an over-eager release are both harmless).

Open
OID
0a537e…a358:47
releaseCommitment details

Inspect func

checkLabel

func(label string)

checkLabel is the ONLY validation a label gets, and it runs before any pull. Bounded in bytes, valid UTF-8, no control characters (which rules out newlines and carriage returns, so the stored text is always one printable line). Everything else — markdown, HTML, pipes — is legal to STORE and is neutralised at render time by renderLabel, the same discipline every address in this realm already goes through.

Open
OID
0a537e…a358:48
checkLabel details

Inspect func

checkInvariant

func()

checkInvariant is the O(1) solvency check run at the end of every state-changing function: the realm must hold at least what it has in escrow. ">=" and not "==", deliberately — wugnot.Transfer is permissionless, so anyone can push wugnot at this address, and an exact check would let a one-ugnot donation brick every duel. Such a donation is simply stranded: SPEC-03 §4 forbids any function that pays anyone other than a duel's own winner, so there is no sweep and no admin drain by design. pendingPot IS PART OF WHAT THIS REALM OWES, and it is checked here for the same reason escrowed is: a booked rake this realm does not hold is money promised to the raffle out of players' stakes. Still ">=" and not "==" — a donation is still stranded, by design. THE BOOKED ENTRIES ARE CHECKED HERE TOO, AND THE RATE CHECK IS THE IMPORTANT ONE. Entries are not money, so they are not part of what the realm holds — but the rate at which this realm hands them out IS the security property of finding 15, and an O(1) statement of it belongs on every mutator's exit. If a future edit ever books entries worth more than the rake that paid for them, this is the line that fails, and it fails BEFORE the raffle's own door would refuse the batch — which matters, because the raffle refusing means a stuck flush and this means a refused Collect with nothing written. IT CANNOT FAIL FOR A REASON IN r/splash/arcade/raffle — AND THAT, NOT "no other realm", IS THE PROPERTY FINDING 14 ACTUALLY NEEDS. Until 2026-09-30 these lines claimed this function "reads no other realm at runtime … so it cannot fail for a reason outside this file", AND THAT WAS FALSE ON ITS FACE: the last check below calls wugnot.BalanceOf(selfAddr()), a live cross-realm read of r/gnoland/wugnot's ledger. The claim is corrected rather than deleted because the narrower one is true and is the one finding 14 rests on: NO LINE OF RAFFLE CODE RUNS ON THIS PATH AND NO RAFFLE STATE IS READ. raffle.EntryUnitPrice is a compile-time const, not a call, and maxEntryUnitsFor is this realm's own arithmetic over this realm's own constants. Finding 14 is about a panic in the RAFFLE taking a winner's payout down with it — Collect used to end in the raffle's own checkInvariant — and that is what this independence buys. THE wugnot READ IS NOT A DEPENDENCY THIS FUNCTION COULD BE WITHOUT, which is why it is disclosed rather than removed: the escrow being checked IS a wugnot balance, so there is no version of this check that does not read the token. It is also not a new exposure on any mutator's exit — every path that reaches here has already moved wugnot in the same transaction (Post pulls, Collect pays), so a token that refused to answer would have aborted before this line. AUDIT-duel-2026-09-27.md addendum 2, finding D-3.

Open
OID
0a537e…a358:49
checkInvariant details

Inspect func

requireUserCall

func(what string)

requireUserCall refuses a Post or an Accept that did not come DIRECTLY FROM A USER. It is the only gate in this file that turns a caller away for what it IS rather than for what it owns or owes, and it was added on 2026-09-30 by AUDIT-duel-2026-09-27.md addendum 2, finding D-1. THIS IS THE RAFFLE'S FINDING-8 REASONING APPLIED TO THIS REALM'S OWN ESCROW, RATHER THAN ONLY TO ITS ENTRIES, AND THAT IS THE WHOLE POINT OF THE CHANGE. r/splash/arcade/raffle refuses realms outright in Buy because a ticket is a claim on an OUTBOUND TRANSFER and a realm's address is PACKAGE-DERIVED: nothing can move wugnot out of it unless that realm happens to contain code that does, so a realm that won a week would strand the whole pot in an address nobody can spend from. This realm imported that reasoning in 2026-09-29's finding 15 and applied it to EXACTLY ONE THING — whether a party earns raffle entries (PosterIsUser, TakerIsUser, bookEntries) — while leaving its own escrow wide open: A REALM COULD POST, ACCEPT, WIN AND COLLECT. Collect is WINNER-ONLY, and VoidStale refuses a resolved duel. So a winning realm that contains no code calling Collect left 2 x stake sitting in this realm's escrow WITH NO EXIT AT ALL — not a delayed exit, not a dear exit, none. Every path out of a resolved duel's pool goes through Collect, and only the winner may walk it. AND THE BOOKS STAYED GREEN OVER IT, which is what made it invisible. The duel record is still there and still counted, so \`escrowed\` still matches it, AuditFailure still returns "" and checkInvariant still passes. The money is accounted for exactly; it is simply unreachable. That is the "money in, no way out" shape SIX of this product's findings have had, and this one was the seventh. WHY BOTH DOORS AND NOT ONLY Accept — THE ASYMMETRY THAT MAKES A ONE-SIDED GATE USELESS. Gating Accept alone leaves the identical defect reachable from the other side, because the losing party's identity is decided by a coin flip AFTER both stakes are escrowed: realm posts, user accepts, THE REALM WINS -> Collect is the realm's alone to call, and the pool is stranded. The user's stake is stranded with it. A one-sided gate would therefore have halved the probability of the defect and fixed none of it — and it would have stranded the money of the party this realm CAN vouch for, which is worse than the symmetric case. Both parties must be addresses that can call Collect, so both doors that make a party are gated. AND NEITHER OF THE OTHER TWO EXITS IS A WAY OUT OF IT, RE-CHECKED AGAINST THE COMMIT-REVEAL COIN ON 2026-09-30 BECAUSE THE OLD SENTENCE'S PREMISE IS GONE. It read "VoidStale is not a way out either: it refuses a resolved duel, and a duel whose round is published cannot be voided at all", and there are no rounds now. The argument survives in a different shape and is stronger, not weaker: VoidStale now refuses EVERY duel that carries a commitment, so it cannot release a realm's pool at all — not before the window, not after it, not ever. Forfeit releases a pool without the winner's cooperation, but only while the duel is UNREVEALED, and it pays THE TAKER. So it rescues exactly one of the two stranded cases (a realm poster who never reveals) and not the case the gate is for: once the coin is revealed and a REALM is the winner, Collect is the realm's alone to call and Forfeit refuses a revealed duel. The gate is still the only thing standing between this realm and a pool nothing in the universe can close. IsUserCall AND NOT IsUser, for the reason raffle.Buy states at length: IsUser also admits a \`maketx run\` ephemeral realm, which is code executing under a user's signature and not an EOA — its address is package-derived too, and it ceases to exist when the transaction ends. The predicate is about THE CALL, not the address, and it is only answerable while the call is on the stack. WHAT IS LOST, STATED PLAINLY BECAUSE IT IS A REAL CAPABILITY REMOVAL. No realm can play a duel any more: no vault, no strategy contract, no "flip on my behalf" wrapper, not even one that does contain a Collect call. This realm cannot tell those apart from the ones that do not — the raffle's Buy comment makes the same point ("this realm cannot tell which realms those are and must not try") — so it refuses them all at the door rather than stranding a pool at settlement time. THE ALTERNATIVE WAS NOT "realms can play safely", it was "a realm can create an escrow position that nothing in the universe can close", and that is not a trade. A realm that wants exposure can hold wugnot and let its user play. FUNCTIONS THIS DOES NOT GATE, AND WHY EACH IS RIGHT TO BE OPEN. Cancel and Collect are restricted to a party of the record, and after this change every such party is an address whose own call was a user call — so gating them again would be redundant on every reachable record and would BRICK any record created before this change if one existed. (Cancel has one permissionless case, a LEGACY offer, and that is the whole reason it is permissionless: a pre-D-1 poster may be a realm that cannot call anything, and the refund's destination is read from the record.) Reveal, Forfeit, VoidStale, FlushPot and SweepStuckRake are permissionless keeper work that pays nobody the caller names. Refusing realms there would remove the bystanders the whole design relies on — and for Reveal it would be worse than useless, because the hash IS the authorisation and a caller check would turn the secret into an off-chain credential held by a de-facto third party to the duel. Read Reveal.

Open
OID
0a537e…a358:50
requireUserCall details

Inspect func

rejectAttachedCoin

func(what string)

rejectAttachedCoin panics if the caller attached native coin to a call that has no use for it. Every money-in path in this realm is the wugnot GRC20 allowance (Post, Accept), never the transaction's send envelope, so any attached ugnot would simply sit in this realm forever with no code path that ever looks at it — AUDIT-2026-10-07.md Y1. Call this as the first statement of every crossing entrypoint so the transaction reverts and the sender keeps their coin instead. unsafe.OriginSend() reads the envelope attached at the CHAIN ROOT, and that envelope is shared by every realm in the call chain: an intermediate realm can consume it, or have already consumed it, before this call is ever reached. Reading it is only sound when the immediate caller is an EOA, so that whatever it describes was paid to THIS realm and nothing else first. cur.Previous().IsUserCall() is that check (IsUser() is not — it also accepts \`maketx run\` ephemeral realms, which can pre-consume the envelope); see r/splash/exchange/exchange.gno's Buy/Seed comment for the fuller argument, which this mirrors rather than contradicts. WHEN THE CALL DID NOT COME DIRECTLY FROM A USER — which, per requireUserCall's comment just above, is a deliberately open door on Cancel/Collect (legacy case), Reveal, Forfeit, VoidStale, FlushPot and SweepStuckRake — THIS FUNCTION DOES NOT LOOK AT THE ENVELOPE AT ALL: it cannot be attributed to this call either way, so there is nothing sound to enforce, and guessing would be worse than staying silent. It does not reopen any of those doors; it is simply inert on them. THE OVERWHELMINGLY COMMON CASE — a plain call with nothing attached — is cheap: one stack-walk for IsUserCall, then unsafe.OriginSend() always allocates (even a len-0 slice) and len(sent) == 0 is a single comparison. No iteration, no second cross-realm call.

Open
OID
0a537e…a358:51
rejectAttachedCoin details

Inspect func

Post

func(rung int64, opponent .uverse.address, label string, commitHash string) int64

Post pulls rungs\[rung] from the caller through the GRC20 allowance path and leaves it standing in the lobby. Returns the new offer's ID. opponent, when non-zero, addresses the offer to one player: only that address may Accept it. Zero ("") leaves it open to anyone, and the offer then behaves exactly as it always did. A non-zero opponent must be a valid address, or the stake would stand for a player who can never exist and the poster's only way out would be Cancel. label is free text for the render, at most maxLabelLen bytes (checkLabel). "" is no label. Neither argument touches settlement: the coin still decides. commitHash IS THE POSTER'S HALF OF THE COIN AND IS NOW MANDATORY (changed 2026-09-30). It is H(commitTag ‖ secret) as 64 lowercase hex characters, for a secret of exactly 32 bytes that the poster keeps and opens at Reveal. There is no salt and no second field: read checkSecret for why one fixed-length field is the requirement and not a preference. THE SECRET MUST COME FROM A CSPRNG AND MUST BE FRESH FOR THIS OFFER. commitHash is a free offline oracle — anybody can test candidate secrets against it forever, at no cost — so a guessable secret is robbed SILENTLY, by a reveal this realm cannot tell from an honest one. There is no on-chain entropy check and there cannot be one. The full statement is item (1) of this file's doc comment; liveCommitments refuses the one reuse case a contract can see. THE CALLER MUST BE A USER, NOT A REALM, SINCE 2026-09-30 (requireUserCall, finding D-1). A realm poster that WINS cannot call the winner-only Collect, which would leave the pool — its own stake AND its opponent's — in escrow with no exit whatever. EVERY CHECK RUNS BEFORE THE PULL, so a failed Post moves nothing and creates nothing (SPEC-03 §8 tests 1 and 2) — and that now includes the commitment's shape and its uniqueness, so a rejected commitment never costs a stake.

Open
OID
0a537e…a358:52
Post details

Inspect func

Cancel

func(offerID int64)

Cancel returns an unaccepted offer's stake to its poster, EXACTLY and with no fee, and deletes the record. Poster only. An accepted offer no longer exists — Accept deletes it in the same transaction that creates the duel — so "Cancel of an accepted offer" and "Cancel of an unknown offer" are the same refusal. That is what makes the Cancel-racing-Accept rule (SPEC-03 §7.5) hold by construction: whichever lands first in block order removes the offer, and the other finds nothing. AND THAT RACE IS NO LONGER HARMLESS, WHICH IS DISCLOSED HERE RATHER THAN ONLY IN THE FILE HEADER. Under the beacon the poster could not predict the outcome, so cancelling under a pending Accept gained them nothing. Since 2026-09-30 the taker's nonce is plaintext in the Accept transaction, so a poster watching the mempool CAN compute the outcome and cancel the ones they are about to lose. It is item (2) of this file's doc comment, it is OPEN, and removing Cancel would not close it — the poster can take their own offer instead, for the rake. It is recorded at the door it is reached through. PERMISSIONLESS FOR A LEGACY OFFER, AND ONLY FOR ONE. An offer with no commitment (Offer.CommitHash == "") can never be accepted — Accept refuses it — so the poster's stake would otherwise sit in the lobby until the poster themselves turned up, and a pre-D-1 poster may be a REALM that contains no code able to call this function. So for that one case any caller may cancel, and it is not a way to steal: the refund's destination is READ FROM THE RECORD and is the poster either way. Nothing is deployed, so no such offer exists; this is requirement 5's "the code must not contain a trap".

Open
OID
0a537e…a358:53
Cancel details

Inspect func

Accept

func(offerID int64, nonce string) int64

Accept takes a standing offer: pulls a stake equal to THE OFFER'S rung — the caller chooses nothing about the money — deletes the offer, records the taker's NONCE and creates the duel. Returns the new duel's ID. nonce IS THE TAKER'S HALF OF THE COIN AND IT TRAVELS IN THE CLEAR: 32 bytes as 64 lowercase hex characters (checkNonce). No commitment and no reveal from the taker, and therefore NO EXTRA TRANSACTION — one more argument on the call they were already making. Read posterWins for why plaintext is safe from the taker's own side: they choose the nonce WITHOUT the secret, so every nonce they could pick is a coin they cannot evaluate, and choosing among coins you cannot see is not choosing. A CSPRNG is the right source here too, though for a different reason than the poster's: a nonce a poster could have ANTICIPATED (all zeroes, the taker's own address, a counter) lets the poster grind their secret against it at Post time, which is the one way the poster-cannot-grind property can be handed away by the other party. THE PLAINTEXT NONCE IS ALSO THIS DESIGN'S ONE OPEN WEAKNESS, and the taker should know it at the door they walk through: a poster watching the mempool can read the nonce out of this very transaction before it is committed, compute the outcome, and try to abort the Accept by cancelling or by self-matching. Item (2) of this file's doc comment has the argument, the two moves and why neither is closed here. AN OFFER WITH NO COMMITMENT IS REFUSED, AND THAT REFUSAL IS THE WHOLE OF REQUIREMENT 5 ON THIS SIDE. A legacy offer (created before 2026-09-30) carries no commitHash, so no secret exists that could ever open it and Reveal could never run. Accepting one would create a duel whose only exit after the window is Forfeit — which pays the TAKER — so anybody could have taken a legacy offer and been handed the poster's stake for nothing. So such an offer cannot be accepted at all; its exit is Cancel, permissionless for exactly this case. THERE IS NO ROUND, NO BEACON AND NOTHING TO WAIT FOR. Everything this function used to do about randomness is gone: no anchor, no staleness tolerance, no offset, no assertion that the bound round is in the future. Read this file's doc comment for why that binding could not be repaired. The consequence a player sees is that settlement is available IN THE NEXT BLOCK rather than 300 seconds and one 17M-gas relay later. The caller MAY be the poster (SPEC-03 §7.3): self-matching wins half the time and pays the rake regardless, a guaranteed 3% loss, so a guard would cost code and buy nothing. THAT REASONING NEEDED RE-CHECKING UNDER THE NEW COIN AND IT SURVIVES, in a sharper form worth writing down: a self-matcher holds the secret AND chooses the nonce, so they can grind the nonce and win with CERTAINTY — against themselves. Winner and loser are one address, the pool returns to it less the rake, and certainty about which pocket it lands in is worth nothing. It is still a guaranteed 3% loss, and it is still the case that the only party a self-match can take money from is the self-matcher. An offer ADDRESSED to an opponent is the one exception, and only by construction: then the opponent is the only address that may accept, and the poster is not it unless they named themself. THE CALLER MUST HOWEVER BE A USER AND NOT A REALM, SINCE 2026-09-30. That is the one thing about WHO may accept that this function refuses, and requireUserCall has the argument: a realm taker that wins cannot call the winner-only Collect, so the pool would have no exit at all — the poster's stake included. It is the raffle's finding-8 reasoning (a package-derived address nothing can spend from) applied to this realm's own escrow rather than only to the raffle entries a duel earns. AUDIT-duel-2026-09-27.md addendum 2, finding D-1. EVERY CHECK RUNS BEFORE THE PULL. The second of two Accepts on one offer in a block therefore finds no offer and fails before touching the caller's wallet: its stake is never pulled, so there is nothing to return (SPEC-03 §7.4). It cannot join, overwrite or silently succeed.

Open
OID
0a537e…a358:54
Accept details

Inspect func

Reveal

func(duelID int64, secret string)

Reveal opens the poster's commitment, names the winner, and is PERMISSIONLESS. IT IS Resolve, AND Resolve IS GONE — MERGED INTO THIS FUNCTION RATHER THAN KEPT BESIDE IT, WHICH REQUIREMENT 4 ASKS TO BE JUSTIFIED. Resolve's entire body was: read the outcome's source, compute the winner, store it, refuse a second call. This function is that same body with a different source — an argument instead of a cross-realm read of beacon.Randomness(d.Round). Keeping both would mean two doors onto ONE state transition, and the second door would have NO SOURCE AT ALL: there is no round to read, so a surviving Resolve could only either do nothing or invent an outcome. A vestigial settlement entry point on a realm with no owner and no upgrade path is precisely the shape this project keeps finding (a dead premise that outlives the thing it depended on), so the transition has exactly one door and the door names what it needs. THE HASH IS THE AUTHORISATION, SO THERE IS DELIBERATELY NO CALLER CHECK — REQUIREMENT 2, AND IT IS A SECURITY PROPERTY AND NOT A CONVENIENCE. Only somebody who holds the secret can pass the check below, and by the time this can be called the secret is worth nothing to anybody: the nonce is already fixed in the record, so the outcome is already determined and revealing cannot change it. Two things follow, both wanted: SETTLEMENT STAYS PERMISSIONLESS. A poster who has revealed to their opponent off-chain, or published the secret anywhere, can have the duel settled by any bystander — exactly as the beacon version let any bystander Resolve. Nothing about a duel's conclusion depends on one particular address being willing to send a transaction. AND NO TRUSTED SECRET-HOLDER IS CREATED. A caller check would make "the address that may reveal" a third role in the contract, and a poster who wanted a front end or a bot to settle for them would have to hand that role the secret while it still mattered. The secret would become a credential held off chain by a de-facto party to the duel. With no check, the secret is a credential for nothing. WHAT IT REFUSES: a duel that does not exist, a duel already revealed (so no later caller and no later secret can move a winner that is already fixed — the same once-and-only-once Resolve had), a secret of the wrong length or spelling (checkSecret), and a secret whose commitment is not this duel's. Nothing is paid here: Collect pays, and deletes. THE REVEAL WINDOW IS NOT CHECKED HERE, AND THAT IS ON PURPOSE. A late reveal is welcome as long as the pool is still there — an honest poster whose transaction was delayed keeps a duel they won, and nobody is harmed by a reveal on day two of an uncollected duel. What the window does is open Forfeit; the RACE between a late Reveal and a Forfeit is settled by block order, and both outcomes are legitimate. The suite drives that ordering both ways (reveal-after-expiry, and forfeit-then-reveal-refused).

Open
OID
0a537e…a358:55
Reveal details

Inspect func

Collect

func(duelID int64)

Collect pays the winner 2 x stake - rake, forwards the rake, and DELETES THE DUEL. Winner only. Conservation (SPEC-03 §7.2): payout + rake == 2 x stake exactly, and the rake's two parts sum exactly to the rake — the raffle pot share takes the remainder. THE RECORD IS DELETED BEFORE ANYTHING IS PAID. That single ordering is what makes a second Collect fail instead of double-paying (SPEC-03 §7.6): by the time the first transfer runs, there is no duel for a re-entrant or repeated call to find. It also reclaims the storage deposit (SPEC-03 §7). AND IT BOOKS THE ENTRIES THE RAKE EARNED ITS TWO PLAYERS (finding 15). Booked here, delivered by FlushPot with the money, and credited to nobody else: the winner takes a third, the LOSER takes two thirds, and the two sum to exactly what that rake would have bought at raffle.EntryUnitPrice. Read rakeEntryUnits for why the rake and never the stake is the price base — it is what stops two accounts flipping each other for cheap entries — and bookEntries for the two reasons a party can earn nothing (they played from realm code, or the undelivered batch is full). NEITHER CAN MAKE THIS FUNCTION FAIL: a winner's collect must never depend on a raffle entry. AN EARNED ENTRY IS NOT A COIN FLIP AND IT CARRIES A DISCLOSED, STILL-OPEN RISK THAT YOUR FLIP DOES NOT. The entry is in r/splash/arcade/raffle's weekly draw, which is settled by a drand round PINNED WHEN THE WEEK OPENED against an estimate of drand's current round derived from chain height. If that estimate drifts — slow blocks, or a beacon nobody has relayed to lately — the week's winning randomness can become publicly readable on api.drand.sh BEFORE the week's ticket sales close, and whoever reads it can buy last and size the buy to own the winning unit. THE POT THEY WOULD BE TAKING IS MOSTLY DUEL RAKE. It is bounded by the beacon's staleness refusal and by a measured margin, it is nobody's key and nobody's privilege, and it is recorded OPEN: the full disclosure, the bound and what it rests on are at rakeEntryUnits above, and the record is AUDIT-2026-09-28.md findings 4, 10, 12, 13 and 20. WHERE THE RAKE GOES (BIBLE.md §4.15 S12): the pot share through raffle.FundPot — the raffle's named, permissionless, caller-pays door, approved for exactly its amount in the same transaction, so a panic anywhere reverts the approval with everything else — and the ops share by plain transfer to r/splash/ops. REPOINTED AT THE RAFFLE 2026-09-28, AND THE SPLIT DID NOT MOVE. The players' side of the rake used to go through two doors, game.FundYield (5000 bps) and game.FundPrizes (the remainder, 3000 bps plus all dust). r/splash/game's permanent-deposit yield product is retired, so there is no yield to fund and the yield share is folded into the pot share rather than dropped: one call, carrying rake - opsShare, which is exactly what the two calls carried between them. The ops share is computed by the same floored formula as before, so the house takes neither more nor less than it did. Integer-division dust was the prize share's and is now the raffle pot's — it has always gone to the players' side, and still does. SPONSORSHIP BUYS NO TICKETS: the raffle records it as house money in the week's pot, attributable to no buyer, so this realm never gains a winning chance in a draw it funds. IT FUNDS WHATEVER WEEK IS LIVE WHEN THE DUEL IS COLLECTED, which is the point — every coin flip makes this Saturday's prize bigger — and needs no coordination: FundPot creates the week's record if the week has not sold a ticket yet, and the live week is uncommitted by construction, so a settlement can never arrive at a frozen pot. (The previous note here recorded that between cd29697 and 2026-09-28 the whole rake went by one transfer to the ops vault, because the game's doors pulled r/splash/token while this realm held wugnot. That detour is over: the players' side now reaches the players, in the one product they can play this week.)

Open
OID
0a537e…a358:56
Collect details

Inspect func

Forfeit

func(duelID int64)

Forfeit ends a duel whose poster never opened their commitment: after revealWindowBlocks the TAKER takes the pool on exactly the terms the winner of a revealed duel would have. PERMISSIONLESS, and the destination is READ FROM THE RECORD. WHY THE TAKER, AND WHY THIS IS THE WHOLE INCENTIVE ARGUMENT FOR THE DESIGN. After Accept the outcome is already determined — the secret is fixed, the nonce is in the record — but only the poster can see it. So the poster's choice is: reveal, or withhold. Paying the taker on withholding makes those two choices IDENTICAL IN MONEY when the poster has lost (they hand over the pool either way) and STRICTLY WORSE when the poster has won (they hand over a pool they could have taken). Withholding therefore never pays, at any rung, and the only thing it buys is up to revealWindowBlocks of delay — which costs the poster the same wait. Pay the POSTER on expiry instead and the design inverts: the poster would withhold every loss and the coin would be a one-way bet. PERMISSIONLESS, FOR THE REASON EVERY EXIT IN THIS FILE IS. The taker is the only party who gains, so the taker has the incentive — but their willingness to send a transaction must not be what stands between the pool and an exit. A bystander, a front end or a keeper may fire it; the money still goes where the record says. Requirement 3. AND IT SETTLES THROUGH THE SAME ARITHMETIC AS Collect, WHICH IS NOT A TIDINESS POINT BUT THE MOST-REPEATED DEFECT IN THIS PROJECT. It calls the same settle() — same rake, same split, same entries, same books. The alternative shapes were considered and both of the safe ones are: the same rake AND the same entries (this), or NO rake and NO entries (the VoidStale shape). WHAT MUST NEVER SHIP IS ENTRIES WITHOUT RAKE: checkInvariant panics on \`pendingEntryUnits > 0 && pendingPot \<= 0\`, so a forfeit that booked entries without booking the rake behind them would panic on its own exit — bricking THE ONLY EXIT this state has and stranding the pool with the books reading green. A separate copy of the payout arithmetic is exactly how that gets written, so there is no separate copy. A LEGACY DUEL IS REFUSED HERE, AND THAT REFUSAL IS THE POINT OF REQUIREMENT 5. A duel with no commitment can never be revealed by anybody, so its expiry is not a poster's silence — it is a certainty from the moment it was accepted. Forfeiting it would hand the taker the poster's stake for nothing. VoidStale refunds both stakes instead, and this function will not touch such a record. Unreachable since Accept refuses commitment-less offers. Panics if the duel does not exist, if it is already revealed (Collect is then the path, and the winner it names may not be the taker), if it carries no commitment, or if the window has not elapsed.

Open
OID
0a537e…a358:57
Forfeit details

Inspect func

settle

func(duelID int64, d *duel.Duel, winner .uverse.address, event string)

settle is THE ONE payout path: it pays \`winner\` pool - rake, books the players' side of the rake and the raffle entries it earned, pays the ops share, deletes the record and emits \`event\`. Collect and Forfeit are both one caller check and a call to this. ONE COPY, DELIBERATELY, AND THE REASON IS REQUIREMENT 3. Two exits that pay the same pool on the same terms must not have two copies of the rake-and-entry arithmetic, because the failure mode of a drifted copy is not a wrong number — it is a booked entry with no booked rake behind it, which checkInvariant refuses on the way out of the very function that created it. That panic would brick the exit and strand the pool while \`escrowed\`, AuditFailure and checkInvariant all still read green over it. Six of this product's findings have had the "money in, no way out" shape; this is the cheapest possible way to not add a seventh. \`winner\` IS AN ARGUMENT AND NOT READ FROM THE RECORD, because the two callers learn it differently: Collect reads d.Winner, which Reveal fixed from the coin, and Forfeit uses d.Taker, which the poster's silence fixed. Both are values of the record; neither is the caller's to choose. It is asserted below that the winner is a party to the duel, which is the property that actually matters and is cheap to state where the money moves.

Open
OID
0a537e…a358:58
settle details

Inspect func

bookEntries

func(holder .uverse.address, units int64, isUser bool) int64

bookEntries credits \`holder\` with \`units\` raffle entry units, booked in this realm for FlushPot to deliver, and returns what it actually booked — which is zero when the holder is not eligible or the batch is full. IT NEVER PANICS AND NEVER REFUSES ITS CALLER: a winner's Collect must not be able to fail over a raffle entry (AUDIT-2026-09-28.md finding 14), so every reason not to credit somebody is a zero and an event, not an abort. TWO REASONS A PARTY EARNS NOTHING, both deliberate and both visible in the log: - THE PARTY DID NOT PLAY FROM A USER CALL. A raffle entry is a claim on the raffle pushing a whole week's pot to that address, and a realm's address is package-derived — nothing can spend from it unless that realm contains code that does, so a realm winning a raffle week would strand the entire pot (AUDIT-2026-09-28.md finding 8, which is why raffle.Buy refuses realms outright). The raffle cannot check this for a bare address, so it is checked here, where the call was. The check is IsUserCall and not IsUser for the reason the raffle's Buy states: a \`maketx run\` ephemeral realm's address is package-derived too. THIS BRANCH IS UNREACHABLE FROM Post AND Accept SINCE 2026-09-30 AND IS KEPT DELIBERATELY. The sentence that stood here — "a realm may still post, accept, win and collect; it simply earns no entries" — described the state finding D-1 closed: settlement was open to realms, so a winning realm stranded 2 x stake in this realm's escrow with no exit, because Collect is winner-only. Both doors now require a direct user call (requireUserCall), so every party of every duel is eligible and \`isUser\` arrives true. The check stays because it is the ONLY place finding 8's property is enforced for entries — the raffle's FundPotWithEntries admits this package path and trusts it to have checked — so a future relaxation of either gate, or a future caller of bookEntries that is not Collect, must fail as a dropped entry and an event rather than as a raffle pot pushed to an address that cannot spend it. A guard that has become unreachable by a change elsewhere is cheap; re-deriving why it was needed is not. - THE BATCH IS FULL. See maxPendingEntryHolders: an unbounded batch could grow past what a block can deliver, which would brick the booked rake's primary exit. Any caller can empty the batch at any time by flushing.

Open
OID
0a537e…a358:59
bookEntries details

Inspect func

entryUnitsOf

func(k string) int64

entryUnitsOf reads the booked entry units for a tree key, zero if absent. avl's Get returns a single value, so existence is a separate Has.

Open
OID
0a537e…a358:60
entryUnitsOf details

Inspect func

pendingGraceBlocks

untyped bigint

pendingGraceBlocks is how long the YOUNGEST CLAIM in the booked batch must have stood before SweepStuckRake's last resort opens: 21 days of blocks, the same window r/splash/arcade/raffle gives a committed week before its refunds open. "THE YOUNGEST CLAIM", NOT "DELIVERY HAS BEEN FAILING", corrected 2026-09-30 by finding D-4. Nothing in this realm can observe a failed flush — a failed flush reverts, taking any record of itself with it — so no window here can measure what the paragraph below once said it measured. Read pendingSince for what it does measure and for the two consequences, one accepted and one closed. WHY A WINDOW AT ALL: without one, SweepStuckRake would be a free way for anyone to divert the players' side of every rake out of the prize pot and into the raffle's ops pot. The window is what makes it a last resort rather than a choice. WHY 21 DAYS AND NOT LESS: every refusal FundPot can raise except a raffle accounting deficit is transient, and the longest of them is bounded by a raffle WEEK — a committed or abandoned current week (both unreachable for the live week by construction, see FundPot) and a beacon nobody has relayed to (clearable by any caller with one Publish). Three weeks is comfortably longer than one, so the sweep cannot fire on a transient refusal that a single FlushPot would have cleared. THERE IS DELIBERATELY NO SECONDS-ON-BOTH-SIDES BUILD GUARD ON THIS ONE, and that is a considered omission rather than the defect AUDIT-2026-09-28.md finding 11 is about. What this window has to outlast is a raffle week, which is itself a BLOCK count in the raffle (raffleWeekDays \* blocksPerDay) — so both sides would come from the same clock, the block time would cancel, and the guard could not fail. A guard that cannot fail is worse than none, because it looks like protection. The relation is stated here instead: pendingGraceBlocks must stay several raffle weeks long, and the raffle's week is blocks, so the two move together whatever the block time turns out to be.

Value

(549822 <untyped> bigint)

FlushPot

func() int64

FlushPot delivers the whole booked rake to r/splash/arcade/raffle's CURRENT week and returns the amount delivered. PERMISSIONLESS: anyone may pay the gas to do it, and the destination is not theirs to choose. THIS IS THE FUNCTION Collect USED TO BE THE SECOND HALF OF. The split is the whole fix: a raffle that will not accept the rake now costs the CALLER OF THIS FUNCTION a reverted transaction and nothing else. The winner has already been paid, the duel record is already gone, and pendingPot is still here for the next attempt. Nothing about a duel depends on the raffle any more. BOOKS FIRST, CALL SECOND, and the revert is what makes that safe. pendingPot is zeroed BEFORE raffle.FundPot runs, so a re-entrant call finds nothing to send twice; and if FundPot panics, the panic aborts this whole transaction and discards the zeroing with it, so the balance is preserved exactly. (Nothing on-chain can catch that panic and continue — the same atomicity Accept and Collect already rely on.) THE APPROVAL IS FOR EXACTLY THE AMOUNT AND IN THE SAME TRANSACTION, as it was in Collect, so a revert takes the approval with it and this realm never leaves a standing allowance behind. IT FUNDS WHATEVER WEEK IS LIVE WHEN IT RUNS, which is a behaviour change from "whatever week was live when the duel was collected" and is recorded rather than hidden. A rake booked on Saturday and flushed on Monday enlarges Monday's week's prize. Nobody's claim moves — sponsorship buys no tickets in any week — and any player who wants their own flip to swell this week's pot can flush it themselves in the same block they collect. Panics if there is nothing booked. AND IT DELIVERS THE EARNED ENTRIES WITH THE MONEY, IN ONE CALL (finding 15). The batch is built from pendingEntries and handed to raffle.FundPotWithEntries, which funds the pot through the same FundPot and appends one ticket index per holder. One call rather than two because the money and the entrants must land in the SAME week: a delivery that could split across a week boundary would put the rake in one draw and the entrants in another, which is the defect finding 15 is about, only subtler. THE ENTRIES FOLLOW THE MONEY'S WEEK, WHICH IS THE WEEK LIVE WHEN THE FLUSH RUNS, AND THAT IS A DELIBERATE CHOICE RATHER THAN THE ONLY ONE. A rake booked on Saturday and flushed on Monday enlarges Monday's pot and earns its entries in Monday's draw — the money and the entrants stay together, which is the property that matters, and any player who wants their own flip in THIS week's draw can flush in the same block they collect (the suite's collectAs helper does exactly that, as it already did for the money). The alternative — booking the settlement's week number and crediting THAT week — was rejected twice over: it would let this realm write ticket weight into a week that is already committed or abandoned, which is the frozen-total rule the raffle refuses Buy for (findings 2 and 10), and a week that had moved on would leave the batch permanently undeliverable, which is a "money in, no way out" shape.

Open
OID
0a537e…a358:61
FlushPot details

Inspect func

SweepStuckRake

func() int64

SweepStuckRake is the LAST RESORT for a booked rake the raffle will not take, and it exists so that pendingPot is not a place money can enter and never leave. It transfers the whole booked amount to the raffle's address by PLAIN wugnot.Transfer and returns it. PERMISSIONLESS, and only once the batch's YOUNGEST CLAIM has stood for pendingGraceBlocks (pendingSince, finding D-4 — it is not, and cannot be, a measure of delivery having been attempted and refused). WHY IT CANNOT BE REFUSED, which is the only property that makes it a real exit: a plain GRC20 transfer runs no raffle code. FlushPot goes through raffle.FundPot and can therefore be refused by anything in the raffle — that is exactly the state this function is for. This one cannot be. AND IT DROPS THE EARNED ENTRIES WITH THE MONEY, WHICH IS THE OTHER HALF OF THE COST (finding 15). The money on this path becomes the raffle's ops pot, so there is no prize left for an entry to be an entry in, and an entry with no rake behind it is a thing this realm's own invariant refuses. So players who earned entries in a batch that reached this function get nothing for them. It takes three weeks of the raffle refusing EVERY caller, the drop is in this function's event, and any player could have prevented it at any moment by flushing — for between 10,378,157 gas (a two-holder batch, measured 2026-09-30) and 351,725,503 (a FULL 128-holder batch against a week grown to 100,096 ticket indices). Read maxPendingEntryHolders for where each figure comes from and why only the first is reproducible by running this package. THE FULL-BATCH END OF THAT RANGE IS THE ONE THAT APPLIES HERE, and citing only the cheap end was a real misstatement, corrected 2026-09-30. This paragraph is about a batch that has been accumulating for three weeks of refusals, i.e. the regime in which the batch is as large as maxPendingEntryHolders allows — so quoting a two-holder figure understated the mitigation's cost by more than thirty times in exactly the paragraph where it is dearest. (The figure said ~1.1M until 2026-09-29 and was stale by finding 15 — see maxPendingEntryHolders and AUDIT-2026-09-28.md finding 19. The argument every version of it supports is unchanged: at 11.7% of a block it is still one affordable transaction available to anybody at any height. AUDIT-duel-2026-09-27.md addendum 2, findings D-4 and D-6.) WHERE IT LANDS, AND THE HONEST COST OF THAT. An unattributed arrival at the raffle is swept by its sweepDonations into its OPS pot, not into any week's prize — the raffle says why: crediting an unattributed arrival to a prize pot would break the identity that makes its refunds safe. So this money reaches the house rather than the players, and the 80/20 split is therefore NOT preserved on this path. That is stated plainly because it is the one place in this change where the split moves: it takes three weeks of the raffle refusing EVERY caller for it to happen, it emits its own event, and it is the alternative to the money never leaving at all. AND THE DESTINATION IS THE RAFFLE, NOT r/splash/ops, DELIBERATELY. gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/ops does not exist — the ops constant in this file is a package-derived address with no code at it — so routing a NEW stream there would be creating exactly the "money in, no way out" shape that six of this product's findings have had. The raffle's ops pot has an exit (WithdrawOps), and whether THAT exit's destination is reachable is a pre-existing question about r/splash/ops which is the owner's and is not made worse here. THE COST OF HAVING A WINDOW AT ALL, RECORDED RATHER THAN LEFT TO BE FOUND: after 21 days in which nobody bothered to call FlushPot — even with the raffle perfectly willing — any caller may move the booked rake to the raffle's ops pot instead of its prize pot. That is a value shift from the players to the house, it costs the caller gas and gains them nothing personally, and it is preventable by anyone at any time by simply flushing — for 10,378,157 gas at two holders and 351,725,503 at a FULL 128-holder batch, which is the end of the range that applies after three weeks of accumulation (see maxPendingEntryHolders for each figure's provenance; corrected from a bare "~8.6M" on 2026-09-30 and from "~1.1M" on 2026-09-29, findings D-4, D-6 and 19). It is accepted because the alternatives are worse: no window makes this a free diversion available on the first block, and no function at all leaves the balance with a single exit that another realm can hold shut for ever. AUDIT-2026-09-28.md finding 14. Panics if there is nothing booked, or if the window has not elapsed.

Open
OID
0a537e…a358:62
SweepStuckRake details

Inspect func

PendingPot

func() int64

PendingPot is the players' side of the rake booked and not yet delivered to the raffle. O(1) read.

Open
OID
0a537e…a358:63
PendingPot details

Inspect func

PendingSince

func() int64

PendingSince is the height SweepStuckRake's window is counted from: the height at which the booked rake last went from zero to non-zero, or at which the batch's most recent NEW HOLDER was admitted, whichever is later. 0 when nothing is booked. O(1) read. Read pendingSince for why a new holder moves it (finding D-4).

Open
OID
0a537e…a358:64
PendingSince details

Inspect func

PendingSweepOpensAt

func() int64

PendingSweepOpensAt is the height at which SweepStuckRake's last resort becomes available, or 0 when nothing is booked. FlushPot is available at every height. IT MOVES LATER WHEN A NEW HOLDER JOINS THE BATCH (finding D-4), so a front end that wants to warn a player must re-read it rather than cache it — and a player who has just earned entries can see that their own claim opened the window they are measured against.

Open
OID
0a537e…a358:65
PendingSweepOpensAt details

Inspect func

PendingEntryUnits

func() int64

PendingEntryUnits is the total raffle entry units booked and not yet delivered. O(1).

Open
OID
0a537e…a358:66
PendingEntryUnits details

Inspect func

PendingEntryHolders

func() int64

PendingEntryHolders is how many distinct addresses the undelivered batch holds. O(1); avl keeps its size. maxPendingEntryHolders is the bound (EntryHolderLimit).

Open
OID
0a537e…a358:67
PendingEntryHolders details

Inspect func

PendingEntriesOf

func(holder .uverse.address) int64

PendingEntriesOf is the raffle entry units booked for one address and not yet delivered: what a player has earned by playing and not yet had credited in the raffle. Zero once a flush has delivered them, at which point the units are the raffle's to report (raffle.TicketsOfWeek). O(log n).

Open
OID
0a537e…a358:68
PendingEntriesOf details

Inspect func

EntryHolderLimit

func() int64

EntryHolderLimit is maxPendingEntryHolders, exported so a front end can say why a player's entries were dropped rather than leaving them to guess. Read maxPendingEntryHolders for what happens at the bound and why it is a drop rather than a refusal.

Open
OID
0a537e…a358:69
EntryHolderLimit details

Inspect func

EntryUnitsForRake

func(rake int64) int64

EntryUnitsForRake is rakeEntryUnits, readable: how many raffle entry units a rake of \`rake\` ugnot earns, at raffle.EntryUnitPrice. A front end can quote a rung's entries with StakeOfRung, and anybody can check the rate that makes this feature safe without reading the source. Plain arithmetic, no state.

Open
OID
0a537e…a358:70
EntryUnitsForRake details

Inspect func

EntriesForRung

func(rung int64) (winnerUnits int64, loserUnits int64)

EntriesForRung is what one duel at \`rung\` earns its two players: the winner's share and the loser's, which sum to the rake-equivalent. Panics on an invalid rung index, exactly as Post would.

Open
OID
0a537e…a358:71
EntriesForRung details

Inspect func

DuelEntryEligibility

func(duelID int64) (bool, bool)

DuelEntryEligibility reports whether each party of a duel played from a direct user call and will therefore earn raffle entries when it is collected: poster first, taker second. Both false for a duel that does not exist. A SEPARATE READ RATHER THAN TWO MORE ELEMENTS OF DuelOf, deliberately: DuelOf keeps the six-value shape SPEC-03 §4 published, and an arity change there is mechanical churn in every caller (the raffle's own audit records doing it three times in one file). Read Duel.PosterIsUser for what these flags do and do not affect — they have no bearing on who wins or who is paid.

Open
OID
0a537e…a358:72
DuelEntryEligibility details

Inspect func

VoidStale

func(duelID int64)

VoidStale refunds BOTH STAKES IN FULL, with no rake and no entries, for a duel that CAN NEVER BE SETTLED BY ANY CALLER — which since 2026-09-30 means exactly one thing: a LEGACY duel, accepted before the commit-reveal coin, carrying no commitment. PERMISSIONLESS. ITS PREMISE CHANGED COMPLETELY AND THE NAME IS KEPT ON PURPOSE. It was "the beacon-outage escape (SPEC-03 §6)": either party, after a 24-hour grace, when the bound round was STILL unpublished, because a stopped publisher would otherwise freeze every pending duel with both stakes locked on a contract that cannot be patched. THERE IS NO PUBLISHER AND NO ROUND ANY MORE, so that state does not exist: the poster can always reveal, and after revealWindowBlocks anybody can Forfeit. What survives is the one shape the old function was really for — a duel with no reachable settlement — and the only way to be in it now is to have been created before the change. The name is kept because it is the published exit for "this duel cannot settle, unwind it"; renaming it would churn every caller to say the same thing. WHY IT MUST EXIST AND MUST NOT BE Forfeit, WHICH IS REQUIREMENT 5. A legacy duel's poster has no secret and never had one: no commitment was recorded, so no preimage exists and Reveal cannot run for anybody. Routing that to Forfeit would pay the TAKER the poster's stake for a silence the poster could not have broken — and worse, it would make taking a legacy offer a free stake, which is why Accept refuses those outright. Both stakes go back. NO GRACE WINDOW, AND NO CALLER CHECK. Both were dropped, and both for the same reason: the record PROVES its own unsettleability from a field on itself (CommitHash == ""), so there is nothing to wait for and nothing for a caller check to protect. Waiting would only hold two refunds hostage to a clock; requiring a party would strand the pool if that party were a pre-D-1 realm with no code able to call anything. The destination is read from the record and is both parties, so a permissionless door cannot name a payee. NEVER ON A REVEALED DUEL, or it would be a way for a loser to cancel a loss — so that check comes first, as it always did. Unreachable together with the commitment check (nothing with an empty commitment can be revealed), and both are stated. NOTHING IS DEPLOYED, SO NO RECORD OF THIS SHAPE EXISTS TODAY. The lobby will be drained as a deployment step regardless. This function is here so that the code contains no trap: requirement 5 is that legacy records must not route to forfeit, and a comment saying "there are none" is not that.

Open
OID
0a537e…a358:73
VoidStale details

Inspect func

OfferOf

func(offerID int64) (.uverse.address, int64, int64)

OfferOf reports an offer: poster, rung, posted-at height. All zero for an offer that does not exist (including one that was cancelled or accepted — both delete it). Plain read. Keeps the three-value shape SPEC-03 §4 published; the challenge fields are read through OfferChallenge.

Open
OID
0a537e…a358:74
OfferOf details

Inspect func

OfferChallenge

func(offerID int64) (.uverse.address, string)
Open
OID
0a537e…a358:75
OfferChallenge details

Inspect func

OfferCommitment

func(offerID int64) string

OfferCommitment is the commitment an offer stands behind: H(commitTag ‖ secret) as 64 lowercase hex characters. "" for an offer that does not exist AND for a LEGACY offer — which a caller can tell apart with OfferOf, since a non-existent offer has a zero poster. A taker should read this before accepting: an offer with no commitment cannot be accepted at all (Accept refuses it). A SEPARATE READ RATHER THAN A SEVENTH VALUE ON OfferOf, for the reason OfferChallenge gives: OfferOf keeps the three-value shape SPEC-03 §4 published.

Open
OID
0a537e…a358:76
OfferCommitment details

Inspect func

DuelLabel

func(duelID int64) string

DuelLabel is the label a duel was accepted under, "" if none or if the duel does not exist. Raw, as OfferChallenge. DuelOf keeps its six-value shape.

Open
OID
0a537e…a358:77
DuelLabel details

Inspect func

DuelCommitment

func(duelID int64) (commitHash string, nonce string, secret string)

DuelCommitment is everything the coin is made of: the poster's commitment, the taker's nonce, and the secret once it has been revealed ("" before that). All three empty for a duel that does not exist, and the commitment empty for a LEGACY duel. IT IS WHAT MAKES A SETTLED DUEL CHECKABLE BY ANYBODY. With these three values and the duel's ID, a caller recomputes H(commitTag ‖ secret) against the commitment and H(outcomeTag ‖ secret ‖ nonce ‖ duelID) against the winner DuelOf reports, off chain, with no trust in this realm at all. The beacon version could not offer that: its randomness lived in another realm and was addressed by a round number.

Open
OID
0a537e…a358:78
DuelCommitment details

Inspect func

DuelOf

func(duelID int64) (.uverse.address, .uverse.address, int64, int64, bool, .uverse.address)

DuelOf reports a duel: poster, taker, rung, REVEAL DEADLINE, resolved, winner. All zero for a duel that does not exist — which, after Collect, Forfeit or VoidStale, is how a caller can see the record really was deleted (SPEC-03 §8 test 19). Plain read. THE FOURTH VALUE IS NO LONGER A BEACON ROUND, AND IT IS REPLACED RATHER THAN REMOVED. It used to be \`Round\` — the drand round the duel settled against — and there is no round any more. The slot now holds THE BLOCK HEIGHT AT WHICH THE REVEAL WINDOW CLOSES, i.e. AcceptedAt + revealWindowBlocks: the height from which anybody may Forfeit this duel to its taker. That keeps the six-value shape SPEC-03 §4 published — an arity change here is mechanical churn in every caller, which the raffle's audit records doing three times in one file — and it keeps the slot's MEANING, which was always "the clock this duel settles against". It is a DERIVED value, not a stored one; a duel carries AcceptedAt and the window is a constant. A CALLER THAT WANTS THE OLD FIELD WILL NOT FIND IT AND MUST NOT GUESS. Reading this as a round number gives a nonsense round in the tens of thousands, which is why the change is stated here, in the event log (DuelAccept's "revealBy" replaced "round"), in Render's column heading and in the addendum — a silently reinterpreted return value is the worst possible way to carry a change like this.

Open
OID
0a537e…a358:79
DuelOf details

Inspect func

RevealWindowBlocks

func() int64

RevealWindowBlocks is revealWindowBlocks, exported so a front end can say how long a poster has to open their commitment before the duel can be forfeited to the taker, without hardcoding the number or reading it out of Render.

Open
OID
0a537e…a358:80
RevealWindowBlocks details

Inspect func

ForfeitOpensAt

func(duelID int64) int64

ForfeitOpensAt is the height from which anybody may Forfeit a duel to its taker: the same value DuelOf returns in its fourth slot. 0 for a duel that does not exist, and 0 for a LEGACY duel — which is never forfeitable at all and goes to VoidStale instead, so a front end that offers a forfeit button must treat 0 as "not this one".

Open
OID
0a537e…a358:81
ForfeitOpensAt details

Inspect func

PageLimit

int64

PageLimit is the most IDs one OfferIDs or DuelIDs call returns. A larger limit is clamped to it, not refused.

Value

100

OfferCount

func() int64

OfferCount is the number of offers standing in the lobby. O(1): avl keeps its size.

Open
OID
0a537e…a358:82
OfferCount details

Inspect func

DuelCount

func() int64

DuelCount is the number of duels in flight: accepted and not yet collected or voided, resolved or not. O(1).

Open
OID
0a537e…a358:83
DuelCount details

Inspect func

OfferIDs

func(offset int64, limit int64) []int64

OfferIDs is a window over the standing offers' IDs in ascending order: the window skips \`offset\` offers and holds at most min(limit, PageLimit). Empty past the end. Negative arguments are a caller error and panic.

Open
OID
0a537e…a358:84
OfferIDs details

Inspect func

DuelIDs

func(offset int64, limit int64) []int64

DuelIDs is OfferIDs for the duels in flight.

Open
OID
0a537e…a358:85
DuelIDs details

Inspect func

idsOf

func(tree *v0.Tree, offset int64, limit int64) []int64

idsOf is the one window walk both listers share. IterateByOffset is avl's own offset-and-count traversal, O(log n + limit); it is not a full scan followed by a slice.

Open
OID
0a537e…a358:86
idsOf details

Inspect func

idOfKey

func(key string) int64

idOfKey inverts idKey. A key that does not parse cannot have come from idKey, so it is a corrupt tree and not a case to paper over.

Open
OID
0a537e…a358:87
idOfKey details

Inspect func

StakeOfRung

func(rung int64) int64

StakeOfRung is the ladder, readable: the stake a rung index costs. Panics on an invalid index, exactly as Post would.

Open
OID
0a537e…a358:88
StakeOfRung details

Inspect func

RungCount

func() int64

RungCount is the number of rungs on the ladder.

Open
OID
0a537e…a358:89
RungCount details

Inspect func

Escrowed

func() int64

Escrowed is the wugnot this realm holds for open offers and uncollected duels. O(1); maintained incrementally.

Open
OID
0a537e…a358:90
Escrowed details

Inspect func

AuditFailure

func() string

AuditFailure re-derives escrowed by iterating every record and returns the empty string when the books balance, otherwise why not. O(n) and READ-ONLY; never called from a state-changing path — checkInvariant is the O(1) guard for those. Exists so anyone can verify off-chain, and so the tests can.

Open
OID
0a537e…a358:91
AuditFailure details

Inspect func

AuditInvariant

func() bool

AuditInvariant is AuditFailure as a bool.

Open
OID
0a537e…a358:92
AuditInvariant details

Inspect func

renderCell

func(s string) string

renderCell is the ONLY way user-originated text reaches Render's output, and everything below that prints an address or a label goes through it. The rule (SPEC-v1.md §7.6) is that any user-originated text reaching Render is sanitised, not that it is assumed clean. sanitize.InlineText escapes markdown and HTML metacharacters and folds any line break to a space; the pipe is escaped on top of that because every such value here is printed inside a markdown table cell, where a raw pipe would break the row. (This is what sanitize.TableCell does too; it is spelled out here so the escaping is visible at the one place it happens.) Applied exactly once per value: the escapers are not idempotent.

Open
OID
0a537e…a358:93
renderCell details

Inspect func

renderAddr

func(a .uverse.address) string

renderAddr renders an address. Every address this realm stores came from cur.Previous().Address() or passed IsValid and is bech32 — but it is sanitised anyway, for the reason renderCell gives.

Open
OID
0a537e…a358:94
renderAddr details

Inspect func

renderLabel

func(label string) string

renderLabel renders a label, which IS arbitrary user text (within checkLabel's bounds) and is the one place in this realm where the sanitiser is not a formality. "-" for no label, so the column is never visually empty.

Open
OID
0a537e…a358:95
renderLabel details

Inspect func

renderOpponent

func(a .uverse.address) string

renderOpponent renders an offer's opponent column: "anyone" for an open offer, the address otherwise.

Open
OID
0a537e…a358:96
renderOpponent details

Inspect func

Render

func(path string) string

Render is the realm's human-readable view: the ladder, the open lobby and the duels in flight. It ITERATES THE avl.Trees in key order, which is ID order because idKey is fixed-width, so the output is byte-identical across calls and across nodes (SPEC-v1.md §7.7). Only the empty path is meaningful; any other path is answered without echoing it.

Open
OID
0a537e…a358:97
Render details

Inspect func
rakeBps : untyped bigint =(300 <untyped> bigint)
bpsDenom : untyped bigint =(10000 <untyped> bigint)
rakePotShareBps : untyped bigint =(6000 <untyped> bigint)
rakeOpsShareBps : untyped bigint =(4000 <untyped> bigint)
rung0 : untyped bigint =(10000000 <untyped> bigint)
rung1 : untyped bigint =(50000000 <untyped> bigint)
rung2 : untyped bigint =(200000000 <untyped> bigint)
rung3 : untyped bigint =(1000000000 <untyped> bigint)
maxStake : untyped bigint =(1000000000 <untyped> bigint)
maxStakeHeadroom : untyped bigint =(230584300921369 <untyped> bigint)
rungs : [4]int64 Inspect
init.11 : func() Inspect
blocksPerDay : untyped bigint =(26182 <untyped> bigint)
revealWindowBlocks : untyped bigint =(26182 <untyped> bigint)
raffleRealmAddr : .uverse.address =<gnolang.StringValue>
opsVaultAddr : .uverse.address =<gnolang.StringValue>
maxLabelLen : untyped bigint =(64 <untyped> bigint)
Offer : type =duel.Offer
Duel : type =duel.Duel
offers : *v0.Tree Inspect
duels : *v0.Tree Inspect
nextOfferID : int64 =3
nextDuelID : int64 =3
liveCommitments : *v0.Tree Inspect
escrowed : int64 =120000000
pendingPot : int64 =0
pendingSince : int64 =0
pendingEntries : *v0.Tree Inspect
pendingEntryUnits : int64 =0
maxPendingEntryHolders : untyped bigint =(128 <untyped> bigint)
realmSelf : .uverse.address =<gnolang.StringValue>
selfAddr : func() .uverse.address Inspect
idKeyWidth : untyped bigint =(19 <untyped> bigint)
idZeroPad : untyped string ="0000000000000000000"
idKey : func(id int64) string Inspect
getOffer : func(id int64) *duel.Offer Inspect
getDuel : func(id int64) *duel.Duel Inspect
stakeOf : func(rung int64) int64 Inspect
mulDiv : func(a uint64, b uint64, d uint64) uint64 Inspect
poolOf : func(stake int64) int64 Inspect
rakeOf : func(pool int64) int64 Inspect
splitRake : func(rake int64) (potShare int64, opsShare int64) Inspect
rakeEntryUnits : func(rake int64) int64 Inspect
splitEntries : func(total int64) (winnerUnits int64, loserUnits int64) Inspect
maxEntryUnitsFor : func(potShare int64) int64 Inspect
hexLen32 : untyped bigint =(64 <untyped> bigint)
commitTag : untyped string ="splash/duel/v1/commit:"
outcomeTag : untyped string ="splash/duel/v1/result:"
isLowerHex32 : func(s string) bool Inspect
checkCommitment : func(commitHash string) Inspect
checkNonce : func(nonce string) Inspect
checkSecret : func(secret string) Inspect
commitmentOf : func(secret string) string Inspect
posterWins : func(secret string, nonce string, duelID int64) bool Inspect
commitKey : func(poster .uverse.address, commitHash string) string Inspect
requireFreshCommitment : func(poster .uverse.address, commitHash string) Inspect
holdCommitment : func(poster .uverse.address, commitHash string) Inspect
releaseCommitment : func(poster .uverse.address, commitHash string) Inspect
checkLabel : func(label string) Inspect
checkInvariant : func() Inspect
requireUserCall : func(what string) Inspect
rejectAttachedCoin : func(what string) Inspect
Post : func(rung int64, opponent .uverse.address, label string, commitHash string) int64 Inspect
Cancel : func(offerID int64) Inspect
Accept : func(offerID int64, nonce string) int64 Inspect
Reveal : func(duelID int64, secret string) Inspect
Collect : func(duelID int64) Inspect
Forfeit : func(duelID int64) Inspect
settle : func(duelID int64, d *duel.Duel, winner .uverse.address, event string) Inspect
bookEntries : func(holder .uverse.address, units int64, isUser bool) int64 Inspect
entryUnitsOf : func(k string) int64 Inspect
pendingGraceBlocks : untyped bigint =(549822 <untyped> bigint)
FlushPot : func() int64 Inspect
SweepStuckRake : func() int64 Inspect
PendingPot : func() int64 Inspect
PendingSince : func() int64 Inspect
PendingSweepOpensAt : func() int64 Inspect
PendingEntryUnits : func() int64 Inspect
PendingEntryHolders : func() int64 Inspect
PendingEntriesOf : func(holder .uverse.address) int64 Inspect
EntryHolderLimit : func() int64 Inspect
EntryUnitsForRake : func(rake int64) int64 Inspect
EntriesForRung : func(rung int64) (winnerUnits int64, loserUnits int64) Inspect
DuelEntryEligibility : func(duelID int64) (bool, bool) Inspect
VoidStale : func(duelID int64) Inspect
OfferOf : func(offerID int64) (.uverse.address, int64, int64) Inspect
OfferChallenge : func(offerID int64) (.uverse.address, string) Inspect
OfferCommitment : func(offerID int64) string Inspect
DuelLabel : func(duelID int64) string Inspect
DuelCommitment : func(duelID int64) (commitHash string, nonce string, secret string) Inspect
DuelOf : func(duelID int64) (.uverse.address, .uverse.address, int64, int64, bool, .uverse.address) Inspect
RevealWindowBlocks : func() int64 Inspect
ForfeitOpensAt : func(duelID int64) int64 Inspect
PageLimit : int64 =100
OfferCount : func() int64 Inspect
DuelCount : func() int64 Inspect
OfferIDs : func(offset int64, limit int64) []int64 Inspect
DuelIDs : func(offset int64, limit int64) []int64 Inspect
idsOf : func(tree *v0.Tree, offset int64, limit int64) []int64 Inspect
idOfKey : func(key string) int64 Inspect
StakeOfRung : func(rung int64) int64 Inspect
RungCount : func() int64 Inspect
Escrowed : func() int64 Inspect
AuditFailure : func() string Inspect
AuditInvariant : func() bool Inspect
renderCell : func(s string) string Inspect
renderAddr : func(a .uverse.address) string Inspect
renderLabel : func(label string) string Inspect
renderOpponent : func(a .uverse.address) string Inspect
Render : func(path string) string Inspect