ETH Registrar
The ETH Registrar manages the registration and renewal of .eth names in ENSv2. It retains the proven commit-reveal pattern from ENSv1 while adding multi-year registration discounts, ERC20 payment support, and integration with the Permissioned Registry.
What Changed from ENSv1
| Feature | ENSv1 | ENSv2 |
|---|---|---|
| Grace period | 90-day grace period after expiry | 28-day grace period |
| Payment | ETH only | ERC20 tokens via safeTransferFrom |
| Pricing | USD-denominated, no duration discounts | USD-denominated, multi-year duration discounts |
| Name ownership | ERC721 on BaseRegistrar (optionally wrapped to ERC1155 via NameWrapper) | ERC1155Singleton token on Permissioned Registry |
| Permissions | Owner has full control; no granular roles | Registrant receives a fixed set of EAC roles |
Pricing
Base Rate
Names are priced annually by character count:
| Name Length | Annual Price |
|---|---|
| 3 characters | $640/yr |
| 4 characters | $160/yr |
| 5+ characters | $8/yr |
Multi-Year Discounts
Longer registrations (and renewals) receive a discount applied to the entire duration. The discount is determined by the total term in a single transaction:
| Term | Discount | 5+ char /yr | 5+ char total | 4-char /yr | 4-char total | 3-char /yr | 3-char total |
|---|---|---|---|---|---|---|---|
| 1 yr | 0% | $8.00 | $8.00 | $160 | $160 | $640 | $640 |
| 2 yr | 12.5% | $7.00 | $14.00 | $140 | $280 | $560 | $1,120 |
| 3 yr | ~31% | $5.50 | $16.50 | $110 | $330 | $440 | $1,320 |
| 4 yr | ~31% | $5.50 | $22.00 | $110 | $440 | $440 | $1,760 |
| 5 yr | ~31% | $5.50 | $27.50 | $110 | $550 | $440 | $2,200 |
| 6 yr | ~44% | $4.50 | $27.00 | $90 | $540 | $360 | $2,160 |
Because the discount rate increases at six years, the total cost for six years is lower than for five. For example, renewing a 5+ character name for one year always costs $8 regardless of the existing expiration; renewing the same name for six years costs $27 ($4.50/yr).
Pricing parameters (base rates and discount tiers) are immutable per oracle deployment. Changing them requires deploying a new oracle and calling setRentPriceOracle().
Expiry Premium
After the 28-day grace period, recently-expired names enter a 21-day temporary premium period to prevent sniping. The premium starts at ~$100M and decays exponentially (halving every day), reaching zero at the end of the window.
Querying Prices
Both the registrar and the oracle expose getRegisterPrice and getRenewPrice, but they serve different purposes:
- Registrar (stateful):
getRegisterPrice(label, duration, paymentToken)returns the actual cost for a specific name right now. It reads registry state to determine how long the name has been available, then forwards to the oracle. Reverts if the name is not available or the duration is below the minimum. - Oracle (stateless):
getRegisterPrice(label, available, duration, paymentToken)takes a fully parameterizedavailableduration and returns what the price would be for any hypothetical scenario. It has no knowledge of registry state or minimums.
Multi-Token Payments
Unlike ENSv1 which only accepted ETH, the ENSv2 registrar supports payment in multiple ERC20 tokens. The StandardRentPriceOracle maintains exchange rate ratios for each accepted token. Check accepted tokens via isPaymentToken(token) on the StandardRentPriceOracle.
Payment is collected via safeTransferFrom to an immutable beneficiary address. The caller must approve the registrar for the payment token before calling register() or renew().
Registering a Name
Like ENSv1, the ETH Registrar uses a commit-reveal scheme to prevent front-running registrations. You first call commit with an opaque commitment hash, wait at least MIN_COMMITMENT_AGE (60 seconds), then call register to reveal the parameters and complete registration.
Commit-Reveal
Generate a commitment hash using makeCommitment:
ETHRegistrar.makeCommitment(
label string, // "alice" for alice.eth (label only, not full name)
owner address, // The address that will own the name
secret bytes32, // A randomly generated 32-byte secret
subregistry IRegistry, // Child registry to set (or address(0) for none)
resolver address, // Resolver contract to set
duration uint64, // Registration duration in seconds
referrer bytes32 // Referral identifier (or bytes32(0))
)
// For example
makeCommitment(
"alice",
0x1234...,
0xabcd..., // Random secret, generate off-chain
address(0), // No subregistry
0x5678..., // Resolver address
31536000, // 1 year in seconds
bytes32(0) // No referrer
);Once you have calculated the commitment hash, submit it on-chain:
ETHRegistrar.commit(commitment bytes32)After committing, wait at least MIN_COMMITMENT_AGE (60 seconds) before calling register. The commitment expires after MAX_COMMITMENT_AGE (typically 24 hours).
Registering
Before initiating registration, ensure that:
isAvailable(label)returnstrueduration>=MIN_REGISTER_DURATION- The commitment is between
MIN_COMMITMENT_AGEandMAX_COMMITMENT_AGEold - The payment token is approved for
base + premium(query viagetRegisterPrice)
ETHRegistrar.register(
label string, // Same label used in makeCommitment
owner address, // Same owner used in makeCommitment
secret bytes32, // Same secret used in makeCommitment
subregistry IRegistry, // Same subregistry used in makeCommitment
resolver address, // Same resolver used in makeCommitment
duration uint64, // Same duration used in makeCommitment
paymentToken IERC20, // ERC20 token to pay with (must be approved)
referrer bytes32 // Same referrer used in makeCommitment
) returns (uint256 tokenId)
// For example
register(
"alice",
0x1234...,
0xabcd..., // Same secret as in commit step
address(0),
0x5678...,
31536000,
0x9abc..., // USDC token address
bytes32(0)
);Roles Granted at Registration
The registration grants the owner a fixed set of roles defined by REGISTRATION_ROLE_BITMAP:
ROLE_SET_SUBREGISTRY: change the name's child registryROLE_SET_SUBREGISTRY_ADMIN: delegateROLE_SET_SUBREGISTRYto othersROLE_SET_RESOLVER: change the name's resolverROLE_SET_RESOLVER_ADMIN: delegateROLE_SET_RESOLVERto othersROLE_CAN_TRANSFER_ADMIN: controls whether the token owner can transfer the name (admin-only; there is no non-adminROLE_CAN_TRANSFER)
Unlike ENSv1, the role set is not configurable per registration; it is hardcoded in the registrar. See Enhanced Access Control for details on the role system.
Renewing a Name
Any account can renew any name, not just the owner. Renewal extends the expiry without changing ownership or permissions. Only the base rate is charged (no premium).
ETHRegistrar.renew(
label string, // The label to renew
duration uint64, // Duration to extend by (in seconds)
paymentToken IERC20, // ERC20 token to pay with (must be approved)
referrer bytes32 // Referral identifier
)BatchRegistrar
The BatchRegistrar is a companion contract used during migration from ENSv1. It has a single owner-only function, batchRegister(registry, resolver, labels, expires):
AVAILABLEnames: registered asRESERVED(no owner, no payment)RESERVEDnames where the target expiry exceeds the current expiry: extended viaETH_REGISTRY.renew()directly (no payment, absolute timestamp)REGISTEREDnames: silently skipped
This contract is intended for pre-migration use, not for general public registration.
EAC Integration
Registrar Governance
The ETH Registrar uses OpenZeppelin Ownable for its own governance. The owner can call setRentPriceOracle() to replace the pricing oracle. Ownership is transferable via transferOwnership().
Roles on the ETH Registry
To register and renew names, the ETH Registrar needs roles on the .eth Permissioned Registry. At deployment it is granted two registry roles on ROOT_RESOURCE:
| Role | Purpose |
|---|---|
ROLE_REGISTRAR | Allows calling register() to create new .eth names |
ROLE_RENEW | Allows calling renew() on any .eth name |
Because these are granted at ROOT_RESOURCE, they apply to all names (the EAC checks both root and token-level roles). The registrar does not hold admin variants of these roles, so it cannot delegate its own authority to other contracts.
Code Examples
Full Registration Flow
import {
createPublicClient,
createWalletClient,
erc20Abi,
http,
keccak256,
toHex,
} from 'viem'
import { mainnet } from 'viem/chains'
const client = createPublicClient({ chain: mainnet, transport: http() })
const wallet = createWalletClient({ chain: mainnet, transport: http() })
const label = 'alice'
const owner = '0x...' // Your address
const duration = 31536000n // 1 year in seconds
const secret = keccak256(toHex(crypto.randomUUID())) // Random secret
const resolver = '0x...' // Resolver address
const subregistry = '0x0000000000000000000000000000000000000000'
const paymentToken = '0x...' // ERC20 token address
const referrer = '0x0000000000000000000000000000000000000000000000000000000000000000'
// 1. Check availability
const available = await client.readContract({
address: ethRegistrarAddress,
abi: ethRegistrarAbi,
functionName: 'isAvailable',
args: [label],
})
if (!available) throw new Error('Name not available')
// 2. Get the price
const [base, premium] = await client.readContract({
address: ethRegistrarAddress,
abi: ethRegistrarAbi,
functionName: 'getRegisterPrice',
args: [label, duration, paymentToken],
})
// 3. Approve the registrar to spend the payment token
const totalCost = base + premium
await wallet.writeContract({
address: paymentToken,
abi: erc20Abi,
functionName: 'approve',
args: [ethRegistrarAddress, totalCost],
})
// 4. Create and submit commitment
const commitment = await client.readContract({
address: ethRegistrarAddress,
abi: ethRegistrarAbi,
functionName: 'makeCommitment',
args: [label, owner, secret, subregistry, resolver, duration, referrer],
})
await wallet.writeContract({
address: ethRegistrarAddress,
abi: ethRegistrarAbi,
functionName: 'commit',
args: [commitment],
})
// 5. Wait at least MIN_COMMITMENT_AGE (e.g., 60 seconds)
// ... wait ...
// 6. Register
await wallet.writeContract({
address: ethRegistrarAddress,
abi: ethRegistrarAbi,
functionName: 'register',
args: [
label,
owner,
secret,
subregistry,
resolver,
duration,
paymentToken,
referrer,
],
})Renewing a Name
const renewPrice = await client.readContract({
address: ethRegistrarAddress,
abi: ethRegistrarAbi,
functionName: 'getRenewPrice',
args: [label, duration, paymentToken],
})
// Approve the registrar to spend the payment token
await wallet.writeContract({
address: paymentToken,
abi: erc20Abi,
functionName: 'approve',
args: [ethRegistrarAddress, renewPrice],
})
await wallet.writeContract({
address: ethRegistrarAddress,
abi: ethRegistrarAbi,
functionName: 'renew',
args: [label, duration, paymentToken, referrer],
})Reference
Write Functions
commit(commitment) | Record a commitment hash on-chain. |
register(label, owner, secret, subregistry, resolver, duration, paymentToken, referrer) | Register a name (consumes a prior commitment). |
renew(label, duration, paymentToken, referrer) | Extend a name's expiry. Any account can renew any name. |
setRentPriceOracle(oracle) | Change the pricing oracle. Callable by the contract owner only. |
View Functions
makeCommitment(label, owner, secret, subregistry, resolver, duration, referrer) | Compute a commitment hash for the given parameters (pure, no state read). |
getRegisterPrice(label, duration, paymentToken) | Get the registration price for a name. |
isAvailable(label) | Check whether a name can be registered. |
commitmentAt(commitment) | Get the timestamp when a commitment was recorded. |
getRenewPrice(label, duration, paymentToken) | Get the renewal price for a name (base rate only, no premium). |
isRenewable(label) | Check whether a name can be renewed (registered or within grace period). |
getRemainingGracePeriod(label) | Get the remaining grace period for an expired name. |
Constants
ETH_REGISTRY | The Permissioned Registry this registrar writes to. |
BENEFICIARY | Address that receives all registration and renewal payments. |
MIN_COMMITMENT_AGE | Minimum seconds before a commitment can be used. |
MAX_COMMITMENT_AGE | Maximum seconds before a commitment expires. |
MIN_REGISTER_DURATION | Shortest allowed registration duration. |
MIN_RENEW_DURATION | Shortest allowed renewal duration (1 second). |
GRACE_PERIOD | Post-expiry window (in seconds) during which a name is renewable but not re-registerable. |
rentPriceOracle | Current pricing oracle. |
Events
CommitmentMade(commitment) | Commitment submitted. |
NameRegistered(tokenId, label, owner, subregistry, resolver, duration, paymentToken, referrer, base, premium) | Name registered. |
NameRenewed(tokenId, label, duration, newExpiry, paymentToken, referrer, amount) | Name renewed. |
RentPriceOracleUpdated(oracle) | Pricing oracle updated. |