Overview
The ETHRegistrar contract manages the registration and renewal of .eth second-level domain names. It implements a two-step commit-reveal registration process to prevent front-running and supports multiple ERC-20 payment tokens.
The registrar uses a commit-reveal scheme to ensure fair registration. Users must first commit to a registration, wait for a minimum period, then reveal and complete the registration within a maximum time window.
Key Features
Commit-reveal registration : Prevents front-running attacks by requiring a two-step process
Flexible pricing : Delegates pricing to a configurable RentPriceOracle
Multi-token payments : Accepts multiple ERC-20 tokens for registration fees
Automatic permissions : Grants registrants full control over their registered names
Renewal support : Allows extending registrations before expiry
Contract Architecture
contract ETHRegistrar is IETHRegistrar , EnhancedAccessControl
Immutable Configuration
Parameter Type Description REGISTRYIPermissionedRegistryThe .eth registry contract BENEFICIARYaddressRecipient of registration payments MIN_COMMITMENT_AGEuint64Minimum wait time after commit (e.g., 60 seconds) MAX_COMMITMENT_AGEuint64Maximum validity period for commitments (e.g., 24 hours) MIN_REGISTER_DURATIONuint64Minimum registration duration (e.g., 28 days)
Storage
IRentPriceOracle public rentPriceOracle;
mapping ( bytes32 commitment => uint64 commitTime) public commitmentAt;
Registration Process
Step 1: Generate Commitment
Create a commitment hash that binds all registration parameters without revealing them onchain.
function makeCommitment (
string calldata label ,
address owner ,
bytes32 secret ,
IRegistry subregistry ,
address resolver ,
uint64 duration ,
bytes32 referrer
) public pure returns ( bytes32 )
import { ethers } from 'ethers' ;
// Generate a random secret
const secret = ethers . hexlify ( ethers . randomBytes ( 32 ));
// Create commitment hash
const commitment = ethers . keccak256 (
ethers . AbiCoder . defaultAbiCoder (). encode (
[ 'string' , 'address' , 'bytes32' , 'address' , 'address' , 'uint64' , 'bytes32' ],
[
'vitalik' , // label
ownerAddress , // owner
secret , // secret
subregistryAddress , // subregistry
resolverAddress , // resolver
31557600 , // duration (1 year in seconds)
ethers . ZeroHash // referrer
]
)
);
Step 2: Submit Commitment
Submit the commitment hash onchain. This starts the waiting period.
function commit ( bytes32 commitment ) external
// Submit commitment
const tx = await registrar . commit ( commitment );
await tx . wait ();
console . log ( 'Commitment submitted. Wait at least MIN_COMMITMENT_AGE before registering.' );
You must wait at least MIN_COMMITMENT_AGE (typically 60 seconds) after committing before you can register. The commitment expires after MAX_COMMITMENT_AGE (typically 24 hours).
Step 3: Complete Registration
After waiting the required time, reveal the commitment parameters and complete registration.
function register (
string calldata label ,
address owner ,
bytes32 secret ,
IRegistry subregistry ,
address resolver ,
uint64 duration ,
IERC20 paymentToken ,
bytes32 referrer
) external returns ( uint256 tokenId )
// Wait for MIN_COMMITMENT_AGE (e.g., 60 seconds)
await new Promise ( resolve => setTimeout ( resolve , 65000 ));
// Approve payment token
const price = await registrar . rentPrice ( 'vitalik' , ownerAddress , duration , usdcAddress );
const totalCost = price . base + price . premium ;
await usdc . approve ( registrarAddress , totalCost );
// Complete registration
const registerTx = await registrar . register (
'vitalik' , // label
ownerAddress , // owner
secret , // secret from step 1
subregistryAddress , // subregistry
resolverAddress , // resolver
31557600 , // duration
usdcAddress , // paymentToken
ethers . ZeroHash // referrer
);
const receipt = await registerTx . wait ();
const tokenId = receipt . logs . find ( log => log . eventName === 'NameRegistered' ). args . tokenId ;
console . log ( `Registered vitalik.eth with tokenId: ${ tokenId } ` );
All parameters in register() must exactly match those used in makeCommitment(), except paymentToken which is chosen at registration time.
Registration Requirements
Name Availability
Check if a name is available before attempting registration:
function isAvailable ( string memory label ) public view returns ( bool )
const available = await registrar . isAvailable ( 'vitalik' );
if ( ! available ) {
console . log ( 'Name is already registered or reserved' );
}
Validation
The registration will fail if:
error NameNotAvailable ( string label);
The name is already registered or reserved. Check availability with isAvailable().
error DurationTooShort ( uint64 duration, uint64 minDuration);
Registration duration must be at least MIN_REGISTER_DURATION (typically 28 days).
error CommitmentTooNew ( bytes32 commitment, uint64 validFrom, uint64 blockTimestamp);
You must wait at least MIN_COMMITMENT_AGE after committing.
error CommitmentTooOld ( bytes32 commitment, uint64 validTo, uint64 blockTimestamp);
The commitment has expired. Submit a new commitment and try again.
Owner address cannot be address(0).
Renewals
Extend an existing registration before it expires:
function renew (
string calldata label ,
uint64 duration ,
IERC20 paymentToken ,
bytes32 referrer
) external
// Calculate renewal cost
const state = await registry . getState ( labelId );
const renewalPrice = await registrar . rentPrice (
'vitalik' ,
state . latestOwner ,
31557600 , // 1 year
usdcAddress
);
// Approve payment (no premium for renewals)
await usdc . approve ( registrarAddress , renewalPrice . base );
// Renew registration
const tx = await registrar . renew (
'vitalik' ,
31557600 , // duration
usdcAddress , // paymentToken
ethers . ZeroHash // referrer
);
await tx . wait ();
Renewals do not require the commit-reveal process and can be called by anyone. The original owner pays no premium price when renewing.
Pricing
Get the cost of registration or renewal:
function rentPrice (
string memory label ,
address owner ,
uint64 duration ,
IERC20 paymentToken
) public view returns ( uint256 base , uint256 premium )
Price Components
Base price : Calculated based on name length and duration
Premium price : Applied to expired names to discourage squatting (decays over time)
const price = await registrar . rentPrice (
'vitalik' ,
ownerAddress ,
31557600 , // 1 year
usdcAddress
);
console . log ( `Base: ${ ethers . formatUnits ( price . base , 6 ) } USDC` );
console . log ( `Premium: ${ ethers . formatUnits ( price . premium , 6 ) } USDC` );
console . log ( `Total: ${ ethers . formatUnits ( price . base + price . premium , 6 ) } USDC` );
Pass address(0) as the owner to exclude premium pricing for estimation purposes.
Payment Tokens
Check if a token is accepted for payment:
function isPaymentToken ( IERC20 paymentToken ) external view returns ( bool )
const acceptsUSDC = await registrar . isPaymentToken ( usdcAddress );
const acceptsDAI = await registrar . isPaymentToken ( daiAddress );
Name Validation
Check if a label is valid for registration:
function isValid ( string calldata label ) external view returns ( bool )
// Check if name meets minimum length requirements
const valid = await registrar . isValid ( 'ab' ); // false - too short
const valid2 = await registrar . isValid ( 'abc' ); // true - valid
Granted Permissions
When a name is registered, the owner automatically receives these permissions:
uint256 constant REGISTRATION_ROLE_BITMAP = 0 |
RegistryRolesLib.ROLE_SET_SUBREGISTRY |
RegistryRolesLib.ROLE_SET_SUBREGISTRY_ADMIN |
RegistryRolesLib.ROLE_SET_RESOLVER |
RegistryRolesLib.ROLE_SET_RESOLVER_ADMIN |
RegistryRolesLib.ROLE_CAN_TRANSFER_ADMIN;
These permissions allow the owner to:
Configure and manage subregistries
Set and update the resolver
Transfer ownership of the name
Events
CommitmentMade
event CommitmentMade ( bytes32 commitment );
Emitted when a commitment is successfully recorded.
NameRegistered
event NameRegistered (
uint256 indexed tokenId ,
string label ,
address owner ,
IRegistry subregistry ,
address resolver ,
uint64 duration ,
IERC20 paymentToken ,
bytes32 referrer ,
uint256 base ,
uint256 premium
);
Emitted when a name is successfully registered.
NameRenewed
event NameRenewed (
uint256 indexed tokenId ,
string label ,
uint64 duration ,
uint64 newExpiry ,
IERC20 paymentToken ,
bytes32 referrer ,
uint256 base
);
Emitted when a name is successfully renewed.
Administrative Functions
Update Price Oracle
Only accounts with ROLE_SET_ORACLE can update the pricing oracle:
function setRentPriceOracle ( IRentPriceOracle oracle ) external
Security Considerations
Front-running Protection : Always use the commit-reveal process. Never reveal your intended registration parameters in the same transaction as the commitment.
Secret Generation : Use a cryptographically secure random value for the secret. Never reuse secrets across different registrations.
Payment Safety : The contract uses OpenZeppelin’s SafeERC20 to handle token transfers safely, protecting against non-standard ERC-20 implementations.
Complete Registration Example
Here’s a complete example showing the full registration flow:
import { ethers } from 'ethers' ;
async function registerName ( registrar , registry , usdc , params ) {
// Step 1: Check availability
const available = await registrar . isAvailable ( params . label );
if ( ! available ) {
throw new Error ( 'Name is not available' );
}
// Step 2: Generate secret and commitment
const secret = ethers . hexlify ( ethers . randomBytes ( 32 ));
const commitment = await registrar . makeCommitment (
params . label ,
params . owner ,
secret ,
params . subregistry ,
params . resolver ,
params . duration ,
params . referrer || ethers . ZeroHash
);
// Step 3: Submit commitment
const commitTx = await registrar . commit ( commitment );
await commitTx . wait ();
console . log ( 'Commitment submitted, waiting...' );
// Step 4: Wait for MIN_COMMITMENT_AGE
const minAge = await registrar . MIN_COMMITMENT_AGE ();
await new Promise ( resolve => setTimeout ( resolve , Number ( minAge ) * 1000 + 5000 ));
// Step 5: Calculate and approve payment
const price = await registrar . rentPrice (
params . label ,
params . owner ,
params . duration ,
params . paymentToken
);
const totalCost = price . base + price . premium ;
const approveTx = await usdc . approve ( await registrar . getAddress (), totalCost );
await approveTx . wait ();
console . log ( `Approved ${ ethers . formatUnits ( totalCost , 6 ) } USDC` );
// Step 6: Complete registration
const registerTx = await registrar . register (
params . label ,
params . owner ,
secret ,
params . subregistry ,
params . resolver ,
params . duration ,
params . paymentToken ,
params . referrer || ethers . ZeroHash
);
const receipt = await registerTx . wait ();
const event = receipt . logs . find ( log => {
try {
return registrar . interface . parseLog ( log )?. name === 'NameRegistered' ;
} catch { return false ; }
});
const tokenId = registrar . interface . parseLog ( event ). args . tokenId ;
console . log ( `Successfully registered ${ params . label } .eth with tokenId: ${ tokenId } ` );
return tokenId ;
}
// Usage
const tokenId = await registerName ( registrar , registry , usdc , {
label: 'vitalik' ,
owner: await signer . getAddress (),
subregistry: ethers . ZeroAddress ,
resolver: resolverAddress ,
duration: 31557600 , // 1 year
paymentToken: usdcAddress ,
referrer: ethers . ZeroHash
});
Source Code
ETHRegistrar.sol View the complete contract implementation