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

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}