Contracts reference

The protocol is made of three contracts on Ethereum Mainnet, without owner or admin functions: NameNFT (the registry, resolver and ERC-721 in one), SubdomainRegistrar (subdomain sales), and NameCard, the stateless renderer that draws every name’s NFT image onchain, pinned immutably by NameNFT’s constructor.

NameNFT and the SubdomainRegistrar are a minimal adaptation of the “wei names” codebase, audited by Cantina, Plainshift and Zellic (reports linked from the audit trail). Every line changed from that audited base is public and reviewable on the audit trail page. NameCard is new code: a stateless, pure renderer that holds no funds and controls no names, published in full alongside the diff.

Identity model

  • tokenId = uint256(namehash) where namehash is the recursive label hash rooted at the .ether node (0xb7f8c26395211ac249dcf196aa8bf23249a87186996f42c47bc4d55dfe608eee).
  • computeId("alice") and computeNamehash("alice.ether") are pure helpers on the contract.
  • ERC-721 with resolver interfaces (addr, text, contenthash) exposed both by uint256 tokenId and standard bytes32 node overloads.

Constants

Constant Value
Registration period 365 days
Grace period 90 days
Commitment window 60 s – 24 h
Premium start / decay 100 ETH → 0 over 21 days
Fees (1/2/3/4/5+ bytes) 0.05 / 0.05 / 0.005 / 0.005 / 0.0005 ETH
Max label length 255 bytes
Max subdomain depth 10

NameNFT: key functions

Registration & lifecycle

function makeCommitment(string label, address owner, bytes32 secret) pure returns (bytes32)
function commit(bytes32 commitment)
function reveal(string label, bytes32 secret) payable returns (uint256 tokenId)
function revealFor(string label, address owner, bytes32 secret) payable returns (uint256 tokenId)
function renew(uint256 tokenId) payable
function isAvailable(string label, uint256 parentId) view returns (bool)
function getFee(uint256 byteLength) pure returns (uint256)
function getPremium(uint256 tokenId) view returns (uint256)
function expiresAt(uint256 tokenId) view returns (uint256)
function isExpired(uint256 tokenId) view returns (bool)
function inGracePeriod(uint256 tokenId) view returns (bool)

Subdomains

function registerSubdomain(string label, uint256 parentId) returns (uint256)
function registerSubdomainFor(string label, uint256 parentId, address to) returns (uint256)

Records & resolution

function setAddr(uint256 tokenId, address addr)
function setAddrForCoin(uint256 tokenId, uint256 coinType, bytes addr)
function setText(uint256 tokenId, string key, string value)
function setContenthash(uint256 tokenId, bytes hash)
function setPrimaryName(uint256 tokenId)          // tokenId 0 clears
function resolve(uint256 tokenId) view returns (address)   // falls back to owner
function reverseResolve(address addr) view returns (string)
function text(uint256 tokenId, string key) view returns (string)
function contenthash(uint256 tokenId) view returns (bytes)

The ETH address has one canonical value across both the legacy addr(node) interface and the multicoin addr(node, 60) interface. setAddr and setAddrForCoin(id, 60, …) write both records together, so the two getters can never disagree. For coin type 60, the address bytes must be exactly 20 bytes, or empty to clear it; clearing (or setting the zero address) makes resolve fall back to the current owner. Each write emits both AddrChanged and AddressChanged (AddressChanged first), matching the ENS resolver convention.

Introspection

function records(uint256 tokenId) view returns (string label, uint256 parent, uint64 expiresAt, uint64 epoch, uint64 parentEpoch)
function getFullName(uint256 tokenId) view returns (string)
function normalize(string label) pure returns (string)
function commitments(bytes32 commitment) view returns (uint256 timestamp)
function primaryName(address addr) view returns (uint256 tokenId)

Events

event NameRegistered(uint256 indexed tokenId, string label, address indexed owner, uint256 expiresAt)
event SubdomainRegistered(uint256 indexed tokenId, uint256 indexed parentId, string label)
event NameRenewed(uint256 indexed tokenId, uint256 newExpiresAt)
event PrimaryNameSet(address indexed addr, uint256 indexed tokenId)
event Committed(bytes32 indexed commitment, address indexed committer)
event AddrChanged(bytes32 indexed node, address addr)
event TextChanged(bytes32 indexed node, string indexed key, string value)
event ContenthashChanged(bytes32 indexed node, bytes contenthash)

SubdomainRegistrar: key functions

function configure(uint256 parentId, address payout, address feeToken, uint256 price,
                   bool enabled, address gateToken, uint256 minGateBalance)
function disable(uint256 parentId)
function deposit(uint256 parentId)                       // escrow mode
function withdrawParent(uint256 parentId, address to)    // leave escrow
function register(uint256 parentId, string label) payable returns (uint256)
function registerFor(uint256 parentId, string label, address to) payable returns (uint256)
function ethBalance(address account) view returns (uint256)
function withdrawETH(address to)
function config(uint256 parentId) view returns (address controller, bool enabled,
                address feeToken, uint96 price, address gateToken, uint96 minGateBalance, address payout)

feeToken = address(0) means ETH. ERC-20 sales transfer directly to the payout; ETH sales accumulate in ethBalance and are withdrawn pull-payment style.

Building on EtherNames

The protocol is designed to be built on without permission and without changes to the contracts. Three patterns cover almost everything.

Store your dapp’s data in text records. setText(tokenId, key, value) takes any string key. Use a reverse-DNS key you control, like com.mydapp.profile, and your dapp has its own namespace on every name: no collisions with other dapps, writes gated to the name’s owner, and the data is automatically wiped when a name expires and is re-registered, so a new owner never inherits a stranger’s records.

Key your own contract’s storage on the tokenId. For anything bigger than a text record, deploy your own contract with mapping(uint256 tokenId => YourData). The tokenId is the name’s permanent identifier (uint256(namehash)), so any contract can attach unlimited state to a name with its own rules, and the registry stays untouched.

Snapshot the epoch when you do. Token ids are deterministic and reused: if a name lapses past grace and someone else registers it, the same tokenId burns and re-mints to the new owner. Store records(tokenId).epoch next to your data (or check ownerOf at read time) and treat a changed epoch as a fresh name, or your data will silently carry over to a stranger. The contract’s own resolver records use the same trick internally.

NameCard: the certificate renderer

function render(string fullName, bytes32 node, Status status) pure returns (string)

NameNFT.tokenURI calls render with the name’s live status (Active, Subdomain, Grace, Expired or Invalid) and embeds the returned SVG as the token image. NameCard is stateless and pure: it holds no funds, owns no names, and only ever returns a string. Its address is set once in NameNFT’s constructor and can never change.