Readme — Technical Reference
A single-file dApp + three-contract suite for the 2015 Frontier-era GlobalRegistrar at
0x33990122638b9132cA29c723BDF037F1a891a70C — register, view, transfer, wrap names as
ERC-721s, and trade them in a trait-categorized marketplace with a merkle-proven Historic registry.
The registrar address is configurable, so the same app drives the Linagee Name Registrar
(0x5564886ca2C518d1964E5FCea4f423b41Db9F561) — same interface family.
The 2015 contract
This is the Frontier GlobalRegistrar (Gav Wood lineage), hardcoded in old go-ethereum as
GlobalRegistrarAddr // frontier; the deployed source was verified against geth v1.3.6
common/registrar/contracts.go. (That client-side common/registrar package
was present through at least late 2016 and was later removed from go-ethereum as the naming
approach moved toward ENS; the on-chain contract itself remains live and unchanged at its original
address.) The hardcoding is corroborated in the channel itself: on
Sep 28, 2015, rfikki surfaced and quoted the exact line —
GlobalRegistrarAddr = "0x33990122…a70c" // frontier from go-ethereum PR #1840
("…I expect geth 1.3.0 to come out next week with this change") — and core dev Viktor Trón
confirmed in real time, "it IS deployed there."
Provenance & significance. Human-readable names on Ethereum weren't an afterthought —
the GlobalRegistrar address was written into the official go-ethereum client itself in the
2015 Frontier era, making on-chain naming part of the earliest client tooling. The design grew out of
open community work: the early Ethereum name-registry discussions (on the Gitter
name-register channel and the ethereum subreddit) among builders including
Linagee, rfikki, and Viktor Trón, working out how a decentralized naming system
should behave in the network's first months. rfikki was among the first to start that
conversation — the opening message in the go-ethereum name-register Gitter channel
(Aug 27, 2015) is his — in the same room where Viktor Trón relayed Gav Wood and Vitalik
Buterin's namereg work, and where, months later, Nick Johnson (Arachnid) first shared his
draft EIP for a new Ethereum Name Service (Apr 2016) and gathered the early feedback that became
ENS — the channel was the through-line from Frontier namereg to ENS. This project continues
that lineage directly: rfikki — its steward — deployed
working instances of this exact GlobalRegistrar code during Frontier, e.g.
0x67b1e40d8e5c28b155e7fa9f7469fa03c693f74d, deployed Aug 25, 2015
(block 138,997, ~4 weeks after mainnet genesis) by collectibletrust.eth. That
deployment is bytecode-identical to the canonical registrar (all twelve function selectors and the
Changed(bytes32)/PrimaryChanged event topics match) and carries Etherscan's
"Global Name Registrar" tag. Every claim here is verifiable on-chain.
Quirks the whole app is built around:
- reserve(bytes32) is free and instant. The short-name auction in the source is a TODO
that never shipped. Any unowned name is one transaction away.
- reserve() and transfer() never revert — they silently no-op on failure. A successful
transaction proves nothing. The app re-reads
owner(name) after every registration and
transfer, with front-run detection on register.
- The
subRegistrar(bytes32) getter was commented out in the deployed source; that
slot is exposed through register(bytes32) instead. The app maps this.
- Events:
Changed(bytes32 indexed name) and
PrimaryChanged(bytes32 indexed, address indexed). Because names are raw bytes32, the
indexed topic is the name itself, not a hash — full history is recoverable from logs.
name(address) is the reverse primary lookup — but it is unauthenticated:
setAddress(name, addr, primary=true) checks only that the sender owns the name, never that
the address consents. Any name owner can plant a reverse record on any address. The app
therefore forward-verifies every reverse result (addr(name) must round-trip) and labels it
✓ verified or ⚠ unverified.
- ENS bridge & capitalized names: with the GlobalRegistrarENSResolver set on
globalregistrar.eth, lowercase names resolve in any ENS wallet as
<name>.globalregistrar.eth. ENS normalizes to lowercase, so capitalized or
unusual-byte names use the hex escape: 0x<bytes>.globalregistrar.eth
(e.g. Gold → 0x476f6c64.globalregistrar.eth) — exact bytes, unambiguous,
universal up to 30-byte names. Lookup shows each name's bridge form.
- No expiry:
renewalDate exists in the struct but nothing in the deployed code
reclaims names. Registration is permanent (until transfer or disown).
- Names are byte-exact. No normalization of any kind:
Satoshi,
satoshi and SATOSHI are three independent records. The same is true of
names differing only by whitespace (a trailing space), invisible characters
(zero-width/control), or lookalike characters (a Cyrillic о vs a Latin
o) — they are distinct names that can look identical, which is a real phishing surface.
The Register tab warns (without blocking) when a name contains these; always treat a name's
bytes, not its appearance, as its identity, and verify ownership on-chain before trusting or
buying a name. The app validates only that a name is non-empty and ≤ 32 bytes; everything else is
DYOR. See the USER_GUIDE for the full breakdown of what the app can and cannot protect against.
Architecture
- One HTML file, ethers v5.7.2 inlined (no CDN, no build step, no server).
- All config lives in the URL hash — RPC, registrar / wrapper / market addresses, and the
watchlist. Nothing touches browser storage. Bookmark the URL to keep a setup; share it to share one.
- Owner scans batch through Multicall3 (
0xcA11bde05977b3631167028862bE2a173976CA11),
120 names per page.
- Default read RPC:
https://ethereum-rpc.publicnode.com; any mainnet RPC can be set
in ⚙ Settings. Wallet transactions go through the injected provider (mainnet chainId enforced, with
a switch request if you're on the wrong network).
Extension architecture (immutable cores, additive growth). The cores — the 2015 registrar,
the wrapper, and the treasury — are immutable and never change. New capabilities attach through two
narrow seams without touching them: the wrapper's four write-once trait-module slots
(metadata), its single write-once animation-module slot (the optional on-chain generative seal,
surfaced as each deed's animation_url), and the treasury's timelocked routing table
(money). A DeedCapabilityRegistry
is the ecosystem's on-chain, read-only directory: it points at the canonical cores and lists
self-describing modules/markets/resolvers so any wallet or integrator can discover the whole system
from one address. It can only list contracts (plain addresses, read-only) — never control your
name or move funds — and the curator can renounce to freeze it forever. Future EIP-driven features,
new historic eras, or (legally-gated) reward mechanics ship as fresh audited contracts into these
seams; see FUTURE_PROOFING.md, FUTURE_ERAS.md, and
GAMIFICATION.md in the repo.
GlobalRegistrarWrapper.sol
ERC-721 (OpenZeppelin v5, Enumerable, ReentrancyGuard) bound to a registrar at deploy time. The
existing LNR wrapper is hard-bound to the Linagee registrar, so GlobalRegistrar names need this fresh
deployment. Wrap flow (LNR-style):
createWrapper(name) — stakes your claim (must own the name)
registrar.transfer(name, wrapper) — hand custody to the contract
wrap(name) — mints the token. Custody is verified by reading
registrar.owner() back, which is what makes the silent-no-op transfer safe.
unwrap(tokenId) reverses; abortWrap(name) bails out mid-flow. While
wrapped, proxySetAddress / proxySetContent / proxySetSubRegistrar keep the name usable
by the token holder. tokenURI is fully on-chain (Base64 JSON + deed-plate SVG) with
base traits Length, Charset (Digits/Letters/Emoji/Unicode/Mixed), Case (lowercase/UPPERCASE/Capitalized/MixedCase — a dimension ENS can't have, since it normalizes everything to lowercase), Club (999/10k/100k digit clubs), Pattern (Repeating/Sequential/Palindrome digit grails), Palindrome, Era, Sealed. The wrapper exposes four
write-once trait-module slots (each openable once by the governor, never re-pointable, read
through a gas-capped hostile-proof staticcall so a broken module can never brick tokenURI). Slot 0
holds the HistoricTraitModule, which once linked via setTraitModule(0, …)
adds Historic (Gold 2015 / Silver 2016), Historic Rank (number) and First
Registered (date), plus a gold or silver frame and a HISTORIC #N mark. The
remaining slots are reserved for future opt-in modules (see the capability registry below).
sealDeed(tokenId) is an irreversible owner-elected fuse that permanently freezes a
deed's records and adds a wax-seal mark, surviving unwrap → rewrap. The wrapper also exposes
contractURI() — fully on-chain collection-level metadata (name, description, banner SVG,
ERC-2981 royalty) so marketplaces like OpenSea populate the collection page automatically; see
MARKETPLACE_LISTING.md in the repo for the listing procedure.
RegistrarGenerativeModule.sol — on-chain animated seal
Delivers each deed's optional generative art, fully on-chain. The art is a Clifford-attractor
canvas rendered by an HTML <canvas> program stored in the contract (a loop,
so byte size is fixed regardless of art density — the escape from per-element SVG cost). The module
only templates the name (and, when historic, the rank) into the renderer and base64-wraps the
result; the actual drawing happens in-browser.
Unique per collection. The art seed is name + DOMAIN, where DOMAIN is this
collection's contract address (set immutably at deploy, typically the wrapper). So the same name
renders a different attractor on each contract instance — a holder's "money" here looks
nothing like "money" on another GlobalRegistrar/Linagee deployment, so the art is genuinely unique to
this collection rather than reproducible by anyone with the name. The app injects the same address
(lowercased), so the in-app seal stays byte-identical to the on-chain output.
- Two faces.
animationURI[Ranked](label[, era, rank]) returns the live HTML
canvas as a data:text/html URI (the marketplace item-page art). fallbackSVG[Ranked](…)
returns an engraved SVG plate (branding + true-case name + charset/case + gold/silver rank) as the
static image for grid/thumbnail views. tokenURIFor[Ranked](…) assembles
the full metadata JSON with both, plus Era / Historic Rank attributes.
- Wrapper integration (write-once). The immutable wrapper gained an optional
animation_url sourced from a module via the write-once setAnimationModule
slot (governor-only, same guarantee as trait slots) and an IDeedAnimationModule
interface. tokenURI appends the animation only when a module is linked, through a
gas-capped, try/catch-guarded path: an EOA, a reverting/garbage/oversized module, or no module all
degrade to no animation_url and can never brick tokenURI. The base plate
image is always present.
- Rank provenance. The seed-derived lines (name, attractor signature, charset/case) need no
external data. Historic rank is not self-derived — it's injected by the caller from the
merkle-proven HistoricAttestor, never fabricated. era 0 (non-historic) draws no rank line and is
byte-identical to the rank-free output.
- App parity. The in-app seal (Lookup view) embeds the byte-identical renderer template, so
what you see in the app is exactly what the contract serves to marketplaces — verified
programmatically on every change.
RegistrarMarket.sol
- Non-custodial, collection-agnostic. Approval-based listings:
list / cancelListing / buy with a stale-listing owner guard.
- Escrowed ETH offers:
makeOffer (auto-refunds outbid offers),
cancelOffer, acceptOffer(minAmount) with a front-run guard.
feeBps capped at 500 (5%). Indexer-friendly events:
Listed, ListingCanceled, Sale, OfferMade, OfferCanceled, OfferAccepted.
HistoricAttestor.sol — Gold 2015 / Silver 2016 on-chain
The EVM can't read historical logs, so first-registration data is indexed off-chain (by this app)
and committed as a single merkle root in the constructor. attest(name, firstBlock,
firstTimestamp, rank, proof) is permissionless and permanent; getHistoric(name)
returns (attested, rank, firstTimestamp, firstBlock, era) with era 1 = Gold 2015,
2 = Silver 2016, derived from verified mainnet block boundaries:
| Era | Condition | Boundary fact |
| Gold 2015 | firstBlock ≤ 778,482 | last block of 2015 (ts 1451606392) |
| Silver 2016 | firstBlock ≤ 2,912,406 | last block of 2016 |
Leaf encoding (the app's exporter produces exactly this; double-hash per OZ MerkleProof guidance,
abi.encode for fixed-width fields):
keccak256(bytes.concat(keccak256(abi.encode(
bytes32 name, uint32 firstBlock, uint40 firstTimestamp, uint32 rank))))
Rank #1 is the earliest first-registration on the contract, ordered by
(blockNumber, logIndex) of the name's first Changed() event — which is
necessarily its original reserve(), since every other mutator requires ownership.
Deployment
All three contracts compile with solc 0.8.26 (pinned; pragma ^0.8.24),
@openzeppelin/contracts@5, evmVersion: cancun (OZ v5 uses mcopy).
forge create contracts/GlobalRegistrarWrapper.sol:GlobalRegistrarWrapper \
--constructor-args 0x33990122638b9132cA29c723BDF037F1a891a70C
forge create contracts/RegistrarMarket.sol:RegistrarMarket \
--constructor-args <feeRecipient> <feeBps>
# Historic pipeline:
# 1. Market → ✦ Historic → Build historic index
# 2. Export attestation tree → historic-attestations.json
forge create contracts/HistoricAttestor.sol:HistoricAttestor --constructor-args <root>
forge create contracts/HistoricTraitModule.sol:HistoricTraitModule --constructor-args <attestor>
# 3. wrapper.setTraitModule(0, <module>) (governor-only, write-once slot)
# 4. attestor.attest(...) per name, proofs from the JSON
# Generative seal (optional, fully on-chain animated art):
forge create contracts/RegistrarGenerativeModule.sol:RegistrarGenerativeModule
# wrapper.setAnimationModule(<module>) (governor-only, write-once slot)
# scripts/deploy.js does all of the above in order; node scripts/deploy.js --local
# runs a full rehearsal on an in-process EVM (deploys a mock registrar).
Paste the deployed addresses into ⚙ Settings — the Wrap and Market trading panels stay dormant
until configured.
Hosting a locked-down instance
The app is open by design — any addresses can be pasted, which is right for the public
build but risky for a hosted product: because config lives in the URL hash, a malicious shared
link could pre-fill a counterfeit wrapper/market to phish approvals. (Editing your own settings
only affects you; the danger is a crafted link given to someone else.) To host safely, fill the
PINNED block near the top of the script with your canonical addresses. Pinned fields are
used as-is, protected from URL-hash overrides, and shown read-only in Settings with a 🔒 notice.
Leave a field empty to keep it user-editable; the read RPC is never locked (read-only, harmless). Pin
every core contract you use — an unpinned field stays overridable. This guards the client only;
the contracts enforce their own security on-chain regardless, and no governor/admin function is
exposed in the UI at all — every app action is an ownership-gated call on your own name.
Caveats
- Wallet flows are verified by code review and unit tests (codec round-trips, classifier, merkle
leaf parity with Solidity, compilation) but not against live mainnet —
exercise the reserve front-run path and a full wrap cycle on an Anvil/Tenderly fork first.
- Wrapping transfers registrar custody to the wrapper, so
name(address) reverse
lookup points at the wrapper while wrapped.
User Guide
Everything here works against mainnet. The app opens in read-only mode through a public
RPC — lookups, scans, and the Historic index need no wallet. Connect a wallet (MetaMask or any
injected provider, mainnet) only when you want to transact.
Getting started
- One-paste setup via the registry. If the project has deployed a
DeedCapabilityRegistry, paste its address in ⚙ Settings and click
Auto-fill from registry — the app reads the canonical wrapper, market, router, and resolver
straight from the on-chain directory, so you don't paste each by hand. Review and Save. You can
still set any address manually.
- Connect wallet (top right) — required for register / manage / wrap / trade.
- ⚙ Settings — pick a Read RPC from three public endpoints (PublicNode is the
default; switch to LlamaNodes or dRPC if one is down or throttling) or choose Custom for
your own / archive-grade node; plus the registrar address (GlobalRegistrar by default; paste the
Linagee address to drive that contract instead), and wrapper and market addresses once deployed.
- Your config and watchlist live in the URL. Bookmark it to keep your setup. Share the URL
to share the exact configuration. There is nothing to "log into" and nothing stored in the browser.
Lookup
- Type a name (up to 32 bytes of UTF-8 — emoji count) or paste an
0x… address for a
reverse primary-name lookup.
- The deed plate renders the raw bytes32 storage word: 32 cells, lit = data, hollow = zero
padding. Short names light few cells; 32-byte names fill the plate.
- You'll see status (unregistered / registered / wrapped), owner,
resolved address, content hash, sub-registrar, token + listing if wrapped — and the
first-registration line: exact UTC timestamp and block of the earliest
Changed() event, with a ✦ Gold 2015 or
✦ Silver 2016 badge when it qualifies, plus rank once the Historic
index is built.
- How your name displays — every form. One name, shown different ways depending on
where it appears, all the same ownership:
- Native — just
gold, no suffix. The real identity on the 2015 contract;
needs no .eth. Everything else points back to this.
- Bridged, lowercase —
gold → gold.globalregistrar.eth, so it
resolves in any ENS wallet (e.g. MetaMask's send field).
- Bridged, hex escape — because ENS lowercases input, a capitalized/odd-byte name can't
safely use the plain form (
Gold and gold would collide). The hex escape
encodes exact bytes: Gold → 0x476f6c64.globalregistrar.eth. The app
shows the right form for you.
- Deed (if wrapped) — the same name also as an ERC-721 token for marketplaces; unwrap and
you're back to forms 1–3 unchanged.
This is a read-only resolution convenience; ownership always lives on the 2015 contract.
- Like Basenames, but fee-immune. Coinbase's Basenames (
name.base.eth) mints a
real ENS subname per holder — your identity lives in ENS. We mint nothing in ENS:
gold.globalregistrar.eth is resolved on the fly (wildcard) by reading the 2015 contract,
so there's no per-name ENS entry, no per-name fee ENS could ever charge, and if ENS changed or
vanished your name still resolves natively through this app. The .eth form is a window
onto your name, never the deed. See ENS_INTEGRATION.md for the full comparison and future-fee
analysis.
- Reverse lookups are forward-verified. Anyone can plant a "primary" name on any address
on this contract (only the name's owner is checked, not the address). So a reverse result is
labeled ✓ verified only when the name resolves back to the address;
otherwise ⚠ unverified — treat those as untrusted.
- Case matters. If your query contains capitals, a "Case variant" row shows the lowercase
record's status with a one-click jump — they are different names on this contract.
- Names owned by an address. Paste an
0x… address and, alongside its reverse
primary, a panel offers to find every name that address currently owns. The 2015 contract has
no owner→names index, so this is derived from Changed() event logs and then re-checked
against live owner() — so transfers are reflected correctly (a name transferred away
won't show; one received will). Two ranges: 2015–2016 (fast, reuses the historic index if
built) or full history → latest (complete but a much longer crawl). The result always states
which range produced it, so an empty result is never mistaken for "owns nothing" when it really
means "owns nothing in that range." Wide scans want an archive-grade RPC.
- Faster repeat scans (cache + delta). The set of names from
Changed() logs is
immutable history, so it's cached for the session and reused — repeat scans only fetch new
blocks (a delta), not all of history. Save index file downloads the cache as JSON
(gr-name-index-v1) so you can Load it next session and skip scanning entirely;
Update to latest (delta) extends a loaded index to the current block. The scanner itself runs
several getLogs in parallel with an adaptive, rate-limit-aware back-off: it
automatically shrinks ranges and cools down when an RPC throttles (429 / "rate limit"), so it speeds
up on archive endpoints yet degrades gracefully on free public ones. Coverage is gap-free by
construction regardless of how ranges get split.
- Hosted warm-start (optional). Because the 2015–2016 historic set is a closed, immutable
list, an operator can scan it once and serve the resulting
gr-name-index.json
beside the app on the same host. On startup the app fetches it (toggle auto-load hosted index,
on by default) and every visitor starts warm — no per-user cold scan, so RPC/archive usage stays
near zero no matter how many holders visit. To make the file: in Market → ✦ Historic, click
Build historic index, then Save name-index (for hosting) — it downloads exactly
gr-name-index.json (a v2 file that warms both Lookup and the Market
ranked list, so neither tab needs to scan), ready to upload beside the app (no rename needed).
Build historic index stays available to regenerate on demand. Regenerate and
re-upload to extend coverage. Fails silently if absent (the app just scans on
demand), and is validated/registrar-guarded exactly like a manually loaded file.
Register
- Check availability first; the Reserve button unlocks only for free names. Registration
costs gas only — the contract charges nothing.
- After the transaction confirms, the app re-reads ownership. This matters: the 2015
contract silently no-ops instead of reverting, so a "successful" tx can still mean someone
front-ran you. The app tells you plainly: Reserved ✓ or
Front-run ✗.
- Capitalized names get a case-sensitivity note showing the lowercase twin's status before you
commit.
- Batch reserve: paste up to 20 names, one per line — each is checked, reserved, and
verified individually.
- Register & wrap: after a successful reserve, an inline panel offers to continue
straight into wrapping the new name into a tradeable ERC-721 deed — no tab switch. It's still three
on-chain transactions (createWrapper → transfer → wrap), the minimum the 2015 contract allows, each
verified before the next unlocks and fully non-custodial. Dismiss it to keep the name unwrapped.
- Secure register (commit–reveal): with a CommitRevealRouter configured in Settings,
step 1 publishes only a hash of (name, salt, you) — bots watching the mempool learn nothing.
After the router's minimum block delay, step 2 reveals: the router reserves the name and hands it to
you atomically, verifying owner() at every step. Pending commitments (name + salt) persist in the URL
hash so the reveal survives a reload — don't share the URL while commitments are pending. Honest
limits: because the 2015 registrar is permissionless, the reveal tx itself can still be sniped by a
direct reserve(); the router then reverts with NameTaken rather than losing silently. For maximum
protection, switch your wallet to a private RPC (Flashbots Protect, rpc.flashbots.net) before
revealing.
Manage
- Load a record you own to transfer it, set its resolved address (optionally as
your primary, enabling reverse lookup), set a 32-byte content hash, or a
sub-registrar.
- Disown deletes the record permanently — the name returns to the
open pool and its current data is gone. The historic first-registration timestamp survives (it's
log history), but ownership does not.
Wrap (ERC-721)
- Requires a deployed wrapper address in Settings. Three steps, each verified on-chain before the
next unlocks: Create (stake your claim) → Transfer (hand the name to the wrapper) →
Wrap (mint the token).
- Check status anytime — the stepper reads live chain state, so you can resume a half-done wrap
or abort it and reclaim the name.
- Unwrap burns the token and transfers the registrar record back to you.
- While wrapped, the token holder controls the name's address/content/sub-registrar through the
wrapper's proxy functions; trading happens on the Market tab.
Why wrap (and why you might not). Wrapping is optional — a raw name works forever as-is.
Wrap when you want the name to be a standard ERC-721 deed: tradeable on any NFT marketplace,
displayable in NFT wallets, carrying on-chain art + traits, ERC-2981 royalties on
resale, and historic rank on its metadata. In return you accept that the wrapper holds the
raw name in custody while wrapped (you hold the deed; unwrap any time to get the exact name back),
a flat wrap fee + gas, and that records are set through the deed while wrapped. If you only want
to hold or resolve a name, you don't need to wrap.
Fees, in full. Registering costs gas only (the 2015 contract charges nothing).
Wrapping charges a flat wrap fee — shown live in the Wrap panel before you confirm, and it may
be zero — which goes to the project treasury. Unwrapping is gas only. On the Market, a sale pays
feeBps (capped at 5%) to the fee recipient, plus any ERC-2981 royalty the
deployment set (≤ 10%) on marketplaces that honor it. Listing, offers, and cancels are gas only. The
app never takes a cut, and no fee is hidden — the wrap fee is read straight from the contract.
Portfolio
- Shows your reverse primary name, every wrapped token you hold (via enumeration), and your
watchlist — paste any names you want tracked; they're verified by batch owner-scan and saved
into the URL hash with the rest of your config.
Market — clubs & trading
- Clubs are trait categories computed from the raw bytes: 999 Club (000–999), 10k Club,
Single Char, Two/Three Letter, ~300 Words, ~90 Emoji — or paste a custom set. Club generators are
lowercase by design; check capitalized variants via custom paste or Lookup.
- Each page of 120 scans current ownership in one Multicall3 round:
available · registered · wrapped — and
invalid for entries that don't fit in 32 bytes (shown dimmed with the
reason, never silently dropped). Custom pastes are deduped byte-exactly; case variants are kept and
flagged, because they're genuinely different names.
- Trading (needs wrapper + market addresses): load a wrapped name, then
Approve & list at a price, Buy a listing, or use escrowed offers — your
ETH is held by the market contract, auto-refunded if outbid, and the owner can
Accept with a minimum-amount front-run guard.
✦ Historic Registry — Gold 2015 / Silver 2016
- Build historic index scans every
Changed() event from block 0 through the
end of the most recent active era (currently 2,912,406, the last block of 2016). The earliest
event per name is its registration; names are ranked #1, #2, #3… by that moment. Expect a
few minutes on a public RPC — the scanner adaptively shrinks its block ranges when the RPC pushes
back. The index lives in memory for the session.
- Gold frame = first registered in 2015 (block ≤ 778,482);
silver = 2016 (≤ 2,912,406). Eras are driven by a single data-driven
table; a future Bronze 2017 tier is scaffolded but dormant, and would light up additively
(a new attestor + module in a fresh write-once slot) without altering existing Gold/Silver — the
closed historic sets are never diluted. Every card shows its rank badge and the exact
first-registration timestamp (resolved per page, cached).
- Case twins: a tag like "2 case twins" means other capitalizations of that name were also
registered historically — each is its own record with its own rank.
- Ranks and timestamps key off the first event ever — later transfers, updates, disowns,
and re-registrations never move them.
- Export attestation tree downloads
historic-attestations.json: the merkle
root plus a proof per name (self-verified before download). Deploy
HistoricAttestor with that root and the HistoricTraitModule over it, then
the governor links the module once via the wrapper's write-once setTraitModule(0, …)
slot. After that anyone can attest() a name — its token metadata instantly gains the
Historic / Rank / First Registered traits and the gold or silver SVG frame.
Generative Seal — live on-chain art
Every deed carries an optional generative seal: a self-contained, fully on-chain
animated artwork seeded by the name's own bytes. It appears below the deed plate in the
Lookup view, and renders identically on marketplaces that support animated media.
- What it is. A Clifford-attractor "medallion" drawn by a tiny HTML
<canvas> program stored on-chain. The program is a loop — so its byte size is
fixed no matter how dense the art, which is how it stays fully on-chain without the size blow-up
that dense SVG art would cause. The same name on the same collection always produces the same art.
- Unique to this collection. The art is seeded by the name and this collection's
contract address — so your "money" here renders a different attractor than "money" on any other
GlobalRegistrar deployment. The piece is genuinely yours, not something anyone can reproduce just
by knowing the name.
- What's engraved on it.
GLOBAL REGISTRAR across the top; the name in its
true case (so satoshi, Satoshi and SATOSHI each get a
distinct seal); the attractor signature (a · b · c · d — the actual Clifford
parameters that generated the image, a self-referential fingerprint); and a charset/case
line (e.g. 7 CHARS · LETTERS · LOWERCASE). Historic names also get a gold
◆ GOLD 2015 · RANK #N or silver ◆ SILVER 2016 · RANK #N line, with the
rank pulled from the real HistoricAttestor — never fabricated.
- Where it shows. The live animated seal renders on the marketplace item page
(via the token's
animation_url) and here in the app. In collection grid /
thumbnail views — which use the token's static image field — you'll instead see
an engraved certificate plate (branding, true-case name, charset/case, and rank), since the dense
animated art can't be represented in a small static image. Both faces are fully on-chain.
- How it reaches deeds. The art comes from a separate
RegistrarGenerativeModule,
linked to the wrapper through a write-once setAnimationModule slot — the same
immutability guarantee as the trait slots. The wrapper stays renderer-agnostic: if no module is
ever linked, deeds simply show the plate, unchanged. The link is defensive — a misbehaving module
can never break a deed's metadata.
Troubleshooting
- "History unavailable / log queries refused" — the RPC limits
eth_getLogs.
Lookup falls back to a chunked 2015–2016 scan automatically; for the full index or stubborn
endpoints, set an archive-grade RPC in ⚙ Settings.
- Historic scan is slow — normal on free public RPCs (it's 2.9M blocks of logs). A paid or
self-hosted node finishes much faster. Progress shows in the scan bar.
- Wallet buttons do nothing — no injected provider in this context (e.g. a sandboxed
preview). Open the file in a normal browser tab with MetaMask on mainnet.
- "Front-run ✗" after registering — someone else's reserve landed first; the 2015 contract
doesn't revert, it just ignores you. Pick another name. This is the contract's behavior, not a bug.
- A name shows as a hex string — its bytes aren't valid UTF-8; the app shows the raw bytes
rather than guessing.
Safety, transparency & DYOR
These are permanent, byte-exact records on an
eleven-year-old immutable contract. Here is exactly what this app can prevent, what it can only
detect and report, and what it fundamentally cannot protect against. Transparency is the protection
— the contracts are immutable and this app is open and self-contained so you can verify everything
yourself.
- Fully prevented (hard validation): empty names and names over 32 bytes are rejected
before any transaction. Note a "character" can be 2–4 bytes (emoji, many non-Latin scripts), so a
short-looking name can still exceed 32 bytes.
- Detected & reported, but not preventable: duplicate
registration / front-running. The contract is first-come and silently no-ops on a taken
name, so the app checks availability and re-reads owner() after every reserve to tell you
the truth (Reserved ✓ / Front-run ✗). It cannot stop a front-run on a public mempool — the
mitigation is commit–reveal (Secure register), and even then the reveal tx can be sniped
(it reverts honestly rather than losing silently). Reveal through a private RPC (Flashbots Protect)
for maximum protection.
- Cannot be prevented — be mindful: the 2015 contract does no normalization, so
every distinct byte sequence is a distinct name. Case variants (
Gold ≠
gold), whitespace variants (a trailing space), invisible characters
(zero-width / control), and lookalike homoglyphs (Cyrillic о vs Latin
o) are all separate, valid names that can look identical to a human. This is a real
phishing surface. The Register tab warns (without blocking) when a name contains
risky characters, but you must treat a name's bytes, not its appearance, as its identity.
- Irreversible & unrecoverable: registration is permanent. There is no expiry to
forget, but also no undo, no support desk, and no way to recover a name sent to a wrong address.
disown deletes the record permanently; transfer is final. Read every
confirmation.
- Do your own research (DYOR). This app is a faithful, non-custodial interface — it does
not vet names, owners, listings, or counterparties, and cannot. Before you register, buy, or trust
a name: verify its exact bytes (Lookup shows them), verify ownership and history
on-chain rather than trusting appearance or a listing's claims, forward-verify any reverse
name (see Lookup — anyone can plant a reverse record on any address), and on the secondary
market confirm you're buying the precise byte-form you intend. Transactions are irreversible.
- Operational: never trust a transaction receipt alone on this contract — trust the
re-read (the app does this everywhere; keep it in mind via other tools). Test wrap and trading
flows on a fork (Anvil / Tenderly) before pointing real ETH at freshly deployed wrapper/market
addresses.