admin.gno
3.59 Kb · 95 lines
1package memba_quest_attestation_v1
2
3// Owner authority and its rotation path.
4//
5// No owner is compiled in: `owner` is seeded at package load from the
6// publishing transaction's signer (on gnoland-1 the samcrew namespace multisig,
7// the stamped creator at enable time), and SetSigner reads the mutable `owner`,
8// so the address holding ownership can be changed after deployment.
9//
10// WHY THIS MATTERS ON MAINNET, specifically here. SetSigner is the ONLY
11// owner-gated entrypoint, and it installs/rotates the backend's offline signer
12// public key — the key every voucher is verified against. Realms are immutable,
13// there is no faucet, and mainnet genesis locks ugnot transfers under
14// Constitution §126, so a compile-time owner cannot be corrected later, and a
15// compile-time testnet owner would freeze the signer at whatever value it last
16// held: a compromised backend signer could never be rotated out, and quest
17// attestation would have to be abandoned rather than repaired.
18//
19// TWO-STEP BY DESIGN: the successor must claim the role with its own
20// transaction, so ownership can never be handed to an address that cannot
21// transact (a typo, or an unfunded address under §126) with no way back.
22//
23// All owner mutations require a current realm frame and authenticate the
24// immediate caller through cur.Previous(), including signer rotation.
25
26import "chain"
27
28var (
29 // owner is the LIVE authority, seeded with the publisher at package load.
30 // Initialized at declaration so there is no init-ordering question about
31 // gates that read it.
32 owner address = publisherAtLoad()
33
34 // pendingOwner is the staged successor; "" when none is in flight.
35 pendingOwner address
36)
37
38// TransferOwnership stages a handoff. Owner only; it takes effect only once
39// newOwner calls AcceptOwnership. Calling it again before acceptance replaces
40// the staged address, which is how a mistyped proposal is corrected.
41func TransferOwnership(cur realm, newOwner address) {
42 assertOwner(cur)
43 if newOwner == "" {
44 panic("newOwner must be non-empty")
45 }
46 pendingOwner = newOwner
47 chain.Emit("OwnershipTransferStarted", "pending", newOwner.String())
48}
49
50// AcceptOwnership completes the handoff. Only the staged pendingOwner may call
51// it — including against the outgoing owner, so the two steps cannot be
52// collapsed into one by the party giving up the role.
53func AcceptOwnership(cur realm) {
54 if !cur.IsCurrent() {
55 panic("spoofed realm")
56 }
57 if pendingOwner == "" {
58 panic("no pending ownership transfer")
59 }
60 if cur.Previous().Address() != pendingOwner {
61 panic("unauthorized: only the pending owner may accept")
62 }
63 owner = pendingOwner
64 pendingOwner = ""
65 chain.Emit("OwnershipTransferAccepted", "owner", owner.String())
66}
67
68// CancelOwnershipTransfer clears a staged handoff. Owner only, so a proposal
69// can be aborted without the proposed address cooperating.
70func CancelOwnershipTransfer(cur realm) {
71 assertOwner(cur)
72 if pendingOwner == "" {
73 panic("no pending ownership transfer")
74 }
75 cancelled := pendingOwner
76 pendingOwner = ""
77 chain.Emit("OwnershipTransferCancelled", "cancelled", cancelled.String())
78}
79
80// GetOwner returns the address that currently holds ownership.
81func GetOwner() string { return owner.String() }
82
83// GetPendingOwner returns the staged successor ("" when none).
84func GetPendingOwner() string { return pendingOwner.String() }
85
86// assertOwner is the gate for this file's entrypoints. It rejects a
87// stale/sibling `cur` first, then takes the caller from cur.Previous().
88func assertOwner(cur realm) {
89 if !cur.IsCurrent() {
90 panic("spoofed realm")
91 }
92 if cur.Previous().Address() != owner {
93 panic("unauthorized: owner multisig only")
94 }
95}