Skip to content

feat: upgrade ENS resolution to the Universal Resolver#109

Draft
yashgo0018 wants to merge 5 commits into
StrobeLabs:mainfrom
ens-integrations:ens-v2-upgrade
Draft

feat: upgrade ENS resolution to the Universal Resolver#109
yashgo0018 wants to merge 5 commits into
StrobeLabs:mainfrom
ens-integrations:ens-v2-upgrade

Conversation

@yashgo0018

@yashgo0018 yashgo0018 commented Jul 21, 2026

Copy link
Copy Markdown

Hi, My name is Yash, I am integrations engineer at ENS Labs. This PR is regarding updating ENS integration in this library to use Universal Resolver which is part of ENS V2 upgrade.

Summary

  • Migrate forward, text, and reverse ENS lookups to the Universal Resolver.
  • Add ENSIP-10 DNS encoding for resolver calls.
  • Add EIP-1577 contenthash resolution with IPFS, IPNS, and Swarm URI decoding.
  • Remove obsolete ens_registry metadata from chain definitions.
  • Allow mainnet integration tests to use ETH_RPC_URL, with a public RPC fallback.
  • Add unit and mainnet integration coverage for the new resolution paths.

Notes

  • EIP-3668 OffchainLookup handling is not included in this change.

Summary by CodeRabbit

  • New Features

    • Added ENS Universal Resolver–based forward resolution (addresses, text records) and EIP-1577 contenthash decoding (IPFS/IPNS/Swarm).
    • Added Universal Resolver–based reverse address lookup.
    • Added DNS-style ENS name encoding with label-length validation (rejects >63-byte labels).
    • Improved chain metadata coverage for Optimism Sepolia.
  • Refactor

    • Migrated ENS resolution from the legacy ENS registry/resolver flow to the Universal Resolver.
    • Updated chain metadata to no longer expose ENS registry configuration.

This commit removes the ens_registry field from the Chain struct and eliminates associated tests across various chain files, including Arbitrum, Base, Ethereum, Optimism, and Polygon. This change simplifies the chain definitions and ensures that all chains are treated uniformly regarding the absence of an ENS registry. All tests have been updated accordingly to reflect this removal.
This commit introduces a new `dnsEncode` function to encode ENS names for Universal Resolver calls, adhering to ENSIP-10 specifications. It also updates the `resolve` and `getText` functions to utilize the Universal Resolver, enhancing compatibility with the new encoding method. Additionally, several tests for the `dnsEncode` function are added to ensure correctness, including checks for label length and handling of empty labels. The previous `labelhash` function and its tests have been removed as they are no longer needed.
This commit introduces a new `getContentHash` function to look up the EIP-1577 contenthash for an ENS name via the Universal Resolver. It also adds a `decodeContentHash` function to decode contenthash bytes into URI strings for IPFS, IPNS, and Swarm encodings. Additionally, a new error type `UnsupportedContentHash` is defined, and tests for the contenthash selector are included to ensure correctness. The resolver now supports enhanced functionality for handling contenthashes.
This commit refactors the ENS resolver to use a new `mainnetTestRpcUrl` function, allowing for dynamic selection of the mainnet RPC URL based on the `ETH_RPC_URL` environment variable. If the variable is not set, it defaults to the publicnode URL. This change enhances flexibility for testing and integration with different Ethereum providers. All relevant tests have been updated to utilize this new function.
Copilot AI review requested due to automatic review settings July 21, 2026 16:17
@vercel

vercel Bot commented Jul 21, 2026

Copy link
Copy Markdown

@yashgo0018 is attempting to deploy a commit to the impolitecompany Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 80438cae-0860-4f37-8924-c1e5931e2995

📥 Commits

Reviewing files that changed from the base of the PR and between da7ef69 and f6cefb2.

📒 Files selected for processing (1)
  • src/chains/ethereum.zig
💤 Files with no reviewable changes (1)
  • src/chains/ethereum.zig

📝 Walkthrough

Walkthrough

The change removes ENS registry metadata from chain definitions and migrates forward and reverse ENS resolution to the Universal Resolver, adding DNS encoding, text and contenthash support, calldata helpers, response decoding, and integration coverage.

Changes

ENS Universal Resolver migration

Layer / File(s) Summary
Remove ENS registry chain metadata
src/chains/chain.zig, src/chains/ethereum.zig, src/chains/arbitrum.zig, src/chains/base.zig, src/chains/optimism.zig, src/chains/polygon.zig
The Chain type and Ethereum configuration no longer expose or initialize ens_registry; related assertions are removed and remaining chain properties are tested.
Add ENS DNS encoding
src/ens/namehash.zig
Adds DNS-style name encoding with empty-label handling, a terminator byte, label-length validation, and test vectors.
Route forward ENS queries through Universal Resolver
src/ens/resolver.zig
Updates address and text resolution, adds contenthash retrieval and decoding, centralizes Universal Resolver calls, and adds calldata and integration tests.
Route reverse ENS lookup through Universal Resolver
src/ens/reverse.zig
Replaces reverse-record resolution with reverse(bytes,uint256) calldata, response decoding, and Universal Resolver integration tests.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant resolve
  participant dnsEncode
  participant callUniversalResolve
  participant provider
  participant UniversalResolver
  resolve->>dnsEncode: encode ENS name
  resolve->>callUniversalResolve: request resolver payload
  callUniversalResolve->>provider: call Universal Resolver
  provider->>UniversalResolver: resolve(bytes,bytes)
  UniversalResolver-->>provider: bytes and address
  provider-->>callUniversalResolve: ABI response
  callUniversalResolve-->>resolve: decoded inner bytes
Loading
sequenceDiagram
  participant lookupAddress
  participant buildReverseCalldata
  participant provider
  participant UniversalResolver
  lookupAddress->>buildReverseCalldata: encode address and ETH coin type
  lookupAddress->>provider: call reverse calldata
  provider->>UniversalResolver: reverse(bytes,uint256)
  UniversalResolver-->>provider: primary name and resolver addresses
  provider-->>lookupAddress: ABI response
  lookupAddress-->>lookupAddress: return copied primary name
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: migrating ENS resolution to the Universal Resolver.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR migrates the library’s ENS integration to ENS V2’s Universal Resolver, adding ENSIP-10 DNS-encoded resolver calls and EIP-1577 contenthash decoding, while removing legacy ens_registry chain metadata.

Changes:

  • Switch forward (addr), text, and reverse lookups to Universal Resolver entrypoints (resolve(...) / reverse(...)) with DNS-encoded names.
  • Add contenthash resolution and decoding for common codecs (IPFS/IPNS CIDv0 via Base58, Swarm to bzz://).
  • Remove ens_registry from chain definitions and update/add mainnet-backed integration tests (configurable via ETH_RPC_URL).

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/ens/reverse.zig Migrates reverse lookup to Universal Resolver reverse(bytes,uint256) and adds a mainnet integration test.
src/ens/resolver.zig Migrates forward/text lookups to Universal Resolver resolve(bytes,bytes), adds contenthash support, DNS encoding usage, and mainnet tests.
src/ens/namehash.zig Adds ENSIP-10 DNS encoding helper + tests; removes unused labelhash.
src/chains/ethereum.zig Removes legacy .ens_registry from mainnet chain definition and associated tests.
src/chains/chain.zig Removes ens_registry from the Chain struct and deletes related tests.
src/chains/polygon.zig Removes ens_registry-related test (field removed from Chain).
src/chains/optimism.zig Removes ens_registry-related test (field removed from Chain).
src/chains/base.zig Removes ens_registry-related test (field removed from Chain).
src/chains/arbitrum.zig Removes ens_registry-related test (field removed from Chain).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/ens/resolver.zig
const resolve_calldata = try buildResolveCalldata(allocator, dns_name, data);
defer allocator.free(resolve_calldata);

const response = provider.call(UNIVERSAL_RESOLVER, resolve_calldata) catch return ResolveError.ProviderError;
Comment thread src/ens/resolver.zig
Comment on lines +69 to +70
const result_bytes = try callUniversalResolve(allocator, provider, name, addr_calldata);
defer allocator.free(result_bytes);
Comment thread src/ens/resolver.zig
Comment on lines +99 to +100
const result_bytes = try callUniversalResolve(allocator, provider, name, text_calldata);
defer allocator.free(result_bytes);
Comment thread src/ens/resolver.zig
Comment on lines +136 to +137
const result_bytes = try callUniversalResolve(allocator, provider, name, ch_calldata);
defer allocator.free(result_bytes);
Comment thread src/ens/reverse.zig
Comment on lines +45 to 46
const response = provider.call(resolver_mod.UNIVERSAL_RESOLVER, calldata) catch
return ReverseResolveError.ProviderError;
Comment thread src/chains/ethereum.zig
Comment on lines 32 to 36
.{ .name = "Etherscan", .url = "https://etherscan.io" },
},
.multicall3 = multicall3_contract,
.ens_registry = ens_registry_contract,
.testnet = false,
};
This commit removes the ens_registry_contract from the Ethereum chain structure, simplifying the chain definitions. This change aligns with the previous removal of the ens_registry field from other chain structures, ensuring uniformity across the codebase. No tests were affected by this change.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/ens/resolver.zig (1)

436-507: 🧹 Nitpick | 🔵 Trivial

Live mainnet RPC calls in the unit test suite.

These tests hit a real network endpoint (publicnode.com or ETH_RPC_URL) during test runs, only skipping on ResolveError.ProviderError. Consider gating these behind a separate integration-test step or an opt-in env flag so default zig build test runs stay hermetic and CI isn't subject to public-RPC flakiness/rate-limiting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ens/resolver.zig` around lines 436 - 507, Gate the mainnet RPC
tests—“resolve ur.integration-tests.eth…”, “getText ur.integration-tests.eth…”,
and “getContentHash ur.integration-tests.eth…”—behind an explicit opt-in
environment flag or separate integration-test path. Ensure default `zig build
test` skips these network-dependent tests without invoking the RPC, while
preserving their existing assertions when integration testing is enabled.
src/ens/reverse.zig (1)

84-95: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Loosen-but-verify: calldata test only spot-checks the encoding.

The assertion calldata.len > 4 + 64 is a weak lower bound rather than the exact expected length (selector + 2 head words + length word + 32-byte padded address = 4 + 64 + 64 = 132 bytes for a 20-byte address), and only the last byte of the coin-type word is checked. Consider asserting the exact length and the full address bytes at their expected offset to catch encoding-order regressions.

♻️ Suggested tightening
     try std.testing.expectEqualSlices(u8, &REVERSE_SELECTOR, calldata[0..4]);
-    // selector + 2 head words (offset, coinType) + bytes length/data
-    try std.testing.expect(calldata.len > 4 + 64);
-    // coin type 60 in the second head word
-    try std.testing.expectEqual(`@as`(u8, 60), calldata[4 + 63]);
+    // selector + 2 head words (offset, coinType) + length word + 32-byte padded address
+    try std.testing.expectEqual(`@as`(usize, 4 + 64 + 64), calldata.len);
+    // coin type 60 in the second head word
+    try std.testing.expectEqual(`@as`(u8, 60), calldata[4 + 63]);
+    // address bytes present in the tail, right-aligned per bytes-type padding
+    try std.testing.expectEqualSlices(u8, &addr, calldata[4 + 64 + 32 - 20 .. 4 + 64 + 32]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ens/reverse.zig` around lines 84 - 95, Strengthen the test
buildReverseCalldata encodes correctly by asserting the exact 132-byte calldata
length instead of a lower bound. Validate the complete coin-type word and the
full 20-byte address at the expected ABI-encoded offset, preserving the existing
selector assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/ens/resolver.zig`:
- Around line 193-222: Update callUniversalResolve to validate both decoded ABI
values, requiring decoded.len >= 2 before accessing them. Inspect decoded[1],
the resolver address returned by Universal Resolver, and return
ResolveError.NoResolver when it indicates no resolver is configured; otherwise
preserve the existing decoded[0] data-copy behavior and InvalidResponse
handling.

---

Nitpick comments:
In `@src/ens/resolver.zig`:
- Around line 436-507: Gate the mainnet RPC tests—“resolve
ur.integration-tests.eth…”, “getText ur.integration-tests.eth…”, and
“getContentHash ur.integration-tests.eth…”—behind an explicit opt-in environment
flag or separate integration-test path. Ensure default `zig build test` skips
these network-dependent tests without invoking the RPC, while preserving their
existing assertions when integration testing is enabled.

In `@src/ens/reverse.zig`:
- Around line 84-95: Strengthen the test buildReverseCalldata encodes correctly
by asserting the exact 132-byte calldata length instead of a lower bound.
Validate the complete coin-type word and the full 20-byte address at the
expected ABI-encoded offset, preserving the existing selector assertion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 980a5038-0b9d-4b59-9c07-368d98249146

📥 Commits

Reviewing files that changed from the base of the PR and between 9da2547 and da7ef69.

📒 Files selected for processing (9)
  • src/chains/arbitrum.zig
  • src/chains/base.zig
  • src/chains/chain.zig
  • src/chains/ethereum.zig
  • src/chains/optimism.zig
  • src/chains/polygon.zig
  • src/ens/namehash.zig
  • src/ens/resolver.zig
  • src/ens/reverse.zig
💤 Files with no reviewable changes (6)
  • src/chains/optimism.zig
  • src/chains/base.zig
  • src/chains/chain.zig
  • src/chains/polygon.zig
  • src/chains/arbitrum.zig
  • src/chains/ethereum.zig

Comment thread src/ens/resolver.zig
Comment on lines +193 to +222
fn callUniversalResolve(
allocator: std.mem.Allocator,
provider: anytype,
name: []const u8,
data: []const u8,
) ![]u8 {
const dns_name = namehash_mod.dnsEncode(allocator, name) catch |err| switch (err) {
error.LabelTooLong => return ResolveError.LabelTooLong,
error.OutOfMemory => return ResolveError.OutOfMemory,
};
defer allocator.free(dns_name);

const values = [_]AbiValue{.{ .fixed_bytes = fb }};
return abi_encode.encodeFunctionCall(allocator, RESOLVER_SELECTOR, &values);
const resolve_calldata = try buildResolveCalldata(allocator, dns_name, data);
defer allocator.free(resolve_calldata);

const response = provider.call(UNIVERSAL_RESOLVER, resolve_calldata) catch return ResolveError.ProviderError;
defer allocator.free(response);

// resolve(bytes,bytes) returns (bytes data, address resolver)
const result_types = [_]AbiType{ .bytes, .address };
const decoded = abi_decode.decodeValues(response, &result_types, allocator) catch
return ResolveError.InvalidResponse;
defer abi_decode.freeValues(decoded, allocator);

if (decoded.len < 1) return ResolveError.InvalidResponse;

const result_bytes = decoded[0].bytes;
const copy = try allocator.alloc(u8, result_bytes.len);
@memcpy(copy, result_bytes);
return copy;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

NoResolver is dead code — resolver address from Universal Resolver is discarded.

resolve(bytes,bytes) returns (bytes data, address resolver), but only decoded[0] is used; decoded[1] (the resolver address) is never inspected, and the length guard checks decoded.len < 1 despite requesting 2 ABI types. As a result, ResolveError.NoResolver (defined at Line 42) is unreachable — when a name has no resolver configured, callers get a generic InvalidResponse/null instead of the semantically correct NoResolver, even though the API contract explicitly promises that error variant.

🛠️ Proposed fix
     // resolve(bytes,bytes) returns (bytes data, address resolver)
     const result_types = [_]AbiType{ .bytes, .address };
     const decoded = abi_decode.decodeValues(response, &result_types, allocator) catch
         return ResolveError.InvalidResponse;
     defer abi_decode.freeValues(decoded, allocator);
 
-    if (decoded.len < 1) return ResolveError.InvalidResponse;
+    if (decoded.len < 2) return ResolveError.InvalidResponse;
+
+    const resolver_addr = decoded[1].address;
+    if (std.mem.eql(u8, &resolver_addr, &primitives.ZERO_ADDRESS)) return ResolveError.NoResolver;
 
     const result_bytes = decoded[0].bytes;
     const copy = try allocator.alloc(u8, result_bytes.len);
     `@memcpy`(copy, result_bytes);
     return copy;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn callUniversalResolve(
allocator: std.mem.Allocator,
provider: anytype,
name: []const u8,
data: []const u8,
) ![]u8 {
const dns_name = namehash_mod.dnsEncode(allocator, name) catch |err| switch (err) {
error.LabelTooLong => return ResolveError.LabelTooLong,
error.OutOfMemory => return ResolveError.OutOfMemory,
};
defer allocator.free(dns_name);
const values = [_]AbiValue{.{ .fixed_bytes = fb }};
return abi_encode.encodeFunctionCall(allocator, RESOLVER_SELECTOR, &values);
const resolve_calldata = try buildResolveCalldata(allocator, dns_name, data);
defer allocator.free(resolve_calldata);
const response = provider.call(UNIVERSAL_RESOLVER, resolve_calldata) catch return ResolveError.ProviderError;
defer allocator.free(response);
// resolve(bytes,bytes) returns (bytes data, address resolver)
const result_types = [_]AbiType{ .bytes, .address };
const decoded = abi_decode.decodeValues(response, &result_types, allocator) catch
return ResolveError.InvalidResponse;
defer abi_decode.freeValues(decoded, allocator);
if (decoded.len < 1) return ResolveError.InvalidResponse;
const result_bytes = decoded[0].bytes;
const copy = try allocator.alloc(u8, result_bytes.len);
@memcpy(copy, result_bytes);
return copy;
fn callUniversalResolve(
allocator: std.mem.Allocator,
provider: anytype,
name: []const u8,
data: []const u8,
) ![]u8 {
const dns_name = namehash_mod.dnsEncode(allocator, name) catch |err| switch (err) {
error.LabelTooLong => return ResolveError.LabelTooLong,
error.OutOfMemory => return ResolveError.OutOfMemory,
};
defer allocator.free(dns_name);
const resolve_calldata = try buildResolveCalldata(allocator, dns_name, data);
defer allocator.free(resolve_calldata);
const response = provider.call(UNIVERSAL_RESOLVER, resolve_calldata) catch return ResolveError.ProviderError;
defer allocator.free(response);
// resolve(bytes,bytes) returns (bytes data, address resolver)
const result_types = [_]AbiType{ .bytes, .address };
const decoded = abi_decode.decodeValues(response, &result_types, allocator) catch
return ResolveError.InvalidResponse;
defer abi_decode.freeValues(decoded, allocator);
if (decoded.len < 2) return ResolveError.InvalidResponse;
const resolver_addr = decoded[1].address;
if (std.mem.eql(u8, &resolver_addr, &primitives.ZERO_ADDRESS)) return ResolveError.NoResolver;
const result_bytes = decoded[0].bytes;
const copy = try allocator.alloc(u8, result_bytes.len);
`@memcpy`(copy, result_bytes);
return copy;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ens/resolver.zig` around lines 193 - 222, Update callUniversalResolve to
validate both decoded ABI values, requiring decoded.len >= 2 before accessing
them. Inspect decoded[1], the resolver address returned by Universal Resolver,
and return ResolveError.NoResolver when it indicates no resolver is configured;
otherwise preserve the existing decoded[0] data-copy behavior and
InvalidResponse handling.

@yashgo0018
yashgo0018 marked this pull request as draft July 21, 2026 16:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants