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.

domain.gno

9.76 Kb · 242 lines
  1package nslogic
  2
  3import (
  4	"strconv"
  5	"strings"
  6	"time"
  7
  8	"gno.land/r/nym-mnsrehearsal001/nsdata/v1"
  9)
 10
 11// RegisterDomain mints a new `*domain` — ADMIN-ONLY (2026-08-15 policy:
 12// domains are never sold or owned by third parties). No payment: the
 13// project isn't paying itself. Domains are also permanently
 14// non-transferable by users (see nft.gno's TransferFrom).
 15func RegisterDomain(cur realm, label string) {
 16	assertNoSend(cur)
 17	assertIsAdmin(cur)
 18	emitAdmin(cur, "RegisterDomain", "label", label)
 19	validateLabel(label)
 20	caller := cur.Previous().Address()
 21	if err := nsdata.RegisterDomainRecord(cross(cur), label, caller, time.Now().Unix()); err != nil {
 22		panic(err)
 23	}
 24}
 25
 26// RenewDomain extends a domain's expiry by exactly one year from its
 27// PREVIOUS due date, not from the payment date. No payment — domains are
 28// always project-owned, so this is self-service upkeep, not a sale.
 29// Deliberately does not call validateLabel — a tightened pattern only
 30// gates new registrations, never renewals of what's already registered.
 31func RenewDomain(cur realm, label string) {
 32	assertNoSend(cur)
 33	caller := cur.Previous().Address()
 34	if err := nsdata.RenewDomainRecord(cross(cur), label, caller, time.Now().Unix()); err != nil {
 35		panic(err)
 36	}
 37}
 38
 39// ReassignDomain moves a domain record to a new owner. ADMIN-ONLY, and
 40// the deliberate exception to "domains are non-transferable".
 41//
 42// It exists because domain ownership is snapshotted into the record at
 43// registration time, while `admin` is a rotatable role. After the
 44// planned EOA-to-multisig rotation, the incoming admin could not renew
 45// or reprice any domain the outgoing key had registered — and if the
 46// rotation happened *because* the old key was compromised, the attacker
 47// could still reprice those domains afterwards. This is the recovery
 48// lever for exactly that situation.
 49//
 50// Not a loophole in the non-transferability policy: that policy exists
 51// to stop domains reaching third parties as tradeable assets, and this
 52// path is reachable only by the current admin, moving a project domain
 53// between project-controlled addresses.
 54//
 55// TIMELOCKED (timelock.gno): a stolen admin key could otherwise move every
 56// project domain to itself in one block. Genesis domains run ten years, so
 57// the honest recovery this exists for loses nothing by waiting.
 58func ReassignDomain(cur realm, label string, newOwner address) {
 59	assertNoSend(cur)
 60	assertIsAdmin(cur)
 61	emitAdmin(cur, "ReassignDomain", "label", label, "to", newOwner.String())
 62	if !newOwner.IsValid() {
 63		panic("nslogic: invalid owner address")
 64	}
 65	requireAction(cur, actionID("ReassignDomain", label, newOwner.String()))
 66	/* THE STOPGAP IS GONE. This used to read the domain's settings back
 67	   and rewrite them around a token transfer, because that transfer
 68	   wiped them. Domains are records now, and nsdata moves the owner
 69	   without touching price, len, nm, open or maxterm. */
 70	if err := nsdata.ReassignDomainRecord(cross(cur), label, newOwner); err != nil {
 71		panic(err)
 72	}
 73}
 74
 75// -- pricing controls (admin) --
 76//
 77// Every amount here is MICRO-USD (1 USD = 1_000_000). Nothing is priced
 78// in the chain token; ugnot is derived at payment time from the stored
 79// rate, so a second settlement token later is a second rate rather than
 80// a second price list.
 81
 82// SetDomainOpen enables or disables registration of new names under a
 83// domain. Existing names are untouched: closing a domain stops new
 84// mints, it does not seize anything already held.
 85func SetDomainOpen(cur realm, label string, open bool) {
 86	assertNoSend(cur)
 87	assertIsAdmin(cur)
 88	emitAdmin(cur, "SetDomainOpen", "label", label, "open", fmtBool(open))
 89	v := "1"
 90	if !open {
 91		v = "0"
 92	}
 93	setDomainExtra(cur, label, keyOpen, v)
 94}
 95
 96// SetDomainBasePrice sets the domain's base price in micro-USD. Every
 97// "%N" rule under the domain is relative to this, so one call reprices
 98// the whole domain. BOUNDED (timelock.gno): a raise up to four times every
 99// ten minutes, a cut up to 25% once an hour; a move to or from free, or
100// anything larger, is a timelocked proposal.
101func SetDomainBasePrice(cur realm, label string, microUsd int64) {
102	assertNoSend(cur)
103	assertIsAdmin(cur)
104	emitAdmin(cur, "SetDomainBasePrice", "label", label, "microUsd", fmtInt(microUsd))
105	if microUsd < 0 || microUsd > maxPriceUSD {
106		panic("nslogic: a price is 0.." + strconv.FormatInt(maxPriceUSD, 10) + " micro-USD")
107	}
108	v := strconv.FormatInt(microUsd, 10)
109	old := DomainBasePriceUSD(label)
110	own := keyPrice + ":" + label
111	clocks := []string{own}
112	if nsdata.GetDomainExtra(label, keyPrice) == "" {
113		// Still on the default: its first own price is a move of the
114		// default's too, on the default's clocks (timelock.gno, re-audit L-1),
115		// and the domain takes the default's clocks with it — or a first
116		// price EQUAL to the default, which moves nothing and stamps nothing,
117		// left a fresh clock for the next cut (review follow-up).
118		inheritClocks(own, keyDefaultPrice)
119		clocks = append(clocks, keyDefaultPrice)
120	}
121	authorizeTweakOn(cur, clocks, old, microUsd, microUsd > old,
122		actionID("SetDomainBasePrice", label, v))
123	setDomainExtra(cur, label, keyPrice, v)
124}
125
126// SetDomainLengthRules prices by label length: "3:2000000,4:%150,5:0"
127// means 3-char names cost $2.00, 4-char names cost 150% of the domain
128// base, and 5-char names are free.
129//
130// TIMELOCKED, with the price rules below: one rule can make any length
131// free ("3:0"), so a rule set is not a tweak whatever it says. Propose the
132// NORMALISED form, which the refusal prints.
133func SetDomainLengthRules(cur realm, label, rules string) {
134	assertNoSend(cur)
135	assertIsAdmin(cur)
136	emitAdmin(cur, "SetDomainLengthRules", "label", label, "rules", rules)
137	norm := normalizeRules(rules, true)
138	requireAction(cur, actionID("SetDomainLengthRules", label, norm))
139	setDomainExtra(cur, label, keyLens, norm)
140}
141
142// SetDomainNameOverrides prices specific labels: "420:5000000,gm:%500".
143// Highest precedence — this beats length rules and the base. TIMELOCKED,
144// like the length rules: "premiumname:0" was a one-call way to take one.
145func SetDomainNameOverrides(cur realm, label, rules string) {
146	assertNoSend(cur)
147	assertIsAdmin(cur)
148	emitAdmin(cur, "SetDomainNameOverrides", "label", label, "rules", rules)
149	norm := normalizeRules(rules, false)
150	requireAction(cur, actionID("SetDomainNameOverrides", label, norm))
151	setDomainExtra(cur, label, keyNames, norm)
152}
153
154// SetDomainField writes an arbitrary key on a domain, for whatever a
155// future feature needs without a vault change — the domain's art among
156// them. It refuses the keys this realm's own setters own, which it used
157// to write straight past ("price" to zero, with no bound).
158func SetDomainField(cur realm, label, key, value string) {
159	assertNoSend(cur)
160	assertIsAdmin(cur)
161	emitAdmin(cur, "SetDomainField", "label", label, "key", key, "value", value)
162	if strings.HasPrefix(key, "_") {
163		panic("nslogic: keys beginning with '_' are reserved")
164	}
165	if managedDomainKey(key) {
166		panic("nslogic: " + key + " has its own setter, with its own rules; use that")
167	}
168	setDomainExtra(cur, label, key, value)
169}
170
171func setDomainExtra(cur realm, label, key, value string) {
172	if err := nsdata.SetDomainExtraRecord(cross(cur), label, key, value); err != nil {
173		panic(err)
174	}
175}
176
177// FreezeDomain / UnfreezeDomain: project-only power to permanently block
178// re-registration of a domain even after its grace period lapses.
179// DeleteDomain destroys a domain and every name beneath it.
180//
181// THE VAULT PRIMITIVE EXISTED BEFORE THIS DID, AND THAT MADE IT
182// UNREACHABLE. nsdata.DeleteDomainRecord gates on assertIsTrustedLogic,
183// so the only way to reach it is a function in this realm — and there
184// was not one. The capability shipped in the vault and could not be
185// used, which is worse than not shipping it, because it reads as
186// present.
187//
188// Bounded by the vault at maxDeletePerCall names per call. It returns
189// how many it removed and whether it finished, so a large domain is
190// several calls rather than a transaction that cannot fit in a block.
191// Repeat until done is true; calling it on a domain that is already gone
192// reports done rather than panicking, so a loop terminates cleanly.
193//
194// ADMIN ONLY, and deliberately not owner-callable: domains are
195// project-owned (SPEC.md §17b) and this destroys other people's names.
196//
197// TIMELOCKED (timelock.gno). It burned every live name under a domain at
198// once — and paid the deleter the holders' storage deposits, about 57 GNOT
199// per hundred names. One proposal, "DeleteDomain <label>", covers every
200// call of a multi-call deletion while it stays matured, and is spent by
201// the call that finishes; recovery can cancel it between calls.
202func DeleteDomain(cur realm, label string) (removed int64, done bool) {
203	assertNoSend(cur)
204	assertIsAdmin(cur)
205	emitAdmin(cur, "DeleteDomain", "label", label)
206	id := actionID("DeleteDomain", label)
207	assertActionReady(id)
208	removed, done = nsdata.DeleteDomainRecord(cross(cur), label)
209	if done {
210		nsdata.ConsumeAction(cross(cur), id)
211	}
212	return removed, done
213}
214
215// SetDomainTTL sets how long a resolver may cache answers about this
216// domain, in seconds, where 0 means never cache — the ENS rule. Admin
217// only, since domains are project-owned.
218func SetDomainTTL(cur realm, label string, seconds int64) {
219	assertNoSend(cur)
220	assertIsAdmin(cur)
221	emitAdmin(cur, "SetDomainTTL", "label", label, "seconds", fmtInt(seconds))
222	if seconds < 0 || seconds > maxTTL {
223		panic("nslogic: a ttl is 0.." + fmtInt(maxTTL) + " seconds")
224	}
225	if err := nsdata.SetDomainTTLRecord(cross(cur), label, seconds); err != nil {
226		panic(err)
227	}
228}
229
230func FreezeDomain(cur realm, label string) {
231	assertNoSend(cur)
232	assertIsAdmin(cur)
233	emitAdmin(cur, "FreezeDomain", "label", label)
234	nsdata.SetDomainFrozen(cross(cur), label, true)
235}
236
237func UnfreezeDomain(cur realm, label string) {
238	assertNoSend(cur)
239	assertIsAdmin(cur)
240	emitAdmin(cur, "UnfreezeDomain", "label", label)
241	nsdata.SetDomainFrozen(cross(cur), label, false)
242}