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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

admin.gno

16.70 Kb · 430 lines
  1package nslogic
  2
  3import (
  4	"strconv"
  5	"strings"
  6
  7	"gno.land/r/nym-mnsrehearsal001/nsdata/v1"
  8)
  9
 10// assertIsAdmin reads nsdata.GetAdmin() fresh every call — correct even
 11// immediately after a TransferAdmin, and correct for a freshly-deployed
 12// logic realm with no local state of its own to go stale.
 13//
 14// Note: admin authority to swap WHICH realm is trustedLogic, or to
 15// transfer admin itself, is deliberately NOT exposed here — those live
 16// only in nsdata's own admin.gno, callable solely by a direct wallet
 17// call from the current admin address, never proxied through logic.
 18// That's the one part of the system a compromised or buggy logic realm
 19// cannot touch.
 20func assertIsAdmin(cur realm) {
 21	if cur.Previous().Address() != nsdata.GetAdmin() {
 22		panic("nslogic: restricted to project admin")
 23	}
 24}
 25
 26// SetLabelPattern no longer changes anything, and refuses anything but
 27// the rule this realm actually implements.
 28//
 29// It used to compile an arbitrary regex and store it, with validateLabel
 30// reading it back. That flexibility cost 15 GNOT of permanent storage
 31// per deploy — see the measurements in validate.gno — for a rule that
 32// has never changed and should not change casually.
 33//
 34// It is kept rather than deleted so that an admin who reaches for it
 35// gets told what happened instead of a "function not found". And it
 36// refuses a different pattern rather than accepting one it will not
 37// enforce: a stored string describing rules the code does not apply is
 38// the exact silent divergence the caching version was written to avoid.
 39func SetLabelPattern(cur realm, pattern string) {
 40	assertNoSend(cur)
 41	assertIsAdmin(cur)
 42	emitAdmin(cur, "SetLabelPattern", "pattern", pattern)
 43	if pattern != labelRule {
 44		panic("nslogic: the label rule is fixed in this realm (" + labelRule +
 45			"). Changing it means deploying a new logic realm — which is deliberate, " +
 46			"because a stored pattern the code does not enforce is worse than no setting at all.")
 47	}
 48	nsdata.SetLabelPatternRecord(cross(cur), pattern)
 49}
 50
 51func SetLabelLengthLimits(cur realm, minLen, maxLen int) {
 52	assertNoSend(cur)
 53	assertIsAdmin(cur)
 54	emitAdmin(cur, "SetLabelLengthLimits", "min", fmtI(minLen), "max", fmtI(maxLen))
 55	if minLen < 1 || maxLen < minLen {
 56		panic("nslogic: invalid length bounds")
 57	}
 58	nsdata.SetLabelLengthRecord(cross(cur), minLen, maxLen)
 59}
 60
 61// SetDefaultNamePrice sets the fallback price in micro-USD, used by any
 62// domain that has not set its own base. BOUNDED (timelock.gno): a raise up
 63// to four times every ten minutes, a cut up to 25% once an hour, or a
 64// timelocked proposal for anything else.
 65func SetDefaultNamePrice(cur realm, microUsd int64) {
 66	assertNoSend(cur)
 67	assertIsAdmin(cur)
 68	emitAdmin(cur, "SetDefaultNamePrice", "microUsd", fmtInt(microUsd))
 69	if microUsd < 0 || microUsd > maxPriceUSD {
 70		panic("nslogic: a price is 0.." + strconv.FormatInt(maxPriceUSD, 10) + " micro-USD")
 71	}
 72	v := strconv.FormatInt(microUsd, 10)
 73	old := DefaultPriceUSD()
 74	authorizeTweak(cur, keyDefaultPrice, old, microUsd, microUsd > old, actionID("SetDefaultNamePrice", v))
 75	setGlobal(cur, keyDefaultPrice, v)
 76}
 77
 78// SetUsdRate records how many ugnot one USD buys — the bridge between
 79// the USD price list and settlement on this chain.
 80//
 81// It is a MANUAL rate, not a market feed: it sits where it was last
 82// written. A stale rate during a sharp move sells names at the wrong
 83// price, and no amount of contract logic fixes that — keeping it current
 84// is an operational duty. Isolated as one global so a future logic realm
 85// can swap in a real oracle without touching the vault or restating a
 86// single price.
 87//
 88// BOUNDED (timelock.gno): a rate of 1 ugnot to the dollar used to be one
 89// call away from selling every name for dust. ASYMMETRIC (Ely,
 90// 2026-10-16): a higher rate — GNOT fell, names cost more GNOT — moves up
 91// to four times every ten minutes, so a 75% crash is followed in one
 92// update; a lower rate — the direction that sells names cheap — moves at
 93// most 25%, once an hour. Anything larger is a timelocked proposal.
 94//
 95// THE KEEPER may call this too (keeper.gno), and nothing else: the same
 96// bound and the same hourly slot, never the timelock, and KeeperAction on
 97// every move. Anybody who is neither gets the admin refusal, as before.
 98func SetUsdRate(cur realm, ugnotPerUsd int64) {
 99	assertNoSend(cur)
100	if cur.Previous().Address() != nsdata.GetAdmin() {
101		keeperSetRate(cur, ugnotPerUsd)
102		return
103	}
104	assertIsAdmin(cur)
105	emitAdmin(cur, "SetUsdRate", "ugnotPerUsd", fmtInt(ugnotPerUsd))
106	if ugnotPerUsd <= 0 || ugnotPerUsd > maxRate {
107		panic("nslogic: a rate is 1.." + strconv.FormatInt(maxRate, 10) + " ugnot per USD")
108	}
109	v := strconv.FormatInt(ugnotPerUsd, 10)
110	old := RateUgnotPerUsd()
111	authorizeTweak(cur, keyRate, old, ugnotPerUsd, ugnotPerUsd > old, actionID("SetUsdRate", v))
112	setGlobal(cur, keyRate, v)
113}
114
115// SetTermLength / SetGracePeriod retune the lifecycle itself. Both were
116// hardcoded constants in the vault until 2026-08-21 — meaning the 1-year
117// term and 90-day grace could never have been changed, on a realm that
118// is never redeployed. There was no reason to accept that.
119// maxLifecycleDays bounds both setters. time.Duration is int64
120// NANOSECONDS, so `days * 24h` overflows above ~106,751 days and wraps to
121// a NEGATIVE duration — which would install a negative term and brick
122// every registration and renewal in the registry. Guarding the input is
123// what keeps the multiplication in range.
124const maxLifecycleDays = 36500 // 100 years
125
126// BOTH ARE TIMELOCKED (timelock.gno). A grace period of zero made every
127// name in grace reclaimable by anybody on the spot — owners who could
128// still have renewed lost their names to whoever registered first — and a
129// term change reprices every renewal and moves every holder's tenure.
130func SetTermLength(cur realm, days int64) {
131	assertNoSend(cur)
132	assertIsAdmin(cur)
133	emitAdmin(cur, "SetTermLength", "days", fmtInt(days))
134	if days <= 0 || days > maxLifecycleDays {
135		panic("nslogic: term out of range (1..36500 days)")
136	}
137	term := days * 24 * 60 * 60
138	// Keep gracePeriod <= termLength. Anchoring a renewal to the previous
139	// due date is only coherent while a renewal cannot happen more than
140	// one full term after it; the vault clamps forward defensively, but
141	// the combination should not be reachable in the first place.
142	if nsdata.GetGracePeriod() > term {
143		panic("nslogic: term must be at least the current grace period")
144	}
145	requireAction(cur, actionID("SetTermLength", strconv.FormatInt(days, 10)))
146	nsdata.SetTermLengthRecord(cross(cur), term)
147}
148
149func SetGracePeriod(cur realm, days int64) {
150	assertNoSend(cur)
151	assertIsAdmin(cur)
152	emitAdmin(cur, "SetGracePeriod", "days", fmtInt(days))
153	if days < 0 || days > maxLifecycleDays {
154		panic("nslogic: grace period out of range (0..36500 days)")
155	}
156	grace := days * 24 * 60 * 60
157	if grace > nsdata.GetTermLength() {
158		panic("nslogic: grace period may not exceed the term length")
159	}
160	requireAction(cur, actionID("SetGracePeriod", strconv.FormatInt(days, 10)))
161	nsdata.SetGracePeriodRecord(cross(cur), grace)
162}
163
164// SetTokenURIBase re-points TokenURI at a different renderer, so the NFT
165// artwork can be replaced without touching the vault.
166func SetTokenURIBase(cur realm, base string) {
167	assertNoSend(cur)
168	assertIsAdmin(cur)
169	emitAdmin(cur, "SetTokenURIBase", "base", base)
170	nsdata.SetTokenURIBaseRecord(cross(cur), base)
171}
172
173func SetExtraLimits(cur realm, keyLen, valueLen, entries int) {
174	assertNoSend(cur)
175	assertIsAdmin(cur)
176	emitAdmin(cur, "SetExtraLimits", "keyLen", fmtI(keyLen), "valueLen", fmtI(valueLen), "entries", fmtI(entries))
177	// NEVER BELOW WHAT THIS REALM ITSELF WRITES: the vault's own defaults
178	// (32-byte keys, 512-byte values, 24 entries). One instant call to a
179	// tiny limit used to make every later global write revert, the keeper's
180	// rate and the instant revocations among them (re-audit L-2).
181	if keyLen < minExtraKeyLen || valueLen < minExtraValueLen || entries < minExtraEntries {
182		panic("nslogic: extra limits are at least " + fmtI(minExtraKeyLen) + " / " + fmtI(minExtraValueLen) +
183			" / " + fmtI(minExtraEntries) + ", the sizes this realm writes")
184	}
185	nsdata.SetExtraLimitsRecord(cross(cur), keyLen, valueLen, entries)
186}
187
188const (
189	minExtraKeyLen   = 32
190	minExtraValueLen = 512
191	minExtraEntries  = 24
192)
193
194// SetGlobalEntryLimit sets how many keys the vault's globals may hold —
195// its own cap now, apart from the per-record extras SetExtraLimits bounds.
196func SetGlobalEntryLimit(cur realm, entries int) {
197	assertNoSend(cur)
198	assertIsAdmin(cur)
199	emitAdmin(cur, "SetGlobalEntryLimit", "entries", fmtI(entries))
200	if entries < 1 || entries > 1024 {
201		panic("nslogic: the globals cap must be 1-1024")
202	}
203	nsdata.SetGlobalEntryLimitRecord(cross(cur), entries)
204}
205
206// SetAccountLimits bounds the vault's per-address store: field name,
207// value, and fields per address. Small numbers, because whoever's
208// transaction writes a field pays for it.
209func SetAccountLimits(cur realm, keyLen, valueLen, entries int) {
210	assertNoSend(cur)
211	assertIsAdmin(cur)
212	emitAdmin(cur, "SetAccountLimits", "keyLen", fmtI(keyLen), "valueLen", fmtI(valueLen), "entries", fmtI(entries))
213	// Never below what this realm writes (the vault's defaults, 16/128/8):
214	// a tiny limit refused every later grant, redemption and referral
215	// count, deletes included — L-2's bug on the per-address store
216	// (review follow-up).
217	if keyLen < 16 || keyLen > 64 || valueLen < 128 || valueLen > 1024 || entries < 8 || entries > 64 {
218		panic("nslogic: account limits must be 16-64 / 128-1024 / 8-64")
219	}
220	nsdata.SetAccountLimitsRecord(cross(cur), keyLen, valueLen, entries)
221}
222
223/*
224SetMarketFee is nsmarket's fee control, living here because this is the
225realm the vault trusts.
226
227nsmarket.SetFee could never work: it wrote through nsdata.SetGlobalRecord
228from a realm deliberately outside the trusted set, so it passed its own
229admin check and then panicked inside the vault for every caller. Rather
230than grant nsmarket vault privileges — which would surrender the property
231that realm is built around — the setter moved to the realm that already
232has the grant, and the validation moved with it.
233
234The spec grammar is nsmarket's, restated here rather than imported so
235this realm does not take a dependency on the marketplace to set a number:
236an amount in ugnot ("1000000"), or a percentage ("%2.5"); "0" for none,
237because "" removes the key and nsmarket then charges its default of one
238GNOT. A malformed spec would otherwise be read on every sale and resolve
239silently to zero.
240
241A CHANGE THAT COULD RAISE THE FEE IS TIMELOCKED. The fee is read at sale
242time, so a raise lands on listings sellers made under the old one — up to
243half of their sale, moved to the treasury, with no notice. A timelocked
244raise is the notice: sellers see ActionProposed and may unlist. Lowering
245it, or switching it off, stays instant.
246*/
247func SetMarketFee(cur realm, spec string) {
248	assertNoSend(cur)
249	assertIsAdmin(cur)
250	emitAdmin(cur, "SetMarketFee", "spec", spec)
251	assertValidFeeSpec(spec)
252	spec = strings.TrimSpace(spec)
253	if feeCouldRise(nsdata.GetGlobal(keyMarketFee), spec) {
254		requireAction(cur, actionID("SetMarketFee", spec))
255	}
256	setGlobal(cur, keyMarketFee, spec)
257}
258
259const (
260	keyMarketFee = "marketfee"
261	// nsmarket's fee when the global is unset: one GNOT, flat. Restated
262	// rather than imported, like the grammar, so this realm takes no
263	// dependency on the marketplace to set a number.
264	defaultMarketFee = "1000000"
265)
266
267// feeCouldRise reports whether moving from the stored spec `old` to `next`
268// could raise the fee on any sale. Both are read the way nsmarket reads
269// them: unset means the default. A move to zero never rises; a change of
270// kind — flat to percentage or back — rises on some price, so it counts.
271func feeCouldRise(old, next string) bool {
272	of, ob := feeParts(old)
273	nf, nb := feeParts(next)
274	switch {
275	case nf == 0 && nb == 0:
276		return false
277	case of == 0 && ob == 0:
278		return true
279	case ob == 0 && nb == 0:
280		return nf > of
281	case of == 0 && nf == 0:
282		return nb > ob
283	}
284	return true
285}
286
287// feeParts splits a spec into (flat ugnot, basis points), the way
288// nsmarket.parseFee does; unset is the default fee, and an unreadable spec
289// is zero — which is also how nsmarket charges it.
290func feeParts(spec string) (flat, bps int64) {
291	spec = strings.TrimSpace(spec)
292	if spec == "" {
293		spec = defaultMarketFee
294	}
295	if !strings.HasPrefix(spec, "%") {
296		n, err := strconv.ParseInt(spec, 10, 64)
297		if err != nil || n < 0 {
298			return 0, 0
299		}
300		return n, 0
301	}
302	body := spec[1:]
303	whole, frac := body, ""
304	if dot := strings.Index(body, "."); dot >= 0 {
305		whole, frac = body[:dot], body[dot+1:]
306	}
307	w, err := strconv.ParseInt("0"+whole, 10, 64)
308	if err != nil {
309		return 0, 0
310	}
311	bps = w * 100
312	if frac != "" {
313		f, err := strconv.ParseInt("0"+frac, 10, 64)
314		if err != nil || len(frac) > 1 {
315			return 0, 0
316		}
317		bps += f * 10
318	}
319	return 0, bps
320}
321
322// assertValidFeeSpec mirrors nsmarket.parseFee. Percentages are carried
323// as basis points so a fee is expressible without floating point
324// anywhere near money.
325func assertValidFeeSpec(spec string) {
326	spec = strings.TrimSpace(spec)
327	if spec == "" {
328		return
329	}
330	if !strings.HasPrefix(spec, "%") {
331		n, err := strconv.ParseInt(spec, 10, 64)
332		if err != nil || n < 0 {
333			panic("nslogic: a fee is an amount in ugnot or a percentage like %2.5 — got " + spec)
334		}
335		return
336	}
337	body := spec[1:]
338	whole, frac := body, ""
339	if dot := strings.Index(body, "."); dot >= 0 {
340		whole, frac = body[:dot], body[dot+1:]
341	}
342	// ONE decimal, matching nsmarket.parseFee exactly. I wrote two here
343	// first, which made this validator MORE permissive than the parser
344	// that has to read the value back — and a spec nsmarket refuses is
345	// read as a zero fee on every sale, silently, forever. A validator
346	// looser than its consumer is worse than no validator: it converts a
347	// rejected input into a silent revenue leak.
348	if len(frac) > 1 {
349		panic("nslogic: a fee percentage carries at most one decimal — got " + spec)
350	}
351	for _, part := range []string{whole, frac} {
352		for i := 0; i < len(part); i++ {
353			if part[i] < '0' || part[i] > '9' {
354				panic("nslogic: a fee percentage must be digits — got " + spec)
355			}
356		}
357	}
358	if whole == "" && frac == "" {
359		panic("nslogic: a fee percentage needs a number — got " + spec)
360	}
361	// Basis points the same way parseFee computes them: whole*100 plus
362	// one tenth-digit worth ten. Not two digits, and not a 100% ceiling
363	// — nsmarket caps at maxFeeBps, which is 50%.
364	w, _ := strconv.ParseInt("0"+whole, 10, 64)
365	bp := w * 100
366	if frac != "" {
367		f, _ := strconv.ParseInt("0"+frac, 10, 64)
368		bp += f * 10
369	}
370	if bp <= 0 || bp > 5000 {
371		panic("nslogic: a fee percentage must be above zero and at most 50 — got " + spec)
372	}
373}
374
375// SetGlobal writes an arbitrary project-wide key into the vault's
376// package-level extension slot — the escape hatch for global state that
377// does not exist yet, and for keys other realms read (cardfx). It refuses
378// every key a setter here owns: the escape hatch used to walk round every
379// bound and timelock on them. See managedGlobal.
380func SetGlobal(cur realm, key, value string) {
381	assertNoSend(cur)
382	assertIsAdmin(cur)
383	emitAdmin(cur, "SetGlobal", "key", key, "value", value)
384	if managedGlobal(key) {
385		panic("nslogic: " + key + " has its own setter, with its own rules; use that")
386	}
387	setGlobal(cur, key, value)
388}
389
390func setGlobal(cur realm, key, value string) {
391	/* THE RESTORE SEAL IS NOT AN ORDINARY GLOBAL.
392	   restore.gno says sealing is "IRREVERSIBLE… There is no argument
393	   that unseals it". One admin call writing key "restore" used to
394	   reopen it. The vault now refuses both keys through its general
395	   setter and refuses to change a seal through its reserved one, so a
396	   logic swap cannot drop the guard; this check stays so the refusal
397	   names the right door. */
398	if key == keyRestore || key == keyRestoredCount {
399		panic("nslogic: " + key + " is written by the restore machinery, not by SetGlobal")
400	}
401	if err := nsdata.SetGlobalRecord(cross(cur), key, value); err != nil {
402		panic(err)
403	}
404}
405
406// SetTreasury moves the revenue-receiving role — independent of
407// nsdata's TransferAdmin, same reasoning as the original build's
408// admin/treasury separation.
409//
410// TIMELOCKED. It took effect at once and said nothing, so a stolen admin
411// key redirected every later sale — names, renewals, skins, market fees —
412// in one call (audit M4). Now it is announced and waits, and recovery can
413// cancel it.
414func SetTreasury(cur realm, newTreasury address) {
415	assertNoSend(cur)
416	assertIsAdmin(cur)
417	emitAdmin(cur, "SetTreasury", "addr", newTreasury.String())
418	if !newTreasury.IsValid() {
419		panic("nslogic: invalid treasury address")
420	}
421	requireAction(cur, actionID("SetTreasury", newTreasury.String()))
422	nsdata.SetTreasuryRecord(cross(cur), newTreasury)
423}
424
425// SelfAddress reports this deployed realm's own address — needed once,
426// at wiring time, so admin knows what to pass to
427// nsdata.ProposeTrustedLogicGrant.
428func SelfAddress(cur realm) address {
429	return cur.Address()
430}