All guides

What Is a Diamond? EIP-2535 Explained

A plain-English introduction to the EIP-2535 Diamond standard: what problem it solves, how facets work, and when you should (and should not) use one.

9 min read

A diamond is a smart contract that gets its functionality from other contracts called facets. It is defined by EIP-2535, a standard for building modular, upgradeable contracts that are not limited by the size cap that applies to a single deployed contract.

If you have used a proxy contract before, a diamond will feel familiar: calls arrive at one address, and that address forwards them somewhere else using delegatecall. The difference is that a normal proxy forwards every call to one implementation, while a diamond forwards each function to a different implementation depending on which function was called.

The problem diamonds solve

Ethereum enforces a hard limit on deployed bytecode size. EIP-170 caps a contract at 24,576 bytes. That sounds like a lot until you build a protocol with lending, staking, governance, and a token all in one system. Teams routinely hit the ceiling and are forced into awkward choices:

  • Split the protocol across several addresses that must then coordinate with each other.
  • Strip out input validation and error strings to claw back a few hundred bytes.
  • Deploy a monolithic proxy and redeploy the entire implementation for a one-line fix.

Diamonds address all three. Because logic lives in separate facet contracts, the 24 KB limit applies to each facet individually rather than to the protocol as a whole. And because facets are registered per-function, you can replace a single function without touching anything else.

How a diamond actually works

Every diamond stores a mapping from a 4-byte function selector to the address of the facet that implements it. When a call comes in, the diamond's fallback function looks up msg.sig in that mapping and delegatecalls the matching facet:

// Simplified fallback found in every EIP-2535 diamond
fallback() external payable {
    // 1. Look up which facet implements msg.sig
    address facet = selectorToFacet[msg.sig];
    require(facet != address(0), "Function does not exist");

    // 2. delegatecall into that facet, keeping the diamond's storage
    assembly {
        calldatacopy(0, 0, calldatasize())
        let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
        returndatacopy(0, 0, returndatasize())
        switch result
        case 0 { revert(0, returndatasize()) }
        default { return(0, returndatasize()) }
    }
}

The critical detail is delegatecall. It runs the facet's code in the diamond's storage context. The facet supplies the logic; the diamond owns all the data. This is why facets are usually described as stateless — they never hold the protocol's state themselves, they only operate on the diamond's storage. Getting this wrong is the single most common source of diamond bugs, which is why storage layout gets its own guide.

Facets, selectors and the loupe

A facet is just a normal Solidity contract. There is nothing special about its code — what makes it a facet is that a diamond has registered some of its function selectors. The same facet can be shared by many diamonds, which is common for utility facets like ownership.

Because the mapping of selectors to facets lives in storage rather than in the bytecode, you cannot learn a diamond's full interface just by looking at its verified source. EIP-2535 solves this by requiring four introspection functions, collectively called the diamond loupe:

  • facets() — every facet address with all of its selectors
  • facetAddresses() — just the facet addresses
  • facetFunctionSelectors(address) — the selectors for one facet
  • facetAddress(bytes4) — which facet handles a given selector

These are what Louper calls when you inspect a contract. The loupe guide covers each function in detail.

Upgrading: the diamondCut function

Changes are made through diamondCut, which takes a list of operations and applies them atomically:

struct FacetCut {
    address facetAddress;
    FacetCutAction action; // Add, Replace or Remove
    bytes4[] functionSelectors;
}

function diamondCut(
    FacetCut[] calldata _diamondCut,
    address _init,
    bytes calldata _calldata
) external;

Each entry uses one of three actions:

  • Add — register selectors that the diamond does not have yet
  • Replace — point existing selectors at a different facet
  • Remove — delete selectors entirely (the facet address must be zero)

The optional _init and _calldata arguments let you run a one-time migration in the same transaction, which matters when an upgrade changes the shape of stored data. See upgrading with diamondCut.

When a diamond is the right choice

Diamonds are a good fit when at least one of these is true:

  • Your protocol genuinely does not fit inside 24 KB.
  • Different parts of the system change at different rates and you want to upgrade them independently.
  • Multiple teams own different modules and need to ship without coordinating deployments.
  • You want fine-grained upgrade permissions, facet by facet.

When it is not

It would be dishonest to present diamonds as a free win. They carry real costs, and for many projects a simpler pattern is the better engineering decision:

  • Every call costs more. The selector lookup and delegatecall add overhead to each transaction compared to a direct call.
  • Tooling support is thinner. Block explorers cannot show the full interface without loupe-aware tooling, which is precisely the gap Louper fills.
  • The attack surface is larger. Shared storage across many facets creates collision risks that a single-implementation proxy simply does not have.
  • Audits cost more. Reviewers must reason about every combination of facets, not one contract.

If your contract fits comfortably in 24 KB and you upgrade it rarely, a transparent or UUPS proxy is usually the more sensible choice. We compare them directly in diamonds vs proxies.

Seeing it in practice

Reading about facets only takes you so far. Open a production diamond such as the LI.FI Diamond in Louper and you will see dozens of facets, each with its own selectors, all served from a single address. Then try inspecting a diamond yourself.