All guides

The Diamond Loupe: Introspecting a Diamond

The four loupe functions defined by EIP-2535, what each one returns, and how tools like Louper use them to reconstruct a diamond ABI.

7 min read

A jeweller's loupe is the small magnifying glass used to examine a gemstone. EIP-2535 borrows the name for the four view functions that let anyone examine a diamond's internal structure. Without them, a diamond would be opaque: the selector-to-facet mapping lives in storage, so no amount of reading the deployed bytecode tells you what the contract can do.

The interface

The standard mandates all four functions. A conforming diamond must implement every one:

interface IDiamondLoupe {
    struct Facet {
        address facetAddress;
        bytes4[] functionSelectors;
    }

    /// @notice Gets all facet addresses and their four byte function selectors.
    function facets() external view returns (Facet[] memory facets_);

    /// @notice Gets all the function selectors supported by a specific facet.
    function facetFunctionSelectors(address _facet)
        external view returns (bytes4[] memory facetFunctionSelectors_);

    /// @notice Get all the facet addresses used by a diamond.
    function facetAddresses()
        external view returns (address[] memory facetAddresses_);

    /// @notice Gets the facet that supports the given selector.
    function facetAddress(bytes4 _functionSelector)
        external view returns (address facetAddress_);
}

What each function is for

facets()

The workhorse. Returns every facet address paired with all the selectors it owns, in one call. This is what Louper calls first, and it is enough to reconstruct the diamond's entire routing table:

// The single call that powers Louper's facet table
const facets = await publicClient.readContract({
  address: diamondAddress,
  abi: parseAbi([
    'function facets() view returns ((address,bytes4[])[])'
  ]),
  functionName: 'facets',
})

// => [
//      ['0xf7993A...', ['0x1f931c1c']],
//      ['0xF5ba8D...', ['0xcdffacc6', '0x52ef6b2c', ...]],
//      ...
//    ]

Because it returns a dynamic array of structs containing dynamic arrays, the response can be large. On a diamond with 40+ facets and several hundred selectors the ABI-encoded result runs to tens of kilobytes. That is fine for an eth_call, but it is why some public RPC endpoints time out on the largest diamonds — Louper uses fallback transports for exactly this reason.

facetAddresses()

Just the list of facet addresses, with no selectors. Cheap, and useful when you only need to know which contracts are involved — for example to check whether every facet is verified on a block explorer.

facetFunctionSelectors(address)

The selectors owned by one specific facet. Useful for targeted checks: before a Replace operation you can confirm exactly which selectors currently point at the old facet.

facetAddress(bytes4)

Reverse lookup: given a selector, which facet handles it? Returns the zero address if the selector is not registered. This is the function to reach for when debugging a "Function does not exist" revert — it tells you immediately whether the selector was ever registered.

Reconstructing a usable ABI

The loupe gives you addresses and selectors, but selectors alone are not enough to actually call a function — you need parameter types to encode arguments. Tools bridge the gap in two steps:

  1. Call facets() to get each facet address and its selectors.
  2. Fetch each facet's verified ABI from a block explorer, then keep only the entries whose computed selector appears in that facet's list.

That filtering step matters. A facet contract often declares more external functions than the diamond has registered for it, and including the unregistered ones would produce an ABI that reverts at runtime. Concatenating every filtered facet ABI yields the combined diamond ABI — which is what Louper's View Diamond ABI button gives you.

Where a facet is unverified, the fallback is a selector-lookup database, and anything still unresolved is shown as unknown_0x.... See function selectors explained for why the names cannot simply be recovered from the chain.

The loupe and ERC-165

EIP-2535 also requires supportsInterface from ERC-165, and a compliant diamond should report support for the loupe interface ID. In practice this is a weaker signal than it looks: plenty of deployed diamonds return false for interfaces they clearly implement, because the flag has to be set explicitly during initialisation and is easy to forget. Calling facets() directly is the more reliable test.

When the loupe is missing

Some contracts use a diamond-style fallback but never implement the loupe. They are not EIP-2535 compliant, and there is no general way to enumerate their facets — you would have to replay every historical DiamondCut event to reconstruct the current state. If Louper reports that it cannot fetch diamond details, a missing facets() function is the most common cause, followed by the address simply not being a diamond at all.