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

admin.gno

7.94 Kb · 233 lines
  1package users
  2
  3import (
  4	"chain"
  5	"chain/runtime"
  6
  7	"gno.land/p/moul/addrset/v0"
  8	"gno.land/p/nt/ufmt/v0"
  9
 10	"gno.land/r/gov/dao"
 11)
 12
 13const initControllerPath = "gno.land/r/sys/users/init"
 14
 15var controllers = addrset.Set{} // caller whitelist
 16
 17func init() {
 18	// auto-whitelist the init controller for bootstrapping for testing chain.
 19	if chainID := runtime.ChainID(); chainID == "dev" {
 20		controllers.Add(chain.PackageAddress(initControllerPath))
 21	}
 22}
 23
 24// AddControllerAtGenesis allows adding a controller during chain genesis (height 0).
 25// This is mostly useful for testing.
 26func AddControllerAtGenesis(_ realm, addr address) {
 27	height := runtime.ChainHeight()
 28	if height > 0 {
 29		panic("AddControllerAtGenesis can only be called at genesis (height 0)")
 30	}
 31
 32	if !addr.IsValid() {
 33		panic(ErrInvalidAddress)
 34	}
 35
 36	controllers.Add(addr)
 37}
 38
 39// ProposeNewController allows GovDAO to add a whitelisted caller
 40func ProposeNewController(cur realm, addr address) dao.ProposalRequest {
 41	if !addr.IsValid() {
 42		panic(ErrInvalidAddress)
 43	}
 44
 45	cb := func(cur realm) error {
 46		return addToWhitelist(addr)
 47	}
 48
 49	desc := "This proposal adds " + addr.String() + " to `sys/users` realm's callers whitelist."
 50	return dao.NewProposalRequest("Add Whitelisted Caller to \"sys/users\" Realm", desc, dao.NewSimpleExecutor(0, cur, cb, ""))
 51}
 52
 53// ProposeControllerRemoval allows GovDAO to add a whitelisted caller
 54func ProposeControllerRemoval(cur realm, addr address) dao.ProposalRequest {
 55	if !addr.IsValid() {
 56		panic(ErrInvalidAddress)
 57	}
 58
 59	cb := func(cur realm) error {
 60		return deleteFromWhitelist(addr)
 61	}
 62
 63	desc := "This proposal removes " + addr.String() + " from `sys/users` realm's callers whitelist."
 64	return dao.NewProposalRequest("Remove Whitelisted Caller From \"sys/users\" Realm", desc, dao.NewSimpleExecutor(0, cur, cb, ""))
 65}
 66
 67// ProposeControllerAdditionAndRemoval allows GovDAO to add a new caller and remove an old caller in the same proposal.
 68func ProposeControllerAdditionAndRemoval(cur realm, toAdd, toRemove address) dao.ProposalRequest {
 69	if !toAdd.IsValid() || !toRemove.IsValid() {
 70		panic(ErrInvalidAddress)
 71	}
 72
 73	cb := func(cur realm) error {
 74		return applyControllerSwap(toAdd, toRemove)
 75	}
 76
 77	desc := ufmt.Sprintf(
 78		"This proposal adds %s and removes %s from `sys/users` realm's callers whitelist.",
 79		toAdd,
 80		toRemove,
 81	)
 82	return dao.NewProposalRequest("Add and Remove Whitelisted Callers From \"sys/users\" Realm", desc, dao.NewSimpleExecutor(0, cur, cb, ""))
 83}
 84
 85// applyControllerSwap is the callback body of ProposeControllerAdditionAndRemoval,
 86// extracted so it can be unit-tested without driving the full GovDAO flow.
 87//
 88// The desired end state is "toAdd is in the whitelist AND toRemove is out".
 89// Both operations are made idempotent so the swap doesn't silently no-op when
 90// the chain state has drifted between proposal creation and execution:
 91//
 92//   - If toAdd is already whitelisted, treat addToWhitelist's
 93//     ErrAlreadyWhitelisted as benign and continue to the remove step.
 94//   - If toRemove is already absent, treat deleteFromWhitelist's
 95//     ErrNotWhitelisted as benign and return success.
 96//
 97// Without this idempotency, the original code returned early on an "already
 98// whitelisted" toAdd and skipped the remove entirely — a swap proposal could
 99// pass governance and silently leave the old controller active. (audit
100// finding #6)
101func applyControllerSwap(toAdd, toRemove address) error {
102	if err := addToWhitelist(toAdd); err != nil && err != ErrAlreadyWhitelisted {
103		return err
104	}
105	if err := deleteFromWhitelist(toRemove); err != nil {
106		if _, alreadyOut := err.(ErrNotWhitelisted); !alreadyOut {
107			return err
108		}
109	}
110	return nil
111}
112
113// ProposeRegisterUser allows GovDAO to register a name without checking
114// controllers. The executor closure runs with ignoreCanonical=true (decision
115// #3): DAO grants always bypass canonical-collision detection. Voters see
116// any collision in the proposal description and can vote NO if unintended.
117func ProposeRegisterUser(cur realm, name string, addr address) dao.ProposalRequest {
118	// Validate the name and address now, even though registerUser will validate again
119	if err := validateName(name); err != nil {
120		panic(err.Error())
121	}
122	if !addr.IsValid() {
123		panic(ErrInvalidAddress)
124	}
125
126	desc := "This proposal registers " + name + " with address " + addr.String() + " in `sys/users`."
127	if existing, taken := IsCanonicalTaken(name); taken && existing != name {
128		desc += "\n\nCANONICAL COLLISION: this name's canonical form matches the existing registration of `" +
129			existing + "`. DAO grants bypass canonical-collision detection — the proposal will succeed if voted in. " +
130			"If the collision is unintended, vote NO."
131	}
132
133	cb := func(cur realm) error {
134		return registerUser(cur, name, addr, true) // bypass canonical (decision #3)
135	}
136
137	return dao.NewProposalRequest("Register User to \"sys/users\" Realm", desc, dao.NewSimpleExecutor(0, cur, cb, ""))
138}
139
140// ProposeUpdateName allows GovDAO to update a name with an alias without
141// checking controllers. Like ProposeRegisterUser, the executor runs with
142// ignoreCanonical=true (decision #3).
143func ProposeUpdateName(cur realm, addr address, newName string) dao.ProposalRequest {
144	if !addr.IsValid() {
145		panic(ErrInvalidAddress)
146	}
147	if err := validateName(newName); err != nil {
148		panic(err.Error())
149	}
150
151	desc := "This proposal updates address " + addr.String() + " with alias " + newName + " in `sys/users`."
152	if existing, taken := IsCanonicalTaken(newName); taken && existing != newName {
153		desc += "\n\nCANONICAL COLLISION: the new alias's canonical form matches the existing registration of `" +
154			existing + "`. DAO grants bypass canonical-collision detection — the proposal will succeed if voted in. " +
155			"If the collision is unintended, vote NO."
156	}
157
158	cb := func(cur realm) error {
159		data := ResolveAddress(addr)
160		if data == nil {
161			return ErrUserNotExistOrDeleted
162		}
163		return data.updateName(newName, true) // bypass canonical (decision #3)
164	}
165
166	return dao.NewProposalRequest("Update Name Alias in \"sys/users\" Realm", desc, dao.NewSimpleExecutor(0, cur, cb, ""))
167}
168
169// ProposeDeleteUser allows GovDAO to delete a user without checking controllers
170func ProposeDeleteUser(cur realm, addr address) dao.ProposalRequest {
171	if !addr.IsValid() {
172		panic(ErrInvalidAddress)
173	}
174
175	cb := func(cur realm) error {
176		data := ResolveAddress(addr)
177		if data == nil {
178			return ErrUserNotExistOrDeleted
179		}
180		return data.delete()
181	}
182
183	desc := "This proposal deletes the user with address " + addr.String() + " in `sys/users`."
184	return dao.NewProposalRequest("Delete User in \"sys/users\" Realm", desc, dao.NewSimpleExecutor(0, cur, cb, ""))
185}
186
187// IsController reports whether the given address is currently in the
188// controller whitelist. Returns the same boolean that gating checks
189// (RegisterUser, UpdateName, Delete) use internally — useful for
190// off-chain monitoring and for governance proposals to inspect state
191// before voting.
192func IsController(addr address) bool {
193	return controllers.Has(addr)
194}
195
196// Controllers returns a snapshot of the current controller whitelist.
197// The returned slice is a fresh copy; mutating it does not affect realm
198// state. Order is the iteration order of the underlying address set.
199//
200// Audit finding #20: without this getter, the controller whitelist was
201// opaque from outside the package — operators had to read source or
202// replay every governance proposal to know who could write to the
203// registry. This is the read-only API that closes that gap.
204func Controllers() []address {
205	out := make([]address, 0, controllers.Size())
206	controllers.IterateByOffset(0, controllers.Size(), func(a address) bool {
207		out = append(out, a)
208		return false
209	})
210	return out
211}
212
213// Helpers
214
215func deleteFromWhitelist(addr address) error {
216	if !controllers.Has(addr) {
217		return ErrNotWhitelisted{Caller: "UserRealm{ " + addr.String() + " }"}
218	}
219
220	if ok := controllers.Remove(addr); !ok {
221		return ErrWhitelistRemoveFailed
222	}
223
224	return nil
225}
226
227func addToWhitelist(newCaller address) error {
228	if !controllers.Add(newCaller) {
229		return ErrAlreadyWhitelisted
230	}
231
232	return nil
233}