governance.gno
24.38 Kb · 704 lines
1package nsnft
2
3import (
4 "chain"
5 "chain/runtime"
6 "strings"
7 "time"
8
9 "gno.land/p/nt/avl/v0"
10)
11
12/*
13GOVERNANCE, AND WHY THE SMALLEST REALM NEEDED SOME.
14
15The first version had one admin key and an instant Trust. Whoever held
16that key could trust a realm of their own, burn any name and mint it to
17themselves, in one block — and none of the vault's protections applied,
18because its timelock, recovery role and guardian all live in nsdata and
19nothing in that flow touches nsdata. In the generation where this is the
20realm that can never change, it had the weakest governance of all. Two
21audits (2026-10-15, H2/H3 and H-2) asked for this before it ships.
22
23The model mirrors nsdata/governance.gno, with the two lessons its own
24audit taught applied from the start:
25
26 admin the warm key. Proposes every power-granting change. Can
27 withdraw a registry's trust or the art instantly.
28 recovery the cold key, a multisig in production. Operates nothing.
29 Cancels ANY pending change, rotates admin, withdraws trust.
30 guardian hot (a monitoring bot), seated by recovery. Withdraws trust
31 or the art, and cancels the admin's grants (a registry, the
32 art, the timelock) but never a rotation. Nothing else, so its
33 theft costs an outage, never a name.
34
35GRANTING IS SLOW, REVOKING IS INSTANT. Trusting a registry (the only way
36anything can mint or burn), approving a view realm for the art, changing
37the timelock and rotating either role all wait `timelockDelay` (an admin
38replacing recovery waits twice that), are announced with an event, and
39can be cancelled in the meantime. Withdrawing a registry's trust or the
40art takes one transaction.
41
42RECOVERY IS THE ROOT, AND THAT IS A DELIBERATE DEPARTURE. nsdata lets
43each role rotate the other with neither able to cancel its own
44replacement, which means a stolen admin key can propose a recovery
45rotation recovery cannot stop. Here recovery may cancel anything and the
46admin only what the admin proposed. A stolen admin key therefore gets
47nothing that survives the timelock: every proposal it makes can be
48cancelled and recovery can evict it. What it CAN do at once is withdraw
49— untrust the registry, clear the art — which stops registrations until
50an honest admin re-grants them, an outage and not a theft. The admin can
51still replace a LOST recovery key — nobody cancels that proposal, so it
52lands after twice the delay — which is the case the symmetry was there
53for. The price is that a stolen recovery key is the one theft this realm
54cannot contain, which is why it must be a cold multisig.
55
56A ROTATION TAKES ITS PREDECESSOR'S PROPOSALS WITH IT. nsdata's audit (M-1)
57found an evicted admin's pending grant surviving the eviction, executable
58by anyone a delay later. Here, whenever a role changes hands, every
59pending change proposed by an address that no longer holds a role is
60dropped, with an event.
61
62RECOVERY WINS THE EVICTION RACE, BY RULE AND NOT BY TIMING (re-audit
632026-10-16, M-1). An eviction used to drop the thief's proposals only at
64the moment it executed, and anybody — the thief included — chose the order
65in which matured proposals ran. So the thief re-proposed a cancelled trust
66the block after recovery's sitting, executed it the moment it matured
67(seconds after the eviction did), and minted through its registry in the
68same transaction; or it executed recovery's own move to a new cold key
69first, which orphaned recovery's eviction. Three rules close it:
70
71 recovery proposing the eviction DROPS everything the admin had
72 pending, and while the eviction is pending the admin proposes nothing
73 and nothing it proposed may execute — so nothing of the admin's is
74 held to spring back if the eviction lapses or is cancelled;
75
76 a veto holds: when recovery or the guardian cancels what the admin
77 proposed, the admin may not propose that kind again for one timelock,
78 so recovery's sitting cannot be re-proposed around while it is still
79 gathering signatures. Recovery's eviction landing clears the vetoes;
80 replacing or standing down a guardian clears that guardian's; nothing
81 else does, not an admin moving to a key of its own;
82
83 recovery moving to a new key HANDS its eviction to that key (it was
84 recovery's, and recovery still holds the role), so executing
85 recovery's own rotation first no longer voids it. An admin replacing a
86 lost recovery still drops the old key's plans.
87
88Cancelling what is no longer pending does nothing, so a thief withdrawing
89its own proposal first cannot make a batched sitting revert.
90
91PROPOSALS EXPIRE. A matured change stays executable for executeWindow and
92is void after it, so a forgotten proposal cannot be executed by a stranger
93a year later (nsdata audit L-1).
94
95ROLES NEVER MERGE. A rotation that would leave admin and recovery the
96same key is refused, at proposal and again at execution: with one key in
97both seats every protection above silently disappears (nsdata audit I-5).
98They start merged on the deployer, which hands them out while the
99timelock is still zero.
100
101ExecutePending is open to anyone, as in nsdata: applying a change that
102was announced and survived its window must not depend on a particular key
103still being available. Every entry point below is a thin wrapper that
104reads the caller and the clock and hands them to an unexported function,
105so the rules themselves can be tested directly.
106*/
107
108const (
109 // The ceiling on the timelock: a constant, not a setting, so a stolen
110 // key cannot raise it and an absurd value cannot brick governance.
111 maxTimelock int64 = 30 * 24 * 60 * 60
112 // How long a matured proposal stays executable before it is void.
113 executeWindow int64 = 14 * 24 * 60 * 60
114 maxPathLen = 256
115
116 kindAdmin = "admin"
117 kindRecovery = "recovery"
118 kindTrust = "trust"
119 kindDelay = "delay"
120 kindRenderer = "renderer"
121)
122
123type pendingOp struct {
124 kind string
125 target string // what it changes to, as text: an address, a package path, or seconds
126 addr address // admin and recovery: the new holder; trust: the registry's address
127 secs int64 // delay: the new timelock
128 at int64 // unix seconds: executable from
129 until int64 // unix seconds: void after
130 proposer address
131}
132
133var (
134 admin address
135 recovery address
136 guardian address
137
138 // Seconds every power-granting change waits. Starts at zero so the
139 // deploy can wire and hand out roles in one sitting; the deploy raises
140 // it before anything is worth stealing. Changing it is itself
141 // timelocked, or a thief would simply set it to zero.
142 timelockDelay int64
143
144 // One slot per kind (kind -> *pendingOp), so one in-flight proposal
145 // cannot freeze the whole governance surface.
146 pending avl.Tree
147
148 // kind -> *veto: until when the admin may not propose that kind again,
149 // because recovery or the guardian vetoed it, and who did (see the note
150 // at the top).
151 vetoes avl.Tree
152)
153
154type veto struct {
155 until int64
156 by address
157}
158
159// -- roles: an empty role matches nobody --
160
161func isAdmin(a address) bool { return admin.IsValid() && a == admin }
162func isRecovery(a address) bool { return recovery.IsValid() && a == recovery }
163func isGuardian(a address) bool { return guardian.IsValid() && a == guardian }
164
165func assertAdmin(caller address) {
166 if !isAdmin(caller) {
167 panic("nsnft: admin only")
168 }
169}
170
171func assertAdminOrRecovery(caller address) {
172 if !isAdmin(caller) && !isRecovery(caller) {
173 panic("nsnft: admin or recovery only")
174 }
175}
176
177func assertCanRevoke(caller address) {
178 if !isAdmin(caller) && !isRecovery(caller) && !isGuardian(caller) {
179 panic("nsnft: admin, recovery or guardian only")
180 }
181}
182
183/*
184isRealmPath accepts only a realm package path on this chain. A registry
185or a view proves what it is by BEING that realm, and its address is
186derived from the path here, so a wallet can never be trusted (nsdata
187audit L-4): no wallet address is the hash of a realm path. Ephemeral
188`maketx run` realms (gno.land/e/...), pure packages (/p/) and sub-realm
189paths (with ':') are refused with it.
190*/
191func isRealmPath(p string) bool {
192 prefix := runtime.ChainDomain() + "/r/"
193 if len(p) <= len(prefix) || len(p) > maxPathLen || !strings.HasPrefix(p, prefix) {
194 return false
195 }
196 if strings.HasSuffix(p, "/") || strings.Contains(p, "//") {
197 return false
198 }
199 for i := 0; i < len(p); i++ {
200 c := p[i]
201 if (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '/' || c == '_' || c == '-' || c == '.' {
202 continue
203 }
204 return false
205 }
206 return true
207}
208
209// -- the pending slots --
210
211// liveOp is the pending change of one kind, or nil — an expired one is
212// treated as absent everywhere, so it can be neither executed nor used to
213// block a new proposal.
214func liveOp(kind string, now int64) *pendingOp {
215 v := pending.Get(kind)
216 if v == nil {
217 return nil
218 }
219 op := v.(*pendingOp)
220 if now > op.until {
221 return nil
222 }
223 return op
224}
225
226/*
227canCancel: recovery may cancel anything; the admin what the admin
228proposed; the guardian the admin's GRANTS — a registry, the art, the
229timelock — and never a rotation. A hot key able to block rotations
230could, with recovery lost, block the one change that replaces recovery
231(and with it, the guardian) for good.
232*/
233func canCancel(op *pendingOp, caller address) bool {
234 if isRecovery(caller) {
235 return true
236 }
237 if isAdmin(caller) && caller == op.proposer {
238 return true
239 }
240 if isGuardian(caller) && op.proposer != recovery &&
241 (op.kind == kindTrust || op.kind == kindRenderer || op.kind == kindDelay) {
242 return true
243 }
244 return false
245}
246
247// delayFor is the wait a proposal gets. An admin replacing RECOVERY waits
248// twice the timelock: it is the one move by which a stolen admin key
249// could eventually take the root role, so it gets the longest window for
250// recovery to see it and cancel it. The case it exists for — recovery
251// lost — only costs a slower replacement.
252func delayFor(kind string, caller address) int64 {
253 if kind == kindRecovery && !isRecovery(caller) {
254 return 2 * timelockDelay
255 }
256 return timelockDelay
257}
258
259/*
260evicting reports a live rotation of the admin that recovery proposed —
261recovery replacing a key it no longer trusts. While admin and recovery are
262still one key, at the deploy, nothing is an eviction.
263*/
264func evicting(now int64) bool {
265 op := liveOp(kindAdmin, now)
266 return op != nil && isRecovery(op.proposer) && !isAdmin(op.proposer)
267}
268
269// byTheAdminAlone: proposed by the admin, and the admin is not also recovery.
270func byTheAdminAlone(a address) bool { return isAdmin(a) && !isRecovery(a) }
271
272// vetoedUntil is when the admin may propose `kind` again, or 0.
273func vetoedUntil(kind string, now int64) int64 {
274 if v := vetoes.Get(kind); v != nil && now < v.(*veto).until {
275 return v.(*veto).until
276 }
277 return 0
278}
279
280// dropVetoesBy lifts the vetoes one key sent: a guardian replaced or stood
281// down, perhaps because it was stolen, does not keep the admin shut out.
282func dropVetoesBy(by address) {
283 var gone []string
284 vetoes.Iterate("", "", func(k string, v any) bool {
285 if v.(*veto).by == by {
286 gone = append(gone, k)
287 }
288 return false
289 })
290 for _, k := range gone {
291 vetoes.Remove(k)
292 chain.Emit("GovernanceVetoLifted", "kind", k, "by", by.String())
293 }
294}
295
296// dropProposalsOf removes every pending change `who` proposed: the admin's
297// plans, the moment recovery proposes to evict it.
298func dropProposalsOf(who address, reason string) {
299 var gone []string
300 pending.Iterate("", "", func(k string, v any) bool {
301 if op := v.(*pendingOp); op.proposer == who && k != kindAdmin {
302 gone = append(gone, k)
303 }
304 return false
305 })
306 for _, k := range gone {
307 op := pending.Get(k).(*pendingOp)
308 pending.Remove(k)
309 chain.Emit("GovernanceInvalidated", "kind", k, "target", op.target,
310 "proposer", op.proposer.String(), "reason", reason)
311 }
312}
313
314func setPending(caller address, op *pendingOp, now int64) {
315 if existing := liveOp(op.kind, now); existing != nil {
316 // You may replace only what you could cancel.
317 if !canCancel(existing, caller) {
318 panic("nsnft: a change of this kind is pending and you may not replace it")
319 }
320 chain.Emit("GovernanceDisplaced", "kind", existing.kind, "target", existing.target, "by", caller.String())
321 }
322 if byTheAdminAlone(caller) {
323 if evicting(now) {
324 panic("nsnft: recovery is replacing the admin; the admin proposes nothing until that lands or lapses")
325 }
326 if until := vetoedUntil(op.kind, now); until > 0 {
327 panic("nsnft: recovery or the guardian vetoed a " + op.kind + " change; the admin may propose one again from unix " + itoa(until))
328 }
329 }
330 op.proposer = caller
331 op.at = now + delayFor(op.kind, caller)
332 op.until = op.at + executeWindow
333 pending.Set(op.kind, op)
334 chain.Emit("GovernanceProposed",
335 "kind", op.kind,
336 "target", op.target,
337 "executableAt", itoa(op.at),
338 "expiresAt", itoa(op.until),
339 "by", caller.String())
340}
341
342func proposeTrust(caller address, pkgPath string, now int64) {
343 assertAdmin(caller)
344 if !isRealmPath(pkgPath) {
345 panic("nsnft: a registry must be a realm package path under " + runtime.ChainDomain() + "/r/")
346 }
347 a := chain.PackageAddress(pkgPath)
348 if trusted.Has(a.String()) {
349 panic("nsnft: that registry is already trusted")
350 }
351 setPending(caller, &pendingOp{kind: kindTrust, target: pkgPath, addr: a}, now)
352}
353
354func proposeRenderer(caller address, pkgPath string, now int64) {
355 assertAdmin(caller)
356 if !isRealmPath(pkgPath) {
357 panic("nsnft: a renderer must be a realm package path under " + runtime.ChainDomain() + "/r/")
358 }
359 if pkgPath == rendererPath {
360 panic("nsnft: that realm is already the approved renderer")
361 }
362 setPending(caller, &pendingOp{kind: kindRenderer, target: pkgPath}, now)
363}
364
365// proposeDelay bounds the value before anything is computed from it, so
366// no input can wrap (nsdata audit L-3), and the proposal shows the value
367// it would set, so a monitor can tell "to zero" from "up".
368func proposeDelay(caller address, seconds int64, now int64) {
369 assertAdmin(caller)
370 if seconds < 0 || seconds > maxTimelock {
371 panic("nsnft: timelock out of range (0 to " + itoa(maxTimelock) + " seconds)")
372 }
373 setPending(caller, &pendingOp{kind: kindDelay, target: itoa(seconds), secs: seconds}, now)
374}
375
376func proposeAdmin(caller, next address, now int64) {
377 assertAdminOrRecovery(caller)
378 if !next.IsValid() {
379 panic("nsnft: invalid address")
380 }
381 if next == admin {
382 panic("nsnft: that address is already the admin")
383 }
384 if next == recovery {
385 panic("nsnft: admin and recovery must stay separate keys")
386 }
387 setPending(caller, &pendingOp{kind: kindAdmin, target: next.String(), addr: next}, now)
388 if isRecovery(caller) && !isAdmin(caller) && admin.IsValid() {
389 // The eviction drops the admin's plans now, not when it lands (see
390 // the note at the top).
391 dropProposalsOf(admin, "recovery is replacing the admin")
392 }
393}
394
395func proposeRecovery(caller, next address, now int64) {
396 assertAdminOrRecovery(caller)
397 if !next.IsValid() {
398 panic("nsnft: invalid address")
399 }
400 if next == recovery {
401 panic("nsnft: that address is already recovery")
402 }
403 if next == admin {
404 panic("nsnft: admin and recovery must stay separate keys")
405 }
406 setPending(caller, &pendingOp{kind: kindRecovery, target: next.String(), addr: next}, now)
407}
408
409func cancelPending(caller address, kind string, now int64) {
410 op := liveOp(kind, now)
411 if op == nil {
412 if !isAdmin(caller) && !isRecovery(caller) && !isGuardian(caller) {
413 panic("nsnft: admin, recovery or guardian only")
414 }
415 return // idempotent, so a batched sitting cannot be made to revert
416 }
417 if !canCancel(op, caller) {
418 panic("nsnft: you may not cancel this change")
419 }
420 pending.Remove(kind)
421 // A veto holds for one timelock (see the note at the top): what recovery
422 // or the guardian cancelled, the admin cannot simply propose again in the
423 // next block. The admin withdrawing its own proposal costs nothing.
424 if byTheAdminAlone(op.proposer) && !isAdmin(caller) && timelockDelay > 0 {
425 vetoes.Set(kind, &veto{until: now + timelockDelay, by: caller})
426 chain.Emit("GovernanceVetoed", "kind", kind, "until", itoa(now+timelockDelay), "by", caller.String())
427 }
428 chain.Emit("GovernanceCancelled", "kind", kind, "target", op.target, "by", caller.String())
429}
430
431func executePending(caller address, kind string, now int64) {
432 op := liveOp(kind, now)
433 if op == nil {
434 panic("nsnft: nothing pending of that kind")
435 }
436 if now < op.at {
437 panic("nsnft: still in the timelock window")
438 }
439 // The eviction lands first, whoever picks the order (see the note at
440 // the top): nothing the admin proposed runs while recovery is replacing
441 // it, and the eviction then drops all of it.
442 if kind != kindAdmin && byTheAdminAlone(op.proposer) && evicting(now) {
443 panic("nsnft: recovery is replacing the admin; the admin's pending changes wait for that, and die with it")
444 }
445 pending.Remove(kind)
446 prev := ""
447 switch kind {
448 case kindAdmin:
449 if op.addr == recovery {
450 panic("nsnft: admin and recovery must stay separate keys")
451 }
452 prev = admin.String()
453 admin = op.addr
454 // Recovery's eviction landing clears the vetoes it was fighting
455 // with; an admin moving to a key of its own choosing does not.
456 if isRecovery(op.proposer) {
457 vetoes = avl.Tree{}
458 }
459 case kindRecovery:
460 if op.addr == admin {
461 panic("nsnft: admin and recovery must stay separate keys")
462 }
463 prev = recovery.String()
464 old := recovery
465 recovery = op.addr
466 // Recovery moving ITSELF keeps its eviction: it is handed to the new
467 // key rather than orphaned. Only the eviction — recovery proposes
468 // nothing else that could still be pending — so the new key never
469 // inherits what an old deploy key proposed while it held both seats.
470 // An admin replacing a lost recovery does not inherit that key's
471 // eviction; it is dropped below with every other orphan.
472 if op.proposer == old && old != admin {
473 if e := liveOp(kindAdmin, now); e != nil && e.proposer == old {
474 e.proposer = recovery
475 chain.Emit("GovernanceHandedOver", "kind", kindAdmin, "target", e.target,
476 "from", old.String(), "to", recovery.String())
477 }
478 }
479 case kindTrust:
480 trusted.Set(op.addr.String(), op.target)
481 chain.Emit("RegistryTrusted", "addr", op.addr.String(), "path", op.target)
482 case kindDelay:
483 prev = itoa(timelockDelay)
484 timelockDelay = op.secs
485 case kindRenderer:
486 // The approval moves; whatever the previous view registered stops
487 // drawing, and wallets see the built-in card until the newly
488 // approved realm registers itself.
489 prev = rendererPath
490 rendererPath = op.target
491 renderer = nil
492 default:
493 panic("nsnft: unknown pending kind")
494 }
495 chain.Emit("GovernanceExecuted", "kind", kind, "target", op.target, "previous", prev, "by", caller.String())
496 if kind == kindAdmin || kind == kindRecovery {
497 dropOrphans()
498 }
499}
500
501// dropOrphans removes every pending change whose proposer no longer
502// holds a role. Called after each rotation; see the note at the top.
503func dropOrphans() {
504 var gone []string
505 pending.Iterate("", "", func(k string, v any) bool {
506 op := v.(*pendingOp)
507 if op.proposer != admin && op.proposer != recovery {
508 gone = append(gone, k)
509 }
510 return false
511 })
512 for _, k := range gone {
513 op := pending.Get(k).(*pendingOp)
514 pending.Remove(k)
515 chain.Emit("GovernanceInvalidated", "kind", k, "target", op.target,
516 "proposer", op.proposer.String(), "reason", "proposer no longer holds a role")
517 }
518}
519
520func untrust(caller, registry address) {
521 assertCanRevoke(caller)
522 v := trusted.Get(registry.String())
523 if v == nil {
524 panic("nsnft: that registry is not trusted")
525 }
526 trusted.Remove(registry.String())
527 chain.Emit("RegistryUntrusted", "addr", registry.String(), "path", v.(string), "by", caller.String())
528}
529
530/*
531setGuardian takes recovery, to seat a guardian as much as to replace or
532stand down a live one — otherwise the first move of a thief holding the
533admin key would be to point the guardian at themselves, disarming the
534emergency stop; and seating an EMPTY seat with its own bot left the thief a
535key that outlived its eviction, untrusting the registry and vetoing the new
536admin's grants, removable only by recovery (re-audit 2026-10-16, L-4). The
537deploy seats it while its key still holds recovery. The address must be
538real (nsdata audit I-4).
539*/
540func setGuardian(caller, g address) {
541 if !g.IsValid() {
542 panic("nsnft: invalid address")
543 }
544 if !isRecovery(caller) {
545 if guardian.IsValid() {
546 panic("nsnft: replacing a live guardian takes the recovery key")
547 }
548 panic("nsnft: seating a guardian takes the recovery key")
549 }
550 if guardian.IsValid() && guardian != g {
551 dropVetoesBy(guardian)
552 }
553 guardian = g
554 chain.Emit("GuardianSet", "addr", g.String(), "by", caller.String())
555}
556
557func clearGuardian(caller address) {
558 if guardian.IsValid() {
559 if !isRecovery(caller) {
560 panic("nsnft: standing down a live guardian takes the recovery key")
561 }
562 dropVetoesBy(guardian)
563 } else {
564 assertAdminOrRecovery(caller)
565 }
566 guardian = ""
567 chain.Emit("GuardianSet", "addr", "", "by", caller.String())
568}
569
570// -- entry points: each reads the caller and the clock, nothing more --
571
572// ProposeTrust starts trusting a registry, by package path, after the
573// timelock. Admin only.
574func ProposeTrust(cur realm, pkgPath string) {
575 assertNoSend(cur)
576 proposeTrust(cur.Previous().Address(), pkgPath, time.Now().Unix())
577}
578
579// ProposeRenderer approves a view realm to draw the art, after the
580// timelock. Admin only. The realm then registers itself (art.gno).
581func ProposeRenderer(cur realm, pkgPath string) {
582 assertNoSend(cur)
583 proposeRenderer(cur.Previous().Address(), pkgPath, time.Now().Unix())
584}
585
586// ProposeTimelockDelay changes the timelock, after the current one.
587// Admin only.
588func ProposeTimelockDelay(cur realm, seconds int64) {
589 assertNoSend(cur)
590 proposeDelay(cur.Previous().Address(), seconds, time.Now().Unix())
591}
592
593// ProposeAdminTransfer rotates the admin after the timelock. Admin or
594// recovery; when recovery proposes it, the admin cannot cancel it.
595func ProposeAdminTransfer(cur realm, next address) {
596 assertNoSend(cur)
597 proposeAdmin(cur.Previous().Address(), next, time.Now().Unix())
598}
599
600// ProposeRecoveryTransfer rotates recovery after the timelock — twice the
601// timelock when the admin proposes it. Admin or recovery; recovery can
602// cancel either, so a stolen admin key cannot take it.
603func ProposeRecoveryTransfer(cur realm, next address) {
604 assertNoSend(cur)
605 proposeRecovery(cur.Previous().Address(), next, time.Now().Unix())
606}
607
608func CancelPending(cur realm, kind string) {
609 assertNoSend(cur)
610 cancelPending(cur.Previous().Address(), kind, time.Now().Unix())
611}
612
613// ExecutePending applies a matured change. Anyone may call it.
614func ExecutePending(cur realm, kind string) {
615 assertNoSend(cur)
616 executePending(cur.Previous().Address(), kind, time.Now().Unix())
617}
618
619// Untrust withdraws a registry's power to mint and burn, at once. Admin,
620// recovery or guardian: removing a power is the safe direction, and a
621// registry that has gone wrong must be stoppable in one transaction.
622func Untrust(cur realm, registry address) {
623 assertNoSend(cur)
624 untrust(cur.Previous().Address(), registry)
625}
626
627func SetGuardian(cur realm, g address) {
628 assertNoSend(cur)
629 setGuardian(cur.Previous().Address(), g)
630}
631
632func ClearGuardian(cur realm) {
633 assertNoSend(cur)
634 clearGuardian(cur.Previous().Address())
635}
636
637// -- reads --
638
639func Admin() address { return admin }
640func Recovery() address { return recovery }
641func Guardian() address { return guardian }
642func TimelockDelay() int64 { return timelockDelay }
643
644// PendingAdmin is the address a pending admin rotation would install, or
645// empty.
646func PendingAdmin() address {
647 if op := liveOp(kindAdmin, time.Now().Unix()); op != nil {
648 return op.addr
649 }
650 return ""
651}
652
653// GetPending describes the live pending change of one kind: what it sets,
654// when it becomes executable and when it lapses (unix seconds), and who
655// proposed it. All empty when nothing is pending.
656func GetPending(kind string) (target string, executableAt, expiresAt int64, proposer address) {
657 op := liveOp(kind, time.Now().Unix())
658 if op == nil {
659 return "", 0, 0, ""
660 }
661 return op.target, op.at, op.until, op.proposer
662}
663
664// ListPending names the kinds with a live pending change, comma-separated.
665func ListPending() string {
666 now := time.Now().Unix()
667 out := ""
668 pending.Iterate("", "", func(k string, v any) bool {
669 if now > v.(*pendingOp).until {
670 return false
671 }
672 if out != "" {
673 out += ","
674 }
675 out += k
676 return false
677 })
678 return out
679}
680
681func IsTrusted(registry address) bool { return registry.IsValid() && trusted.Has(registry.String()) }
682
683// Trusted lists every address that may mint or burn, comma-separated.
684// nsdata compares this with its own address before adopting tokens, so
685// the format is part of the contract: addresses only, no spaces.
686func Trusted() string {
687 out := ""
688 trusted.Iterate("", "", func(a string, _ any) bool {
689 if out != "" {
690 out += ","
691 }
692 out += a
693 return false
694 })
695 return out
696}
697
698// TrustedPath reports the package path a registry was trusted as, or "".
699func TrustedPath(registry address) string {
700 if v := trusted.Get(registry.String()); v != nil {
701 return v.(string)
702 }
703 return ""
704}