Skip to main content
The L2ReverseRegistrar is a specialized reverse registrar designed for Layer 2 networks. It combines resolver and registrar functionality into a standalone contract that doesn’t require the ENS registry, making it efficient for L2 deployment.

Overview

The L2ReverseRegistrar provides reverse resolution capabilities on L2 networks with key features:
  • Standalone operation - Works without ENS registry dependency
  • Signature-based setting - Enables gasless name updates via meta-transactions
  • Multi-chain support - Unified validator addresses across L2s
  • Contract ownership - Supports reverse records for Ownable contracts

Contract Details

Inherits: IL2ReverseRegistrar, ERC165, StandaloneReverseRegistrar Key Storage:
  • coinType - Immutable coin type for the chain (derived from chain ID)
Errors:
error NotOwnerOfContract();
error CoinTypeNotFound();
error Unauthorised();

Constructor

constructor(uint256 coinType_)
coinType_
uint256
The coin type for the chain this contract is deployed to, converted from the chain ID

Functions

setName

Sets the caller’s reverse record name.
function setName(string calldata name) external
name
string
The ENS name to set for the caller’s address
Authorization: Caller must be authorized for msg.sender (automatically true) Example:
l2ReverseRegistrar.setName("alice.eth");

setNameForAddr

Sets the reverse record name for a specific address (requires authorization).
function setNameForAddr(
    address addr,
    string calldata name
) external
addr
address
The address to set the reverse record for
name
string
The ENS name to set for the address
Authorization: Caller must be:
  • The address itself (addr == msg.sender), or
  • Owner of the contract at addr (via Ownable pattern)
Example:
// As contract owner
l2ReverseRegistrar.setNameForAddr(contractAddress, "my-contract.eth");

setNameForAddrWithSignature

Sets the reverse record name using a signature, enabling gasless transactions.
function setNameForAddrWithSignature(
    address addr,
    uint256 signatureExpiry,
    string calldata name,
    uint256[] calldata coinTypes,
    bytes calldata signature
) external
addr
address
The address to set the reverse record for
signatureExpiry
uint256
Unix timestamp when the signature expires (maximum 1 hour in the future)
name
string
The ENS name to set for the address
coinTypes
uint256[]
Array of coin types where this name should be set (must include this chain’s coin type)
signature
bytes
EIP-191 signature from the address owner
Signature Format: The signature is created over the following message (per EIP-191):
keccak256(abi.encodePacked(
    validatorAddress,        // Contract address
    functionSignature,       // 0x2023a04c (selector)
    addr,                   // Address to set name for
    signatureExpiry,        // Signature expiry timestamp
    name,                   // ENS name
    coinTypes               // Array of coin types
))
Validator Addresses:
  • Mainnet: 0xa4a5CaA360A81461158C96f2Dbad8944411CF3fd
  • Testnet: 0xAe91c512BC1da8B00cd33dd9D9C734069e6E0fcd
Validation:
  • Signature must be valid for addr
  • signatureExpiry must be in the future but within 1 hour
  • coinTypes array must include this chain’s coinType
Example:
// Off-chain: User signs message
const message = ethers.utils.solidityKeccak256(
    ["address", "bytes4", "address", "uint256", "string", "uint256[]"],
    [
        validatorAddress,
        "0x2023a04c",
        userAddress,
        expiryTimestamp,
        "alice.eth",
        [coinType]
    ]
);
const signature = await signer.signMessage(ethers.utils.arrayify(message));

// On-chain: Anyone can submit
l2ReverseRegistrar.setNameForAddrWithSignature(
    userAddress,
    expiryTimestamp,
    "alice.eth",
    [coinType],
    signature
);

setNameForOwnableWithSignature

Sets the reverse record for an Ownable contract using the owner’s signature.
function setNameForOwnableWithSignature(
    address contractAddr,
    address owner,
    uint256 signatureExpiry,
    string calldata name,
    uint256[] calldata coinTypes,
    bytes calldata signature
) external
contractAddr
address
The contract address to set the reverse record for
owner
address
The owner of the contract (signature will be verified against this address)
signatureExpiry
uint256
Unix timestamp when the signature expires (maximum 1 hour in the future)
name
string
The ENS name to set for the contract
coinTypes
uint256[]
Array of coin types where this name should be set (must include this chain’s coin type)
signature
bytes
EIP-191 signature from the contract owner, supporting ERC-1271 and ERC-6492
Signature Format: The signature is created over the following message (per EIP-191):
keccak256(abi.encodePacked(
    validatorAddress,        // Contract address
    functionSignature,       // 0x975713ad (selector)
    contractAddr,           // Contract to set name for
    owner,                  // Owner of contract
    signatureExpiry,        // Signature expiry timestamp
    name,                   // ENS name
    coinTypes               // Array of coin types
))
Validator Addresses:
  • Mainnet: 0xa4a5CaA360A81461158C96f2Dbad8944411CF3fd
  • Testnet: 0xAe91c512BC1da8B00cd33dd9D9C734069e6E0fcd
Validation:
  • contractAddr must be a contract
  • owner must be the owner of contractAddr (via Ownable pattern)
  • Signature must be valid for owner (supports ERC-1271/ERC-6492)
  • signatureExpiry must be in the future but within 1 hour
  • coinTypes array must include this chain’s coinType
Reverts:
  • NotOwnerOfContract() - If owner is not the owner of contractAddr
  • CoinTypeNotFound() - If this chain’s coin type is not in coinTypes array
Example:
// Off-chain: Contract owner signs message
const message = ethers.utils.solidityKeccak256(
    ["address", "bytes4", "address", "address", "uint256", "string", "uint256[]"],
    [
        validatorAddress,
        "0x975713ad",
        contractAddress,
        ownerAddress,
        expiryTimestamp,
        "my-contract.eth",
        [coinType]
    ]
);
const signature = await ownerSigner.signMessage(ethers.utils.arrayify(message));

// On-chain: Anyone can submit
l2ReverseRegistrar.setNameForOwnableWithSignature(
    contractAddress,
    ownerAddress,
    expiryTimestamp,
    "my-contract.eth",
    [coinType],
    signature
);

Multi-Chain Deployment

The L2ReverseRegistrar is designed to be deployed at the same address across multiple L2 networks. This enables powerful multi-chain capabilities:

Unified Signatures

A single signature can authorize name changes across multiple L2s by including multiple coin types:
// One signature, multiple chains
const coinTypes = [
    60,    // Ethereum
    10,    // Optimism  
    42161, // Arbitrum
];

// Same signature can be used on all three chains
l2ReverseRegistrar.setNameForAddrWithSignature(
    addr,
    expiry,
    "alice.eth",
    coinTypes,
    signature
);

Deployment Process

Consistent addresses are achieved through:
  1. Safe multisigs - Control deployment across chains
    • Testnet: 0x343431e9CEb7C19cC8d3eA0EE231bfF82B584910
    • Mainnet: 0x353530FE74098903728Ddb66Ecdb70f52e568eC1
  2. CREATE3 - Deterministic deployment helper
  3. UniversalSigValidator - Dependency deployed alongside

Validator Address

The validator address (contract address) is embedded in signatures per EIP-191:
  • Mainnet: 0xa4a5CaA360A81461158C96f2Dbad8944411CF3fd
  • Testnet: 0xAe91c512BC1da8B00cd33dd9D9C734069e6E0fcd
This ensures signatures are only valid for the intended contract deployment.

Authorization Model

The contract uses a simple authorization model:

Direct Calls

modifier authorised(address addr) {
    if (addr != msg.sender && !_ownsContract(addr, msg.sender)) {
        revert Unauthorised();
    }
    _;
}
Authorized if:
  1. Caller is the address itself, or
  2. Caller owns the contract at the address (via Ownable)

Signature-Based Calls

For signature-based functions:
  • EOA addresses: Standard ECDSA signature validation
  • Contract addresses: ERC-1271 signature validation (with ERC-6492 support)

Coin Type Validation

The validCoinTypes modifier ensures the chain’s coin type is included:
modifier validCoinTypes(uint256[] calldata coinTypes) {
    _validateCoinTypes(coinTypes);
    _;
}

function _validateCoinTypes(uint256[] calldata coinTypes) internal view {
    for (uint256 i = 0; i < coinTypes.length; i++) {
        if (coinTypes[i] == coinType) return;
    }
    revert CoinTypeNotFound();
}
This prevents signatures intended for other chains from being replayed.

Integration Examples

Basic Usage

// User sets their own name
l2ReverseRegistrar.setName("alice.eth");

Gasless Meta-Transactions

// Backend service submits transaction for user
function submitSignedName(
    address user,
    string calldata name,
    uint256 expiry,
    bytes calldata sig
) external {
    l2ReverseRegistrar.setNameForAddrWithSignature(
        user,
        expiry,
        name,
        [coinType],
        sig
    );
}

Multi-Chain Name Setting

// Set name across multiple L2s with one signature
const chains = [
    { rpc: optimismRPC, coinType: 10 },
    { rpc: arbitrumRPC, coinType: 42161 },
];

for (const chain of chains) {
    const contract = new ethers.Contract(
        l2RegistrarAddress,
        abi,
        new ethers.providers.JsonRpcProvider(chain.rpc)
    );
    
    await contract.setNameForAddrWithSignature(
        addr,
        expiry,
        "alice.eth",
        [10, 42161], // Both coin types in signature
        signature
    );
}

Safe/Multisig Contract Names

// Set name for a Safe multisig
const safeOwner = await safe.getOwner(); // Get one of the owners

// Owner signs message
const signature = await ownerSigner.signMessage(message);

// Submit transaction
await l2ReverseRegistrar.setNameForOwnableWithSignature(
    safeAddress,
    safeOwner,
    expiry,
    "my-safe.eth",
    [coinType],
    signature
);

See Also

Build docs developers (and LLMs) love