1type Draw struct {
2 Week int64
3
4 // Pot is the prize pot in ugnot: the sum of every buyer's contribution
5 // AFTER the house cut, PLUS any house money sponsored into this week
6 // (see Sponsored). Zeroed by Resolve when it pays the winner, and
7 // decremented by Refund as each buyer reclaims their share.
8 //
9 // THE IDENTITY IS NOW Pot + Cut == sum(spendOf) + Sponsored, and it is what makes
10 // Refund unable to overdraw. It was Pot == sum(spendOf) + Sponsored until
11 // 2026-09-29, when spendOf started recording the buyer's GROSS payment and the
12 // house's fee started being escrowed in Cut instead of banked at sale time. Read
13 // Cut, and AUDIT-2026-09-28.md finding 25.
14 Pot int64
15
16 // Cut is THE HOUSE'S FEE FOR THIS WEEK, HELD IN ESCROW AND NOT YET EARNED: the sum
17 // of HouseCutBps of every ticket sold into this week. In ugnot, like everything
18 // else here.
19 //
20 // IT IS NOT PART OF Pot AND THE WINNER MUST NEVER WIN IT. Resolve pays Pot to the
21 // winner and moves Cut to opsPot, in that order, as two separate amounts. If the
22 // fee were folded into Pot the winner would be paid the house's money; if it were
23 // in opsPot from the moment of sale — which is what happened until 2026-09-29 —
24 // then a week that never drew would refund only 95% of what its buyers paid.
25 //
26 // THE RULE IT IMPLEMENTS IS THE OWNER'S: WE EARN WHEN THE GAME RUNS, NOT WHEN IT
27 // FAILS. So the fee has exactly two destinations and no third:
28 //
29 // - A SUCCESSFUL Resolve moves it to opsPot. The draw happened; we ran it.
30 // - ANY OTHER OUTCOME REFUNDS IT. Refund returns each buyer's GROSS spend and
31 // decrements this field by exactly the part of that gross which came here in
32 // the first place (see Refund's split, and cutPerTicket's build guard for why
33 // that re-derivation is exact rather than approximately right). An ABANDONED
34 // week therefore hands the fee back with the prize money, which is the same
35 // principle finding 16 applied to Sponsored: we did not run a draw, so we do
36 // not keep the fee.
37 //
38 // AND IT IS A TERM IN BOTH SOLVENCY STATEMENTS, not a note on the side. The realm
39 // holds this wugnot, so checkInvariant is heldPots + heldCuts + opsPot == balance
40 // and AuditFailure re-derives both new terms by iteration. A field the realm holds
41 // money for and the invariant does not mention is the shape of a hole.
42 //
43 // A SWEPT OR DRAWLESS WEEK HAS Cut == 0 ONLY AFTER foldSponsorCut RUNS, AND IT USED
44 // TO SAY 'BY CONSTRUCTION'. That was true until Sponsor gave a fee a second way in,
45 // with no ticket sale behind it, and the stale sentence is exactly what produced the
46 // 2026-09-30 audit's RED finding: SweepDrawlessWeek asserted the old claim and
47 // stranded the whole week. The original reasoning, kept because it is still why the
48 // assertion is worth making, because both sweepable
49 // shapes require sum(spendOf) == 0 and a fee can only arrive with a ticket sale.
50 // SweepDrawlessWeek asserts that rather than assuming it.
51 Cut int64
52
53 // Sponsored is the part of Pot that came from FundPot rather than from a
54 // ticket sale: house or rake money, ATTRIBUTABLE TO NO BUYER. It buys no
55 // tickets and no winning chance; it only makes the prize bigger.
56 //
57 // THE MONEY IS STILL ATTRIBUTABLE TO NO BUYER AFTER FINDING 15, AND THAT IS
58 // WORTH BEING PRECISE ABOUT. Duel play now earns ENTRIES as well as sending
59 // rake (FundPotWithEntries), so the same transaction that raises this field
60 // also appends ticket weight for the two players. The two are still separate
61 // facts: the entries are weight with no spendOf record, this field is money
62 // with no owner, and neither is a claim on the other. The funder — the duel
63 // realm — still gets nothing at all, so a realm that funds a pot still cannot
64 // be the address a payout is pushed to.
65 //
66 // IT IS A SEPARATE FIELD BECAUSE THE REFUND SAFETY PROPERTY IS AN IDENTITY,
67 // NOT AN INEQUALITY. Every ugnot of a week's pot must be attributable either
68 // to a buyer who can reclaim exactly their own contribution, or to this
69 // field. Adding sponsorship straight into Pot with nothing recording it
70 // would leave Pot > sum(spends) — refunds could then no longer be shown to
71 // be unable to overdraw, and the books could not tell house money from a
72 // buyer's claim. So the invariant is Pot == sum(spendOf) + Sponsored, and
73 // both checkInvariant (through heldSponsored) and AuditFailure (by
74 // iteration) are written in those terms.
75 //
76 // IT IS NEVER LEFT BEHIND IN A POT NOBODY CAN REACH, and there are two such
77 // pots. On an ABANDONED week (see Refund) nobody can reclaim it, so leaving it in Pot
78 // while every spend is zeroed and nothing could ever move it would strand it forever.
79 // On a finished week that NEVER SOLD A TICKET (see SweepDrawlessWeek) that week can
80 // never be committed, so it can never be resolved or refunded either, and the money
81 // would be just as stranded. Both are the same class of defect as the unreachable week
82 // the 2026-09-28 audit's first finding was about.
83 //
84 // WHAT REACHES IT IN BOTH CASES IS retireSponsorship, SINCE 2026-09-29 (SECOND
85 // REVISION — finding 31). This field is zeroed AT THE MOMENT THE WEEK IS ABANDONED and
86 // the money is turned into A PER-UNIT CLAIM ON THIS SAME WEEK (ClaimPerUnit, Claims),
87 // payable to the people who hold units IN THIS WEEK. It never changes weeks, never
88 // waits in a register, and has no destination anybody can name. So `Sponsored > 0 on an
89 // abandoned week` is now a STATE THAT CANNOT EXIST, and AuditFailure says so.
90 //
91 // THE DESTINATION USED TO DISAGREE WITH ITSELF, THEN AGREED, THEN BECAME CHOOSABLE,
92 // THEN BECAME A REGISTER, AND IS NOW GONE. The full sequence, because it is the whole
93 // lesson:
94 //
95 // - Until 2026-09-29 the abandoned-week path sent this money to opsPot while the
96 // drawless-week path rolled it forward, over the same field, in the same file
97 // (AUDIT-2026-09-28.md finding 16: that asymmetry is a finding, not untidiness).
98 // - Both then rolled it into currentWeek() — the week live at the moment a
99 // permissionless caller fired, which let the caller pick the destination by picking
100 // the moment (finding 28).
101 // - Then a REGISTER (carryPending) held it until the next week record pulled it in at
102 // its own birth. That was meant to make the destination unchoosable. It did not:
103 // a caller could BUY ONE TICKET IN THE FINAL BLOCK OF A QUIET WEEK, which both
104 // created that week's record and absorbed every pending carry into a week whose
105 // entire ticket set was their own — 100% capture for the price of the house cut
106 // (finding 31). The "destination is young" property the design rested on was
107 // asserted in a test and never enforced in the code.
108 // - Now there is NO DESTINATION AT ALL. The money goes back to the addresses that
109 // hold units in the week it was already in.
110 Sponsored int64
111
112 // SponsorCut is HOW MUCH OF d.Cut CAME FROM SPONSORS rather than from ticket
113 // sales, and it exists for exactly one reason: so that fee has a way out of an
114 // abandoned week.
115 //
116 // THE DEFECT IT PREVENTS, STATED PLAINLY BECAUSE IT IS INVISIBLE OTHERWISE. Refund
117 // is what retires a buyer's share of d.Cut, and it does so by RE-DERIVING the fee
118 // from that buyer's own spendOf. A sponsor has no spendOf — sponsorship buys no
119 // units, that is its definition — so no Refund call will ever reach a sponsor's
120 // fee. Left in d.Cut it would be money that entered the realm and could not leave,
121 // and checkInvariant would go on balancing happily the whole time, because a
122 // balanced invariant is not evidence of a reachable exit.
123 //
124 // AND FOLDING SPONSORSHIP INTO spendOf TO REUSE Refund IS NOT THE ALTERNATIVE. That
125 // re-derivation is exact ONLY because a spendOf is a whole number of TicketPrice;
126 // an arbitrary sponsorship is not, so d.Cut would go negative and a BUYER's own
127 // reclaim would start panicking. That arithmetic is AUDIT-2026-09-28.md finding 31.
128 //
129 // SO IT LEAVES BY THE DOOR THE MONEY IT WAS TAKEN FROM LEAVES BY: retireSponsorship
130 // folds it back into Sponsored on the abandonment transition, and it is divided
131 // among that week's unit holders with the rest. On a week that DOES draw it is not
132 // folded and goes to ops with every other fee, which is the owner's rule for fees
133 // generally — earned only if the draw actually runs.
134 SponsorCut int64
135
136 // SponsorPaid is the GROSS total of every Sponsor call on this week, and it is
137 // what separates money with an owner from money without one.
138 //
139 // d.Sponsored MIXES TWO SOURCES AND ONLY ONE OF THEM HAS A PAYER. FundPot — the
140 // duel's door — puts ownerless rake there, money attributable to no address.
141 // Sponsor puts a named person's payment there. Until 2026-09-30 an abandoned week
142 // treated both the same and divided them among unit holders, or sent them to the
143 // house when there were no earned units. The owner ruled that out: a sponsor's
144 // money is "either returned to sponsor or rolls over until a draw happens", and
145 // rolling forward is what AUDIT-2026-09-28.md findings 28 and 31 removed, because
146 // the caller picked the destination week by picking when to call. So it is
147 // returned, and this is the figure that says how much of the pot is owed back.
148 SponsorPaid int64
149
150 // SponsorRefundable is what is left of SponsorPaid once the week has died and
151 // before its sponsors have pulled it back. It is still sitting in d.Pot.
152 SponsorRefundable int64
153
154 // sponsorRefundedOf marks the addresses that have already pulled their refund, so
155 // a second call pays nothing rather than paying twice.
156 sponsorRefundedOf *avl.Tree
157
158 // Sponsors is the credit line each sponsorship bought, in the order they were
159 // bought, bounded by maxSponsorsPerWeek.
160 //
161 // THE TEXT IS A STRANGER'S AND IT IS PERMANENT. This is the first untrusted text
162 // this realm has ever stored. NOTHING ESCAPES IT TODAY, and an earlier version of
163 // this sentence claimed both ends did. checkSponsorLabel deliberately does not
164 // sanitise — escaping belongs where the target syntax is known — and Render does
165 // not yet emit this field at all.
166 //
167 // WHOEVER ADDS THAT DISPLAY MUST ESCAPE IT. Render builds a PIPE-DELIMITED
168 // MARKDOWN TABLE; the label may legally hold '|', '[', ']', '<', '>' and
169 // backticks, and 64 bytes is ample to forge extra cells and fake a column. Use the
170 // duel's approach: sanitize.InlineText, plus escaping '|'. Flagged by the
171 // 2026-09-30 audit, finding Y3.
172 //
173 // Escaping cannot strip CONTENT either way, and nothing can delete it
174 // once written — a later realm version does not erase what an earlier one wrote.
175 // The owner was told exactly that on 2026-09-30 and accepted it: "whatever, we can
176 // always redeploy with 2.0 or whatever, it's a game".
177 Sponsors []Sponsorship
178
179 // ClaimPerUnit is the UGNOT EACH *EARNED* ENTRY UNIT OF THIS WEEK MAY RECLAIM from the
180 // sponsorship this week's draw never got to pay out. ZERO ON EVERY LIVE WEEK and
181 // written exactly once, by retireSponsorship, at the moment the week is abandoned.
182 //
183 // "EARNED" IS LOAD-BEARING AND THIS COMMENT SAID "EACH TICKET UNIT" UNTIL
184 // AUDIT-2026-09-28.md finding 34 — so did the code. A BOUGHT unit contributed nothing to
185 // the sponsorship and recovers its own money in full through Refund; an earned unit IS the
186 // sponsorship, at 150,000 ugnot, and has no other route home. Dividing on both kinds paid
187 // a refunded buyer out of the duel players' rake. Read Draw.earnedOf and
188 // retireSponsorship.
189 //
190 // IT IS THE WHOLE OF finding 31's FIX AND IT IS A RATE, NOT A BALANCE. Writing one
191 // number instead of one tree entry per holder is what keeps the abandonment transition
192 // O(1): the transition happens inside Refund, which MUST NOT FAIL (a buyer is waiting),
193 // and a loop over an unbounded holder set is a gas limit waiting to make a week's
194 // refunds impossible — which would be a fresh "money in, no way out" path created by
195 // the fix. A holder's claim is derived on demand in ClaimDeadWeek as
196 // earnedOf[holder] * this, which is O(log N) and cannot be starved.
197 //
198 // IT IS A WHOLE NUMBER OF UGNOT PER EARNED UNIT, so the shares sum to
199 // ClaimPerUnit * earnedTotal EXACTLY and never to more than the sponsorship. The
200 // remainder — strictly less than the week's EARNED unit count, in ugnot, which is under
201 // seven parts per million of the sponsorship — is dust with no
202 // unit to attach to; retireSponsorship moves it to opsPot in the same statement and
203 // records it in the event, so it is never left in a pot with no claim on it. Read that
204 // function for the arithmetic, for the improved bound, and for why the alternatives (a
205 // largest-remainder pass, or first-claimant-takes-the-dust) were rejected.
206 //
207 // NON-ZERO IMPLIES Abandoned, always, and AuditFailure re-derives that. An unabandoned
208 // week can still sell tickets and still resolve, so a claim rate on one would be a
209 // claim on a prize somebody is about to win.
210 ClaimPerUnit int64
211
212 // Claims is the UNCLAIMED part of what retireSponsorship distributed: it starts at
213 // ClaimPerUnit * earnedTotal and falls by each holder's share as ClaimDeadWeek pays
214 // it. IT IS PART OF Pot, exactly as Sponsored was, so no wugnot moved when it came into
215 // existence and the realm's balance did not change.
216 //
217 // THE DIVISOR IN THAT PRODUCT WAS tickets.Total() UNTIL AUDIT-2026-09-28.md finding 34,
218 // and the sum of every possible claim is EXACTLY this number under either basis — that was
219 // never the defect. What was wrong is WHO the claimants were: bought units took a share of
220 // money they had not contributed. Read Draw.earnedOf.
221 //
222 // IT IS A TERM IN BOTH SOLVENCY STATEMENTS. The per-week identity is
223 //
224 // Pot + Cut == sum(spendOf) + Sponsored + Claims
225 //
226 // and the global one gains heldClaims. A field the realm holds money for and the
227 // invariant does not mention is the shape of a hole — the same sentence Cut carries,
228 // for the same reason.
229 Claims int64
230
231 // claimedOf is string(address) -> the amount that holder has already taken out of
232 // Claims. AN ENTRY MEANS "PAID", so ClaimDeadWeek is all-or-nothing per holder and can
233 // never pay twice: a holder's EARNED unit count cannot change after abandonment (an
234 // abandoned week is always a past week, and FundPotWithEntries — the only writer of
235 // earnedOf — only ever touches currentWeek()), so their share is a constant and one entry
236 // settles it for ever.
237 //
238 // THE FROZEN-BASIS PROPERTY IS ENFORCED BY THREE PANICS AND NOT BY THIS SENTENCE, which
239 // is the difference between this and finding 31's second attempt — that one rested on a
240 // property asserted in a test and never enforced in code. The three, named so a future
241 // reader knows they are load-bearing and not hygiene:
242 //
243 // 1. FundPot's refusal of a committed or abandoned week. It is the money path
244 // FundPotWithEntries calls before it appends anything, so no earned unit can ever
245 // reach a week that is already dead.
246 // 2. retireSponsorship's "only an abandoned, unresolved week's sponsorship may be
247 // retired". The rate cannot be written before the week's exit is one-way.
248 // 3. retireSponsorship's "that week's sponsorship has already been retired". The rate
249 // cannot be written twice, so it cannot be re-derived against a basis that has moved.
250 //
251 // TestFix34_ThreePanicsEnforceTheFrozenBasis names all three and drives each one.
252 claimedOf *avl.Tree
253
254 Committed bool
255 FrozenTotal int64 // ticket units at commit. NEVER recomputed at resolve.
256
257 // Round is the beacon round this week is drawn on. WRITTEN EXACTLY ONCE, by
258 // getOrCreateDraw, at the moment this record is created — which is before the
259 // week's first ticket exists — and never written again by anything, Commit
260 // included. Read pinOffsetRounds for why, and for what that does and does not
261 // protect against; the one-sentence version is that the committer used to
262 // choose this number after the ticket set was already frozen and public, which
263 // handed the beacon's key holder a deterministic win.
264 Round int64
265
266 // CommittedAt is the CHAIN HEIGHT at which Commit froze this week, and it is
267 // the only clock the refund grace window reads. Zero on an uncommitted week.
268 //
269 // IT IS STORED, AND IT CANNOT BE DERIVED FROM ANYTHING ELSE. The week number
270 // comes from the same global clock, but a week may be committed at any time
271 // after it ends (Commit takes the week as an argument precisely so a stranded
272 // week is always recoverable), so "when was this week committed" is not a
273 // function of which week it is. Round cannot stand in for it either: Round is
274 // beacon state, and the whole point of this field is to give Refund a gate the
275 // beacon cannot hold shut. See refundGraceBlocks.
276 CommittedAt int64
277
278 // Randomness is the round's randomness, copied in by Capture. Empty means "not
279 // captured"; Resolve then reads the beacon directly. See Capture — and note that
280 // since the drand migration this field is a convenience, not a rescue: the
281 // beacon cannot lose a round, so nothing is preserved by copying it here.
282 Randomness string
283
284 Resolved bool
285 Winner address
286
287 // Abandoned is set by the FIRST successful Refund for this week and is
288 // never cleared by anything. It makes the week's exit one-way: once one
289 // buyer has taken their money back, this week can never be resolved, and
290 // every later refund proceeds without re-asking whether the draw looks
291 // resolvable again.
292 //
293 // IT CANNOT BE DERIVED FROM THE BEACON, which is why it is stored — AND THE
294 // DRAND MIGRATION MADE THAT MORE TRUE, NOT LESS. The old reason was that
295 // resolvable() was not monotone: Publish had no window check and Prune removed a
296 // round without moving latestRound, so a pruned round's Published() went back to
297 // false and the same round could be published again, arbitrarily late. There is no
298 // Prune now, so Published() IS monotone — but resolvable() is gone entirely and
299 // a pinned round can be relayed by ANYONE, from drand, AT ANY TIME, FOR EVER. So a
300 // week in refund would otherwise stay resolvable indefinitely, which is worse than
301 // a window that occasionally reopened. Without this flag, anyone could let refunds
302 // open on a
303 // week, wait for some buyers to reclaim, then re-publish or late-publish the
304 // round and Resolve — drawing over the ORIGINAL frozen range, so a buyer who
305 // had already been refunded could win and collect what was left in the pot:
306 // the money of the buyers who had not reclaimed yet. Someone holding the
307 // signature did strictly better still, computing the winner first and then
308 // choosing Refund if it was not them and publish-and-Resolve if it was. Pot
309 // == sum(spends) held throughout, so neither checkInvariant nor AuditFailure
310 // saw anything — the identity is sound, it is just blind to WHO is paid.
311 Abandoned bool
312
313 // tickets is the weighted ticket index for this week. ONE INDEX PER
314 // PURCHASE is appended and no existing index's value is ever changed —
315 // that immutability is the whole frozen-ownership argument on Commit.
316 //
317 // SINCE FINDING 15 A PURCHASE IS NOT THE ONLY THING THAT APPENDS ONE: an
318 // earned entry credited through FundPotWithEntries appends exactly as a
319 // purchase does, through the same appendTickets, and is indistinguishable
320 // here by design. What distinguishes the two is spendOf, which an earned
321 // entry does not touch. Nothing may ever INSERT or ADJUST an index — fenwick
322 // refuses negative deltas by design and Commit's argument depends on it.
323 tickets *fenwick.Tree
324
325 // ownerOf is index -> string(address), the reverse lookup that turns a
326 // drawn unit back into a winner. MANY INDICES MAP TO THE SAME ADDRESS and
327 // that is expected: one entry per purchase.
328 ownerOf *avl.Tree
329
330 // ticketsOf is string(address) -> int64 ticket units, a read-side running
331 // total so TicketsOf is O(log N) and never has to walk the index. It is
332 // NOT read by the draw: Select and ownerOf decide the winner.
333 //
334 // IT IS NOT THE DEAD-WEEK CLAIM BASIS AND IT USED TO BE — AUDIT-2026-09-28.md finding
335 // 34. It counts BOUGHT and EARNED units together, which is exactly right for the draw
336 // (a unit is a unit, whoever paid for it, and that indistinguishability is finding 15's
337 // whole design) and exactly wrong for dividing up a dead week's sponsorship. Read
338 // earnedOf below for the arithmetic.
339 ticketsOf *avl.Tree
340
341 // earnedOf is string(address) -> int64 EARNED entry units: the subset of ticketsOf that
342 // was credited by FundPotWithEntries rather than bought with Buy. IT IS THE BASIS ON
343 // WHICH A DEAD WEEK'S SPONSORSHIP IS DIVIDED, and that is the whole of
344 // AUDIT-2026-09-28.md finding 34.
345 //
346 // WHY THE DRAW AND THE CLAIM NEED DIFFERENT BASES, WHICH IS THE ONE THING WORTH
347 // UNDERSTANDING HERE. Both kinds of unit are identical in the draw and must stay so.
348 // They are NOT identical in what they contributed to the thing a dead week divides:
349 //
350 // An EARNED unit put EntryUnitPrice * arcadeRakePotShareBps / arcadeBpsDenom =
351 // 150,000 ugnot INTO Sponsored — it IS the sponsorship — and its holder can recover
352 // that money by no other route, because an earned entry has no spendOf record and
353 // Refund pays spendOf and nothing else.
354 //
355 // A BOUGHT unit put NOTHING into Sponsored. It put TicketPrice / unitsPerTicket =
356 // 250,000 ugnot into Pot and Cut, and its holder recovers THAT IN FULL through Refund
357 // — the whole gross, fee included, since finding 25.
358 //
359 // SO A REFUNDED BUYER ON AN ABANDONED WEEK ENDS WITH ZERO NET STAKE. Their money came
360 // back entirely and they KEEP THEIR UNITS, deliberately (fenwick rejects negative deltas
361 // by design, so a ticket cannot be retired once sold — see Refund). Dividing the
362 // sponsorship on ticketsOf therefore handed that buyer a positive claim on the duel
363 // players' rake for nothing at all: with equal counts of the two kinds, each player
364 // recovered HALF of what their own rake contributed and a refunded buyer took the other
365 // half. Paying those players is finding 31's entire stated purpose, so that was not a
366 // rounding error in the fix, it was the fix missing its target.
367 //
368 // sum(earnedOf) == earnedTotal IDENTICALLY, and it is guaranteed the same way
369 // sum(ticketsOf) == tickets.Total() is: appendEarnedTickets is the ONLY writer of either,
370 // it writes both in one statement group from the same `units` argument, and nothing in
371 // this file ever decrements or removes an entry. There is no second writer to drift from
372 // the first. AuditFailure re-derives the sum by iteration anyway
373 // (deadWeekClaimFailure), because an identity nothing re-derives is an identity nobody
374 // finds out about.
375 earnedOf *avl.Tree
376
377 // earnedTotal is the sum of earnedOf — the O(1) counter that keeps the abandonment
378 // transition constant-time. IT IS NOT tickets.Total() AND THE GAP IS THE BOUGHT UNITS:
379 // 0 <= earnedTotal <= tickets.Total(), with equality exactly on a week where no ticket
380 // was ever bought (the PURE-EARNED shape SweepDrawlessWeek calls shape 2).
381 //
382 // A SEPARATE COUNTER RATHER THAN AN ITERATION, for the same reason ClaimPerUnit is a rate
383 // rather than a tree of balances: retireSponsorship runs inside Refund, Refund MUST NOT
384 // FAIL, and a week's earned-holder set is unbounded (two more holders for every duel
385 // played in the week). Reading a field is O(1); summing a tree is a gas ceiling on a
386 // payout path. Read retireSponsorship.
387 //
388 // ZERO IS A REAL AND REACHABLE VALUE ON A WEEK WITH SPONSORSHIP: plain FundPot money from
389 // any caller that is not the duel, a duel rake too small to buy one whole entry unit, or
390 // a duel both of whose parties played from realm code. That money has no player behind
391 // it, so retireSponsorship's no-claimant branch sends it to opsPot — the destination
392 // nobody can choose. A week that has ever had an earned holder can never fall into that
393 // branch, because this counter only ever increases.
394 earnedTotal int64
395
396 // spendOf is string(address) -> int64, the buyer's GROSS payment for this week's
397 // tickets — the whole ticket price, house fee included — which is exactly what
398 // Refund returns.
399 //
400 // IT WAS THE POST-CUT CONTRIBUTION UNTIL 2026-09-29 and the old comment said "the
401 // house cut is not in here and is never refunded". BOTH HALVES OF THAT ARE NOW
402 // FALSE and the sentence is corrected rather than removed, because it is the one a
403 // reader will have in their head. The fee is escrowed in Cut for the week rather
404 // than banked at sale time, so it IS part of what a buyer can reclaim; what a
405 // refund returns is the gross, and the week's Pot and Cut are each decremented by
406 // the part of that gross they received. AUDIT-2026-09-28.md finding 25.
407 //
408 // IT IS NOT A TICKET COUNT AND SINCE FINDING 15 THE DIFFERENCE IS REACHABLE.
409 // An earned entry (FundPotWithEntries) adds ticket weight and NOTHING here,
410 // so an address can hold tickets in a week with a zero spend, and a whole
411 // week can have tickets and no spend records at all. That is intended: the
412 // money behind those entries is in Sponsored, which no refund can return.
413 // Refund reads this tree and only this tree.
414 spendOf *avl.Tree
415}