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

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/sinopia/safe/v1"
 12	"gno.land/p/g19dzk54hhsdc3gllpksuj6gu4a3e6fl8cr6rn4w/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.
 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}