Registrar Controller Update

Name Wrapper arrived! Follow the technical changes on the main contract of the ecosystem!

Guihcneves
Blockful

--

ENS cycle update is here bringing us a more dynamic approach when managing our domain names, with a lot of editability of our name's descriptions, a modular node approach when handling ERC1155, and inheritance features allowing fuses to dictate ownership controls.

This very recent update has been at boiling point for a while and now it's time to describe what is happening from Blockful's perspective.

The following chapters will record the changes on the smart contract known as ETHRegistrarController.sol.

ETHRegistrarController Update:

From v0.5.16 to v0.8.17

Pragma Solidity Changed

The previous version of the contract belongs to “pragma solidity >= 0.5.0”, which stands for any compiler version that equals or above 0.5.0. In the case of “pragma solidity ~0.8.17”, it means that the contract is compatible with any version of the Solidity compiler that is greater than or equal to 0.8.17 and less than 0.9.0. This is because the tilde symbol specifies a range that includes all patch versions within the same minor version, but excludes the next major version.

Imports Changed

The new syntax, introduced in Solidity 0.8.0, is known as “import aliases”. It provides a way to selectively import specific contracts, libraries, or other symbols from a module, rather than importing the entire module as a single unit. This can help to reduce gas costs and simplify the code by allowing only the necessary symbols to be imported.

Following are the new imports and main changes at the ETHRegistrarController with a commented description of each one:

// Will be used at the constructor, when calling ReverseClaimer.
import {ENS} from "../registry/ENS.sol"

// ReverseClaimer contract is being inherited by the ETHRegistrarController
// contract by specifying it as a base contract in the constructor.
// This means that the ETHRegistrarController contract will inherit all of
// the state variables and functions defined in ReverseClaimer, and can use
// them in its own code.
// This means that the Reverse ENS record of the Registrar will be transferred
// to the new owner, a.k.a contract deployer.
import {ReverseClaimer} from "../reverseRegistrar/ReverseClaimer.sol"

// This library was added to execute low-level calls in the "_setRecords".
import {Address} from "@openzeppelin/contracts/utils/Address.sol";

// This contract was introduced in the implementations to create a "reverse
// name" at the same time the ENS name is registered.
import {ReverseRegistrar} from "../reverseRegistrar/ReverseRegistrar.sol";

// The baseRegistrar was handling all of the registrations business inside
// the "registerWithConfig" function. Nevertheless the nameWrapper will be
// doing this job from now on, replacing its mentions in the main "register"
// function for a single "nameWrapper" function call, literally a "wrap" of
// the token content.
// The NameWrapper will also be the one storing the public names from now on.
// It will handle expirations, renews and other stuff relating any process
// that involves ENS names. We'll be reviewing all this in the function scope
// of this doc.
import {INameWrapper} from "../wrapper/INameWrapper.sol";

// Contract is used to recover ERC20 tokens sent to the contract by mistake.
// The contract is Ownable and only the owner of the Registrar can call the
// recovery function.
import {ERC20Recoverable} from "../utils/ERC20Recoverable.sol"

Introducing Error Handlings

Error handlers were introduced in the new version, changing all “require” statements to their corresponding error using revert. The use of the "if" statement alongside error handlings with "revert" utilize less gas than a "require" - which will always be a string memory, to pre-compiled typed errors which will assist in both protocol development and for devs implementing the smart contract.

error CommitmentTooNew(bytes32 commitment);
error CommitmentTooOld(bytes32 commitment);
error NameNotAvailable(string name);
error DurationTooShort(uint256 duration);
error ResolverRequiredWhenDataSupplied();
error UnexpiredCommitmentExists(bytes32 commitment);
error InsufficientValue();
error Unauthorised(bytes32 node);
error MaxCommitmentAgeTooLow();
error MaxCommitmentAgeTooHigh();

Contract “is” More Stuff:

This will follow the import statements we overlooked in the beginning and tell the principal contract that those importations are part of it.

contract ETHRegistrarController is
Ownable,
IETHRegistrarController,
IERC165,
ERC20Recoverable, // new
ReverseClaimer // new
{

State Variables Changed

Now it is setting interfaces in a better way by utilizing ERC165 for supportsInterface(…) and actually implementing an interface-type contract. The contract used to have all the interfaces as hashed bytes while XORing to encode the functions and find their interface ID:

bytes4 private constant COMMITMENT_CONTROLLER_ID =
bytes4(
keccak256("rentPrice(string,uint256)") ^
keccak256("available(string)") ^
keccak256("makeCommitment(string,address,bytes32)") ^
keccak256("commit(bytes32)") ^
keccak256("register(string,address,uint256,bytes32)") ^
keccak256("renew(string,uint256)")
);

// It will be a bigger brainner to just leave like this:
type(IETHRegistrarController).interfaceId;

A quite odd found here in the contract is the following line:

uint64 private constant MAX_EXPIRY = type(uint64).max:

A private variable can only be accessed from within the same contract and funny as it is, this line doesn’t affect anything in the contract, and it's not even worth a getter function apparently. 😆

Variables initialized in the constructor for later use in eligible functions:

BaseRegistrarImplementation immutable base;
ReverseRegistrar public immutable reverseRegistrar;
INameWrapper public immutable nameWrapper;

And nevertheless, the hardcoded bytes at contract deployment. Used in a lot of validations and auths in the ENS protocol:

bytes32 private constant ETH_NODE:

New Functions:

function _setRecords(…)

Internal function _setRecords(…) will set the records for a given ENS name at the time of registration. Records might be realized as data in the form of contexts and content which the token owner can save on-chain — or off-chain in case an off-chain resolver is being used.

Input parameters:

function _setRecords(
address resolverAddress,
bytes32 label,
bytes[] calldata data
) internal

_setRecords(…) calls the resolver to run a for loop in case data had a length bigger than 0, and checks after the first 4 bytes of data if the bytes32 result matches the nodehash, which was calculated in the ETHRegistrarController. Let's untangle this.

It will use a low-level function to call each data imputed to set the records. The function in the ETHRegistrarController at version 0.8.17 is:

// Get the nodehash and send along with the data to a resolver address 
// calling multicallWithNodeCheck.
bytes32 nodehash = keccak256(abi.encodePacked(ETH_NODE, label));
Resolver resolver = Resolver(resolverAddress);
resolver.multicallWithNodeCheck(nodehash, data);

Inside the "multicallWithNodeCheck", the nodehash is verified to be different than empty bytes — it will never be empty since the ETH_NODE and the bytes32 label of the name will never result in the 0 address. Perhaps it is being used for a certain function when called from another contract in the ecosystem.

The first 4 bytes are matched to the selector while the following bytes should match the nodehash, which was passed as an input parameter alongside the data array.

The multicall will then delegate the call passing the data to be executed. If it fails, the transaction will be reverted:

(bool success, bytes memory result) = address(this).delegatecall(
data[i]
);
require(success);

function setReverseRecord(…)

This will simply call the reverseRegistrar contract and try setNameForAddr(…). As input parameters, it will take “msg.sender”, “owner”, “resolver” and “name”+”.eth”.

After trying to claim ownership of the name, it will call setName(…) in the NameResolver contract in case the owner matches the sender.

Unchanged Functions

Old but gold.

function rentPrice()
function valid()
function available()
function withdraw()

Change of Mechanics

function supportsInterface(…)

// From manual XORing:
interfaceID == INTERFACE_META_ID
interfaceID == COMMITMENT_CONTROLLER_ID
interfaceID == COMMITMENT_WITH_CONFIG_CONTROLLER_ID

// To light and legible syntax:
interfaceID == type(IERC165).interfaceId
interfaceID == type(IETHRegistrarController).interfaceId

function makeCommitment(…)

This function was an extension from makeCommitmentWithConfig(…) in case no “resolver” or “addr” was provided.

The function would have the same effect as makeCommitmentWithConfig(…) in the case called using address 0 as the input for the resolver and addr parameters.

The idea is to validate some of the inputs for the commit to happen, then generate a 32 bytes commit from all provided data to avoid front-running. But if the data was provided without a resolver, the function will revert as the resolver is needed when using _setRecords(…).

The arguments needed for commit changed to allow more setups to be made when registering the name for the first time. The removal of the commitment with config took place to have all fields in the same function:

+--------------+----------------------+
| v0.5.16 args | v0.8.17 args |
+--------------+----------------------+
| name | name |
| owner | owner |
| secret | duration |
| resolver | secret |
| addr | resolver |
| ---- | data |
| ---- | reverseRecor |
| ---- | ownerControlledFuses |
+--------------+----------------------+

A few things have changed:

  • We don’t use “addr” anymore in the register parameters but rather set through records (data arg)
  • “duration” was introduced in the commitment phase instead of directly being inputted as a register argument.
  • “resolver” and “ownerControlledFuses” are now being used with the nameWrapper contract to handle the validations, mint the ENS name, and also set the parental control.
  • “data” was introduced to allow setting different records for names at the time of registration.
  • “reverseRecord” is now available to be set at the commit phase as well.
  • Fuses are been upgraded to the following
CANNOT_UNWRAP = 1;
CANNOT_BURN_FUSES = 2;
CANNOT_TRANSFER = 4;
CANNOT_SET_RESOLVER = 8;
CANNOT_SET_TTL = 16;
CANNOT_CREATE_SUBDOMAIN = 32;
CANNOT_APPROVE = 64;
PARENT_CANNOT_CONTROL = 1 << 16;
IS_DOT_ETH = 1 << 17;
CAN_EXTEND_EXPIRY = 1 << 18;
CAN_DO_EVERYTHING = 0;
PARENT_CONTROLLED_FUSES = 0xFFFF0000;
USER_SETTABLE_FUSES = 0xFFFDFFFF;

function commit(…)

The require statement was changed for the if statement using revert to handle custom errors.

function _consumeCommitment(…)

Was previously calculating and returning the cost as uint256, requiring that “msg.value >= price”. This validation is now being called at register(…) directly, using “IPriceOracle”.

Now it doesn’t have returns, it rather validates transaction data and deletes the commitment afterward.

The require statement was changed for the if statement using revert to handle custom errors.

function register(…)

This function changed drastically, with a lot of new inputs and contract calls to handle the registration. It was previously calling registerWithConfig(…) passing resolver and address as the 0 address.

Here is the complete change in the input structure:

+--------------------+-----------------------------+
| v0.5.16 args | v0.8.17 args |
+--------------------+-----------------------------+
| string memory name | string calldata name |
| address owner | address owner |
| uint256 duration | uint256 duration |
| bytes32 secret | bytes32 secret |
| ---- | address resolver |
| ---- | bytes[] calldata data |
| ---- | bool reverseRecord |
| ---- | uint16 ownerControlledFuses |
+--------------------+-----------------------------+

The costs are calculated firstly using the price oracle and will revert in case the amount is incorrect.

Variable “cost” is now price.base and price.premium. Which is a struct type from the IPriceOracle interface.

The “expiry” is calculated automatically at the NameWrapper, which will also handle the NFT creation. Although expiry is settled as an uint256 variable in the register(…) function, it will only be used to be emitted in the registration event.

uint256 expires = nameWrapper.registerAndWrapETH2LD(
name,
owner,
duration,
resolver,
ownerControlledFuses
);

Function _setRecords(…) will check for a length above 0 in the "data" argument before being called. The resolver must implement the multicall and also be allowed to manage records.

Function _setReverseRecord(…) will be triggered if the boolean argument reverseRecord is set to true.

The event emission has changed the "cost" argument to emit both price.base and price.premium.

The refund is done based on the sum of price.base and price.premium.

function renew(…)

It didn’t change much at all, inputs remain the same, but the require is now following the error handling using ifs and revert.

The variable "cost" of type uint is now handled by the type IPriceOracle using only price.base, not mentioning price.premium.

Expiration is now handled by the nameWrapper instead of the baseRegistrar.

Removed Functions

This won't be part of the new contract. And the reason for that is because simply it doesn't make any sense to call a function just to fill inputs with the zero address.

function makeCommitmentWithConfig(…)

It could be called passing the address for the resolver or the address for the primary name. It could also be called from makeCommitment(…) which would fill resolver and addr parameters with the zero address.

function registerWithConfig(…)

Same as making a commitment with a configuration, it takes the resolver and the addr inputs to perform a complete name configuration. In the new version, the register(…) function is the one doing this.

We can imagine that both functions with the suffix "WithConfig" turned out to become the names without the suffixes, which takes all input arguments. While the purposes of the old functions makeCommitment(…) and register(…) to fill zero addresses vanished from existence in the new contracts.

Now the makeCommitment(…) and register(…) are the big deal in the new contracts while the suffix “WithConfig” will remain in the game for those that still want to use the cheapest gas usage option available when registering an ENS name.

Blockful is at the forefront of blockchain technology, building impactful solutions that innovate the way we relate to markets. We are a company specializing in Smart Contracts and DeFi Infrastructure, Consulting, and Auditing for Blockchain projects. We have a multidisciplinary team specialized in the development of Blockchain solutions. From idea to final product.

Follow us and join the Blockful!

Blockful | 🐦 | 🐱 | 👤

References

ens-contracts on GitHub. Last access on July, 16.

ETHRegistrarCotroller — v0.5.16

ETHRegistrarController — v0.8.17

--

--