admin.gno
6.65 Kb · 245 lines
1package core
2
3import (
4 "chain"
5 "chain/runtime"
6 "strconv"
7 "strings"
8
9 "gno.land/p/nt/avl/v0"
10
11 "gno.land/p/g19dzk54hhsdc3gllpksuj6gu4a3e6fl8cr6rn4w/minhwa/safe/v1"
12 "gno.land/p/g19dzk54hhsdc3gllpksuj6gu4a3e6fl8cr6rn4w/minhwa/types/v1"
13)
14
15// Admin (design §2.5.2, §7.5). The admin can only change the parameters
16// listed in validateOp, each inside a hard cap, after a public timelock;
17// it can pause new activity for a bounded time. It can never move, approve
18// or burn a token, touch a collection's configuration, or receive funds
19// (I-1). With no collection yet there is nothing to protect, so ops execute
20// without delay — that is how the platform is configured after deploy.
21//
22// TODO(mainnet): the initial admin and treasury must be the 2/3 multisig.
23var (
24 admin = address("g19dzk54hhsdc3gllpksuj6gu4a3e6fl8cr6rn4w")
25 pendingAdmin address
26 fees = types.Fees{PrimaryBps: 250, SecondaryBps: 250, Treasury: address("g19dzk54hhsdc3gllpksuj6gu4a3e6fl8cr6rn4w")}
27 beacon address
28 minters avl.Tree // minter pkgpath -> *minterRecord
29 ops avl.Tree // safe.PadInt(op id) -> *pendingOp
30 opCount int64
31 pausedUntil avl.Tree // scope -> int64 height (exclusive)
32)
33
34type minterRecord struct {
35 deprecated bool
36}
37
38type pendingOp struct {
39 id int64
40 op, arg string
41 proposed int64
42}
43
44// Ops and their argument formats.
45const (
46 OpFeeCreate = "fee.create" // ugnot, <= CreateFeeCap
47 OpFeePrimary = "fee.primary" // bps, <= ProtocolFeeCapBps
48 OpFeeSecondary = "fee.secondary" // bps, <= ProtocolFeeCapBps
49 OpTreasury = "treasury" // address
50 OpBeacon = "beacon" // address, or "" to disable new beacon launches
51 OpMinterAdd = "minter.add" // realm pkgpath
52 OpMinterDeprecate = "minter.deprecate" // realm pkgpath: no new collections
53 OpEngineReview = "engine.review" // engine id: reviewed badge (UI only)
54 OpAdminTransfer = "admin.transfer" // address; the new admin must AcceptAdmin
55)
56
57var pauseScopes = []string{"create", "mint", "trade"}
58
59func ProposeOp(cur realm, op, arg string) string {
60 assertNoDeposit(cur)
61 assertAdmin(cur)
62 validateOp(op, arg)
63 opCount++
64 p := &pendingOp{id: opCount, op: op, arg: arg, proposed: height()}
65 ops.Set(safe.PadInt(p.id, cidWidth), p)
66 id := itoa(p.id)
67 chain.Emit("OpProposed", "id", id, "op", op, "arg", arg, "earliest", itoa(earliestExecution(p)))
68 return id
69}
70
71// ExecuteOp applies a proposed op once its timelock has passed. The delay is
72// judged at execution, so an op proposed during bootstrap still waits the
73// full timelock if a collection appeared in the meantime.
74func ExecuteOp(cur realm, opID string) {
75 assertNoDeposit(cur)
76 assertAdmin(cur)
77 p := mustOp(opID)
78 if height() < earliestExecution(p) {
79 panic("timelocked until height " + itoa(earliestExecution(p)))
80 }
81 validateOp(p.op, p.arg)
82 applyOp(p.op, p.arg)
83 ops.Remove(safe.PadInt(p.id, cidWidth))
84 chain.Emit("OpExecuted", "id", opID, "op", p.op, "arg", p.arg)
85}
86
87func CancelOp(cur realm, opID string) {
88 assertNoDeposit(cur)
89 assertAdmin(cur)
90 p := mustOp(opID)
91 ops.Remove(safe.PadInt(p.id, cidWidth))
92 chain.Emit("OpCancelled", "id", opID)
93}
94
95func AcceptAdmin(cur realm) {
96 assertNoDeposit(cur)
97 who := caller(cur)
98 if pendingAdmin == "" || who != pendingAdmin {
99 panic("no pending admin transfer to this address")
100 }
101 chain.Emit("AdminTransferred", "from", admin.String(), "to", who.String())
102 admin = who
103 pendingAdmin = ""
104}
105
106// Pause stops new activity in one scope for at most MaxPauseBlocks. It
107// never blocks exits (I-2).
108func Pause(cur realm, scope string) {
109 assertNoDeposit(cur)
110 assertAdmin(cur)
111 assertScope(scope)
112 until := height() + MaxPauseBlocks
113 pausedUntil.Set(scope, until)
114 chain.Emit("Paused", "scope", scope, "until", itoa(until))
115}
116
117func Unpause(cur realm, scope string) {
118 assertNoDeposit(cur)
119 assertAdmin(cur)
120 assertScope(scope)
121 pausedUntil.Remove(scope)
122 chain.Emit("Unpaused", "scope", scope)
123}
124
125// Paused reports whether scope ("create", "mint", "trade") is paused now.
126func Paused(scope string) bool {
127 v := pausedUntil.Get(scope)
128 return v != nil && height() < v.(int64)
129}
130
131func assertNotPaused(scope string) {
132 if Paused(scope) {
133 panic("paused: " + scope)
134 }
135}
136
137func assertScope(scope string) {
138 for _, s := range pauseScopes {
139 if s == scope {
140 return
141 }
142 }
143 panic("pause scope must be one of " + strings.Join(pauseScopes, ", "))
144}
145
146func assertAdmin(cur realm) {
147 if caller(cur) != admin {
148 panic("admin only")
149 }
150}
151
152func bootstrapping() bool { return collCount == 0 }
153
154func earliestExecution(p *pendingOp) int64 {
155 if bootstrapping() {
156 return p.proposed
157 }
158 return p.proposed + TimelockBlocks
159}
160
161func mustOp(opID string) *pendingOp {
162 v := ops.Get(safe.PadInt(safe.ParseID("opID", opID), cidWidth))
163 if v == nil {
164 panic("unknown op " + opID)
165 }
166 return v.(*pendingOp)
167}
168
169// validateOp is the closed list of admin powers (I-1).
170func validateOp(op, arg string) {
171 switch op {
172 case OpFeeCreate:
173 n := parseAmount(arg)
174 if n > CreateFeeCap {
175 panic("create fee above cap " + itoa(CreateFeeCap))
176 }
177 case OpFeePrimary, OpFeeSecondary:
178 safe.AssertBps(op, parseAmount(arg), ProtocolFeeCapBps)
179 case OpTreasury, OpAdminTransfer:
180 if !address(arg).IsValid() {
181 panic(op + ": invalid address")
182 }
183 case OpBeacon:
184 if arg != "" && !address(arg).IsValid() {
185 panic(op + ": invalid address")
186 }
187 case OpMinterAdd:
188 safe.AssertText("minter", arg, 256)
189 if !strings.HasPrefix(arg, runtime.ChainDomain()+"/r/") || strings.Contains(arg, "#") {
190 panic("minter must be a realm path")
191 }
192 if minters.Has(arg) {
193 panic("minter already added")
194 }
195 case OpMinterDeprecate:
196 if !minters.Has(arg) {
197 panic("unknown minter")
198 }
199 case OpEngineReview:
200 mustEngine(arg)
201 default:
202 panic("unknown op " + strconv.Quote(op))
203 }
204}
205
206func applyOp(op, arg string) {
207 switch op {
208 case OpFeeCreate:
209 fees.CreateFee = parseAmount(arg)
210 case OpFeePrimary:
211 fees.PrimaryBps = parseAmount(arg)
212 case OpFeeSecondary:
213 fees.SecondaryBps = parseAmount(arg)
214 case OpTreasury:
215 fees.Treasury = address(arg)
216 case OpBeacon:
217 beacon = address(arg)
218 case OpMinterAdd:
219 minters.Set(arg, &minterRecord{})
220 case OpMinterDeprecate:
221 minters.Get(arg).(*minterRecord).deprecated = true
222 case OpEngineReview:
223 mustEngine(arg).reviewed = true
224 case OpAdminTransfer:
225 pendingAdmin = address(arg)
226 }
227}
228
229func assertActiveMinter(path string) {
230 v := minters.Get(path)
231 if v == nil {
232 panic("caller " + strconv.Quote(path) + " is not an approved minter")
233 }
234 if v.(*minterRecord).deprecated {
235 panic("minter " + path + " is deprecated")
236 }
237}
238
239func parseAmount(s string) int64 {
240 n, err := strconv.ParseInt(s, 10, 64)
241 if err != nil || n < 0 {
242 panic("expected a non-negative integer, got " + strconv.Quote(s))
243 }
244 return n
245}