Overview
The ETHRegistrar contract manages the registration and renewal of .eth second-level domain names in ENS v2. It implements a commit-reveal registration scheme to prevent frontrunning and supports multiple payment tokens with configurable pricing through a rent price oracle.
Contract Address: Deployed per network (see deployment addresses)
Interface ID: 0x29071951
Key Features
- Two-step commit-reveal registration process
- Support for multiple ERC-20 payment tokens
- Configurable pricing with premium pricing for recently expired names
- Integration with ENS registry for name ownership
- Access control for administrative functions
Constants
REGISTRY
IPermissionedRegistry public immutable REGISTRY
The ENS registry contract that stores name ownership records.
BENEFICIARY
address public immutable BENEFICIARY
The address that receives registration and renewal payments.
MIN_COMMITMENT_AGE
uint64 public immutable MIN_COMMITMENT_AGE
Minimum time (in seconds) that must pass between commitment and registration. Prevents frontrunning by requiring commitments to be mined before registration.
MAX_COMMITMENT_AGE
uint64 public immutable MAX_COMMITMENT_AGE
Maximum time (in seconds) a commitment remains valid. Commitments expire after this period to prevent commitment reuse.
MIN_REGISTER_DURATION
uint64 public immutable MIN_REGISTER_DURATION
Minimum registration duration (in seconds) for new name registrations.
State Variables
rentPriceOracle
IRentPriceOracle public rentPriceOracle
The pricing oracle contract used to calculate registration and renewal costs.
commitmentAt
mapping(bytes32 commitment => uint64 commitTime) public commitmentAt
Maps commitment hashes to their creation timestamps.
The commitment hash from makeCommitment()
The block timestamp when the commitment was made, or 0 if no commitment exists
Functions
commit
function commit(bytes32 commitment) external
Registration step 1: Record your intent to register a name without revealing the actual name or parameters. This prevents frontrunning attacks.
The commitment hash generated by makeCommitment()
Events Emitted:
CommitmentMade(bytes32 commitment) - When the commitment is successfully recorded
Reverts:
UnexpiredCommitmentExists(bytes32 commitment) - If an unexpired commitment already exists for this hash
Example:
// First, create a commitment hash
bytes32 commitment = registrar.makeCommitment(
"myname",
msg.sender,
secret,
subregistry,
resolver,
365 days,
referrer
);
// Then commit it
registrar.commit(commitment);
// Wait for MIN_COMMITMENT_AGE before calling register()
register
function register(
string calldata label,
address owner,
bytes32 secret,
IRegistry subregistry,
address resolver,
uint64 duration,
IERC20 paymentToken,
bytes32 referrer
) external returns (uint256 tokenId)
Registration step 2: Reveal your committed registration parameters and complete the registration. You must have a valid commitment that is between MIN_COMMITMENT_AGE and MAX_COMMITMENT_AGE old.
The name to register (without .eth suffix)
The address that will own the registered name. Cannot be zero address.
A secret value used in the commitment to prevent others from guessing your commitment
The initial registry contract for subdomains under this name
The initial resolver address for this name
Registration duration in seconds. Must be at least MIN_REGISTER_DURATION.
The ERC-20 token to use for payment. Must be supported by the price oracle.
A referrer identifier (can be zero bytes if no referrer)
The registry token ID for the newly registered name
Events Emitted:
NameRegistered(uint256 indexed tokenId, string label, address owner, IRegistry subregistry, address resolver, uint64 duration, IERC20 paymentToken, bytes32 referrer, uint256 base, uint256 premium)
Reverts:
DurationTooShort(uint64 duration, uint64 minDuration) - If duration is less than MIN_REGISTER_DURATION
InvalidOwner() - If owner is the zero address
NameNotAvailable(string label) - If the name is already registered
CommitmentTooNew(bytes32 commitment, uint64 validFrom, uint64 blockTimestamp) - If commitment hasn’t aged enough
CommitmentTooOld(bytes32 commitment, uint64 validTo, uint64 blockTimestamp) - If commitment has expired
PaymentTokenNotSupported(IERC20 paymentToken) - If the payment token isn’t supported
NotValid(string label) - If the label is not valid per the price oracle rules
Example:
// Generate a secret (keep this private!)
bytes32 secret = keccak256(abi.encodePacked("my-random-secret", msg.sender, block.timestamp));
// Step 1: Commit (wait at least MIN_COMMITMENT_AGE after this)
bytes32 commitment = registrar.makeCommitment(
"myname",
msg.sender,
secret,
subregistry,
resolver,
365 days,
bytes32(0)
);
registrar.commit(commitment);
// Step 2: Wait for MIN_COMMITMENT_AGE...
// Step 3: Approve payment token
IERC20 paymentToken = IERC20(usdcAddress);
(uint256 base, uint256 premium) = registrar.rentPrice("myname", msg.sender, 365 days, paymentToken);
paymentToken.approve(address(registrar), base + premium);
// Step 4: Register
uint256 tokenId = registrar.register(
"myname",
msg.sender,
secret,
subregistry,
resolver,
365 days,
paymentToken,
bytes32(0)
);
renew
function renew(
string calldata label,
uint64 duration,
IERC20 paymentToken,
bytes32 referrer
) external
Extend the registration period for an existing name. No commitment is required for renewals.
The name to renew (without .eth suffix)
The duration to extend the registration by, in seconds. Must be greater than 0.
The ERC-20 token to use for payment. Must be supported by the price oracle.
A referrer identifier (can be zero bytes if no referrer)
Events Emitted:
NameRenewed(uint256 indexed tokenId, string label, uint64 duration, uint64 newExpiry, IERC20 paymentToken, bytes32 referrer, uint256 base)
Reverts:
NameIsAvailable(string label) - If the name is available (not registered)
PaymentTokenNotSupported(IERC20 paymentToken) - If the payment token isn’t supported
NotValid(string label) - If the label is not valid per the price oracle rules
Example:
// Renew "myname.eth" for another year
IERC20 paymentToken = IERC20(usdcAddress);
// Get the renewal price (no premium for current owner)
(uint256 base, ) = registrar.rentPrice(
"myname",
currentOwner,
365 days,
paymentToken
);
// Approve payment
paymentToken.approve(address(registrar), base);
// Renew
registrar.renew(
"myname",
365 days,
paymentToken,
bytes32(0)
);
isAvailable
function isAvailable(string memory label) public view returns (bool)
Check if a name is available for registration.
The name to check (without .eth suffix)
true if the name is available for registration, false if it’s registered or locked
Note: This function does not validate if the label is normalized or valid according to naming rules. It only checks registry availability.
Example:
if (registrar.isAvailable("myname")) {
// Name is available for registration
}
isValid
function isValid(string calldata label) external view returns (bool)
Check if a label is valid according to the price oracle rules (e.g., minimum length, valid characters).
true if the label is valid for registration
Example:
if (registrar.isValid("myname")) {
// Label meets validation requirements
}
isPaymentToken
function isPaymentToken(IERC20 paymentToken) external view returns (bool)
Check if a token is supported for payment.
The ERC-20 token address to check
true if the token is accepted for payments
Example:
if (registrar.isPaymentToken(IERC20(usdcAddress))) {
// USDC is accepted
}
rentPrice
function rentPrice(
string memory label,
address owner,
uint64 duration,
IERC20 paymentToken
) public view returns (uint256 base, uint256 premium)
Calculate the registration or renewal price for a name.
The prospective owner address. Set to current owner to exclude premium, or zero address for estimation.
The registration duration in seconds
The ERC-20 token to use for payment
The base registration price in payment token units
The premium price (if applicable) in payment token units. Premium applies to recently expired names.
Reverts:
PaymentTokenNotSupported(IERC20 paymentToken) - If the payment token isn’t supported
NotValid(string label) - If the label is not valid
Example:
// Get price for registering "myname" for 1 year
(uint256 base, uint256 premium) = registrar.rentPrice(
"myname",
msg.sender,
365 days,
IERC20(usdcAddress)
);
uint256 totalCost = base + premium;
makeCommitment
function makeCommitment(
string calldata label,
address owner,
bytes32 secret,
IRegistry subregistry,
address resolver,
uint64 duration,
bytes32 referrer
) public pure returns (bytes32)
Generate a commitment hash from registration parameters. This is a pure function that can be called off-chain.
A secret value to prevent commitment guessing
The initial subregistry contract
The initial resolver address
Registration duration in seconds
The keccak256 hash of all parameters encoded together
Example:
bytes32 secret = keccak256(abi.encodePacked("my-secret", msg.sender));
bytes32 commitment = registrar.makeCommitment(
"myname",
msg.sender,
secret,
subregistry,
resolver,
365 days,
bytes32(0)
);
Administrative Functions
setRentPriceOracle
function setRentPriceOracle(IRentPriceOracle oracle) external
Update the rent price oracle contract. Requires ROLE_SET_ORACLE permission on the root resource.
The new price oracle contract address
Events Emitted:
RentPriceOracleChanged(IRentPriceOracle oracle)
Access Control: Only callable by accounts with ROLE_SET_ORACLE role.
Events
CommitmentMade
event CommitmentMade(bytes32 commitment)
Emitted when a commitment is recorded.
The commitment hash that was 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.
The registry token ID for the registered name
The registered name (without .eth)
The initial subregistry contract
The initial resolver address
Registration duration in seconds
The ERC-20 token used for payment
NameRenewed
event NameRenewed(
uint256 indexed tokenId,
string label,
uint64 duration,
uint64 newExpiry,
IERC20 paymentToken,
bytes32 referrer,
uint256 base
)
Emitted when a name registration is extended.
The registry token ID for the renewed name
The renewed name (without .eth)
The extension duration in seconds
The new expiration timestamp
The ERC-20 token used for payment
The base price paid (no premium for renewals)
RentPriceOracleChanged
event RentPriceOracleChanged(IRentPriceOracle oracle)
Emitted when the rent price oracle is updated.
The new price oracle contract address
Errors
NameIsAvailable
error NameIsAvailable(string label)
The name is available for registration (cannot be renewed).
Error Selector: 0xf7681f14
NameNotAvailable
error NameNotAvailable(string label)
The name is not available for registration (already registered).
Error Selector: 0x477707e8
DurationTooShort
error DurationTooShort(uint64 duration, uint64 minDuration)
The registration duration is less than the minimum required.
Error Selector: 0xa096b844
error MaxCommitmentAgeTooLow()
Constructor validation error: max commitment age must be greater than min commitment age.
Error Selector: 0x3e5aa838
UnexpiredCommitmentExists
error UnexpiredCommitmentExists(bytes32 commitment)
A valid (unexpired) commitment already exists for this hash.
Error Selector: 0x0a059d71
CommitmentTooNew
error CommitmentTooNew(bytes32 commitment, uint64 validFrom, uint64 blockTimestamp)
The commitment has not aged enough. Wait until validFrom timestamp.
Error Selector: 0x6be614e3
CommitmentTooOld
error CommitmentTooOld(bytes32 commitment, uint64 validTo, uint64 blockTimestamp)
The commitment has expired. Must create a new commitment.
Error Selector: 0x0cb9df3f
Registration Flow
Two-Step Process
- Commit: Call
makeCommitment() to generate a hash, then call commit() to record it on-chain
- Wait: Wait for at least
MIN_COMMITMENT_AGE seconds (but less than MAX_COMMITMENT_AGE)
- Register: Call
register() with the same parameters used in the commitment
Why Commit-Reveal?
The commit-reveal scheme prevents frontrunning attacks. Without it:
- Attacker sees your registration transaction in the mempool
- Attacker submits their own transaction with higher gas
- Attacker registers the name before you
With commit-reveal:
- Your commitment reveals nothing about the name or parameters
- Even if someone frontruns your commitment, they can’t guess your secret
- Your registration transaction can only succeed if you have the matching commitment
Integration Examples
Complete Registration Flow
contract MyDApp {
ETHRegistrar public registrar;
IERC20 public paymentToken;
function registerName(
string memory label,
IRegistry subregistry,
address resolver
) external {
// Generate secret
bytes32 secret = keccak256(abi.encodePacked(
"secret",
msg.sender,
block.timestamp,
label
));
// Create and submit commitment
bytes32 commitment = registrar.makeCommitment(
label,
msg.sender,
secret,
subregistry,
resolver,
365 days,
bytes32(0)
);
registrar.commit(commitment);
// Store secret and commitment for later
// User must wait MIN_COMMITMENT_AGE before proceeding
}
function completeRegistration(
string memory label,
bytes32 secret,
IRegistry subregistry,
address resolver
) external {
// Get price
(uint256 base, uint256 premium) = registrar.rentPrice(
label,
msg.sender,
365 days,
paymentToken
);
// Approve payment
paymentToken.approve(address(registrar), base + premium);
// Complete registration
registrar.register(
label,
msg.sender,
secret,
subregistry,
resolver,
365 days,
paymentToken,
bytes32(0)
);
}
}