ops.gno
6.17 Kb · 147 lines
1// Package ops is the house's till.
2//
3// WHAT IT IS FOR. The three arcade realms each take a cut -- 5% of a draw ticket,
4// 3% of a duel stake -- and each pushes it to ONE compile-time address,
5// opsVaultAddr, which is the address derived from THIS package's path. Until this
6// realm existed, that address had no code at it: the money left the games and
7// landed somewhere nothing could ever spend from. That is
8// AUDIT-2026-10-07.md RED-2 and the arcade's own source names it, in terms, as
9// "still blocking for deployment ... the owner's address is the missing input".
10//
11// This realm is that input, and nothing else. It holds wrapped GNOT and lets one
12// address take it out.
13//
14// WHAT IT DELIBERATELY DOES NOT DO, because every one of these is a way for a
15// till to become a liability:
16//
17// - NO ACCESS TO ANY PRIZE POT. It cannot reach one. The draws pay winners from
18// their own balances and this realm has no call into them. The owner key
19// cannot touch a player's money because there is no code here that could.
20// - NO OWNERSHIP TRANSFER. owner is a compile-time constant with no setter, so
21// there is no function an attacker can reach to become the owner, and no
22// function the owner can be tricked into calling. The cost is stated below.
23// - NO PAUSE, NO UPGRADE, NO ADMIN. Nothing here can be turned off or changed
24// after deployment, by anyone.
25// - NO NATIVE COIN. chain/banker is not imported. This realm can move wrapped
26// GNOT and nothing else, which is what the games actually send.
27//
28// THE COST OF A FIXED OWNER, SAID PLAINLY RATHER THAN DISCOVERED LATER: if that
29// key is lost, every coin in this realm is lost with it. There is no recovery
30// path and cannot be one without adding exactly the kind of privileged function
31// the list above refuses. That trade is the owner's and it was made knowingly.
32package ops
33
34import (
35 "chain"
36 "chain/runtime/unsafe"
37 "strconv"
38
39 "gno.land/r/gnoland/wugnot"
40)
41
42// owner is the only address that may withdraw. Compile-time, no setter.
43//
44// IT IS THE OWNER'S PERSONAL WALLET AND NOT A REALM, which is the opposite of
45// how the games address this one. That is deliberate: a till wants a person at
46// the end of it, and putting another contract there would just move the same
47// question one step along.
48const owner address = "g1srj6dxvyd378vm7787r45drryzvalcu32rdkfu"
49
50// Owner reports who may withdraw. Exported so the claim is checkable from
51// outside rather than taken from this comment.
52func Owner() address { return owner }
53
54// Balance is the wrapped GNOT this realm holds, read from wugnot rather than
55// tracked in a counter of our own. A realm that keeps its own idea of its
56// balance is a realm that can disagree with the truth.
57func Balance(cur realm) int64 { return wugnot.BalanceOf(cur.Address()) }
58
59// rejectAttachedCoin panics if the caller attached native coin to a call that
60// has no use for it. Identical in shape and reasoning to the guard the three
61// arcade realms gained on 2026-10-07: every money path here is wrapped GNOT, so
62// attached ugnot would sit in this realm for ever with nothing able to reach it.
63//
64// Inert on a non-user call, for the reason r/splash/exchange's Buy comment
65// gives: unsafe.OriginSend() reads the envelope attached at the chain root,
66// shared by every realm in the call chain, so reading it is only sound when the
67// immediate caller is an EOA.
68func rejectAttachedCoin(cur realm, what string) {
69 if !cur.Previous().IsUserCall() {
70 return
71 }
72 if sent := unsafe.OriginSend(); len(sent) != 0 {
73 panic("ops: " + what + " does not take native coin")
74 }
75}
76
77// requireOwner is the only authorisation in this realm.
78//
79// cur.Previous().Address() AND NOT runtime.OriginCaller(). The origin caller is
80// whoever signed the transaction, which is the same thing right up until the
81// owner is persuaded to call some other realm that calls this one -- at which
82// point an origin check would authorise a withdrawal the owner never asked for.
83// The immediate caller is the one that cannot be borrowed.
84func requireOwner(cur realm) {
85 if cur.Previous().Address() != owner {
86 panic("ops: only the owner may withdraw")
87 }
88}
89
90// Withdraw sends `amount` of wrapped GNOT to the owner.
91//
92// THE DESTINATION IS NOT AN ARGUMENT. It is always the owner's own address, so
93// there is no call shape in which this realm pays somebody else -- not by
94// mistake, not by a malformed argument, not by a caller who has somehow got past
95// requireOwner. A withdrawal function that takes a recipient is one typo away
96// from being a drain.
97func Withdraw(cur realm, amount int64) int64 {
98 rejectAttachedCoin(cur, "Withdraw")
99 requireOwner(cur)
100 if amount <= 0 {
101 panic("ops: amount must be positive")
102 }
103 have := wugnot.BalanceOf(cur.Address())
104 if amount > have {
105 panic("ops: asked for " + strconv.FormatInt(amount, 10) +
106 " and hold " + strconv.FormatInt(have, 10))
107 }
108 wugnot.Transfer(cross(cur), owner, amount)
109 chain.Emit("OpsWithdraw",
110 "to", string(owner),
111 "amount", strconv.FormatInt(amount, 10),
112 )
113 return amount
114}
115
116// WithdrawAll empties the till to the owner. Refuses on an empty one rather than
117// succeeding with a zero transfer: "it worked" and "there was nothing there"
118// are different answers and a caller deserves to be told which.
119func WithdrawAll(cur realm) int64 {
120 rejectAttachedCoin(cur, "WithdrawAll")
121 requireOwner(cur)
122 have := wugnot.BalanceOf(cur.Address())
123 if have == 0 {
124 panic("ops: nothing to withdraw")
125 }
126 wugnot.Transfer(cross(cur), owner, have)
127 chain.Emit("OpsWithdraw",
128 "to", string(owner),
129 "amount", strconv.FormatInt(have, 10),
130 )
131 return have
132}
133
134func Render(path string) string {
135 return "# SPLASH ops\n\n" +
136 "The house's till. It holds the cut the three arcade games take " +
137 "(5% of a draw ticket, 3% of a duel stake) and lets exactly one address " +
138 "withdraw it.\n\n" +
139 "- **Owner:** `" + string(owner) + "`\n" +
140 "- **No ownership transfer, no pause, no upgrade, no admin.** None of " +
141 "those functions exist here.\n" +
142 "- **It cannot reach a prize pot.** There is no call from this realm " +
143 "into any game.\n\n" +
144 "If the owner's key is lost, the balance is lost with it. There is no " +
145 "recovery path and adding one would mean adding the privileged function " +
146 "this realm refuses to have.\n"
147}