admin.gno
4.72 Kb · 115 lines
1package escrow_v3
2
3// Admin authority and its rotation path.
4//
5// No admin is compiled in: `admin` below is seeded at package load from the
6// publishing transaction's signer (on gnoland-1 the samcrew namespace
7// multisig, the stamped creator at enable time). Every gate in this realm
8// reads the mutable `admin`, so the address that holds admin can be changed
9// after deployment.
10//
11// WHY THIS IS NOT OPTIONAL ON MAINNET. Realms are immutable once published and
12// there is no faucet; mainnet genesis additionally locks ugnot transfers under
13// Constitution §126. A compile-time admin therefore cannot be corrected after
14// the fact. A compile-time testnet key would permanently disable Pause/Unpause
15// and ResolveDispute, the only path that settles a disputed milestone,
16// stranding custodied funds with nobody able to move them. Redeploying at a
17// new path is not a recovery: it abandons the funds already held at this one.
18//
19// TWO-STEP BY DESIGN. The handoff stages a pending address that must claim it
20// with its own transaction. A one-step setter would let admin be handed to an
21// address that cannot act — a typo, or an unfunded address under §126 — with no
22// way back, which is precisely the failure this file exists to prevent.
23//
24// CALLER AUTH: `cur.IsCurrent()` then `cur.Previous().Address()`, matching
25// memba_arcade_leaderboard_v1 / memba_points_v1 and the project's contract-review
26// checklist #9. This file deliberately imports none of the frame-unverified
27// caller package that checklist flags — a new path should not inherit it.
28//
29// escrow.gno's older gates still read the frame-unverified caller and stay
30// grandfathered in antipattern-prevrealm-baseline.txt. That is not a dangerous
31// mix: for the direct multisig calls these entrypoints are for, both forms
32// resolve to the same address, and `IsCurrent()` rejects the sibling/stale-`cur`
33// cases where they could differ at all — so this file is strictly the tighter
34// of the two.
35
36import "chain"
37
38var (
39 // admin is the LIVE authority, seeded with the publisher at package load.
40 // Initialized at declaration rather than in init() so there is no
41 // init-ordering question about gates that read it.
42 admin address = publisherAtLoad()
43
44 // feeFallback receives the protocol fee only when memba_market_config
45 // reports no treasury (see resolveFee). It is the publisher too, so a config
46 // misread routes fees to the namespace multisig rather than to a testnet
47 // key or to nobody.
48 feeFallback address = publisherAtLoad()
49
50 // pendingAdmin is the staged successor; "" when no handoff is in flight.
51 pendingAdmin address
52)
53
54// TransferOwnership stages a handoff to newAdmin. Admin only. The transfer does
55// not take effect until newAdmin calls AcceptOwnership, so admin is never moved
56// to an address that has not demonstrated it can transact.
57//
58// Calling it again before acceptance replaces the staged address, which is how
59// a mistyped proposal is corrected.
60func TransferOwnership(cur realm, newAdmin address) {
61 assertAdmin(cur)
62 if newAdmin == "" {
63 panic("newAdmin must be non-empty")
64 }
65 pendingAdmin = newAdmin
66 chain.Emit("OwnershipTransferStarted", "pending", newAdmin.String())
67}
68
69// AcceptOwnership completes the handoff. Only the staged pendingAdmin may call
70// it — including against the outgoing admin, so the two steps cannot be
71// collapsed into one by the party giving up the role.
72func AcceptOwnership(cur realm) {
73 if !cur.IsCurrent() {
74 panic("spoofed realm")
75 }
76 if pendingAdmin == "" {
77 panic("no pending ownership transfer")
78 }
79 if cur.Previous().Address() != pendingAdmin {
80 panic("unauthorized: only the pending admin may accept")
81 }
82 admin = pendingAdmin
83 pendingAdmin = ""
84 chain.Emit("OwnershipTransferAccepted", "admin", admin.String())
85}
86
87// CancelOwnershipTransfer clears a staged handoff. Admin only — the outgoing
88// admin must be able to abort a proposal it no longer wants, without needing
89// the proposed address to cooperate.
90func CancelOwnershipTransfer(cur realm) {
91 assertAdmin(cur)
92 if pendingAdmin == "" {
93 panic("no pending ownership transfer")
94 }
95 cancelled := pendingAdmin
96 pendingAdmin = ""
97 chain.Emit("OwnershipTransferCancelled", "cancelled", cancelled.String())
98}
99
100// GetAdmin returns the address that currently holds admin.
101func GetAdmin() string { return admin.String() }
102
103// GetPendingAdmin returns the staged successor ("" when none is in flight).
104func GetPendingAdmin() string { return pendingAdmin.String() }
105
106// assertAdmin is the authorization gate for this file's entrypoints. It rejects
107// a stale/sibling `cur` first, then takes the caller from cur.Previous().
108func assertAdmin(cur realm) {
109 if !cur.IsCurrent() {
110 panic("spoofed realm")
111 }
112 if cur.Previous().Address() != admin {
113 panic("unauthorized: only admin may perform this action")
114 }
115}