Reverse resolution allows you to look up the ENS name associated with an Ethereum address. This enables applications to display human-readable names instead of addresses.
Overview
Reverse resolution uses a special .addr.reverse domain where each Ethereum address has a corresponding subdomain:
- Address
0x1234...5678 maps to 1234...5678.addr.reverse
- The resolver for this reverse record stores the associated ENS name
How Reverse Resolution Works
Address to Reverse Node
Convert an Ethereum address to its reverse node:import { namehash } from 'viem'
function getReverseNode(address) {
// Remove 0x prefix and convert to lowercase
const addr = address.slice(2).toLowerCase()
// Create reverse domain: <address>.addr.reverse
const reverseName = `${addr}.addr.reverse`
return namehash(reverseName)
}
const reverseNode = getReverseNode('0x1234567890123456789012345678901234567890')
Query Reverse Resolver
Get the resolver for the reverse node and query the name:const ensRegistry = await viem.getContractAt(
'ENSRegistry',
ENS_REGISTRY_ADDRESS
)
const resolverAddress = await ensRegistry.read.resolver([reverseNode])
if (resolverAddress !== zeroAddress) {
const resolver = await viem.getContractAt(
'PublicResolver',
resolverAddress
)
const name = await resolver.read.name([reverseNode])
console.log('Reverse resolved name:', name)
}
Forward Resolution Check
Always verify that the name resolves back to the address:// Forward resolution to verify
const forwardNode = namehash(name)
const forwardResolver = await ensRegistry.read.resolver([forwardNode])
if (forwardResolver !== zeroAddress) {
const resolver = await viem.getContractAt(
'PublicResolver',
forwardResolver
)
const resolvedAddress = await resolver.read.addr([forwardNode])
if (resolvedAddress.toLowerCase() === originalAddress.toLowerCase()) {
console.log('Verified! Name:', name)
} else {
console.log('Warning: Reverse record does not match forward resolution')
}
}
Always perform forward resolution to verify the reverse record. Anyone can set their reverse record to any name, so you must confirm the name actually points back to the address.
Setting Up Reverse Resolution
Using ReverseRegistrar
The ReverseRegistrar contract manages reverse records:
const REVERSE_REGISTRAR_ADDRESS = '0x...' // Get from deployment
const reverseRegistrar = await viem.getContractAt(
'ReverseRegistrar',
REVERSE_REGISTRAR_ADDRESS
)
Claim Reverse Record
Simple Claim
Claim your reverse record with the default resolver:// Claims reverse record for msg.sender
const tx = await reverseRegistrar.write.claim([ownerAddress])
await tx.wait()
console.log('Reverse record claimed!')
This sets you as the owner of your reverse record using the default resolver.Source: ~/workspace/source/contracts/reverseRegistrar/ReverseRegistrar.sol:64-66 Claim with Custom Resolver
Specify a custom resolver when claiming:const tx = await reverseRegistrar.write.claimWithResolver([
ownerAddress,
customResolverAddress
])
await tx.wait()
Source: ~/workspace/source/contracts/reverseRegistrar/ReverseRegistrar.sol:93-98 Set the Name
Set the actual name for your reverse record:// Sets name for msg.sender's reverse record
const tx = await reverseRegistrar.write.setName(['myname.eth'])
await tx.wait()
console.log('Reverse record set to myname.eth')
This combines claiming the reverse record and setting the name in one transaction.Source: ~/workspace/source/contracts/reverseRegistrar/ReverseRegistrar.sol:105-113
Setting Reverse Records for Other Addresses
Controllers and authorized accounts can set reverse records for other addresses:
// Only works if you're a controller or authorized
const tx = await reverseRegistrar.write.setNameForAddr([
targetAddress, // Address to set reverse record for
ownerAddress, // Owner of the reverse record
resolverAddress, // Resolver to use
'example.eth' // Name to set
])
await tx.wait()
Source: ~/workspace/source/contracts/reverseRegistrar/ReverseRegistrar.sol:123-132
This function requires authorization. You must be:
- The address itself
- A controller on the ReverseRegistrar
- Approved for all by the address in the ENS registry
- The owner of the address if it’s a contract
Setting Reverse Records During Registration
You can set reverse records when registering a new .eth name:
import { namehash, encodeFunctionData } from 'viem'
const registration = {
label: 'myname',
owner: ownerAddress,
duration: 31536000,
secret: secretBytes,
resolver: publicResolverAddress,
data: [],
reverseRecord: 1, // 1 = Ethereum, 2 = default, 3 = both
referrer: zeroHash
}
const tx = await ethRegistrarController.write.register(
[registration],
{ value: price }
)
Reverse Record Options
0 - Don’t set reverse record
1 - Set Ethereum reverse record (addr.reverse)
2 - Set default reverse record (default.reverse)
3 - Set both Ethereum and default reverse records
Source: ~/workspace/source/contracts/ethregistrar/ETHRegistrarController.sol:319-330
Setting the reverse record during registration saves a separate transaction and ensures your name is immediately visible when your address is reverse resolved.
Calculating Reverse Node
The ReverseRegistrar provides a helper to calculate the reverse node:
const reverseNode = await reverseRegistrar.read.node([address])
console.log('Reverse node:', reverseNode)
Source: ~/workspace/source/contracts/reverseRegistrar/ReverseRegistrar.sol:137-142
Manual Calculation
You can also calculate it manually:
import { keccak256, encodePacked, namehash } from 'viem'
function calculateReverseNode(address) {
// Remove 0x prefix and convert to lowercase
const addr = address.slice(2).toLowerCase()
// addr.reverse node
const ADDR_REVERSE_NODE =
'0x91d1777781884d03a6757a803996e38de2a42967fb37eeaca72729271025a9e2'
// Calculate sha3 of hex address
const labelHash = keccak256(encodePacked(['string'], [addr]))
// Combine with addr.reverse node
return keccak256(
encodePacked(
['bytes32', 'bytes32'],
[ADDR_REVERSE_NODE, labelHash]
)
)
}
Source: ~/workspace/source/contracts/reverseRegistrar/ReverseRegistrar.sol:13-15
Complete Reverse Resolution Example
import { namehash, zeroAddress } from 'viem'
async function reverseResolve(address) {
try {
// 1. Get reverse node
const reverseRegistrar = await viem.getContractAt(
'ReverseRegistrar',
REVERSE_REGISTRAR_ADDRESS
)
const reverseNode = await reverseRegistrar.read.node([address])
// 2. Get resolver for reverse node
const ensRegistry = await viem.getContractAt(
'ENSRegistry',
ENS_REGISTRY_ADDRESS
)
const resolverAddress = await ensRegistry.read.resolver([reverseNode])
if (resolverAddress === zeroAddress) {
return { error: 'No reverse record set' }
}
// 3. Get name from resolver
const resolver = await viem.getContractAt(
'PublicResolver',
resolverAddress
)
const name = await resolver.read.name([reverseNode])
if (!name) {
return { error: 'No name set in reverse record' }
}
// 4. Verify forward resolution
const forwardNode = namehash(name)
const forwardResolverAddress = await ensRegistry.read.resolver([forwardNode])
if (forwardResolverAddress === zeroAddress) {
return {
name,
verified: false,
error: 'Name has no resolver'
}
}
const forwardResolver = await viem.getContractAt(
'PublicResolver',
forwardResolverAddress
)
const resolvedAddress = await forwardResolver.read.addr([forwardNode])
const verified = resolvedAddress.toLowerCase() === address.toLowerCase()
return {
name,
verified,
resolvedAddress
}
} catch (error) {
console.error('Reverse resolution failed:', error)
return { error: error.message }
}
}
// Usage
const result = await reverseResolve('0x1234567890123456789012345678901234567890')
if (result.verified) {
console.log('Verified name:', result.name)
} else {
console.log('Unverified or no name')
}
Authorization
The ReverseRegistrar checks authorization before allowing changes:
modifier authorised(address addr) {
require(
addr == msg.sender ||
controllers[msg.sender] ||
ens.isApprovedForAll(addr, msg.sender) ||
ownsContract(addr),
"ReverseRegistrar: Caller is not a controller or authorised by address or the address itself"
);
_;
}
You can set a reverse record for an address if:
- You are the address itself
- You are a controller on the ReverseRegistrar
- You are approved for all by the address in ENS
- You own the contract at that address
Source: ~/workspace/source/contracts/reverseRegistrar/ReverseRegistrar.sol:40-49
Events
ReverseClaimed
Emitted when a reverse record is claimed:
event ReverseClaimed(
address indexed addr,
bytes32 indexed node
)
Source: ~/workspace/source/contracts/reverseRegistrar/ReverseRegistrar.sol:23
DefaultResolverChanged
Emitted when the default resolver is updated:
event DefaultResolverChanged(
NameResolver indexed resolver
)
Source: ~/workspace/source/contracts/reverseRegistrar/ReverseRegistrar.sol:24
Default Resolver
The ReverseRegistrar has a default resolver that’s used for simple claims:
// Get current default resolver
const defaultResolver = await reverseRegistrar.read.defaultResolver()
// Set default resolver (owner only)
const tx = await reverseRegistrar.write.setDefaultResolver([
newResolverAddress
])
Source: ~/workspace/source/contracts/reverseRegistrar/ReverseRegistrar.sol:51-58
Best Practices
Always verify reverse records with forward resolution. Anyone can claim their reverse record points to any name, so you must verify the name actually resolves back to the address.
Cache reverse resolution results but with a reasonable TTL (e.g., 5-10 minutes). Users may update their reverse records at any time.
Display addresses with unverified reverse records differently in your UI to indicate they haven’t been validated.
UI Display Pattern
async function getDisplayName(address) {
const result = await reverseResolve(address)
if (result.verified) {
return {
display: result.name,
verified: true,
showCheckmark: true
}
} else if (result.name) {
return {
display: `${result.name} (unverified)`,
verified: false,
showWarning: true
}
} else {
return {
display: `${address.slice(0, 6)}...${address.slice(-4)}`,
verified: false
}
}
}
Contract Addresses
Mainnet
- ReverseRegistrar: Check latest deployment
- Default Resolver: PublicResolver (check deployment)
Sepolia
- ReverseRegistrar: Check latest deployment
- Default Resolver: PublicResolver (check deployment)
Next Steps