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.

freetier.gno

13.54 Kb · 352 lines
  1package nslogic
  2
  3import (
  4	"strconv"
  5	"time"
  6
  7	"gno.land/r/nym-mnsrehearsal001/nsdata/v1"
  8)
  9
 10/*
 11THE FREE TIER: a name somebody can hold without ever owning a wallet.
 12
 13The vault has supported this since it was written — RegisterNameRecord
 14takes mintNFT=false and stores a record with no GRC-721 token, and
 15UpgradeFreeRecord is described there as "the paid free-tier upgrade".
 16Nothing in the logic realm ever reached either. This is that wiring.
 17
 18WHY A RECORD WITHOUT A TOKEN IS THE RIGHT SHAPE, and not a compromise:
 19
 20	IT IS A REAL NAME. Owned, resolvable, renewable, on chain, visible
 21	to anyone. Not a promise, not a queue position, not a row in our
 22	database. Everything the paid tier does except be sold.
 23
 24	IT CANNOT BE SOLD, and that is what makes giving it away safe.
 25	nsmarket escrows a TOKEN, and there is no token — so a squatter who
 26	farms a thousand of these has farmed a thousand things nobody can
 27	buy from them. The anti-abuse property falls out of the data model
 28	rather than out of a rule somebody has to enforce.
 29
 30	IT BECOMES SELLABLE BY BECOMING PAID. UpgradeFreeName takes the full
 31	list price and mints the token. So the same name costs nothing and
 32	costs full price, and which one depends entirely on whether you want
 33	to sell it.
 34
 35WHY THE PROJECT PAYS. The whole point is somebody with no GNOT, and
 36possibly no wallet, who is going to put this name in front of an
 37audience. Measured on Pearl: a registration is about 0.04 GNOT of fee
 38plus a small storage deposit, and a free-tier record is smaller than a
 39paid one because there is no token and no ledger entry. That is a
 40marketing cost per name, bounded by a daily cap we apply off chain,
 41where it can be changed without a deploy.
 42
 43WHAT IS DELIBERATELY NOT HERE. No wordlist: storage is 100 ugnot a byte,
 44so a ten-thousand-word dictionary is about 57 GNOT paid forever to
 45encode a marketing decision that will change. The contract carries the
 46structural rules in reserved.gno and nothing else; the lists live off
 47chain with the issuer, which is us, because the free path is granted
 48rather than bought. See tools/gen-reserved.py.
 49*/
 50
 51const (
 52	keyFreeDomain = "free"     // per-domain: "1" means the free tier is open here
 53	keyGrantEnd   = "grantend" // global: unix seconds, or "never"
 54	keyGranter    = "granter"  // global: the address allowed to issue grants
 55)
 56
 57/*
 58THE GRANTER IS NOT THE ADMIN, and that separation is the point.
 59
 60Free names are issued by a machine — something has to sign the grant
 61after a person proves an X account, and that key has to sit somewhere
 62warm. If that key were the admin key, a compromise of the issuing
 63machine would be a compromise of the registry: DeleteDomain destroys
 64every name under a domain and burns their tokens, instantly, with no
 65timelock.
 66
 67So there is a second role that can do exactly one kind of thing: give
 68names away on the project's behalf — free-tier grants and renewals, and
 69RegisterFor, which spends a holder's own credit into the holder's own
 70wallet (credits.gno). Worst case for a stolen granter key is that
 71somebody gives away free names, or spends people's credits on names they
 72did not pick, until we notice and clear the role — a bounded, reversible,
 73embarrassing loss rather than an unbounded one.
 74
 75Stored in the vault's globals rather than in this realm, so it survives
 76a logic redeploy. Unset means nobody but the admin may grant, which is
 77the right default for a role that exists to be delegated deliberately.
 78*/
 79func Granter() address {
 80	return address(nsdata.GetGlobal(keyGranter))
 81}
 82
 83// SetGranter delegates grant issuance, or withdraws it. Passing the
 84// empty address is the revocation, and it is the first thing to reach
 85// for if the issuing machine is ever in doubt — instant, always.
 86//
 87// NAMING a granter is TIMELOCKED (timelock.gno): a stolen admin key would
 88// otherwise hand itself a second, quieter key that gives names away and
 89// spends people's credits, and keep it after the admin key is rotated out.
 90func SetGranter(cur realm, a address) {
 91	assertNoSend(cur)
 92	assertIsAdmin(cur)
 93	emitAdmin(cur, "SetGranter", "addr", a.String())
 94	if a != "" && !a.IsValid() {
 95		panic("nslogic: not an address")
 96	}
 97	if a != "" {
 98		requireAction(cur, actionID("SetGranter", a.String()))
 99	}
100	setGlobal(cur, keyGranter, a.String())
101}
102
103/*
104SetFreeLengths tunes which lengths the free tier will issue.
105
106In the panel rather than in a redeploy: nslogic is about 22 GNOT to
107deploy, and "should four-character names be free this month" is a
108marketing question that should cost a signature, not a deployment.
109
110IT DOES NOT REACH BACKWARDS. Raising the minimum stops NEW issuance of
111short names; every one already granted stays granted and keeps renewing
112free. FreeLabelAllowed is called by GrantFreeName and by nothing else,
113which is what makes that true — see the note on it in reserved.gno.
114*/
115func SetFreeLengths(cur realm, min, max int) {
116	assertNoSend(cur)
117	assertIsAdmin(cur)
118	emitAdmin(cur, "SetFreeLengths", "min", fmtI(min), "max", fmtI(max))
119	if min < 1 || max > 63 || min > max {
120		panic("nslogic: a free-tier length range must sit inside 1..63 and be the right way round")
121	}
122	setGlobal(cur, keyFreeMin, strconv.Itoa(min))
123	setGlobal(cur, keyFreeMax, strconv.Itoa(max))
124}
125
126func assertMayGrant(cur realm) {
127	caller := cur.Previous().Address()
128	if caller == nsdata.GetAdmin() {
129		return
130	}
131	g := Granter()
132	if g == "" || caller != g {
133		panic("nslogic: only the admin or the granter may issue free names")
134	}
135}
136
137// DomainFree reports whether a domain accepts free-tier registrations.
138// Off by default, and per-domain on purpose: the free tier is a
139// promotion aimed at a few domains, not a property of the registry.
140func DomainFree(domainLabel string) bool {
141	return nsdata.GetDomainExtra(domainLabel, keyFreeDomain) == "1"
142}
143
144// SetDomainFree opens or closes the free tier for one domain.
145func SetDomainFree(cur realm, domainLabel string, on bool) {
146	assertNoSend(cur)
147	assertIsAdmin(cur)
148	emitAdmin(cur, "SetDomainFree", "label", domainLabel, "on", fmtBool(on))
149	if !nsdata.DomainExists(domainLabel) {
150		panic("nslogic: no such domain")
151	}
152	v := ""
153	if on {
154		v = "1"
155	}
156	if err := nsdata.SetDomainExtraRecord(cross(cur), domainLabel, keyFreeDomain, v); err != nil {
157		panic(err)
158	}
159}
160
161/*
162GrantWindowOpen reports whether free grants are being issued at all.
163
164A single global switch that closes everything at once, because the
165failure this guards against is not a clever attack — it is noticing on a
166Sunday that something is wrong and wanting one call that stops it. An
167unset value means open; "never" means open with no end date, which is
168what the admin panel already writes.
169*/
170func GrantWindowOpen() bool {
171	return readGrantWindow(nsdata.GetGlobal(keyGrantEnd), time.Now().Unix())
172}
173
174// readGrantWindow is GrantWindowOpen with the clock and the stored value
175// handed in, so the reading can be tested without a chain.
176func readGrantWindow(v string, now int64) bool {
177	if v == "" || v == "never" {
178		return true
179	}
180	end, err := strconv.ParseInt(v, 10, 64)
181	if err != nil {
182		// An unparseable window is a misconfiguration, and the safe
183		// reading of a misconfigured gate is "closed". Giving names away
184		// because a setting was mistyped is the wrong way to fail.
185		return false
186	}
187	return now < end
188}
189
190/*
191GrantFreeName registers a free-tier record to somebody else.
192
193ADMIN OR GRANTER, and that is the whole trust model: the eligibility rules —
194which Twitter account, how old, how many followers, whether the label is
195a dictionary word — live off chain with the issuer. Putting them here
196would freeze a marketing policy into a realm and cost storage to do it.
197What the chain enforces is the part that must not depend on our
198correctness: the label is structurally allowed, the domain opted in, and
199the window is open.
200
201`owner` may be an address the recipient generated in their browser
202seconds ago and has never funded. That is the point.
203*/
204func GrantFreeName(cur realm, label, domainLabel string, owner address) {
205	assertNoSend(cur)
206	assertMayGrant(cur)
207	emitAdmin(cur, "GrantFreeName", "label", label, "domain", domainLabel, "owner", owner.String())
208	assertNotRestoring()
209
210	if !GrantWindowOpen() {
211		panic("nslogic: free grants are closed")
212	}
213	// Recovery's quarantine holds every giveaway shut, whatever the window
214	// says (nsdata incident.gno, re-audit 2026-10-16, M-2).
215	if q := nsdata.QuarantineUntil(); time.Now().Unix() < q {
216		panic("nslogic: recovery has quarantined giveaways until unix " + strconv.FormatInt(q, 10))
217	}
218	if !DomainFree(domainLabel) {
219		panic("nslogic: *" + domainLabel + " does not offer free names")
220	}
221	if !DomainOpen(domainLabel) {
222		panic("nslogic: this domain is not accepting new names")
223	}
224	validateLabel(label)
225	assertNotBlocked(label)
226	// The structural backstop. The issuer has already applied the full
227	// policy; this is what stops a bug there giving away a four-letter
228	// name or a pure-numeric one.
229	if !FreeLabelAllowed(label) {
230		panic("nslogic: " + label + " is not available on the free tier")
231	}
232	if !owner.IsValid() {
233		panic("nslogic: a grant needs a recipient")
234	}
235
236	// paid=false — a token minted with the free-tier hold already on it,
237	// so it can never be approved or sold until UpgradeFreeName lifts it.
238	if err := nsdata.RegisterNameRecord(cross(cur), label, domainLabel, owner,
239		time.Now().Unix(), false); err != nil {
240		panic(err)
241	}
242}
243
244/*
245UpgradeFreeName mints the token for a free-tier record, at full price.
246
247This is the only way a free name becomes sellable, and the price is the
248ordinary list price with no referral discount — a discount is for
249bringing somebody new, and this person is already here.
250
251`newOwner` exists because the common case is somebody who has decided to
252sell and, in the same moment, wants the name in a wallet they actually
253control rather than the browser key they were given. The vault handles
254that move inside UpgradeFreeRecord.
255
256Callable by the record's current owner only. Anyone may PAY for
257somebody else's upgrade in the sense that they can hand over the money,
258but they cannot perform it — a stranger promoting your name into a token
259and choosing where it lands is a theft with a receipt.
260*/
261func UpgradeFreeName(cur realm, label, domainLabel string, newOwner address) {
262	// Not during a restore window: the price rules may not be back yet, and
263	// the name would be made sellable at the genesis price (restore.gno).
264	assertNotRestoring()
265	caller := cur.Previous().Address()
266	if nsdata.GetNameOwner(label, domainLabel) != caller {
267		panic("nslogic: restricted to name owner")
268	}
269	if !nsdata.IsNameValid(label, domainLabel) {
270		panic("nslogic: this name has expired")
271	}
272	if newOwner == "" {
273		newOwner = caller
274	}
275	if !newOwner.IsValid() {
276		panic("nslogic: invalid destination address")
277	}
278
279	// Full list price, resolved now rather than at grant time — the
280	// name's worth is what it is worth today, and a free name held for a
281	// year should not be upgradable at last year's rate.
282	takePaymentSplit(cur, NamePrice(domainLabel, label), "", 0)
283
284	if err := nsdata.UpgradeFreeRecord(cross(cur), label, domainLabel, newOwner); err != nil {
285		panic(err)
286	}
287}
288
289/*
290GrantFreeRenewal extends a free-tier name at no charge.
291
292THE DEAL IS THE DISPLAY, and it is checked off chain: the issuer signs
293somebody in on X, reads their display name, and finds the name in it.
294That single act proves both halves at once — that a real account still
295exists, and that it is still carrying the name — which is why renewal is
296one click a year rather than a form.
297
298FREE-TIER RECORDS ONLY. A paid name renewed for nothing is revenue given
299away by accident, and the two are told apart by whether a token was ever
300minted — now, whether the token still carries the free-tier hold. The
301hold is the registry's flag, read through GetNameInfo's `paid`.
302
303NO OWNERSHIP PROOF IS ASKED FOR, and that is deliberate rather than
304missing. Renewing somebody else's name is a GIFT — it takes nothing,
305moves nothing, and grants the caller no power over it. The only cost of
306allowing it is ours, and the vault's renewal cap already bounds how far
307any single name can be pushed. A proof requirement here would buy
308nothing and would cost the one-click renewal that makes the deal work.
309
310RenewNameRecord insists the caller is the owner, so the owner is looked
311up and passed. That is not a bypass: this realm is the trusted logic,
312and what it is asserting is exactly true — this renewal is on that
313owner's behalf.
314*/
315func GrantFreeRenewal(cur realm, label, domainLabel string) {
316	assertNoSend(cur)
317	assertMayGrant(cur)
318	emitAdmin(cur, "GrantFreeRenewal", "label", label, "domain", domainLabel)
319	// A free renewal is a giveaway too: not during a restore window, and not
320	// while recovery's quarantine holds (review follow-up).
321	assertNotRestoring()
322	if q := nsdata.QuarantineUntil(); time.Now().Unix() < q {
323		panic("nslogic: recovery has quarantined giveaways until unix " + strconv.FormatInt(q, 10))
324	}
325
326	if !nsdata.NameExists(label, domainLabel) {
327		panic("nslogic: no such name")
328	}
329	owner, _, _, _, _, paid := nsdata.GetNameInfo(label, domainLabel)
330	if paid {
331		panic("nslogic: " + label + "*" + domainLabel +
332			" is a paid name — free renewal is for free-tier records")
333	}
334	if RenewalsLeft(label, domainLabel) <= 0 {
335		panic("nslogic: this name is already paid as far ahead as a name may run")
336	}
337	if err := nsdata.RenewNameRecord(cross(cur), label, domainLabel, owner,
338		time.Now().Unix()); err != nil {
339		panic(err)
340	}
341}
342
343// IsFreeTier reports whether a name is a record without a token — the
344// free tier. Exported because the site and the issuer both need to ask
345// before offering a free renewal, and neither should infer it.
346func IsFreeTier(label, domainLabel string) bool {
347	if !nsdata.NameExists(label, domainLabel) {
348		return false
349	}
350	_, _, _, _, _, paid := nsdata.GetNameInfo(label, domainLabel)
351	return !paid
352}