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}