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.

v2 state

Back to all declarations

TicketPrice

int64

Value

1000000

houseCutBps

int64

Value

500

bpsDenom

int64

Value

10000

cutPerTicket

int64

Value

50000

opsVaultAddr

.uverse.address

Value

<gnolang.StringValue>

blocksPerDay

untyped bigint

blocksPerDay is the block-count day unit, copied verbatim from r/splash/game, r/splash/arcade/raffle, r/splash/arcade/duel and r/splash/beacon, all of which use the same figure: this toolchain exposes no block-timestamp getter, so the only clock a realm can read is chain height, and 26182 blocks is one day at the assumed 3.29998 s a block. IT IS AN ASSUMPTION AND IT IS DEPLOYMENT-BLOCKING, and that is the beacon's finding-12 residual, inherited here unchanged: if the chain's average block time settles persistently BELOW 3.29998 s, beacon.EstimatedLatestRound's refusal becomes permanent and no new window can ever be opened in this realm. Already-open windows still resolve or refund on their own clocks and WithdrawOps still runs, so no money is trapped; new business stops. Measured on live mainnet 2026-09-28 at 3.306-3.311 s, i.e. on the SAFE side of that threshold by ~0.2%, which is not much margin — see beacon.maxEstimateStaleBlocks.

Value

(26182 <untyped> bigint)

windowBlocks

untyped bigint

windowBlocks is ONE DRAW WINDOW in blocks: one hour. THE DERIVATION. blocksPerDay / 24 = 26182/24 = 1090.9167, and a block count must be an integer, so the integer division truncates to 1090 blocks. At the assumed 3.29998 s a block that is 3596.98 real seconds — 3.02 s short of an hour, an error of 0.08%, which is two orders of magnitude smaller than the ±10% block-time band every margin below is stated against. Rounding UP to 1091 (3600.28 s) would be equally defensible; truncation is chosen because \`blocksPerDay / 24\` is a derivation a reader can check without a calculator, and because a slightly SHORT window is the safe direction for the pin guard (the pin must outlast the window). THERE IS NO INTERMEDIATE DAY UNIT AND NO windowHours DIAL. The raffle has currentDay() and raffleWeekDays because a week is naturally seven of something; an hour is not naturally n of anything shorter that this chain can measure, so this constant is the single dial and currentWindow() divides by it directly.

Value

(1090 <untyped> bigint)

randomnessHexLen

untyped bigint

randomnessHexLen is the length of a beacon randomness string: sha256, hex-encoded, 64 characters.

Value

(64 <untyped> bigint)

roundsPerWindow

untyped bigint

roundsPerWindow is one draw window in DRAND ROUNDS: 28800/24 = 1200 rounds an hour at drand evmnet's three-second period. IT IS TAKEN FROM THE BEACON AND IS NOT A NUMBER TYPED INTO THIS FILE, and that is the single most important line of the migration this realm inherits. The weekly raffle carried \`const beaconRoundsPerDay = 288\` — the cadence of the beacon SPLASH itself used to run, one round every five minutes — and when r/splash/beacon switched to drand evmnet (period THREE seconds) every round-denominated constant derived from it was wrong by a factor of 100 the moment the key changed, silently re-opening finding 10. Reading the cadence from beacon.RoundsPerDay means the two can never disagree again. See AUDIT-2026-09-28.md section A1.

Value

(1200 <untyped> bigint)

pinOffsetWindows

untyped bigint

\--- pinning the window's beacon round ----------------------------------- THE ROUND A WINDOW IS DRAWN ON IS FIXED WHEN THE WINDOW'S RECORD IS CREATED — by its first ticket sale — AND NEVER WHEN IT IS COMMITTED. getOrCreateDraw writes Draw.Round exactly once and nothing else in this file ever writes it again. WHY, IN ONE PARAGRAPH, BECAUSE THE FULL ACCOUNT IS AUDIT-2026-09-28.md FINDING 10 AND IT IS THE WORST DEFECT OF THAT DAY. If the committer chose the round — the raffle bound Round = beacon.Latest() + 1 inside Commit — then the round was chosen AFTER the ticket set was already frozen and public, so anybody who could produce or foresee a signature could enumerate candidate rounds off-chain, compute ownerOfUnit(d, randomness(r) mod FrozenTotal) for each, and steer the draw onto one that named them. Pinning at record creation removes the choice entirely: at the instant the number is written there is no ticket set for it to be advantageous against, and Commit has nothing to compute. THE PROPERTY THE OFFSET MUST HAVE, AND IT IS A REAL-TIME PROPERTY: THE PINNED ROUND MUST BE SIGNED AFTER ENTRIES CLOSE. If it is signed before, the last buyer of the window can read its randomness off api.drand.sh, compute the winning unit for every candidate purchase size, and buy exactly enough tickets to land on a unit they own. That is not a theoretical attack against drand: drand's randomness is PUBLIC the moment it is signed, which makes this strictly easier than the version finding 10 describes (there it needed the beacon's key). THE WORST CASE IS A WINDOW WHOSE RECORD IS CREATED IN ITS FIRST BLOCK: the whole window still has to elapse before entries close, so the offset must exceed ONE WHOLE WINDOW of real time. That is 3597 s at the nominal block time, and more when the chain is slow, because a window is a BLOCK count while the offset is a ROUND count and drand rounds are real time by definition. THE DERIVATION OF THE MARGIN, WHICH IS ONE WHOLE WINDOW AND NOT A FRACTION OF ONE. The raffle's margin is one day on a seven-day week — 1/7 — and copying that ratio here would give ~8.5 minutes, which is NOT ENOUGH, and the reason is the term that does not shrink with the cadence: the beacon's own admitted extrapolation error. beacon.EstimatedLatestRound is a line fitted at the assumed block time, so its error is |A - B| seconds of drand time per block since its anchor was last raised — 0.33 s a block at the edges of the ±10% band — and over the beacon's default backstop of one day of blocks that is 8640 s, 2.4 HOURS. That single term is 2.4 times a whole hourly window. So: EITHER the pin offset is several hours (and the game stops being hourly, because Resolve cannot run until the pinned round is signed), OR the staleness tolerance is tightened far below the beacon's backstop so that the error term shrinks to fit inside an hour-scale margin. THE SECOND, and that is what speedMaxStaleBlocks below is. It is the reason beacon.EstimatedLatestRoundWithin exists ("a consumer can tighten and cannot loosen"), and r/splash/arcade/duel already uses it at the other extreme of the scale (450 blocks, for a 300-second bind margin). SO: ONE WINDOW FOR THE WINDOW, PLUS ONE WINDOW OF MARGIN. pinOffsetWindows = 2 pinOffsetRounds = 2 * 1200 = 2400 rounds = 7200 seconds EXACTLY (drand's clock) MARGIN AT THE NOMINAL BLOCK TIME = 7200 - 3597 = 3603 s (1.00 h) MARGIN AT THE SLOW EDGE (3.63 s) = 7200 - 3956 = 3244 s (0.90 h) LESS THE BEACON'S ADMITTED ERROR = 3244 - 719 = 2525 s (0.70 h) and the last line is asserted at compile time by guard 5 below. A 42-minute margin on a one-hour window, in the worst admitted combination of a 10% block-time drift and a maximally stale beacon. WHAT THE MARGIN COSTS, STATED BECAUSE IT IS NOT FREE: a window pinned in its LAST block has its round signed pinOffsetRounds = 2 hours after it closes, so Resolve may have to wait that long after Commit. That wait is the bounded part of the honest cycle, and the refund grace window has to outlast it — see refundGraceBlocks, where this constant reappears. WHY A ROUND NUMBER AND NOT A HEIGHT: rounds are a beacon counter with no on-chain mapping to chain height, so the only anchor a realm has is what the beacon reports at pin time. That is also why pinning requires the beacon to have published at least once — see getOrCreateDraw.

Value

(2 <untyped> bigint)

pinOffsetRounds

untyped bigint

\--- pinning the window's beacon round ----------------------------------- THE ROUND A WINDOW IS DRAWN ON IS FIXED WHEN THE WINDOW'S RECORD IS CREATED — by its first ticket sale — AND NEVER WHEN IT IS COMMITTED. getOrCreateDraw writes Draw.Round exactly once and nothing else in this file ever writes it again. WHY, IN ONE PARAGRAPH, BECAUSE THE FULL ACCOUNT IS AUDIT-2026-09-28.md FINDING 10 AND IT IS THE WORST DEFECT OF THAT DAY. If the committer chose the round — the raffle bound Round = beacon.Latest() + 1 inside Commit — then the round was chosen AFTER the ticket set was already frozen and public, so anybody who could produce or foresee a signature could enumerate candidate rounds off-chain, compute ownerOfUnit(d, randomness(r) mod FrozenTotal) for each, and steer the draw onto one that named them. Pinning at record creation removes the choice entirely: at the instant the number is written there is no ticket set for it to be advantageous against, and Commit has nothing to compute. THE PROPERTY THE OFFSET MUST HAVE, AND IT IS A REAL-TIME PROPERTY: THE PINNED ROUND MUST BE SIGNED AFTER ENTRIES CLOSE. If it is signed before, the last buyer of the window can read its randomness off api.drand.sh, compute the winning unit for every candidate purchase size, and buy exactly enough tickets to land on a unit they own. That is not a theoretical attack against drand: drand's randomness is PUBLIC the moment it is signed, which makes this strictly easier than the version finding 10 describes (there it needed the beacon's key). THE WORST CASE IS A WINDOW WHOSE RECORD IS CREATED IN ITS FIRST BLOCK: the whole window still has to elapse before entries close, so the offset must exceed ONE WHOLE WINDOW of real time. That is 3597 s at the nominal block time, and more when the chain is slow, because a window is a BLOCK count while the offset is a ROUND count and drand rounds are real time by definition. THE DERIVATION OF THE MARGIN, WHICH IS ONE WHOLE WINDOW AND NOT A FRACTION OF ONE. The raffle's margin is one day on a seven-day week — 1/7 — and copying that ratio here would give ~8.5 minutes, which is NOT ENOUGH, and the reason is the term that does not shrink with the cadence: the beacon's own admitted extrapolation error. beacon.EstimatedLatestRound is a line fitted at the assumed block time, so its error is |A - B| seconds of drand time per block since its anchor was last raised — 0.33 s a block at the edges of the ±10% band — and over the beacon's default backstop of one day of blocks that is 8640 s, 2.4 HOURS. That single term is 2.4 times a whole hourly window. So: EITHER the pin offset is several hours (and the game stops being hourly, because Resolve cannot run until the pinned round is signed), OR the staleness tolerance is tightened far below the beacon's backstop so that the error term shrinks to fit inside an hour-scale margin. THE SECOND, and that is what speedMaxStaleBlocks below is. It is the reason beacon.EstimatedLatestRoundWithin exists ("a consumer can tighten and cannot loosen"), and r/splash/arcade/duel already uses it at the other extreme of the scale (450 blocks, for a 300-second bind margin). SO: ONE WINDOW FOR THE WINDOW, PLUS ONE WINDOW OF MARGIN. pinOffsetWindows = 2 pinOffsetRounds = 2 * 1200 = 2400 rounds = 7200 seconds EXACTLY (drand's clock) MARGIN AT THE NOMINAL BLOCK TIME = 7200 - 3597 = 3603 s (1.00 h) MARGIN AT THE SLOW EDGE (3.63 s) = 7200 - 3956 = 3244 s (0.90 h) LESS THE BEACON'S ADMITTED ERROR = 3244 - 719 = 2525 s (0.70 h) and the last line is asserted at compile time by guard 5 below. A 42-minute margin on a one-hour window, in the worst admitted combination of a 10% block-time drift and a maximally stale beacon. WHAT THE MARGIN COSTS, STATED BECAUSE IT IS NOT FREE: a window pinned in its LAST block has its round signed pinOffsetRounds = 2 hours after it closes, so Resolve may have to wait that long after Commit. That wait is the bounded part of the honest cycle, and the refund grace window has to outlast it — see refundGraceBlocks, where this constant reappears. WHY A ROUND NUMBER AND NOT A HEIGHT: rounds are a beacon counter with no on-chain mapping to chain height, so the only anchor a realm has is what the beacon reports at pin time. That is also why pinning requires the beacon to have published at least once — see getOrCreateDraw.

Value

(2400 <untyped> bigint)

speedMaxStaleBlocks

untyped bigint

speedMaxStaleBlocks is how stale this realm is willing to let r/splash/beacon's anchor be before it refuses to open a new window: TWO WINDOWS of blocks, 2180, about two hours. IT IS PASSED TO beacon.EstimatedLatestRoundWithin RATHER THAN USING beacon.EstimatedLatestRound, AND THAT IS NOT A MICRO-OPTIMISATION — THE DEFAULT IS FAR TOO LOOSE FOR AN HOURLY DRAW. The beacon's backstop is beacon.MaxEstimateStaleBlocks = blocksPerDay = 26182 blocks, sized for the weekly raffle, and it admits 26182 \* 0.33 = 8640 s = 2.4 HOURS of error in the pin. The weekly raffle's nominal pin margin is 24 hours, so 2.4 hours fits with room. THIS realm's whole pin margin is ONE HOUR. Anchoring against the default would admit an error 2.4x the entire margin, which is finding 10 with the guard satisfied — the pinned round's randomness public before entries close, and no guard in the file able to see it, because nothing distinguishes a fresh anchor from a stale one. THE DERIVATION, FROM THE MARGIN IT PROTECTS: The error is |A - B| seconds of drand time per block since the anchor was last RAISED, where A = 3.29998 s is assumed and B is real — so 0.33 s a block at the edges of the stated ±10% band (2.97 s .. 3.63 s; the band is r/splash/beacon's, stated there, and 2026-09-28's live measurement of 3.306-3.311 s sits well inside it). For S blocks of staleness the error is S * 0.33 seconds. The margin it must fit inside is the slow-edge pin margin, 7200 - 3956 = 3244 s. Spending ALL of it would leave zero, so the tolerance is sized to spend about a fifth of it: S = 2 * windowBlocks = 2180 blocks -> 2180 * 0.33 = 719 s of error remaining margin -> 3244 - 719 = 2525 s = 42 minutes i.e. a 4.5x margin over the error admitted, against the raffle's 3x. Guard 5 above asserts exactly this relation at compile time. WHY TWO WINDOWS AND NOT ONE, OR TEN. It is a LIVENESS price, not a safety one, and the two directions are not symmetric: - TOO TIGHT and nobody can open a window unless somebody relayed a drand round very recently, so the game is unplayable between relays. One relay costs ~17.0M gas (~0.57% of a 3e9 block), so this is a real cost somebody has to want to pay. - TOO LOOSE and the pin margin goes, which is finding 10. Two hours is chosen because it is the same order as the honest cycle it sits beside: resolving a window requires relaying its pinned round, which is signed 2 hours after the window opened, so A REALM BEING PLAYED AT ALL PRODUCES A RELAY ROUGHLY EVERY WINDOW. The tolerance is set to twice that so one skipped hour does not close the game, and any caller can clear a refusal with a single Publish of drand's current round. AND THE RELAY THAT CLEARS IT IS ALWAYS AVAILABLE IN THE DIRECTION THE CHAIN ACTUALLY DRIFTS. Raising the beacon's anchor needs a signature for a round ABOVE its current estimate, which exists precisely when the estimate is BEHIND drand's head — i.e. when the chain is slower than 3.29998 s a block. Live mainnet measures 3.306-3.311 s, so that is today's direction and the refusal is clearable. In the other direction it is not, and that is the beacon's finding-12 residual, recorded at blocksPerDay above.

Value

(2180 <untyped> bigint)

maxPinnableLatest

int64

maxPinnableLatest is the highest anchor round from which a round can still be pinned without the int64 addition wrapping. A wrapped pin would be a NEGATIVE round — a round nobody can ever publish, and therefore a window that could never be resolved. getOrCreateDraw refuses instead, loudly. IT IS BELT-AND-BRACES ON AN ADDITION AND NOT A LIVE GRIEFING SURFACE, because drand cannot sign a round whose time has not come: the beacon's latest round is bounded above by drand's real head (~2.1e7, gaining ~1.05e7 a year, reaching 9.2e18 in about 880 billion years). It stays because the arithmetic still must not wrap, and because a future change of beacon must not silently inherit the assumption that it cannot. See AUDIT-2026-09-28.md section C.

Value

9223372036854773407

refundGraceWindows

untyped bigint

\--- the refund grace window --------------------------------------------- refundGraceBlocks IS A BEACON-INDEPENDENT FLOOR ON BUYER ACCESS, and it is the ONLY thing in this file that lets a committed window's pot out when nobody relays its pinned round. Read Refund with it. THE DEFECT IT CLOSES, inherited from AUDIT-2026-09-28.md finding 7 rather than rediscovered: a window's round is pinned at anchor + pinOffsetRounds, and if nobody ever relays that round then Resolve panics "beacon round not published yet" for ever. Refund requires Committed, so not committing is not a defence — the only route out of a stranded window runs through the very act that would set the trap. Without a clock-based exit the window's whole pot — the buyers' OWN money — would be unreachable by every path in this file while heldPots went on counting it, so checkInvariant and AuditFailure would both stay green over money that was gone. That is the same class of defect as findings 1, 5, 6 and 7 and it is the class this realm was written to avoid reproducing. A BEACON-STATE GATE CANNOT DO THIS JOB, and the reason is worth keeping: since the drand migration nothing is ever LOST — every round is served for ever by at least three public relays and beacon.Publish is permissionless — so the failure mode is pure LIVENESS, "nobody has bothered to relay it". No property of the beacon's state distinguishes "not relayed yet" from "never will be". A CLOCK CAN. That is precisely what this constant is, and it is why r/splash/arcade/raffle deleted its resolvable() helper entirely rather than rescaling it (see the note at Refund). THE DERIVATION, because the number must not be picked. THE BOUNDED PART OF THE HONEST CYCLE, measured from Commit because Commit is what CommittedAt records: 1. WAITING FOR THE PINNED ROUND TO BE SIGNED: up to pinOffsetRounds = 2400 rounds = 7200 s = 2 HOURS EXACTLY. The worst case is a window whose record was created in its final block, so the pin is measured from almost exactly the moment entries close and the round lands the whole offset after Commit. (A window opened early pins a round that is already signed by Commit time, which is fine and is the point: nobody CHOSE it.) 2. WAITING FOR SOMEBODY TO RELAY IT: UNBOUNDED. Nothing forces anyone to spend ~17.0M gas relaying a round for a pot of a few tenths of a GNOT, and no deadline exists past which the round becomes unobtainable. So this term cannot appear in the derivation at all, and the grace window is not one of two independent openers — IT IS THE ONLY ONE. SO THE GRACE MUST BE STRICTLY LONGER THAN 2 HOURS, or refunds would open on windows whose pinned round has not even been signed yet, which is a FUND DRAIN and not an inconvenience: a buyer who can compute that they did not win would refund, annulling a live draw. TWELVE WINDOWS — TWELVE HOURS — IS CHOSEN, giving a 6x margin over the bounded part (43164 s against 7200 s; asserted below). The ten hours above the bound are the RELAY ALLOWANCE: how long this realm is willing to wait for somebody to relay one round before it hands the money back. WHY NOT THE RAFFLE'S RATIO. The raffle's window is 21 days against an 8-day bound, 2.6x. Copying 2.6x here would give ~5 hours. Twelve hours is chosen instead for a reason specific to this cadence, and it is the one place the hourly draw is WORSE than the weekly one: THE FAIRNESS RACE FINDING 7 RECORDS GETS CHEAPER AS THE WINDOW SHORTENS. Past the grace window, on a window whose round HAS since been relayed, Resolve and Refund are both open and the first caller wins — so a buyer who can see they did not win refunds instead of losing, which sets Abandoned and ANNULS THE DRAW for everyone (every other buyer then takes their own spend back too). Nobody can take anybody else's money — that is what Abandoned and the per-buyer spend record guarantee — so this is a fairness and liveness cost, not a solvency one. But the state that produces it needs only "nobody relayed one round for twelve hours" here, where the raffle needs "nobody relayed one round for three weeks". A LONGER WINDOW MAKES THE STATE RARER; a shorter one gets buyers their money back sooner. Twelve hours (same-day) is the balance struck, it is A DIAL, and closing the race properly still means a two-phase refund — announce, then claim — which is a separate change in this realm exactly as it is in the raffle.

Value

(12 <untyped> bigint)

refundGraceBlocks

untyped bigint

\--- the refund grace window --------------------------------------------- refundGraceBlocks IS A BEACON-INDEPENDENT FLOOR ON BUYER ACCESS, and it is the ONLY thing in this file that lets a committed window's pot out when nobody relays its pinned round. Read Refund with it. THE DEFECT IT CLOSES, inherited from AUDIT-2026-09-28.md finding 7 rather than rediscovered: a window's round is pinned at anchor + pinOffsetRounds, and if nobody ever relays that round then Resolve panics "beacon round not published yet" for ever. Refund requires Committed, so not committing is not a defence — the only route out of a stranded window runs through the very act that would set the trap. Without a clock-based exit the window's whole pot — the buyers' OWN money — would be unreachable by every path in this file while heldPots went on counting it, so checkInvariant and AuditFailure would both stay green over money that was gone. That is the same class of defect as findings 1, 5, 6 and 7 and it is the class this realm was written to avoid reproducing. A BEACON-STATE GATE CANNOT DO THIS JOB, and the reason is worth keeping: since the drand migration nothing is ever LOST — every round is served for ever by at least three public relays and beacon.Publish is permissionless — so the failure mode is pure LIVENESS, "nobody has bothered to relay it". No property of the beacon's state distinguishes "not relayed yet" from "never will be". A CLOCK CAN. That is precisely what this constant is, and it is why r/splash/arcade/raffle deleted its resolvable() helper entirely rather than rescaling it (see the note at Refund). THE DERIVATION, because the number must not be picked. THE BOUNDED PART OF THE HONEST CYCLE, measured from Commit because Commit is what CommittedAt records: 1. WAITING FOR THE PINNED ROUND TO BE SIGNED: up to pinOffsetRounds = 2400 rounds = 7200 s = 2 HOURS EXACTLY. The worst case is a window whose record was created in its final block, so the pin is measured from almost exactly the moment entries close and the round lands the whole offset after Commit. (A window opened early pins a round that is already signed by Commit time, which is fine and is the point: nobody CHOSE it.) 2. WAITING FOR SOMEBODY TO RELAY IT: UNBOUNDED. Nothing forces anyone to spend ~17.0M gas relaying a round for a pot of a few tenths of a GNOT, and no deadline exists past which the round becomes unobtainable. So this term cannot appear in the derivation at all, and the grace window is not one of two independent openers — IT IS THE ONLY ONE. SO THE GRACE MUST BE STRICTLY LONGER THAN 2 HOURS, or refunds would open on windows whose pinned round has not even been signed yet, which is a FUND DRAIN and not an inconvenience: a buyer who can compute that they did not win would refund, annulling a live draw. TWELVE WINDOWS — TWELVE HOURS — IS CHOSEN, giving a 6x margin over the bounded part (43164 s against 7200 s; asserted below). The ten hours above the bound are the RELAY ALLOWANCE: how long this realm is willing to wait for somebody to relay one round before it hands the money back. WHY NOT THE RAFFLE'S RATIO. The raffle's window is 21 days against an 8-day bound, 2.6x. Copying 2.6x here would give ~5 hours. Twelve hours is chosen instead for a reason specific to this cadence, and it is the one place the hourly draw is WORSE than the weekly one: THE FAIRNESS RACE FINDING 7 RECORDS GETS CHEAPER AS THE WINDOW SHORTENS. Past the grace window, on a window whose round HAS since been relayed, Resolve and Refund are both open and the first caller wins — so a buyer who can see they did not win refunds instead of losing, which sets Abandoned and ANNULS THE DRAW for everyone (every other buyer then takes their own spend back too). Nobody can take anybody else's money — that is what Abandoned and the per-buyer spend record guarantee — so this is a fairness and liveness cost, not a solvency one. But the state that produces it needs only "nobody relayed one round for twelve hours" here, where the raffle needs "nobody relayed one round for three weeks". A LONGER WINDOW MAKES THE STATE RARER; a shorter one gets buyers their money back sooner. Twelve hours (same-day) is the balance struck, it is A DIAL, and closing the race properly still means a two-phase refund — announce, then claim — which is a separate change in this realm exactly as it is in the raffle.

Value

(13080 <untyped> bigint)

Draw

type

Draw is one hourly window's draw: the pot, the ticket index that decides it, and the beacon round it is committed to. THERE IS NO Sponsored FIELD, AND ITS ABSENCE IS A DELIBERATE SIMPLIFICATION, NOT AN OVERSIGHT. The raffle needs one because house money in a prize pot belongs to no buyer, so its refund-safety property has to be stated as Pot == sum(spendOf) + Sponsored, and several separate mechanisms exist only to keep that term from being stranded (heldSponsored, retireSponsorship, the per-unit claim it writes, ClaimDeadWeek's whole entry point, and SweepDrawlessWeek). THE THREE THIS SENTENCE USED TO NAME WERE "Refund's move to opsPot, SweepDrawlessWeek's roll-forward, and heldSponsored", and the first two of those no longer exist: an abandoned week's sponsorship goes to NEITHER the house NOR a later week — it becomes a per-unit claim on the week it is already in, payable to the holders of that week's EARNED entries (AUDIT-2026-09-28.md findings 16, 28, 31 and 34). The count went UP, not down, which is the point this paragraph is making. HERE THE POT IS ENTRY MONEY ONLY, so the identity is simply Pot + Cut == sum(spendOf) for every unresolved window, all three mechanisms are unnecessary, and the sole-entrant attack the sponsorship enables cannot exist. Read the package comment before adding one. THE Cut TERM IS NEW SINCE 2026-09-29 AND IT IS NOT SPONSORSHIP BY ANOTHER NAME. It is the house's own fee, held in escrow for the window rather than banked at sale time, so that a window which never draws returns the FULL ticket price. It belongs to specific buyers until a draw happens, which is exactly why it appears on the same side of the identity as the pot and not as a third party's money. AUDIT-2026-09-28.md finding 25.

Value

v2.Draw

opsPot

int64

Value

2350000

heldPots

int64

Value

0

heldCuts

int64

Value

0

launchHeight

int64

Value

254844

realmSelf

.uverse.address

realmSelf is this realm's own address, resolved ONCE at package initialisation and never recomputed. WHY NOT unsafe.CurrentRealm() PER USE: CurrentRealm resolves against whatever frame is executing. Inside a crossing entrypoint that is this realm, which is correct — but Render and the audit reads are deliberately NOT crossing functions (anyone must be able to read them), so when they are called directly the "current realm" is the CALLER and BalanceOf(selfAddr()) would silently read the caller's wallet. r/splash/game learned this the hard way; the fix is to resolve the address once, since it is constant for the life of the realm.

Value

<gnolang.StringValue>

keyWidth

untyped bigint

keyWidth is enough decimal digits for any non-negative int64 (max is 9223372036854775807, 19 digits). Keys are fixed-width and zero-padded so that avl.Tree's lexicographic order matches numeric order — the same convention as r/splash/beacon's roundKey, and the reason Render lists windows as 1, 2, ... 10 rather than 1, 10, 2.

Value

(19 <untyped> bigint)

zeroPad

untyped string

Value

"0000000000000000000"

windowKey

func(window int64) string
Open
OID
07ebcb…6ac6:15
windowKey details

Inspect func

currentWindow

func() int64

currentWindow is the window tickets are being sold into right now: the single global clock this realm reads, never a per-ticket one. See windowBlocks.

Open
OID
07ebcb…6ac6:17
currentWindow details

Inspect func

windowEndHeight

func(window int64) int64

windowEndHeight is the chain height at which a window stops being the live one — the FIRST height of window+1. Derived from the same constant and the same launchHeight currentWindow reads, so the two clocks can never disagree: for every height h >= launchHeight, currentWindow() == w exactly while h is in \[windowEndHeight(w-1), windowEndHeight(w)). IT GATES NOTHING, and that is the difference between this and the raffle's weekEndHeight, which exists for SweepDrawlessWeek's ticketless clock. This realm's sweep compares window indices (\`window >= currentWindow()\`) and its refund clock counts from CommittedAt, so NO MONEY PATH IN THIS FILE READS THIS FUNCTION. It is here only so WindowEndHeight has one place to compute the boundary, in the clock section where the rest of the clock lives rather than in the read surface. NO OVERFLOW for any window this realm has a record for: such a window is bounded by the chain's own height divided by windowBlocks.

Open
OID
07ebcb…6ac6:18
windowEndHeight details

Inspect func

mulDiv

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

mulDiv returns floor(a\*b/d) via a 128-bit intermediate product. Raw a\*b/d on token-scale quantities wraps silently on the GnoVM (see ../../../../tools/muldiv\_probe.gno) instead of panicking, so every split in this realm goes through here.

Open
OID
07ebcb…6ac6:19
mulDiv details

Inspect func

maxInt64

uint64

maxInt64 as a uint64, for the overflow guards below.

Value

9223372036854775807

ticketCost

func(tickets int64) int64

ticketCost is tickets \* TicketPrice, GUARDED AGAINST int64 OVERFLOW. A raw multiplication here is the classic money bug: on the GnoVM an int64 product wraps silently, so a caller passing a large enough ticket count would buy an enormous number of ticket units for a small — or negative — cost. The product is therefore taken in 128 bits and REJECTED unless it fits in an int64: the high word must be zero AND the low word must be within int64 range, because hi == 0 alone still admits values above 2^63-1 that would land negative.

Open
OID
07ebcb…6ac6:20
ticketCost details

Inspect func

hexDigit

func(c uint8) (uint64, bool)

hexDigit decodes one lowercase-or-uppercase hex character.

Open
OID
07ebcb…6ac6:21
hexDigit details

Inspect func

randomnessMod

func(randomness string, m int64) int64

randomnessMod reduces a 256-bit hex randomness string modulo m, which gives the draw's winning ticket unit. Verbatim in behaviour from r/splash/arcade/raffle, for the reasons stated there: The randomness is 256 bits; an int64 holds 63. Any truncation (taking the first or last 16 hex digits, say) throws away most of the beacon's entropy and does it in a way an attacker can reason about. math/big does not exist on this toolchain, so the reduction is schoolbook: fold four bits at a time, reducing as we go. The intermediate r\*16 + digit does NOT fit in 64 bits when m is near the int64 maximum, so it goes through the 128-bit path rather than wrapping. \`hi %= um\` before the divide is required by bits.Div64 (which panics unless the high word is smaller than the divisor) and is sound because hi\*2^64 == (hi mod m)\*2^64 (mod m).

Open
OID
07ebcb…6ac6:22
randomnessMod details

Inspect func

getDraw

func(window int64) *v2.Draw

getDraw returns the draw for window, or nil. avl's Get returns a SINGLE value and existence is a separate Has, so the check comes first — otherwise a missing window type-asserts nil and panics with avl's message rather than this realm's.

Open
OID
07ebcb…6ac6:23
getDraw details

Inspect func

getOrCreateDraw

func(window int64) *v2.Draw

getOrCreateDraw returns the window's draw, creating an empty one if needed. Called only AFTER a sale's money has moved, so a rejected Buy never leaves an empty record behind. THIS IS WHERE THE WINDOW'S BEACON ROUND IS PINNED, AND IT IS THE ONLY WRITER OF Draw.Round IN THIS FILE. Read pinOffsetRounds for the attack this closes and for the derivation of the offset. Buy is its only caller in v1; it is factored out anyway, because the raffle's history is that a SECOND caller appeared (FundPot) and the pin had to happen exactly once per window whichever of them got there first. A second pin would be a second choice, and choice is the whole defect. THE ANCHOR IS beacon.EstimatedLatestRoundWithin(speedMaxStaleBlocks), AND EVERY WORD OF THAT IS LOAD-BEARING: - NOT beacon.Latest(). That is whatever round somebody last had a reason to relay, which under an on-demand record is arbitrarily stale; pinning against a stale anchor measures the offset FROM THE PAST, so the pinned round lands earlier in real time than intended and its randomness can be public before entries close. With a one-hour window and a two-hour offset, 3600 seconds of staleness — about 1090 blocks — is enough to destroy the property completely. That is AUDIT-2026-09-28.md finding 10's last-buyer attack and no guard in this file can see it, because nothing here distinguishes a fresh anchor from a stale one. - NOT the default EstimatedLatestRound() either, which tolerates a whole day of staleness — an admitted error of 2.4 hours against this realm's one-hour pin margin. See speedMaxStaleBlocks for the derivation of the tighter tolerance. IT THEREFORE NEEDS A BEACON THAT HAS PUBLISHED AT LEAST ONCE AND HAS BEEN RELAYED RECENTLY, and if either is untrue THE SALE IS REFUSED with the beacon's own message. That is the safe direction and is strictly better than what it replaces: before the pin existed, such a Buy succeeded and the window could then never be committed, never resolved and never refunded, so the money entered a window no path in the realm could ever empty. A refused sale leaves the money in the buyer's wallet. The refusal happens after the wugnot pull inside Buy, which is harmless — a panic reverts the whole transaction, exactly as checkInvariant's panics already rely on. AND THE REFUSAL COSTS NOTHING CROSS-REALM, WHICH IS WHY IT COULD BE ADOPTED AT THE TIGHTER TOLERANCE. Nothing outside this realm calls into it (see the package comment: there is no FundPot and no rake feed), so a refusal here reverts one buyer's transaction and affects nobody else. The raffle could not say that until AUDIT-2026-09-28.md finding 14 severed duel.Collect's dependency on its bookkeeping, and the cross-realm coupling is exactly why bounding the extrapolation looked like a liveness cost there for as long as it did.

Open
OID
07ebcb…6ac6:24
getOrCreateDraw details

Inspect func

treeInt64

func(t *v0.Tree, key string) int64
Open
OID
07ebcb…6ac6:25
treeInt64 details

Inspect func

appendTickets

func(d *v2.Draw, addr .uverse.address, units int64)

appendTickets records a purchase of units by addr by APPENDING A NEW INDEX to this window's ticket index. It never touches an existing index, and that is the whole point — see Commit's frozen-ownership argument.

Open
OID
07ebcb…6ac6:26
appendTickets details

Inspect func

ownerOfUnit

func(d *v2.Draw, k int64) .uverse.address
Open
OID
07ebcb…6ac6:27
ownerOfUnit 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 (see Buy), 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, 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. 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
07ebcb…6ac6:28
rejectAttachedCoin details

Inspect func

sweepDonations

func()

sweepDonations accounts for any wugnot that has arrived without going through Buy. IT HAS TO EXIST. wugnot.Transfer is permissionless: any address may push wugnot at this realm with no memo, no callback and no way for this realm to learn who sent it or why. Without a sweep, one unsolicited transfer would fail checkInvariant for every later caller and BRICK THE REALM. IT GOES TO opsPot, NOT TO THE CURRENT WINDOW'S POT. Here Pot == sum(spendOf) is a load-bearing identity — it is what makes Refund unable to pay out more than was put in, and what makes a sole entrant win exactly their own stake back — and a donation credited to a pot would break it, silently converting somebody's gift into refundable balance nobody has a claim on. The ops pot has no such identity to violate. IT EMITS A Donation EVENT FOR EVERY NON-ZERO ABSORPTION, and that is not bookkeeping decoration: nothing else in this realm distinguishes a swept arrival from the house cut, since both land in opsPot and opsPot is a single number. WHAT THE INVARIANT'S EQUALITY IS ACTUALLY WORTH, recorded here so the next reader does not overtrust it (AUDIT-2026-09-28.md finding 9). Suppose heldPots OVERCOUNTS by D because of a bug in this file — the realm believes it owes D more than it holds — and donations totalling X > D have arrived. This function computes extra = bal - (heldPots + opsPot) = X - D, absorbs it, and afterwards heldPots + opsPot == bal EXACTLY. checkInvariant then passes and so does AuditFailure's one-sided balance test. THE DEFICIT HAS NOT BEEN DETECTED, IT HAS BEEN PAID FOR OUT OF A STRANGER'S MONEY. The equality therefore says "the books add up after absorbing whatever arrived", not "the books were already right". This event is what makes the absorption visible so the case can be noticed off-chain — a Donation for an amount no donor sent is the signature of exactly that state. Fixing it properly needs a separate counter for absorbed donations so the invariant can state the pre-donation equality, which changes what checkInvariant MEANS in a realm where every mutator asserts it; it is a separately-audited change, here as there.

Open
OID
07ebcb…6ac6:29
sweepDonations details

Inspect func

absorbDonation

func() int64

absorbDonation is sweepDonations without the event: it credits opsPot with any unattributed arrival and RETURNS THE AMOUNT IT ABSORBED, zero if there was nothing. IT IS SPLIT OUT FOR A TESTABILITY REASON, STATED PLAINLY: this toolchain gives a Gno test no way to read emitted events, so the only assertable statements about the Donation event are the value it would carry and whether it fires at all. Both are exactly this return value, and speed\_test.gno asserts it on both sides. Do not inline it back without putting the event under test some other way.

Open
OID
07ebcb…6ac6:30
absorbDonation details

Inspect func

checkInvariant

func()

checkInvariant is this realm's solvency statement: the wugnot it holds is exactly the sum of every unresolved window's pot plus the ops pot, and nothing else. O(1), using the incrementally-maintained heldPots; AuditFailure re-derives the same thing by iteration. CALLED AT THE END OF EVERY STATE-CHANGING FUNCTION. An equality, not a ">=": sweepDonations runs first in each of them, so unattributed arrivals are already accounted for and any remaining gap is a bug in this file. THERE IS NO SPONSORSHIP TERM HERE and there is nothing missing: the per-window identity is Pot + Cut == sum(spendOf) with no house money in it, and AuditFailure re-derives it ONE WINDOW AT A TIME (finding 27 — it used to do it summed over the unresolved windows, which two windows with compensating errors passed through silently). See Draw.

Open
OID
07ebcb…6ac6:31
checkInvariant details

Inspect func

AuditFailure

func() string

AuditFailure re-derives the invariant BY ITERATION and reports the empty string when the books balance, otherwise which check failed and by how much. Never called from a state-changing path — checkInvariant is the O(1) guard for that — and exists so anyone can verify off-chain, and so a test can assert it after every operation. IT MUST BE ROBUST AGAINST ANYONE WHO CAN SEND THIS REALM TOKENS, which is everyone: wugnot.Transfer needs no permission. So the balance test below is ONE-SIDED — a shortfall is a violation, an unswept surplus is not. See the comment on that check.

Open
OID
07ebcb…6ac6:32
AuditFailure details

Inspect func

AuditInvariant

func() bool

AuditInvariant is AuditFailure reduced to a bool.

Open
OID
07ebcb…6ac6:33
AuditInvariant details

Inspect func

Buy

func(tickets int64)

Buy sells \`tickets\` tickets in the CURRENT window to the caller. Cost is tickets \* TicketPrice, pulled from the caller with the GRC20 allowance path, so the caller wraps native ugnot with wugnot.Deposit and approves this realm first. houseCutBps of the cost goes to the ops pot; the remainder — and all integer-division dust, since the cut is floored — joins this window's prize pot and is recorded in the caller's spend for the window. EVERY CHECK RUNS BEFORE THE PULL, and the window record is created only after it, so a failed Buy moves nothing and creates nothing. Each purchase APPENDS ITS OWN ticket index rather than growing an existing one. That is not a style choice: it is the property Commit depends on, and the reason is spelled out there. THE WINDOW'S FIRST BUY PINS THE WINDOW'S BEACON ROUND, as a side effect of creating the record (getOrCreateDraw). That is deliberate and is the only moment the round is ever chosen. Two consequences a caller can see. First, a sale needs a beacon that has published at least once AND been relayed within speedMaxStaleBlocks, or it is refused with the beacon's own message — a refused sale rather than money accepted into a window that could never be drawn fairly. Second, the buyer who opens a window does not get to pick anything useful: the round is fixed two hours ahead of the moment the ticket set closes, so at the instant it is pinned there is no ticket set for it to be advantageous against. Panics if tickets is not positive, if the cost overflows int64, if the allowance is short, or if THIS WINDOW IS ALREADY COMMITTED — the last of which is the frozen-total rule, not a convenience check. Once a window's total is frozen, no further ticket may join it. THAT LAST REFUSAL IS UNREACHABLE THROUGH THE ENTRY POINTS, and is kept deliberately. Commit only accepts a window that is already over, and Buy only ever sells into currentWindow(), so the live window cannot be in a committed state — which is exactly why a commit can no longer shut a window's sales down early (AUDIT-2026-09-28.md finding 2). The check stays as a second lock on the frozen-total rule: if the window's identity ever stops being "the one the clock is on", this is the line that still refuses to grow a frozen ticket set. ONLY A USER MAY BUY, AND THE REASON IS THE PAYOUT, NOT THE PURCHASE (AUDIT-2026-09-28.md finding 8). A ticket is a claim on two transfers this realm will later push OUT: Resolve's whole pot to the winner's address, and Refund's spend back to the buyer's. Both go to cur.Previous().Address(), which for a realm caller is that realm's PACKAGE-DERIVED address — an address whose only "key" is its package path, so nothing can move wugnot out of it unless that realm happens to contain code that does. A realm with no such code that won a draw would have the entire window's pot transferred into an address it can never spend from: money that has left this realm's books (heldPots drops, the invariant stays green) and is unreachable by anybody, for ever. This realm cannot tell which realms those are and must not try, so it refuses them all at the door instead of stranding a pot at settlement time. cur.Previous().IsUserCall() IS THE CHECK — AND IsUser() IS NOT. IsUser() also accepts a \`maketx run\` ephemeral realm, which is code executing under a user's signature and NOT an EOA: its address is still package-derived, so it can win a pot and then cease to exist with the money in it. r/splash/exchange states the same distinction about the same word for a different failure mode (a shared send envelope); the property being asked for — "the thing in front of me is a wallet somebody holds a private key for" — is identical. THERE IS NO SECOND, UNGUARDED ENTRY POINT HERE, and that is a difference from the raffle worth naming: the raffle must leave FundPot realm-callable for the duel's rake, so its asymmetry has to be documented at both ends to stop someone "making it consistent". THIS REALM HAS NO FundPot (see the package comment), so THE GUARD ABOVE IS THE ONLY DOOR MONEY COMES IN BY, and every ugnot in a pot is therefore attributable to a user address that can receive it back.

Open
OID
07ebcb…6ac6:34
Buy details

Inspect func

Commit

func(window int64) int64

Commit commits an ALREADY-FINISHED window to its pinned beacon round. Returns that round. THE WINDOW IS A PARAMETER AND IT MUST BE OVER — window \< currentWindow(). Both halves are load-bearing, and each closes a defect the raffle's first version had: - IT TAKES A WINDOW, so any past window can be committed at any time, by anyone. A window whose clock ran out before anybody called it would otherwise be unreachable for ever — Resolve and Refund both require Committed — so its whole pot would be lost while heldPots went on counting it and both solvency checks stayed green over money nobody could ever get back. That is AUDIT-2026-09-28.md finding 1, and at an hourly cadence it would happen 24 times a day rather than once a week: MOST WINDOWS OF A QUIET DAY WILL BE COMMITTED LATE BY SOMEBODY WHO WANTS THEIR MONEY, and that path has to work. - THE LIVE WINDOW CAN NEVER BE COMMITTED, so a commit can no longer cut a window's sales short. Otherwise anyone could Buy(1) and immediately Commit: that makes them the sole owner of a FrozenTotal of 1 — the guaranteed winner — and, because Buy refuses a committed window, shuts the window's sales for the rest of its hour. That is AUDIT-2026-09-28.md finding 2. Note that the PRIZE half of that attack is already dead here by the sole-entrant property (winning a one-ticket window returns the attacker's own 95,000 ugnot out of the 100,000 they staked), so what remains is pure griefing — and refusing the live window closes it at the root anyway, rather than with a minimum-tickets or minimum-elapsed heuristic. THIS USED TO SAY THE GRIEFER'S WIN "costs them the 5,000 cut", AND IT COSTS THEM NOTHING NOW: since finding 25 the fee is escrowed and earned only by a draw, so the griefer who DOES NOT resolve pays nothing at all and reclaims the whole 100,000 through Refund. The reason the attack is dead is the refusal below, not a 5,000 ugnot price on it. AUDIT-2026-09-28.md finding 29. PERMISSIONLESS: there is no keeper and no admin. Anyone who wants the draw to happen — every ticket holder, for a start — can call it once the window is up. It freezes two numbers and CHOOSES NOTHING: - FrozenTotal = the window's final ticket total. The window is over and Buy only ever sells into currentWindow(), so this total can no longer grow BY CONSTRUCTION; it is still assigned explicitly, because Resolve reads this field and not the live total. - CommittedAt, the height the refund grace window counts from. - AND IT USES Draw.Round EXACTLY AS PINNED, which was written when this window's record was created and is not this function's business. THE SECURITY RULE, AND THE ONLY REASON THIS IS THREE PHASES: the ticket set must be frozen BEFORE the randomness is knowable. If the total were read at resolve time, anybody who saw the published round could compute the winning unit and then buy precisely enough tickets to own it — which turns a draw into a purchase. THERE IS DELIBERATELY NO "THE ROUND MUST STILL BE IN THE FUTURE" CHECK HERE. A reader who expects one has the pin backwards. A window is committed at least an hour after its round was pinned, so by now the pinned round MAY LEGITIMATELY ALREADY BE SIGNED AND RELAYED — that is the ordinary case for a window whose record opened early in its hour, and it is harmless precisely because nobody CHOSE it. Requiring it to be unpublished would refuse the honest case and, worse, would reintroduce round selection through the back door, since the only way to satisfy such a check would be to recompute the round here. The int64 wrap case lives where the arithmetic is, at maxPinnableLatest. WHY FREEZING THE TOTAL ALSO FREEZES WHO OWNS UNIT k — the part r/splash/game got wrong first time, and the reason Buy appends: - Every purchase APPENDS A NEW ticket index (appendTickets). No index's value is ever increased afterwards, and fenwick.Add is not called from this realm at all. - An appended index therefore starts at a prefix equal to the whole total at the moment it was appended, so anything appended after Commit lies at or beyond FrozenTotal — outside the frozen range by construction, not by convention. - And because no earlier index's value changes, no earlier prefix moves: the owner of every unit below FrozenTotal is fixed from the instant that unit was sold. WITH ONE INDEX PER BUYER INSTEAD, a further purchase would grow that buyer's existing value, widening their range IN PLACE, from its start, inside the frozen range, and pushing every later buyer along. The earliest buyer (prefix 0 for ever) could then wait for the round to publish, compute k = randomness mod FrozenTotal, buy until their own value exceeded k, and call Resolve to take the pot: a free option on the whole pot. That exploit was real in r/splash/game. Panics if the window is not over yet, if it is already committed, or if it has NO TICKETS AT ALL. The last is not a formality: there is no rollover, so a drawless window must never be allowed to exist — there is no unit to draw and no owner to pay. It reads no beacon state at all; the "beacon must exist and be fresh" refusal lives at the pin, which is to say in Buy, where a sale is refused rather than accepted into a window that could never be drawn.

Open
OID
07ebcb…6ac6:35
Commit details

Inspect func

Capture

func(window int64)

Capture copies a committed window's beacon randomness into the draw. IT IS A CONVENIENCE AND NOT A SAFETY MECHANISM, and saying so is the point of this comment. In the raffle's pre-drand design its job was "save the randomness before the beacon prunes it"; r/splash/beacon has no retention window and no Prune, drand serves every round for ever from at least three public relays, and Publish is permissionless, so THERE IS NOTHING TO PRESERVE THE VALUE FROM. What it still does: - it copies a public value into the draw's own state, so Resolve needs no beacon read later and the settled draw is self-contained; - it makes the value visible through CapturedRandomness and the Capture event before the window is settled, which a front end and an auditor can both use. NOTHING IN THIS REALM'S SAFETY DEPENDS ON ANYONE CALLING IT. PERMISSIONLESS, deliberately. Its only effect is to store a value that is already public — the round's randomness, readable by anyone the moment it is relayed. Gating it on an operator would buy nothing and would put a liveness dependency for the whole pot onto one key. WHAT IS STORED IS THE 64-CHARACTER HEX RANDOMNESS — sha256 of the signature, a hash, not the signature itself — and it is NOT re-verified here. The pairing check happened once already inside beacon.Publish on the way in; redoing it would be expensive duplicated work on an input that is already known good, and the beacon has no route by which a published round's randomness can change afterwards. A SECOND Capture NEVER OVERWRITES THE FIRST, and is a silent no-op rather than a panic: two callers racing to perform a permissionless public good should not make one of them fail. Capturing before the round is relayed PANICS. There is nothing to preserve yet, and storing anything else — least of all some already-published value — would hand the draw a randomness for a round nobody pinned, which is the exact attack the pin exists to prevent. COMMIT DOES NOT CAPTURE, AND MUST NOT START. Since the round is pinned at record creation, a window's round may well be relayed by the time it is committed, so a capture there would often succeed — and it would put a beacon read back inside the one function whose job is to choose nothing.

Open
OID
07ebcb…6ac6:36
Capture details

Inspect func

Resolve

func(window int64)

Resolve picks the window's winner and PAYS THEIR WALLET. PERMISSIONLESS. The whole pot leaves this realm in a single wugnot.Transfer to the winner's address. No internal balance is credited, there is nothing left to claim afterwards, and no buyer's money is held past the window they bought into. winner = ownerOfUnit(randomness mod FrozenTotal). FrozenTotal, NOT the live ticket total: recomputing it here would reopen the buy-the-winning-unit attack described on Commit. The pot pays out UNTAXED. The house cut was already taken on the way in, at sale time; taxing the payout as well would be charging twice for the same ticket. RANDOMNESS SOURCE: a captured value if there is one, otherwise the beacon directly — so the happy path needs no extra transaction. THERE IS ONE FAILURE MESSAGE AND THERE CAN ONLY BE ONE. "Not relayed yet" is always a wait-and-retry, never "this draw is dead": no round is ever unobtainable, so anybody who sees that message can make it stop being true by relaying the round themselves for ~17.0M gas. A realm that offered a second, terminal message would be steering buyers into refunds they do not need. THERE IS NO HEIGHT GATE HERE AND THERE MUST NEVER BE ONE. refundGraceBlocks does not appear in this function and CommittedAt is not read: the grace window is a FLOOR ON BUYER ACCESS, NOT A DEADLINE ON THE DRAW. It only ever opens Refund; it never closes Resolve. An honest relay that arrives a day after Commit, before anybody has refunded, should still pay the winner it always would have, over exactly the FrozenTotal it was frozen with. Adding a height deadline here would manufacture a second class of dead window — past the grace, relayed, and resolvable by nobody — which is the very shape of defect the grace window exists to remove. WHAT DOES CLOSE THIS FUNCTION IS Abandoned, and that is enough. Once the grace window has opened, Resolve and Refund are both available and the first one called wins: a refund sets Abandoned and this function refuses for ever, a resolve pays the winner and Refund's Resolved check refuses for ever. NEITHER ORDERING CAN PAY THE SAME UGNOT TWICE, which is the property that matters. The fairness cost of that race — a buyer who can see they did not win refunds instead, annulling the draw — is AUDIT-2026-09-28.md finding 7's recorded residual, and it is CHEAPER HERE THAN THERE because the window that produces it is twelve hours rather than three weeks. It is derived and discussed at refundGraceBlocks rather than buried here.

Open
OID
07ebcb…6ac6:37
Resolve details

Inspect func

refundGraceElapsed

func(d *v2.Draw) bool

refundGraceElapsed reports whether refundGraceBlocks have passed since Commit froze this window. IT READS NO BEACON STATE AT ALL, and that is its entire purpose: it is the one condition in this file that a stopped, lost or simply uninterested relayer cannot hold false. See refundGraceBlocks for the derivation. IT IS MONOTONE: chain height only rises and CommittedAt never moves once set, so this goes true exactly once and stays true. A ZERO CommittedAt RETURNS false, and false is the safe direction. It is unreachable through the entry points — Refund tests Committed first, and Commit sets both fields in the same statement group. If it ever were reachable, withholding the escape hatch is a liveness cost, whereas granting it would open refunds on a live draw at any height past refundGraceBlocks.

Open
OID
07ebcb…6ac6:38
refundGraceElapsed details

Inspect func

Refund

func(window int64)

Refund returns the caller's own ticket money for a window nobody settled. PERMISSIONLESS, and self-service only: a caller can only ever reclaim their own record. WHAT IT IS FOR IS LIVENESS, NOT LOSS. A window's pot must not be hostage to whether anybody spends ~17.0M gas relaying one drand round, and at an hourly cadence with pots of a few tenths of a GNOT that is not an exotic worry — IT IS THE EXPECTED OUTCOME OF A QUIET HOUR. That is the only failure left, it is unbounded in time, so the exit is A CLOCK: refunds open refundGraceBlocks (twelve hours) after Commit and not before. THE PRECONDITION IS THEREFORE NOT THE COMPLEMENT OF Resolve's — the two OVERLAP for ever once the window opens, and that is fine and is handled by Abandoned. There is no state in which Resolve is impossible, so a refund gate that waited for one would never open at all. AND THE EXIT IS ONE-WAY, WHICHEVER WAY IT GOES. Refund and Resolve are permanently mutually exclusive per window: the first refund that pays out sets Abandoned, after which Resolve and Capture refuse that window for ever and every later refund proceeds without consulting the beacon again. Read Draw.Abandoned for the theft this prevents — it is AUDIT-2026-09-28.md finding 3 and it carries more weight since the drand migration, not less, because a pinned round can be relayed by anyone at any time for ever. EACH BUYER RECLAIMS EXACTLY THEIR OWN RECORDED SPEND, ONCE. The record is zeroed before the transfer, so a second call finds nothing and panics; and because Pot + Cut == sum(spendOf) for an unresolved window — WITH NO SPONSORSHIP TERM TO GET IN THE WAY, which is the whole simplification this realm buys by having no FundPot — the refunds can never total more than the realm holds against this window. THE HOUSE FEE \*IS\* REFUNDED, SINCE 2026-09-29, AND THE PARAGRAPH THAT USED TO BE HERE SAID THE OPPOSITE. It read: "What a buyer gets back is their post-cut contribution — the part that was ever in the prize pot — not the gross ticket price. The 5% was taken at sale time and is already in the ops pot; clawing it back would mean the ops pot could go negative, or that this realm had to hold every cut in escrow against the chance of an unrelayed round." IT IS KEPT VERBATIM BECAUSE IT NAMES THE COST OF THE FIX AND THE FIX PAID IT: the realm now DOES hold every fee in escrow per window (Draw.Cut), which is the second option that sentence dismissed, and the ops pot cannot go negative because nothing credits it until a draw succeeds. WHAT A BUYER GETS BACK IS THEREFORE THE WHOLE TICKET PRICE. A refunded window is a window that did not run, and the fee was a charge for a draw that never happened. This is a REDUCTION IN HOUSE REVENUE in the failure case, which needs no permission and is the safe direction. AUDIT-2026-09-28.md finding 25. A WINDOW THAT WAS NEVER COMMITTED IS NOT REFUNDABLE HERE, and needs no separate path — but only because Commit TAKES THE WINDOW AS AN ARGUMENT and any window that is over can be committed by anyone at any time. So a buyer holding a stranded window's tickets names that window to Commit themselves, and then either Resolves it (the normal outcome, once somebody relays the round) or refunds it here twelve hours later. Before Commit took that argument the equivalent sentence in the raffle was simply FALSE, and that was AUDIT-2026-09-28.md finding 1.

Open
OID
07ebcb…6ac6:39
Refund details

Inspect func

SweepDrawlessWindow

func(window int64) int64

SweepDrawlessWindow moves a FINISHED, TICKETLESS window's pot to the ops pot, so that money in such a window can leave. PERMISSIONLESS, like Commit. Returns the amount moved. READ THIS WHOLE COMMENT BEFORE DELETING THE FUNCTION AS DEAD CODE, because in v1 IT IS UNREACHABLE THROUGH EVERY ENTRY POINT and that is deliberate. WHY IT IS UNREACHABLE HERE. A Draw record comes into existence in exactly one place (getOrCreateDraw) with exactly one caller (Buy), and Buy appends a positive ticket count in the same transaction; a panic anywhere after the record is created reverts the whole transaction, record included. So there is NO SEQUENCE OF CALLS that leaves a window with a record, a zero ticket total and a positive pot. That argument is asserted by the suite (a ticketless window cannot be produced through Buy) and the function's own preconditions are exercised by installing the state directly. WHY IT EXISTS ANYWAY. It is the shape of AUDIT-2026-09-28.md's fifth fix, and the defect it exists against is the single most common one in that document — SIX of the fourteen findings are "money is in the realm and no path can ever get it out", and four of those six were found by an engineer implementing something else, not by an audit. In the raffle the state is REACHABLE: FundPot credits the live week and creates its record, so a week in which the duel's rake arrived but nobody bought a ticket ends holding money in a Draw that Commit rightly refuses (no tickets), that Resolve and Refund cannot touch (both require Committed) and that FundPot will never revisit (it only funds the live week). ADDING ANY OF THE THINGS THE PACKAGE COMMENT WARNS AGAINST — sponsorship, a rake feed, a jackpot, a rollover — MAKES IT REACHABLE HERE TOO, and the day that happens this exit should already be in place and tested rather than being the thing somebody forgot. WHERE THE MONEY GOES, AND WHY IT IS NOT "ROLLED FORWARD" INTO THE LIVE WINDOW'S POT. THE PARAGRAPH THAT STOOD HERE RECOMMENDED A DESIGN TWO AUDITS FOUND EXPLOITABLE, AND THAT IS WHY IT IS QUOTED RATHER THAN QUIETLY REPLACED — AUDIT-2026-09-28.md finding 36. It read: "The raffle rolls a drawless week's sponsorship into the live week's pot, AND IT IS RIGHT TO: that money is the PLAYERS' side of the duel rake, so handing it to the house would take the players' share away from players, and the raffle has a Sponsored field that lets the destination week's pot exceed its spend records without breaking anything." EVERY CLAUSE OF IT IS NOW FALSE, AND THE "AND IT IS RIGHT TO" WAS THE DEFECT. The raffle does not roll a dead week's sponsorship anywhere: rolling it into the live week was AUDIT-2026-09-28.md finding 28 (a destination evaluated at call time is a destination the permissionless caller chooses, by choosing when to fire), and the register that replaced it for one day was finding 31 (the caller who opens the destination week is the caller who calls Buy, so they could be its only entrant and capture the whole amount for the price of the house cut). A dead week's sponsorship now becomes a PER-UNIT CLAIM ON THE WEEK IT IS ALREADY IN, payable to the holders of that week's EARNED entries — read the raffle's retireSponsorship and ClaimDeadWeek. There is no destination at all. SO A READER WHO PORTS SPONSORSHIP INTO THIS FILE MUST NOT PORT THE SENTENCE ABOVE. The half of it that was sound — this money is the PLAYERS' share and must not end up with the house — is satisfied by giving it back to the players who earned it, in place, and never by moving it to a week somebody can pick. This comment is where an engineer adding a rake feed here would look first, which is exactly why a recommendation for a broken design sitting in it is a defect and not untidiness. NEITHER HALF OF THE ORIGINAL APPLIED HERE IN ANY CASE. - This realm has no sponsorship and no rake, so there is no players' share to protect: any money in a ticketless window is unattributable by construction — exactly like a stranger's unsolicited transfer, which sweepDonations also sends to opsPot. - Crediting the live window's Pot would BREAK Pot == sum(spendOf) for that window, which is the identity that makes Refund unable to overdraw and makes a sole entrant's prize exactly their own stake. AuditFailure would then report the live window as short, correctly, and the only way to "fix" it would be to add the Sponsored field and the three mechanisms that come with it — porting the complexity the package comment says not to port, to launder money that cannot exist. SO opsPot — AND SINCE 2026-09-29 THE TWO REALMS AGREE ON THIS SHAPE RATHER THAN DIFFERING, which is the opposite of what this paragraph used to say. It read: "SO opsPot, WHICH IS A DELIBERATE DEVIATION FROM THE WEEKLY REALM'S CHOICE AND IS NAMED AS ONE ... the routing question ('where does it belong') is answered differently because the money has a different origin." The raffle's own no-claimant branch now sends a dead week's sponsorship to opsPot for exactly the reason given here — there is provably no address holding a unit of that week, so there is nobody to wrong — and it only pays holders when holders exist. Read the raffle's retireSponsorship: the branch is on its EARNED unit total and nothing else. The essential property of the fifth fix is preserved in full either way: money in the realm has a path out, here through WithdrawOps. Panics if the window is not over yet (a live window may still sell its first ticket, which would make it a real draw whose buyers are entitled to everything in its pot), if there is no record for it, if it sold ANY tickets, if it holds nothing, or — defensively — if it is committed, resolved or abandoned.

Open
OID
07ebcb…6ac6:40
SweepDrawlessWindow details

Inspect func

WithdrawOps

func() int64

WithdrawOps pushes the whole ops pot to opsVaultAddr and returns the amount moved. PERMISSIONLESS. THE DEFECT IT CLOSES IS AUDIT-2026-09-28.md FINDING 6, and it is in this file from the start rather than added later: without it opsPot only ever increases — the 5% cut on every ticket (the product's entire revenue), every swept donation, and anything SweepDrawlessWindow ever moves — and all of it would be permanently unreachable by every path in the realm while checkInvariant and AuditFailure both stayed green, because both count opsPot as money the realm holds and neither has an opinion about whether anybody can reach it. PERMISSIONLESS, AND THAT IS SAFE PRECISELY BECAUSE THE DESTINATION IS FIXED AT COMPILE TIME. A reader will look for a caller check here and there is none, on purpose: opsVaultAddr is a const, so the caller does not choose where the money goes, cannot name themselves, and cannot name a window's pot. All they can do is push the house's money to the house — and paying the gas to do somebody else's bookkeeping is a favour, not an attack. Gating it on an operator key would buy nothing and would put a liveness dependency for the realm's revenue onto one key, exactly as it would for Commit, Capture and Resolve. The property that makes it sound is the one tools/speed\_addr\_test.go exists to keep true. IT NEVER REACHES A PLAYER'S MONEY. No Draw is touched: not Pot, not spendOf, not a ticket. The only book that moves is opsPot, which by construction holds nothing any buyer can claim — the fee in it was EARNED BY A WINDOW THAT ACTUALLY DREW (Resolve releases that window's escrow; a window that fails refunds it with the gross) and a donation belongs to whoever chose to give it away. THE CITATION THAT USED TO STAND HERE IS RETIRED: this paragraph said "the cut was taken at sale time (see Refund's 'THE HOUSE CUT IS NOT REFUNDED')", and neither half survives — finding 25 stopped the fee being taken at sale time and deleted that section of Refund. A citation that outlives the thing it cites is how a reader ends up trusting a rule the code stopped following. AUDIT-2026-09-28.md finding 29. sweepDonations RUNS FIRST, as in every other mutator, so a donation that arrived since the last state change is included in this withdrawal rather than left behind. BOOKS FIRST, TRANSFER SECOND, exactly as Resolve and Refund do it: opsPot is zeroed BEFORE the wugnot leaves, so a re-entrant call — which wugnot's Transfer cannot cause today, having no hook or callback, but a future GRC20 might — finds an empty pot and panics rather than a balance it can take twice. THE INVARIANT IS EXACT ACROSS IT, not merely preserved one-sidedly: checkInvariant asserts heldPots + opsPot == balance, and this call reduces both sides by the same amount. heldPots is deliberately untouched. Panics if there is nothing to withdraw.

Open
OID
07ebcb…6ac6:41
WithdrawOps details

Inspect func

PotSize

func() int64

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

Open
OID
07ebcb…6ac6:42
PotSize details

Inspect func

TicketTotal

func() int64

TicketTotal is the CURRENT window's ticket units sold.

Open
OID
07ebcb…6ac6:43
TicketTotal details

Inspect func

TicketTotalOfWindow

func(window int64) int64

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

Open
OID
07ebcb…6ac6:44
TicketTotalOfWindow details

Inspect func

TicketsOf

func(addr .uverse.address) int64

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

Open
OID
07ebcb…6ac6:45
TicketsOf details

Inspect func

TicketsOfWindow

func(window int64, addr .uverse.address) int64

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

Open
OID
07ebcb…6ac6:46
TicketsOfWindow details

Inspect func

SpendOf

func(addr .uverse.address) int64

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

Open
OID
07ebcb…6ac6:47
SpendOf details

Inspect func

SpendOfWindow

func(window int64, addr .uverse.address) int64

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

Open
OID
07ebcb…6ac6:48
SpendOfWindow details

Inspect func

CurrentWindow

func() int64

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

Open
OID
07ebcb…6ac6:49
CurrentWindow details

Inspect func

WindowBlocks

func() int64

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

Open
OID
07ebcb…6ac6:50
WindowBlocks details

Inspect func

WindowEndHeight

func(window int64) int64

WindowEndHeight is windowEndHeight, EXPORTED: the chain height at which a given window stops being the live one. A window is live for heights \[WindowEndHeight(window-1), WindowEndHeight(window)) — the returned height is the FIRST height of window+1, NOT the last height of window, so at exactly this height CurrentWindow already reads window+1. A countdown that showed "1 block left" here would be off by one. IT IS A BLOCK HEIGHT AND DELIBERATELY NOT A TIME. A window is windowBlocks = blocksPerDay/24 = 1090 blocks, and blocksPerDay = 26182 encodes an ASSUMED 86400/26182 = 3.29998 s per block (read blocksPerDay and windowBlocks for how much that assumption is allowed to be wrong). The chain does not actually run at that rate: 3.306 s measured on gno.land mainnet and 3.376 s on onyx-1. So a caller wanting a wall-clock countdown must multiply by a block time IT chooses and own the error; this realm will not return seconds or "time remaining", because that would bake one clock's rounding into every front end that reads it and make the contract the source of a number it cannot know. Nothing on chain converts blocks to seconds and neither does this. WHY THIS AND NOT AN EXPORTED launchHeight: WindowBlocks is already exported, so LaunchHeight() + WindowBlocks() would indeed let a caller derive the boundary — but it would leave the +1 above to be re-implemented off chain in every reader, and the raffle cannot offer the same pair at all (both raffleWeekDays and blocksPerDay are unexported there). One read with one name in both realms beats two constants and a formula the caller has to get right twice. This read also subsumes the constant: launchHeight is WindowEndHeight(-1). NEGATIVE OR FAR-FUTURE window IS NOT REFUSED and returns the arithmetic for it, exactly as windowEndHeight does — this is a pure function of window and launchHeight with no record lookup, so there is no "no such window" state to report.

Open
OID
07ebcb…6ac6:51
WindowEndHeight details

Inspect func

CurrentWindowEndHeight

func() int64

CurrentWindowEndHeight is the chain height at which the LIVE window ends: the one number a countdown wants. WindowEndHeight(CurrentWindow()), read in a single call. IT IS ONE CALL ON PURPOSE, and it matters more here than in the raffle because this realm's window is an HOUR rather than a week. Composing the two exported reads off chain means two queries at two different chain heights, and a window that turns over between them yields a boundary for a window that has already ended — a countdown that jumps backwards by a whole hour exactly at the moment it matters most. Read here, both halves see one height. A BLOCK HEIGHT, NOT A TIME, for the reasons on WindowEndHeight.

Open
OID
07ebcb…6ac6:52
CurrentWindowEndHeight details

Inspect func

PinOffsetRounds

func() int64

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

Open
OID
07ebcb…6ac6:53
PinOffsetRounds details

Inspect func

MaxStaleBlocks

func() int64

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

Open
OID
07ebcb…6ac6:54
MaxStaleBlocks details

Inspect func

DrawOf

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

DrawOf reports a window's record: committed, frozen ticket total, beacon round, pot, resolved, winner, abandoned, committed-at-height. All zero for a window that never sold a ticket. ABANDONED IS PART OF THIS TUPLE rather than a separate read because it is not a detail: it is the difference between a window that is waiting for its randomness and a window that is paying its buyers back and will never have a winner. A front end that showed "unresolved" for both would be lying to a buyer about whether to expect a draw or to go and claim a refund. COMMITTED-AT IS THE LAST ELEMENT, and it is here so a front end can compute the one number a buyer of a stuck window actually wants: CommittedAt + refundGraceBlocks, the height at which their money becomes reclaimable whatever the beacon does. RefundOpensAt does that arithmetic; this returns the raw height so a caller can also show how long the window has been waiting. Zero on an uncommitted window, which is exactly the "no refund clock is running yet" case. THERE IS NO SPONSORED ELEMENT, unlike the raffle's nine-tuple: this realm's pot is entry money only, so "pot minus sponsored" — the number a raffle buyer needs to know which part of the pot is a claim of theirs — is just the pot.

Open
OID
07ebcb…6ac6:55
DrawOf details

Inspect func

RefundGraceBlocks

func() int64

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

Open
OID
07ebcb…6ac6:56
RefundGraceBlocks details

Inspect func

RefundOpensAt

func(window int64) int64

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

Open
OID
07ebcb…6ac6:57
RefundOpensAt details

Inspect func

CapturedRandomness

func(window int64) string

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

Open
OID
07ebcb…6ac6:58
CapturedRandomness details

Inspect func

OpsPot

func() int64

OpsPot is the house's EARNED revenue this realm holds, in ugnot: the fees of windows that actually drew, plus absorbed donations, plus any swept ticketless window's pot. IT IS NOT "THE ACCUMULATED HOUSE CUT" ANY MORE, which is what this comment said until 2026-09-29. A live window's fee is NOT in here — it is escrowed in that window and is still refundable. Read EscrowedCut and HeldCuts, and finding 25.

Open
OID
07ebcb…6ac6:59
OpsPot details

Inspect func

EscrowedCut

func(window int64) int64

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

Open
OID
07ebcb…6ac6:60
EscrowedCut details

Inspect func

HeldCuts

func() int64

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

Open
OID
07ebcb…6ac6:61
HeldCuts details

Inspect func

PageLimit

int64

PageLimit is the most window numbers one Windows call returns. A larger limit is clamped to it, not refused.

Value

100

WindowCount

func() int64

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

Open
OID
07ebcb…6ac6:62
WindowCount details

Inspect func

Windows

func(offset int64, limit int64) []int64

Windows is a window over the recorded window numbers in ASCENDING ORDER: it skips \`offset\` of them and holds at most min(limit, PageLimit). Empty past the end. Negative arguments are a caller error and panic. Ascending order is window order, not merely key order, because windowKey is fixed-width and zero-padded (see keyWidth) — the same property Render relies on. A CALLER FINDS THE WINDOWS THAT STILL NEED SOMETHING DOING by walking this and keeping those where window \< CurrentWindow() and DrawOf reports not resolved: those need a Commit if not committed, a Resolve once their round is relayed, and a Refund past RefundOpensAt if nobody relays it.

Open
OID
07ebcb…6ac6:63
Windows details

Inspect func

windowOfKey

func(key string) int64

windowOfKey inverts windowKey. ParseInt reads the zero padding fine; a key that does not parse cannot have come from windowKey, so it is a corrupt tree and not a case to paper over.

Open
OID
07ebcb…6ac6:64
windowOfKey details

Inspect func

Render

func(path string) string

Render is the human-readable view. It ITERATES THE avl.Tree in key order, which is window order because windowKey is fixed-width and zero-padded, so the output is byte-identical across runs. A Go map here would randomise the order and make the rendered state unreproducible. IT LISTS EVERY RECORDED WINDOW, and at 24 a day that table grows without bound — which is the same unbounded growth r/splash/arcade/raffle's Render has at 52 a year, reached ~168x faster. It is left unbounded here deliberately rather than silently truncated: Windows/PageLimit is the paged reader for a front end, and a Render that quietly stopped at N would be a page that lies about the realm's state. If gnoweb's response size ever becomes the binding constraint, the right change is an explicit "showing the last N of M" line, not a silent cap.

Open
OID
07ebcb…6ac6:65
Render details

Inspect func
TicketPrice : int64 =1000000
houseCutBps : int64 =500
bpsDenom : int64 =10000
cutPerTicket : int64 =50000
opsVaultAddr : .uverse.address =<gnolang.StringValue>
blocksPerDay : untyped bigint =(26182 <untyped> bigint)
windowBlocks : untyped bigint =(1090 <untyped> bigint)
randomnessHexLen : untyped bigint =(64 <untyped> bigint)
roundsPerWindow : untyped bigint =(1200 <untyped> bigint)
pinOffsetWindows : untyped bigint =(2 <untyped> bigint)
pinOffsetRounds : untyped bigint =(2400 <untyped> bigint)
speedMaxStaleBlocks : untyped bigint =(2180 <untyped> bigint)
maxPinnableLatest : int64 =9223372036854773407
refundGraceWindows : untyped bigint =(12 <untyped> bigint)
refundGraceBlocks : untyped bigint =(13080 <untyped> bigint)
Draw : type =v2.Draw
draws : *v0.Tree Inspect
opsPot : int64 =2350000
heldPots : int64 =0
heldCuts : int64 =0
launchHeight : int64 =254844
init.21 : func() Inspect
realmSelf : .uverse.address =<gnolang.StringValue>
selfAddr : func() .uverse.address Inspect
keyWidth : untyped bigint =(19 <untyped> bigint)
zeroPad : untyped string ="0000000000000000000"
pad : func(s string) string Inspect
windowKey : func(window int64) string Inspect
indexKey : func(i int) string Inspect
currentWindow : func() int64 Inspect
windowEndHeight : func(window int64) int64 Inspect
mulDiv : func(a uint64, b uint64, d uint64) uint64 Inspect
maxInt64 : uint64 =9223372036854775807
ticketCost : func(tickets int64) int64 Inspect
hexDigit : func(c uint8) (uint64, bool) Inspect
randomnessMod : func(randomness string, m int64) int64 Inspect
getDraw : func(window int64) *v2.Draw Inspect
getOrCreateDraw : func(window int64) *v2.Draw Inspect
treeInt64 : func(t *v0.Tree, key string) int64 Inspect
appendTickets : func(d *v2.Draw, addr .uverse.address, units int64) Inspect
ownerOfUnit : func(d *v2.Draw, k int64) .uverse.address Inspect
rejectAttachedCoin : func(what string) Inspect
sweepDonations : func() Inspect
absorbDonation : func() int64 Inspect
checkInvariant : func() Inspect
AuditFailure : func() string Inspect
AuditInvariant : func() bool Inspect
Buy : func(tickets int64) Inspect
Commit : func(window int64) int64 Inspect
Capture : func(window int64) Inspect
Resolve : func(window int64) Inspect
refundGraceElapsed : func(d *v2.Draw) bool Inspect
Refund : func(window int64) Inspect
SweepDrawlessWindow : func(window int64) int64 Inspect
WithdrawOps : func() int64 Inspect
PotSize : func() int64 Inspect
TicketTotal : func() int64 Inspect
TicketTotalOfWindow : func(window int64) int64 Inspect
TicketsOf : func(addr .uverse.address) int64 Inspect
TicketsOfWindow : func(window int64, addr .uverse.address) int64 Inspect
SpendOf : func(addr .uverse.address) int64 Inspect
SpendOfWindow : func(window int64, addr .uverse.address) int64 Inspect
CurrentWindow : func() int64 Inspect
WindowBlocks : func() int64 Inspect
WindowEndHeight : func(window int64) int64 Inspect
CurrentWindowEndHeight : func() int64 Inspect
PinOffsetRounds : func() int64 Inspect
MaxStaleBlocks : func() int64 Inspect
DrawOf : func(window int64) (bool, int64, int64, int64, bool, .uverse.address, bool, int64) Inspect
RefundGraceBlocks : func() int64 Inspect
RefundOpensAt : func(window int64) int64 Inspect
CapturedRandomness : func(window int64) string Inspect
OpsPot : func() int64 Inspect
EscrowedCut : func(window int64) int64 Inspect
HeldCuts : func() int64 Inspect
PageLimit : int64 =100
WindowCount : func() int64 Inspect
Windows : func(offset int64, limit int64) []int64 Inspect
windowOfKey : func(key string) int64 Inspect
Render : func(path string) string Inspect