Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}