Back to blog

IPCM and ENS: How to Use Hybrid ENS Resolvers to Bridge Onchain Data

IPCM and ENS: How to Use Hybrid ENS Resolvers to Bridge Onchain Data

Steve

If you’ve been in the blockchain space, then chances are you’ve heard of ENS. It’s by far the most popular onchain identity protocol that allows you to provide a name for your address. When your friend wants to send you crypto, they don’t have the memorize your address, they can just send it to pinnie.eth and it resolve to your address. Beyond address resolution, ENS also provides other helpful identity records like social profiles, avatars, even decentralized websites through IPFS.

The combo of ENS and the contentHashrecord has been used to reference and store websites on IPFS for years now, and it’s used regularly by people like Vitalik. Eth.limo has built a public service that allows anyone to add .limo to their ENS in any browser and it will resolve their contentHash record, and thus load their website. The only downside to this is the fact that any time you want to update your site you have to provide a new CID and that means paying L1 fees to update it. Most people have used IPNS as a work around, but it’s still not fast, user friendly, or flexible.

To counter these difficulties, we built IPCM which handles the CID state onchain. We’ve talked a good bit about it recently, including some of the major pros and cons between IPNS and IPCM. It includes benefits like tying its deployment to onchain identities and version history through events fired from the contract. By deploying it to an L2 like Base it drastically reduces the gas costs to update the hash. However, since ENS resolvers are on Eth Mainnet, this proves to be a challenge to bridge the two.

In this post, we’ll go into details on how we solved this with a custom Hybrid ENS Resolver contract and offchain gateway API, which enables anyone to bridge data between an L2 contract and ENS resolvers on Mainnet.

Hybrid ENS Resolver

The problem of bridging two different chains for ENS resolution isn’t all that new, and there have been several solutions, including custom ENS Resolvers. All ENS names will generally point to a public resolver where all the records are recorded and retrieved, however anyone can update their resolver to be another contract that meets the requirements. One of the most popular versions is a Cross Chain / Offchain Resolver, implementing CCIP Read from EIP-3668. This EIP helps standardize a way to query an offchain API for read only data, and with an Offchain Resolver you can set an API endpoint that would return particular data for a given ENS name.

Image courtesy of docs.ens.domains

The only downside with these offchain resolvers is that all records are being stored offchain, which may not be ideal for critical records like your address. A solution to this presented by Greg Skril from ENS Labs is a Hybrid Offchain Resolver. With this approach, you can use the offchain resolution as a backup of sorts, and check onchain records first. This was promising and proved to work pretty well in testing, however we did encounter one issue. The onchain records that were being checked first were the records for that hybrid resolver. Generally, when someone adds records to their ENS, they are stored in the public resolver, so when someone switches their resolver, all of those records are lost and you basically start with a blank slate.

Once again, with the help of Greg at ENS, we were able to build a new Hybrid Resolver. In this implementation, the contract will do a resolution check with the public resolver first, then the legacy resolver, then the hybrid resolver itself, and finally the offchain API endpoint.

	function resolve(bytes calldata name, bytes memory data) external view virtual returns (bytes memory) {
	  // If we have an onchain result in this contract, return it
	  bytes memory internalResult = resolveOnchain(address(this), data);
	  if (internalResult.length > 0) return internalResult;
	
	  // If we have an onchain result in the latest public resolver, return it
	  bytes memory publicResResult = resolveOnchain(publicResolver, data);
	  if (publicResResult.length > 0) return publicResResult;
	
	  // If we have an onchain result in the legacy public resolver, return it
	  bytes memory legacyResResult = resolveOnchain(legacyResolver, data);
	  if (legacyResResult.length > 0) return legacyResResult;
	
	  // Otherwise, fallback to offchain lookup
	  return resolveOffchain(name, data);
}

This is a huge unlock as it reduces the friction a user might go through to have a dynamic contentHash from an IPCM contract on an L2. If a user decides to use a custom Hybrid Resolver for their ENS, then all their old records will remain. As their content hash is blank onchain, then it will resolve with the offchain API endpoint.

Offchain API Gateway

The Hybrid Resolver handles half of the work, and the other half is the API endpoint set in the contract. The CCIP standard sets {sender} and {data} as path parameters required to be in the endpoint, and with those you can parse the data and return the ENS resolution for that field. Greg and ENS have build many examples which we gathered a lot of inspiration from. We built one for IPCM and it looks something like this:

<https://worker.ipcm.dev/loopup/{sender}/{data}>

The content in the data field gives us all the information we need — such as the ENS name that we’re looking up and a function to specify what record is requested. We could either support just one name and fetch the same IPCM contract data each time, or you could setup something more dynamic to handle multiple names or records. For this example, we have it point toward the IPCM contract supporting ipcm.dev to grab the info and return it.

import { createPublicClient, http, stringToHex } from 'viem'
import { base } from 'viem/chains'
import { ResolverQuery } from './utils'
import { abi } from "./ipcm-abi";
import { encode } from '@ensdomains/content-hash'

export async function getRecord(query: ResolverQuery) {
  const { functionName } = query

  try {
    const publicClient = createPublicClient({
      transport: http(),
      chain: base
    })

    if (functionName !== 'contenthash') {
      return ''
    }

    const mapping: any = await publicClient.readContract({
      address: "0xD5B0CE88928569Cdc4DBF47F0A4a1D8B31f6311D",
      abi: abi,
      functionName: "getMapping",
    });

    const cid = mapping.split('ipfs://')[1]
    const encodedContenthash = '0x' + encode('ipfs', cid)
    return encodedContenthash
  } catch (err) {
    return ''
  }
}

This function will receive the query to get the functionName and if it’s contenthash, then we do a read function of getMapping on the IPCM contract on Base to get the latest CID state, encode / format it, then return it.

app.get('/lookup/:sender/:data', async (c) => {
  const { sender, data } = c.req.param()

  const safeParse = schema.safeParse({ sender, data })

  if (!safeParse.success) {
    return c.json({ error: safeParse.error }, 400)
  }

  let result: string

  try {
    const { name, query } = decodeEnsOffchainRequest(safeParse.data)
    result = await getRecord(query)
  } catch (error) {
    const isHttpRequestError = error instanceof HttpRequestError
    const errMessage = isHttpRequestError ? error.message : 'Unable to resolve'
    return c.json({ message: errMessage }, 400)
  }

  const encodedResponse = await encodeEnsOffchainResponse(
    safeParse.data,
    result,
    c.env.PRIVATE_KEY
  )

  console.log("Full response to resolver: ", encodedResponse)
  return c.json({ data: encodedResponse }, 200)
})

In the end, we have a relatively simple process of not only fetching and returning dynamic cross chain data, but also securely using signatures on both the Resolver Contract and the Gateway API.

We would like to once again thank Greg from ENS for all of his help on this project, as well as the team at Eth.limo which put us on the right trail to follow. Both the Hybrid Resolver Contract and the Gateway API are open sourced and linked below!

GitHub - PinataCloud/ipcm-resolver
Contribute to PinataCloud/ipcm-resolver development by creating an account on GitHub.
GitHub - PinataCloud/ipcm-ens-gateway
Contribute to PinataCloud/ipcm-ens-gateway development by creating an account on GitHub.

Wrapping Up

With the combination of a Hybrid Resolver contract and a Gateway API, users can benefit from having a dynamic contentHash on their ENS through an IPCM contract on Base. It’s actually setup and running live for ipcm.eth, and you can visit the contentHash or website using the .limo extension: https://ipcm.eth.limo. We’re really excited to see what more IPCM is capable of and what it will bring to the IPFS ecosystem.

Happy Pinning! :)

Subscribe to paid plan image

Share this post:

Stay up to date

Join our newsletter for the latest stories & product updates from the Pinata community.