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.

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}