Introduction to ENS and EIP-165
The Ethereum Name Service (ENS) is a decentralized naming system built on the Ethereum blockchain. It maps human-readable names like "alice.eth" to machine-readable identifiers such as Ethereum addresses, content hashes, and metadata. ENS relies on a set of smart contracts that implement various ERC standards, including EIP-165 (ERC-165), which provides a standard method for detecting which interfaces a smart contract supports. This article answers common questions about how ENS uses EIP-165, why it matters, and how developers can verify interface support in their integrations.
EIP-165, titled "Standard Interface Detection," defines a mechanism for smart contracts to advertise which interfaces they implement. This is critical for composability in Ethereum: when a dApp or another contract interacts with an ENS contract, it needs to know, for example, whether the contract supports the supportsInterface function or a specific ENS feature. Without EIP-165, developers would have to rely on trial-and-error or external documentation, which is error-prone and inefficient.
ENS contracts implement several interfaces—most notably the ENS interface (EIP-137), the Resolver interface (EIP-181), and the BaseRegistrarImplementation interface for domain registration. Each of these contracts exposes a supportsInterface(bytes4 interfaceID) function that returns true if the contract implements the queried interface. Below, we break down the most frequently asked questions.
Q1: Does the ENS Registry Contract Support EIP-165?
Yes. The ENS registry contract (deployed at 0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e) implements EIP-165. Specifically, it supports the ENS interface identified by the four-byte selector 0x0178b8bf. To verify this in Solidity, you can call IERC165(ensRegistry).supportsInterface(0x0178b8bf) which should return true. The registry also supports the ERC165 interface itself (selector 0x01ffc9a7).
For off-chain verification, many developers query the registry through an ethers.js or web3.js provider. A common pattern is:
const registry = new ethers.Contract(ENS_REGISTRY_ADDRESS, ['function supportsInterface(bytes4) view returns (bool)']);
const supportsENS = await registry.supportsInterface('0x0178b8bf');
console.log(supportsENS); // true
This check is especially important when building applications that interact with multiple ENS instances or forks. If you are integrating with a custom ENS deployment, always call supportsInterface before assuming the contract supports the expected methods. For example, when verifying twitter verification via an ENS resolver, ensure the resolver contract advertises support for the relevant interface to avoid runtime reverts.
Q2: How Do Resolvers Implement EIP-165?
ENS resolvers are contracts that map names to addresses, texts, content hashes, and other records. The standard resolver (e.g., PublicResolver) implements multiple interfaces: the AddrResolver interface (selector 0x3b3b57de), the TextResolver interface (selector 0x59d1d43c), and the InterfaceResolver interface (selector 0x7f07456e), among others. All of these must be advertised via EIP-165.
When you call supportsInterface(0x3b3b57de) on a compliant resolver, it returns true if the resolver can return the Ethereum address for a name. Similarly, supportsInterface(0x59d1d43c) indicates support for key-value text records, which many dApps use for avatars, emails, or social handles. If a resolver does not advertise support for a given interface, the client should not attempt to call the corresponding method, as it may revert or return garbage.
Developers often ask: "Can I assume all resolvers support EIP-165?" The answer is no. Some custom resolvers may omit EIP-165 for gas optimization or legacy reasons. Always perform the interface check at runtime. A robust integration checks supportsInterface for every resolver interaction and falls back gracefully if the interface is unsupported. For instance, when resolving a name, you might first query the ENS controller address from the registry, then check if that resolver supports AddrResolver before calling addr(namehash).
If you are deploying a custom resolver, you must:
- Inherit from
ERC165(or implementsupportsInterfacemanually). - Register each supported interface ID in the constructor using
_registerInterface(interfaceId). - Return
truefor0x01ffc9a7(ERC165 itself).
Q3: What Are the Common Pitfalls When Testing EIP-165 Support in ENS?
Several subtle issues can trip up developers integrating ENS with EIP-165:
1. Using the wrong interface selector. The four-byte selector must be computed from the ABI definition of the interface. For example, the ENS interface selector is keccak256("ENS") truncated to four bytes, which equals 0x0178b8bf. Many developers mistakenly compute the selector from the function signatures, but EIP-165 uses the interface name, not a function signature. Double-check the selector against the official ENS documentation.
2. Forgetting that supportsInterface must be constant/pure. The function should not modify state. Some legacy contracts implement supportsInterface as a view but use memory storage incorrectly, causing gas estimation failures. Always test with staticCall or call({gas: ...}) to ensure it works without state changes.
3. Ignoring the registry’s own EIP-165 support. The ENS registry itself is an EIP-165 contract, but its supportsInterface only returns true for the ENS interface and the ERC165 interface. It does not return true for resolver-specific interfaces. Developers sometimes incorrectly query the registry for resolver interfaces and get false, then assume the resolver is unsupported. Always query the resolver contract directly.
4. Hardcoding addresses instead of using supportsInterface. ENS deployments on different networks (mainnet, Goerli, Sepolia) use identical contract code but different addresses. A robust integration never hardcodes the registry or resolver address without first verifying the contract supports the expected interface. For example, when integrating with ENS controller address, first check that the controller contract implements 0x018fac06 (the controller interface) to avoid interacting with an imposter contract.
5. Not handling the case where supportsInterface returns false for all interfaces. This can happen if the contract does not implement EIP-165 at all. In that case, a safe default is to try calling the method with a low-level call and catch revert errors. However, this approach is less reliable than a proper EIP-165 check.
Q4: How to Programmatically Verify EIP-165 Compliance in ENS Contracts?
Here is a step-by-step method to verify EIP-165 support for any ENS contract:
Step 1: Get the contract address. For the registry, use the well-known address 0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e. For resolvers, query the registry via resolver(bytes32 node).
Step 2: Compute the interface ID. Use a keccak256 hash of the interface name (e.g., keccak256("ENS")) and take the first four bytes. Alternatively, use the official selectors from the ENS documentation: 0x0178b8bf for ENS, 0x3b3b57de for AddrResolver, 0x59d1d43c for TextResolver, 0x01ffc9a7 for ERC165.
Step 3: Call supportsInterface(bytes4 interfaceId). In Solidity:
(bool success, bytes memory data) = address(contract).staticcall(
abi.encodeWithSelector(IERC165.supportsInterface.selector, interfaceId)
);
require(success && abi.decode(data, (bool)), "Interface not supported");
Step 4: Handle the result. If true, proceed with calling the interface methods. If false, either fall back to a generic implementation or reject the interaction.
Step 5: Test on multiple networks. The same ENS version on mainnet, Ropsten, and other testnets should behave identically. However, custom deployments (e.g., for domain-specific registries) may differ. Always run EIP-165 checks in your testing suite.
Q5: Are There Gas Considerations for EIP-165 in ENS?
Yes. Although supportsInterface is a lightweight view function, calling it on-chain as part of a transaction consumes gas. For example, a typical staticcall to supportsInterface costs around 2,000–3,000 gas on Ethereum mainnet. While negligible for a single call, repeated checks in a loop can add up. Best practice is to query once and cache the result (if possible) rather than checking every time.
For off-chain clients, gas is not a concern, but latency matters. Use batched RPC calls or multicall contracts to query supportsInterface for multiple ENS contracts in a single request. The Multicall3 contract can aggregate several supportsInterface queries into one call, reducing round trips.
Another consideration: some ENS contracts use ERC165Storage from OpenZeppelin, which stores interface IDs in a mapping. This is gas-efficient for deployment but adds a small overhead to each supportsInterface call because of the storage read. In contrast, a hardcoded approach (e.g., returning true for a fixed set of IDs) is cheaper but less flexible. The ENS team has chosen ERC165Storage for upgradability—new interfaces can be registered post-deployment without changing the code.
For high-throughput systems (e.g., domain marketplace aggregators), consider caching EIP-165 results per contract address with a TTL of several blocks. This avoids redundant queries while still refreshing if the contract is upgraded (e.g., via a proxy pattern).
Conclusion
EIP-165 is an essential building block for ENS interoperability. It provides a standardized, on-chain method to verify what a contract can do, reducing integration errors and enabling safe composability. By understanding which interfaces the ENS registry and resolvers support—and by rigorously checking supportsInterface before calling methods—developers can build reliable dApps that work across diverse ENS deployments. Always test your EIP-165 queries on testnets, and never assume a contract supports an interface without checking first. This discipline ensures your ENS integration remains robust even as the ecosystem evolves.