The problem


OpenSea pulls metadata for a collection subject to the respective interfaces of the ERC721 / ERC1155 token standards. Unfortunately the ENS Base Registrar was deployed back in 2020 and didn't implement the appropriate interfaces. Whilst the NameWrapper was deployed more recently and does implement the ERC1155MetadataExtension as of 25th July 2023 the method returns the wrong uri.

Wrong URI

So down the rabbit hole I jumped. How is it the case that OpenSea is showing appropriate metadata for ENS names given these issues?

The answer is that the ENS metadata service URLs are hard coded by OpenSea (this is not documented anywhere 🥳). This was verified on the ENS developer discord after many hours of sleuthing.

Updating complex and heavily used smart contracts is an arduous process (especially when controlled by a DAO). There is also the issue that not all names are wrapped using the NameWrapper and as such it is not simply a case of updating the uri returned. With big updates scheduled down the line I suspect the intention is to batch updates/fix in a much bigger change down the line. I've asked that these intricacies be documented for developers and the response was positive 🥳


A valid implementation

The OpenSea docs outline how to validate the metadata for a particular collection item.

If you enter a valid tokenId (the tokenId for subnames is the uint256 representation of the namehash) but the uri returned by the ERC1155 uri method does not return valid metadata JSON you will get a null response for the token_uri parameter. See here for an example - in this case the contract was returning the incorrect uri.

When a uri is returned that contains valid JSON the token_uri value will be set as expected. See here. In this case we return the following uri (appended with the tokenId):

 `https://metadata.ens.domains/${network.name}/${nameWrapper.address}/`

I test responses using cast.

cast call 0xe7e58c61f154da643f377b01f585e296afc7a933 "uri(uint256)" 79000071871582768877798882117943814940776653313313746436450885002487748229893 --rpc-url https://eth-goerli.g.alchemy.com/v2/API_KEY_HERE

You can decode the response using a tool like this.


Once you've deployed your contract and it returns a valid metadata uri you can force update OpenSea data for your item at any point as outlined in the docs here. You can see example JSON here with the appropriate data pulled from the metadata service.

{
  "is_normalized": true,
  "name": "funny.fivedollars.eth",
  "description": "funny.fivedollars.eth, an ENS name.",
  "attributes": [
    {
      "trait_type": "Created Date",
      "display_type": "date",
      "value": 1690277412000
    },
    {
      "trait_type": "Length",
      "display_type": "number",
      "value": 5
    },
    {
      "trait_type": "Segment Length",
      "display_type": "number",
      "value": 5
    },
    {
      "trait_type": "Character Set",
      "display_type": "string",
      "value": "letter"
    },
    {
      "trait_type": "Namewrapper Fuse States",
      "display_type": "object",
      "value": {
        "parent": {
          "PARENT_CANNOT_CONTROL": true,
          "CAN_EXTEND_EXPIRY": true,
          "IS_DOT_ETH": false,
          "unnamed": {
            "524288": false,
            "1048576": false,
            "2097152": false,
            "4194304": false,
            "8388608": false,
            "16777216": false,
            "33554432": false,
            "67108864": false,
            "134217728": false,
            "268435456": false,
            "536870912": false,
            "1073741824": false,
            "2147483648": false
          }
        },
        "child": {
          "CANNOT_UNWRAP": true,
          "CANNOT_BURN_FUSES": false,
          "CANNOT_TRANSFER": false,
          "CANNOT_SET_RESOLVER": false,
          "CANNOT_SET_TTL": false,
          "CANNOT_CREATE_SUBDOMAIN": false,
          "CANNOT_APPROVE": false,
          "CAN_DO_EVERYTHING": false,
          "unnamed": {
            "128": false,
            "256": false,
            "512": false,
            "1024": false,
            "2048": false,
            "4096": false,
            "8192": false,
            "16384": false,
            "32768": false
          }
        }
      }
    },
    {
      "trait_type": "Namewrapper Expiry Date",
      "display_type": "date",
      "value": 1721813412000
    },
    {
      "trait_type": "Namewrapper State",
      "display_type": "string",
      "value": "Locked"
    }
  ],
  "url": "https://app.ens.domains/name/funny.fivedollars.eth",
  "last_request_date": 1690279011055,
  "version": 2,
  "background_image": "https://metadata.ens.domains/goerli/avatar/funny.fivedollars.eth",
  "image": "https://metadata.ens.domains/goerli/0x114D4603199df73e7D157787f8778E21fCd13066/0xaea874c4e881dcf71aa1a406e904354e21dd38fa3e825c5235b6773753d9fb05/image",
  "image_url": "https://metadata.ens.domains/goerli/0x114D4603199df73e7D157787f8778E21fCd13066/0xaea874c4e881dcf71aa1a406e904354e21dd38fa3e825c5235b6773753d9fb05/image"
}

The result is a a collection with metadata on OpenSea.

Hurrah !