← Back to Realms

Realm

gno.land/r/sys/users

Overview

Realm Path
gno.land/r/sys/users
Exported Functions
21
State Entries
26
Source Files
11
Total Package Entries
54

Exported Functions

21 exported functions

State

26 state entries

Source Code

FILES
admin.gno
go
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}
234

Raw Package Data

Raw JSON data