WithdrawOps
Ask AI
WithdrawOps pushes the whole ops pot to opsVaultAddr and returns the amount moved. PERMISSIONLESS.
THE DEFECT IT CLOSES, found 2026-09-28 by the engineer implementing that day's fifth fix rather than by the audit: opsPot only ever increased. It was written in three places — Buy's house cut (now Resolve's released escrow, finding 25), sweepDonations' absorbed arrivals, and Refund's move of an abandoned week's sponsorship — and read by nothing but the invariant and the reads. There was no outflow anywhere in this file, so three separate streams of money became permanently unreachable by every path in the realm, THE PRODUCT'S ENTIRE REVENUE AMONG THEM. checkInvariant and AuditFailure both stayed green over it, because both count opsPot as money the realm holds and neither has an opinion about whether anybody can ever reach it. That is the same class of defect as the unreachable week (the audit's first finding) and the stranded drawless-week sponsorship (SweepDrawlessWeek).
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 week'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. This is r/splash/exchange's SettleOps shape, and the property that makes it sound is the one tools/raffle_addr_test.go exists to keep true.
IT NEVER REACHES A PLAYER'S MONEY. No Draw is touched: not Pot, not Sponsored, 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 DRAW THAT ACTUALLY RAN (Resolve releases the week's escrow; a week that fails refunds it with the gross, finding 25), a donation belongs to whoever chose to give it away, and a dead week's sponsorship was never attributable to a buyer at all.
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 there is no such section in Refund any more — finding 25 deleted the behaviour and Refund records the retired paragraph verbatim in its place. 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. NOTE THAT THE THIRD STREAM IS NOW ALMOST CLOSED: since 2026-09-29 a dead week's sponsorship goes forward to a future week's prize and reaches this counter only when no week can be opened at all (Refund's transition, and AUDIT-2026-09-28.md finding 16). That is a deliberate reduction in house revenue and it is the safe direction.
AND SINCE 2026-09-29 IT CANNOT REACH AN UNEARNED FEE EITHER, which is the point of finding 25 from this function's side. The house cut used to arrive in opsPot the instant a ticket was sold, so this call could push out money for draws that had not happened yet — and the buyers of a week that then failed got 95% back while the missing 5% had already left the realm. The fee now sits in Draw.Cut until Resolve releases it, heldCuts is a separate term in checkInvariant, and this function does not mention either. A caller who withdraws every block still cannot touch a live week's escrow.
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 for the next one.
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 and heldSponsored are deliberately untouched.
Panics if there is nothing to withdraw.
Command
# WARNING: This command is running in an INSECURE mode.
# It is strongly recommended to use a hardware device for signing
# and avoid trusting any computer connected to the internet,
# as your private keys could be exposed.
gnokey maketx call -pkgpath "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/arcade/raffle/v2" -func "WithdrawOps" -gas-fee 1000000ugnot -gas-wanted 1_000_000_000 -send "" -chainid "onyx-1" -remote "https://rpc.onyx.testnets.gno.land" ADDRESSgnokey query -remote "https://rpc.onyx.testnets.gno.land" auth/accounts/ADDRESS
gnokey maketx call -pkgpath "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/arcade/raffle/v2" -func "WithdrawOps" -gas-fee 1000000ugnot -gas-wanted 1_000_000_000 -send "" -broadcast=false ADDRESS > call.tx
gnokey sign -tx-path call.tx -chainid "onyx-1" -account-number ACCOUNTNUMBER -account-sequence SEQUENCENUMBER ADDRESS
gnokey broadcast -remote "https://rpc.onyx.testnets.gno.land" call.tx