feat: upgrade ENS resolution to the Universal Resolver#109
Conversation
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.
|
@yashgo0018 is attempting to deploy a commit to the impolitecompany Team on Vercel. A member of the Team first needs to authorize it. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
💤 Files with no reviewable changes (1)
📝 WalkthroughWalkthroughThe 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. ChangesENS Universal Resolver migration
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
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
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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_registryfrom chain definitions and update/add mainnet-backed integration tests (configurable viaETH_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.
| 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; |
| const result_bytes = try callUniversalResolve(allocator, provider, name, addr_calldata); | ||
| defer allocator.free(result_bytes); |
| const result_bytes = try callUniversalResolve(allocator, provider, name, text_calldata); | ||
| defer allocator.free(result_bytes); |
| const result_bytes = try callUniversalResolve(allocator, provider, name, ch_calldata); | ||
| defer allocator.free(result_bytes); |
| const response = provider.call(resolver_mod.UNIVERSAL_RESOLVER, calldata) catch | ||
| return ReverseResolveError.ProviderError; |
| .{ .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.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/ens/resolver.zig (1)
436-507: 🧹 Nitpick | 🔵 TrivialLive mainnet RPC calls in the unit test suite.
These tests hit a real network endpoint (
publicnode.comorETH_RPC_URL) during test runs, only skipping onResolveError.ProviderError. Consider gating these behind a separate integration-test step or an opt-in env flag so defaultzig build testruns 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 winLoosen-but-verify: calldata test only spot-checks the encoding.
The assertion
calldata.len > 4 + 64is 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
📒 Files selected for processing (9)
src/chains/arbitrum.zigsrc/chains/base.zigsrc/chains/chain.zigsrc/chains/ethereum.zigsrc/chains/optimism.zigsrc/chains/polygon.zigsrc/ens/namehash.zigsrc/ens/resolver.zigsrc/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
| 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; |
There was a problem hiding this comment.
🎯 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.
| 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.
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
ens_registrymetadata from chain definitions.ETH_RPC_URL, with a public RPC fallback.Notes
OffchainLookuphandling is not included in this change.Summary by CodeRabbit
New Features
Refactor