A set of ENS contracts that enable users to claim address-based subnames under your ENS name, with optional L1 → L2 CCIP-Read resolution.
This project provides three main contracts:
| Contract | Description |
|---|---|
| ConfigResolver | A general-purpose ENS resolver for setting records (text, address, contenthash, etc.) |
| AddressSubnameRegistrar | Enables users to claim 0x<address>.yourname.eth subnames |
| L1ConfigResolver | Reads L2 ConfigResolver records from L1 via CCIP-Read (Unruggable Gateways) |
| Contract | Network | Address |
|---|---|---|
| ConfigResolver | Base Sepolia | 0xA66c55a6b76967477af18A03F2f12d52251Dc2C0 |
| L1ConfigResolver | Sepolia | 0x380e926f5D78F21b80a6EfeF2B3CEf9CcC89356B |
| Contract | Network | Address |
|---|---|---|
| ConfigResolver | Base | TBD |
| L1ConfigResolver | Ethereum | TBD |
If you own ethconfig.eth, users can claim subnames like:
0x8d25687829d6b85d9e0020b8c89e3ca24de20a89.ethconfig.eth
The address is normalized to lowercase hex (42 characters, with 0x prefix).
forge buildforge testDeploy ConfigResolver + AddressSubnameRegistrar:
# Sepolia
PARENT_NODE=$(cast namehash "yourname.eth") \
forge script script/Deploy.s.sol \
--rpc-url https://eth-sepolia.g.alchemy.com/v2/$ALCHEMY_API_KEY \
--account deployer \
--broadcast \
--verify
# Mainnet
PARENT_NODE=$(cast namehash "yourname.eth") \
forge script script/Deploy.s.sol \
--rpc-url https://eth-mainnet.g.alchemy.com/v2/$ALCHEMY_API_KEY \
--account deployer \
--broadcast \
--verifyDeploy ConfigResolver only:
forge script script/Deploy.s.sol --sig "deployConfigResolver()" \
--rpc-url $RPC_URL \
--account deployer \
--broadcast \
--verifyDeploy L1ConfigResolver (for reading L2 records from L1):
L2_CONFIG_RESOLVER=0x... \
forge script script/Deploy.s.sol --sig "deployL1Resolver()" \
--rpc-url $RPC_URL \
--account deployer \
--broadcast \
--verifyDeploy L1 AddressSubnameRegistrar (for L1 claiming with L2 storage):
PARENT_NODE=$(cast namehash "yourname.eth") \
L1_CONFIG_RESOLVER=0x... \
forge script script/Deploy.s.sol --sig "deployL1Registrar()" \
--rpc-url $RPC_URL \
--account deployer \
--broadcast \
--verifySee DEPLOYMENT.md for the full deployment and setup guide.
A resolver contract that supports all standard ENS record types:
- Address records (
addr) - Text records (
text) - Content hash (
contenthash) - ABI (
ABI) - Public key (
pubkey) - DNS records (
DNS) - Interface (
interfaceImplementer) - Name (
name)
Authorization is based on:
- Owning the ENS node
- Being an approved operator (
setApprovalForAll) - Being an approved delegate (
approve) - Owning the reverse node (for your own address)
Allows users to claim subnames based on their Ethereum address:
// User claims their subname
registrar.claim();
// Or claim for another address (if approved)
registrar.claimForAddr(addr, owner);
// Check availability
registrar.available(addr);
// Get the label for an address
registrar.getLabel(addr); // "0x8d25687829d6b85d9e0020b8c89e3ca24de20a89"
// Get the node hash
registrar.node(addr);An L1 resolver that reads ENS records from a ConfigResolver deployed on L2 (Base) using CCIP-Read. Implements the IL1ConfigResolver interface.
// Supports standard ENS resolution methods
resolver.addr(node); // Get ETH address
resolver.text(node, "url"); // Get text record
resolver.contenthash(node); // Get contenthash
// Also supports ENSIP-10 extended resolution
resolver.resolve(name, data);
// IL1ConfigResolver interface
resolver.l2ChainId(); // Get the L2 chain ID
resolver.l2ConfigResolver(); // Get the L2 ConfigResolver addressDefault Verifiers (Base):
| Network | Verifier | L2 Chain ID |
|---|---|---|
| Sepolia | 0x7F68510F0fD952184ec0b976De429a29A2Ec0FE3 |
84532 (Base Sepolia) |
| Mainnet | 0x0bC6c539e5fc1fb92F31dE34426f433557A9A5A2 |
8453 (Base) |
Custom verifiers and L2 chain IDs can be specified via environment variables during deployment.
- Foundry
- Node.js (for dependencies)
npm installforge test -vvforge fmtforge snapshotUsers claim subnames on L1 (Ethereum) and can change their resolver. Records are stored on L2 (Base) for lower gas costs.
┌─────────────────────────────────────────────────────────────────────────┐
│ L1 (Ethereum) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────┐ ┌───────────────────────────────┐ │
│ │ AddressSubnameRegistrar │ │ L1ConfigResolver │ │
│ │ ───────────────────────── │ │ ─────────────────────────── │ │
│ │ • Users call claim() │────▶│ • Default resolver for │ │
│ │ • Creates ENS node on L1 │ │ claimed subnames │ │
│ │ │ │ • Reads via CCIP-Read │ │
│ └─────────────────────────────┘ └───────────────┬───────────────┘ │
│ │ │
│ User owns ENS node → can change resolver if desired │ │
└───────────────────────────────────────────────────────┼──────────────────┘
│ CCIP-Read
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ L2 (Base) │
├─────────────────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ ConfigResolver - stores records (text, address, contenthash) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
User Flow:
- Claim subname on L1 →
registrar.claim() - Set records on L2 →
resolver.setText(node, "url", "https://...") - L1 resolution reads from L2 via CCIP-Read
- Optionally change resolver on L1 (user owns the ENS node)
┌──────────────────┐ 1. Call ┌─────────────────────┐
│ Your dApp │ ───────────────► │ L1ConfigResolver │
│ (Frontend) │ │ (Ethereum L1) │
└──────────────────┘ └────────┬────────────┘
▲ │
│ 2. Reverts with
│ OffchainLookup
│ │
│ 5. Return ▼
│ verified ┌─────────────────────────────┐
│ data │ Gateway (off-chain) │
│ └─────────────┬───────────────┘
│ │
│ 3. Fetch proofs from L2
│ │
│ ▼
│ ┌─────────────────────────────┐
│ │ ConfigResolver (Base L2) │
│ └─────────────────────────────┘
│ │
│ 4. Return proofs
└─────────────────────────────────┘
MIT