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.

admin.gno

8.60 Kb · 307 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/g1pq6pxhgxk2uw5shg5le6glfydpxcu74k0zh069/sinopia/safe/v1"
 12	"gno.land/p/g1pq6pxhgxk2uw5shg5le6glfydpxcu74k0zh069/sinopia/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.
 23// SA-I6: CreateFee starts at 0 here so tests and bootstrap stay simple;
 24// scripts/deploy-onyx.sh sets a non-zero fee.create during bootstrap.
 25var (
 26	admin        = address("g1pq6pxhgxk2uw5shg5le6glfydpxcu74k0zh069")
 27	pendingAdmin address
 28	fees         = types.Fees{PrimaryBps: 250, SecondaryBps: 250, Treasury: address("g1pq6pxhgxk2uw5shg5le6glfydpxcu74k0zh069")}
 29	beacon       address
 30	minters      avl.Tree // minter pkgpath -> *minterRecord
 31	ops          avl.Tree // safe.PadInt(op id) -> *pendingOp
 32	opCount      int64
 33	pauses       avl.Tree // scope -> *pauseRecord
 34)
 35
 36// pauseRecord is one scope's pause and its budget (SA-L2): within a window
 37// of PauseWindowBlocks a scope can be paused for at most MaxPauseBlocks in
 38// total; a pause cannot be renewed while it runs. Worst case two budgets
 39// meet at a window boundary (<= 2 x MaxPauseBlocks in a row), never more.
 40type pauseRecord struct {
 41	windowStart int64
 42	used        int64 // blocks of budget charged in this window
 43	until       int64 // exclusive
 44}
 45
 46type minterRecord struct {
 47	deprecated bool
 48}
 49
 50type pendingOp struct {
 51	id       int64
 52	op, arg  string
 53	proposed int64
 54}
 55
 56// Ops and their argument formats.
 57const (
 58	OpFeeCreate       = "fee.create"       // ugnot, <= CreateFeeCap
 59	OpFeePrimary      = "fee.primary"      // bps, <= ProtocolFeeCapBps
 60	OpFeeSecondary    = "fee.secondary"    // bps, <= ProtocolFeeCapBps
 61	OpTreasury        = "treasury"         // address
 62	OpBeacon          = "beacon"           // address, or "" to disable new beacon launches
 63	OpMinterAdd       = "minter.add"       // realm pkgpath
 64	OpMinterDeprecate = "minter.deprecate" // realm pkgpath: no new collections
 65	OpEngineReview    = "engine.review"    // engine id: reviewed badge (UI only)
 66	OpAdminTransfer   = "admin.transfer"   // address; the new admin must AcceptAdmin
 67)
 68
 69var pauseScopes = []string{"create", "mint", "trade"}
 70
 71func ProposeOp(cur realm, op, arg string) string {
 72	assertNoDeposit(cur)
 73	assertAdmin(cur)
 74	validateOp(op, arg)
 75	opCount++
 76	p := &pendingOp{id: opCount, op: op, arg: arg, proposed: height()}
 77	ops.Set(safe.PadInt(p.id, cidWidth), p)
 78	id := itoa(p.id)
 79	chain.Emit("OpProposed", "id", id, "op", op, "arg", arg, "earliest", itoa(earliestExecution(p)))
 80	return id
 81}
 82
 83// ExecuteOp applies a proposed op once its timelock has passed. The delay is
 84// judged at execution, so an op proposed during bootstrap still waits the
 85// full timelock if a collection appeared in the meantime.
 86func ExecuteOp(cur realm, opID string) {
 87	assertNoDeposit(cur)
 88	assertAdmin(cur)
 89	p := mustOp(opID)
 90	if height() < earliestExecution(p) {
 91		panic("timelocked until height " + itoa(earliestExecution(p)))
 92	}
 93	validateOp(p.op, p.arg)
 94	applyOp(p.op, p.arg)
 95	ops.Remove(safe.PadInt(p.id, cidWidth))
 96	chain.Emit("OpExecuted", "id", opID, "op", p.op, "arg", p.arg)
 97}
 98
 99func CancelOp(cur realm, opID string) {
100	assertNoDeposit(cur)
101	assertAdmin(cur)
102	p := mustOp(opID)
103	ops.Remove(safe.PadInt(p.id, cidWidth))
104	chain.Emit("OpCancelled", "id", opID)
105}
106
107func AcceptAdmin(cur realm) {
108	assertNoDeposit(cur)
109	who := caller(cur)
110	if pendingAdmin == "" || who != pendingAdmin {
111		panic("no pending admin transfer to this address")
112	}
113	chain.Emit("AdminTransferred", "from", admin.String(), "to", who.String())
114	admin = who
115	pendingAdmin = ""
116}
117
118// Pause stops new activity in one scope for the rest of its budget (at
119// most MaxPauseBlocks per PauseWindowBlocks). It never blocks exits (I-2)
120// and cannot be renewed while it runs (SA-L2).
121func Pause(cur realm, scope string) {
122	assertNoDeposit(cur)
123	assertAdmin(cur)
124	assertScope(scope)
125	h := height()
126	r := pauseOf(scope)
127	if h < r.until {
128		panic("already paused until height " + itoa(r.until) + "; a pause cannot be renewed")
129	}
130	if h >= r.windowStart+PauseWindowBlocks {
131		r.windowStart, r.used = h, 0
132	}
133	left := MaxPauseBlocks - r.used
134	if left <= 0 {
135		panic("pause budget of " + scope + " is spent until height " + itoa(r.windowStart+PauseWindowBlocks))
136	}
137	r.used += left
138	r.until = h + left
139	pauses.Set(scope, r)
140	chain.Emit("Paused", "scope", scope, "until", itoa(r.until))
141}
142
143// Unpause ends a pause early and gives the unused blocks back to the budget.
144func Unpause(cur realm, scope string) {
145	assertNoDeposit(cur)
146	assertAdmin(cur)
147	assertScope(scope)
148	h := height()
149	r := pauseOf(scope)
150	if h < r.until {
151		r.used -= r.until - h
152	}
153	r.until = 0
154	chain.Emit("Unpaused", "scope", scope)
155}
156
157func pauseOf(scope string) *pauseRecord {
158	if v := pauses.Get(scope); v != nil {
159		return v.(*pauseRecord)
160	}
161	return &pauseRecord{windowStart: -PauseWindowBlocks}
162}
163
164// Paused reports whether scope ("create", "mint", "trade") is paused now.
165func Paused(scope string) bool {
166	v := pauses.Get(scope)
167	return v != nil && height() < v.(*pauseRecord).until
168}
169
170// PauseBudget is the scope's pause record as JSON: {until, used,
171// windowStart, windowEnd, left}.
172func PauseBudget(scope string) string {
173	assertScope(scope)
174	r := pauseOf(scope)
175	used := r.used
176	if height() >= r.windowStart+PauseWindowBlocks && height() >= r.until {
177		used = 0
178	}
179	w := &safe.JSONWriter{}
180	w.BeginObject()
181	w.IntField("until", r.until)
182	w.IntField("used", used)
183	w.IntField("windowStart", r.windowStart)
184	w.IntField("windowEnd", r.windowStart+PauseWindowBlocks)
185	w.IntField("left", MaxPauseBlocks-used)
186	w.EndObject()
187	return w.String()
188}
189
190func assertNotPaused(scope string) {
191	if Paused(scope) {
192		panic("paused: " + scope)
193	}
194}
195
196func assertScope(scope string) {
197	for _, s := range pauseScopes {
198		if s == scope {
199			return
200		}
201	}
202	panic("pause scope must be one of " + strings.Join(pauseScopes, ", "))
203}
204
205func assertAdmin(cur realm) {
206	if caller(cur) != admin {
207		panic("admin only")
208	}
209}
210
211func bootstrapping() bool { return collCount == 0 }
212
213func earliestExecution(p *pendingOp) int64 {
214	if bootstrapping() {
215		return p.proposed
216	}
217	return p.proposed + TimelockBlocks
218}
219
220func mustOp(opID string) *pendingOp {
221	v := ops.Get(safe.PadInt(safe.ParseID("opID", opID), cidWidth))
222	if v == nil {
223		panic("unknown op " + opID)
224	}
225	return v.(*pendingOp)
226}
227
228// validateOp is the closed list of admin powers (I-1).
229func validateOp(op, arg string) {
230	switch op {
231	case OpFeeCreate:
232		n := parseAmount(arg)
233		if n > CreateFeeCap {
234			panic("create fee above cap " + itoa(CreateFeeCap))
235		}
236	case OpFeePrimary, OpFeeSecondary:
237		safe.AssertBps(op, parseAmount(arg), ProtocolFeeCapBps)
238	case OpTreasury, OpAdminTransfer:
239		if !address(arg).IsValid() {
240			panic(op + ": invalid address")
241		}
242		if op == OpTreasury {
243			assertSpendable(op, address(arg), "", "") // SA-L5
244		}
245	case OpBeacon:
246		if arg != "" && !address(arg).IsValid() {
247			panic(op + ": invalid address")
248		}
249	case OpMinterAdd:
250		safe.AssertText("minter", arg, 256)
251		if !strings.HasPrefix(arg, runtime.ChainDomain()+"/r/") || strings.Contains(arg, "#") {
252			panic("minter must be a realm path")
253		}
254		if minters.Has(arg) {
255			panic("minter already added")
256		}
257	case OpMinterDeprecate:
258		if !minters.Has(arg) {
259			panic("unknown minter")
260		}
261	case OpEngineReview:
262		mustEngine(arg)
263	default:
264		panic("unknown op " + strconv.Quote(op))
265	}
266}
267
268func applyOp(op, arg string) {
269	switch op {
270	case OpFeeCreate:
271		fees.CreateFee = parseAmount(arg)
272	case OpFeePrimary:
273		fees.PrimaryBps = parseAmount(arg)
274	case OpFeeSecondary:
275		fees.SecondaryBps = parseAmount(arg)
276	case OpTreasury:
277		fees.Treasury = address(arg)
278	case OpBeacon:
279		beacon = address(arg)
280	case OpMinterAdd:
281		minters.Set(arg, &minterRecord{})
282	case OpMinterDeprecate:
283		minters.Get(arg).(*minterRecord).deprecated = true
284	case OpEngineReview:
285		mustEngine(arg).reviewed = true
286	case OpAdminTransfer:
287		pendingAdmin = address(arg)
288	}
289}
290
291func assertActiveMinter(path string) {
292	v := minters.Get(path)
293	if v == nil {
294		panic("caller " + strconv.Quote(path) + " is not an approved minter")
295	}
296	if v.(*minterRecord).deprecated {
297		panic("minter " + path + " is deprecated")
298	}
299}
300
301func parseAmount(s string) int64 {
302	n, err := strconv.ParseInt(s, 10, 64)
303	if err != nil || n < 0 {
304		panic("expected a non-negative integer, got " + strconv.Quote(s))
305	}
306	return n
307}