admin.gno
4.07 Kb · 105 lines
1package memba_reviews_v2
2
3// Moderator authority and its rotation path.
4//
5// No moderator is compiled in: `moderator` below is seeded at package load from
6// the publishing transaction's signer (on gnoland-1 the samcrew namespace
7// multisig, the stamped creator at enable time), and assertModerator reads the
8// mutable `moderator`, so the address holding moderation can be changed after
9// deployment.
10//
11// WHY THIS MATTERS ON MAINNET. Realms are immutable once published, there is no
12// faucet, and mainnet genesis locks ugnot transfers under Constitution §126 — so
13// a compile-time moderator cannot be corrected after the fact: a testnet key
14// compiled in would permanently disable HideTarget/UnhideTarget/ClearFlags,
15// leaving this realm with no way to act on a flagged review ever again.
16//
17// TWO-STEP BY DESIGN: the successor must claim the role with its own transaction,
18// so moderation can never be handed to an address that cannot transact (a typo,
19// or an unfunded address under §126) with no way back.
20//
21// CALLER AUTH here uses `cur.IsCurrent()` + `cur.Previous().Address()`, per
22// contract-review checklist #9, matching memba_arcade_leaderboard_v1 /
23// memba_points_v1. The realm's existing assertModerator keeps its older
24// frame-unverified caller read (grandfathered in
25// antipattern-prevrealm-baseline.txt); only the value it compares against
26// changed, which is the smallest edit that removes the immutability.
27
28import "chain"
29
30var (
31 // moderator is the LIVE authority, seeded with the publisher at package load.
32 // Initialized at declaration so there is no init-ordering question about
33 // gates that read it.
34 moderator address = publisherAtLoad()
35
36 // pendingModerator is the staged successor; "" when none is in flight.
37 pendingModerator address
38)
39
40// TransferOwnership stages a handoff of the moderator role. Moderator only; it
41// takes effect only once newModerator calls AcceptOwnership. Calling it again
42// before acceptance replaces the staged address, which is how a mistyped
43// proposal is corrected.
44func TransferOwnership(cur realm, newModerator address) {
45 assertModeratorCur(cur)
46 if newModerator == "" {
47 panic("newModerator must be non-empty")
48 }
49 if !newModerator.IsValid() {
50 panic("invalid address: " + newModerator.String())
51 }
52 if newModerator == moderator {
53 panic("newModerator is the current moderator")
54 }
55 pendingModerator = newModerator
56 chain.Emit("OwnershipTransferStarted", "pending", newModerator.String())
57}
58
59// AcceptOwnership completes the handoff. Only the staged pendingModerator may
60// call it — including against the outgoing moderator, so the two steps cannot be
61// collapsed into one by the party giving up the role.
62func AcceptOwnership(cur realm) {
63 if !cur.IsCurrent() {
64 panic("spoofed realm")
65 }
66 if pendingModerator == "" {
67 panic("no pending ownership transfer")
68 }
69 if cur.Previous().Address() != pendingModerator {
70 panic("unauthorized: only the pending moderator may accept")
71 }
72 moderator = pendingModerator
73 pendingModerator = ""
74 chain.Emit("OwnershipTransferAccepted", "moderator", moderator.String())
75}
76
77// CancelOwnershipTransfer clears a staged handoff. Moderator only, so a
78// proposal can be aborted without the proposed address cooperating.
79func CancelOwnershipTransfer(cur realm) {
80 assertModeratorCur(cur)
81 if pendingModerator == "" {
82 panic("no pending ownership transfer")
83 }
84 cancelled := pendingModerator
85 pendingModerator = ""
86 chain.Emit("OwnershipTransferCancelled", "cancelled", cancelled.String())
87}
88
89// GetModerator returns the address that currently holds moderation.
90func GetModerator() string { return moderator.String() }
91
92// GetPendingModerator returns the staged successor ("" when none).
93func GetPendingModerator() string { return pendingModerator.String() }
94
95// assertModeratorCur is the gate for this file's entrypoints. Named apart from
96// the realm's existing assertModerator so the older call sites keep their
97// signature and semantics untouched.
98func assertModeratorCur(cur realm) {
99 if !cur.IsCurrent() {
100 panic("spoofed realm")
101 }
102 if cur.Previous().Address() != moderator {
103 panic("unauthorized: moderator multisig only")
104 }
105}