ERC-8153: Facet-Based Diamonds


title: Facet-Based Diamonds
description: Simplifies deployment and upgrades by standardizing facet introspection.
author: Nick Mudge (@mudgen)
discussions-to:
status: Draft
type: Standards Track
category: ERC
created: 2026-02-07
requires: 8109

Abstract

This ERC standardizes a facet introspection function, packedSelectors(), that every facet MUST implement. The function returns a list of function selectors implemented by the facet, enabling diamonds to discover selectors on-chain at deployment and upgrade time.

This ERC also specifies an optional upgradeDiamond function. upgradeDiamond uses packedSelectors() to determine which selectors to add, replace, or remove when applying facet additions, replacements, or removals.

This standard builds on and extends ERC-8109 Diamonds, Simplified.

Motivation

Deploying and upgrading diamonds suffer from:

  1. High gas costs
  2. Function selector management complexity
    Deploying or upgrading a diamond requires assembling function selectors off-chain. Since common tooling (e.g., Hardhat, Foundry) does not natively manage diamond selectors, developers rely on custom scripts or third-party libraries to handle diamond “plumbing”.

This standard reduces gas costs and eliminates off-chain selector management:

  • Diamonds become less expensive to deploy.
  • Function selectors no longer need to be gathered off-chain.
  • Standard deployment tools can be used without special diamond support.
  • ERC-2535 and ERC-8109 introspection functions have simple implementations.

Specification

ERC-8109 Compliance

Diamonds that implement this standard are also ERC-8109 diamonds.

Diamonds that implement this standard MUST implement the Implementation Requirements section of ERC-8109.

Facet Introspection

Each facet MUST implement the following pure introspection function:

function packedSelectors() external pure returns (bytes memory selectors)

packedSelectors() returns a bytes array containing one or more 4-byte function selectors. The returned bytes array length is a multiple of 4, and each 4-byte chunk is a selector.

The bytes array contains selectors of functions implemented by the facet that are intended to be added to a diamond.

This enables a diamond to discover selectors directly from facets at deployment or upgrade time. A diamond calls packedSelectors() on each facet to determine which selectors to add, replace, or remove.

Selector gathering is therefore no longer an off-chain responsibility.

This also means diamonds implementing this ERC are facet-based rather than function-based. Deployment and upgrades operate on facets.

upgradeDiamond Function

Implementing upgradeDiamond is OPTIONAL.

This function is specified for interoperability with tooling (e.g., GUIs and command-line tools) so that upgrades can be executed with consistent and predictable behavior.

upgradeDiamond adds, replaces, and removes any number of facets in a single transaction. It can also optionally execute a delegatecall to perform initialization or state migration.

The upgradeDiamond function works as follows:

Adding a Facet

  1. Call packedSelectors() on the facet to obtain its function selectors.
  2. Add each selector to the diamond, mapping it to the facet address.

Replacing a Facet

  1. Call packedSelectors() on the old facet to obtain its packed selectors.
  2. Call packedSelectors() on the new facet to obtain its packed selectors.
  3. For selectors present in the new facet but not the old facet: add them.
  4. For selectors present in both: replace them to point to the new facet.
  5. For selectors present in the old facet but not the new facet: remove them.

Removing a Facet

  1. Call packedSelectors() on the facet to obtain its packed selectors.
  2. Remove each selector from the diamond.

Errors and Types

/**
 * @notice The upgradeDiamond function below detects and reverts
 *         with the following errors.
 */
error NoSelectorsForFacet(address _facet);
error NoBytecodeAtAddress(address _contractAddress);
error CannotAddFunctionToDiamondThatAlreadyExists(bytes4 _selector);
error CannotRemoveFacetThatDoesNotExist(address _facet);
error CannotReplaceFacetWithSameFacet(address _facet);
error FacetToReplaceDoesNotExist(address _oldFacet);
error DelegateCallReverted(address _delegate, bytes _delegateCalldata);
error FunctionSelectorsCallFailed(address _facet);

/**
 * @dev This error means that a function to replace exists in a
 *      facet other than the facet that was given to be replaced.
 */
error CannotReplaceFunctionFromNonReplacementFacet(bytes4 _selector);

/**
 * @notice This struct is used to replace old facets with new facets.
 */
struct FacetReplacement {
    address oldFacet;
    address newFacet;
}

Function Signature

/**
 * @notice Upgrade the diamond by adding, replacing, or removing facets.
 *
 * @dev
 * Facets are added first, then replaced, then removed.
 *
 * These events are emitted to record changes to functions:
 * - `DiamondFunctionAdded`
 * - `DiamondFunctionReplaced`
 * - `DiamondFunctionRemoved`
 *
 * If `_delegate` is non-zero, the diamond performs a `delegatecall` to
 * `_delegate` using `_delegateCalldata`. The `DiamondDelegateCall` event is
 *  emitted.
 *
 * The `delegatecall` is done to alter a diamond's state or to
 * initialize, modify, or remove state after an upgrade.
 *
 * However, if `_delegate` is zero, no `delegatecall` is made and no
 * `DiamondDelegateCall` event is emitted.
 *
 * If _tag is non-zero or if _metadata.length > 0 then the
 * `DiamondMetadata` event is emitted.
 *
 * @param _addFacets        Facets to add.
 * @param _replaceFacets    (oldFacet, newFacet) pairs, to replace old with new.
 * @param _removeFacets     Facets to remove.
 * @param _delegate         Optional contract to delegatecall (zero address to skip).
 * @param _delegateCalldata Optional calldata to execute on `_delegate`.
 * @param _tag              Optional arbitrary metadata, such as release version.
 * @param _metadata         Optional arbitrary data.
 */
function upgradeDiamond(
    address[] calldata _addFacets,
    FacetReplacement[] calldata _replaceFacets,
    address[] calldata _removeFacets,
    address _delegate,
    bytes calldata _delegateCalldata,
    bytes32 _tag,
    bytes calldata _metadata
);

The upgradeDiamond function MUST adhere to the following requirements:

The complete definitions of the events referenced below are given in ERC-8109.
The complete definitions of custom errors referenced below are given earlier in this section.

  1. Inputs

    • _addFacets array of facet addresses to add.
    • _replaceFacets array of (oldFacet, newFacet) pairs.
    • _removeFacets array of facet addresses to remove.
  2. Execution Order

    1. Add facets
    2. Replace facets
    3. Remove facets
  3. Event Emission

    • Every change to a selector mapping MUST emit exactly one of:
      • DiamondFunctionAdded
      • DiamondFunctionReplaced
      • DiamondFunctionRemoved
  4. Error Conditions

    • The implementation MUST detect and revert with the specified error when:
      • Adding a selector that already exists: CannotAddFunctionToDiamondThatAlreadyExists.
      • Removing a facet that does not exist: CannotRemoveFacetThatDoesNotExist.
      • Replacing a facet with itself: CannotReplaceFacetWithSameFacet.
      • Replacing a facet that does not exist: FacetToReplaceDoesNotExist.
      • Replacing a selector that exists in the diamond but is mapped to a facet different than the facet being replaced: CannotReplaceFunctionFromNonReplacementFacet.
  5. Facet Validation

    • If any facet address contains no contract bytecode, revert with NoBytecodeAtAddress.
    • If packedSelectors() is missing, reverts, or cannot be called successfully, revert with FunctionSelectorsCallFailed.
    • If packedSelectors() returns zero selectors, revert with NoSelectorsForFacet.
  6. Delegate Validation

    • If _delegate is non-zero but contains no bytecode, revert with NoBytecodeAtAddress.
  7. Delegatecall Execution

    • If _delegate is non-zero, the diamond MUST delegatecall _delegate with _delegateCalldata.
    • If the delegatecall fails and returns revert data, the diamond MUST revert with the same revert data.
    • If the delegatecall fails and returns no revert data, revert with DelegateCallReverted.
    • If a delegatecall is performed, the diamond MUST emit the DiamondDelegateCall event.
    • _delegateCalldata MAY be empty. If empty, the delegatecall executes with no calldata.
  8. Metadata Event

    • If _tag is non-zero or _metadata.length > 0, the diamond MUST emit the DiamondMetadata event.

After adding, replacing, or removing facets, the diamond MAY perform a delegatecall to initialize, migrate, or clean up state.

It is also valid to call upgradeDiamond solely to perform a delegatecall (i.e., without adding, replacing, or removing any facets).

To skip an operation, supply an empty array for its parameter (for example, new address[](0) for _addFacets).

Rationale

Eliminating Selector Management

To deploy a facet-based diamond implementing this ERC, the deployer provides an array of facet addresses to the diamond constructor. The constructor calls packedSelectors() on each facet and registers those selectors in the diamond.

Because facets self-describe their selectors, deployers no longer need to gather selectors off-chain or depend on specialized selector tooling.

Reducing Deployment Gas Costs

Reducing Calldata

In a non-facet-based diamond, selectors are typically passed to the constructor as one or more bytes4[] arrays. These arrays are paid for in calldata and then copied into memory, incurring additional gas.

In a facet-based diamond, only facet addresses are passed to constructors. The diamond calls packedSelectors() on each facet to obtain selectors on-chain, avoiding calldata costs for selector lists. While calling packedSelectors() introduces some overhead, non-facet-based diamonds typically perform code-existence checks (e.g., extcodesize) on facet addresses anyway; the marginal difference is small.

Reducing Storage

In a non-facet-based diamond, function selectors are stored directly for introspection, typically in a bytes4[] selectors array (or an equivalent structure). Because a storage slot is 32 bytes, each slot can hold up to eight bytes4 selectors. As more functions are added, additional storage slots are required, so storage usage grows linearly with the number of selectors.

In a facet-based diamond, introspection data can be stored per facet instead of per function. Each facet only needs a single representative selector. This means one 32-byte storage slot can represent up to eight facets, regardless of how many function selectors each facet implements. Storage usage therefore grows with the number of facets, not the number of functions.

Alternatively, a facet-based diamond can be implemented as a linked list of facets. With this design, introspection requires a single 32-byte storage slot, while supporting any number of facets and any number of selectors per facet.

packedSelectors() Function Return Value

packedSelectors() returns bytes rather than bytes4[] for two reasons:

bytes4[] Wastes Memory

Each element of a bytes4[] array occupies 32 bytes in memory, but only 4 bytes are meaningful. This wastes 87.5% of allocated memory, increasing gas costs. Packing selectors into bytes reduces memory overhead.

Simple Syntax For Facets

Facets can implement packedSelectors() concisely using Solidity’s built-in function bytes.concat. Example:

function packedSelectors() external pure returns (bytes memory) {
    return bytes.concat(
        this.facetAddress.selector,
        this.facetFunctionSelectors.selector,
        this.facetAddresses.selector,
        this.facets.selector,
        this.functionFacetPairs.selector
    );
}

The diamond can traverse the returned bytes and extract selectors efficiently.

Backwards Compatibility

Diamonds implementing this ERC fully comply with ERC-8109.

It is also straightforward for diamonds implementing this ERC to additionally implement ERC-2535 introspection functions.

Security Considerations

The security considerations of ERC-8109 apply.

Only trusted and verified facets should be added to facet-based diamonds.

packedSelectors() MUST be pure and should not contain logic that varies the returned bytes. Facets should be immutable so returned selectors cannot change over time.

If a facet’s packedSelectors() output changes, upgrades that rely on it may add/remove/replace the wrong selectors and corrupt diamonds.

Upgrade Integrity Checks

The specified upgradeDiamond behavior prevents a number of upgrade mistakes. Upgrades revert when:

  • A facet is added that already exists in the diamond.
  • A facet is replaced or removed that does not exist in the diamond.
  • A selector is added that already exists in the diamond.
  • A selector is replaced that exists in the diamond but it is from a different facet than the facet being replaced.
  • A facet address contains no bytecode.
  • A facet does not implement packedSelectors() successfully.
  • A facet provides zero selectors.
  • A facet is replaced with itself (same contract address).

Selector collisions (two different signatures with the same 4-byte selector) are handled as “selector already exists” and are therefore prevented.

Copyright

Copyright and related rights waived via CC0.

4 Likes

For the multifunction facets such introspection is a great improvement for related tooling and initial deployment / configuration scripts. @mudgen :clap:

For the security and enforcing immutability in a hardened way - packedSelectors() could return the pointers for EXTCODECOPY, i.e. offset and size. This ensures the selectors data have to be deployed within the contract being always present. I assume this approach to be even more gas efficient.

Within ERC-8109 we discussed aspect of Single Function Facets. I hope we would be able to make this ERC and Single Function Facets compatible for the sake of the tooling ecosystem.

The problem - Single Function Facets naturally have only either 1 selector, or no selector at all ( i.e. fallback() if compiled by Solidity) yet representing certain selector. How such Facet could report its selector?

opt A) selector declared in CBOR metadata of the single function facet

opt B) indication in diamond’s upgradeDiamond function

That is enhanced with the flags indicating whether the facet address is multifunction or singlefunction, e.g.

function upgradeDiamond(
    bytes32[] calldata _addFacets,
 ...

where bytes32 consists of:

  • bytes4 multifacet indicator or selector
  • bytes8 zero data (reserved)
  • bytes20 address

Where if the bytes4 multifacet indicator is 0x00000000 → address consists of multifunction facet and packedSelectors() can be called.

If the bytes4 multifacet indicator is not 0x0 → address consists of single function facet and the selector is extracted from the indicator.

opt C) passthrough “meta-facet”

  • Meaning in diamond upgrade, etc. the meta-facet address is provided, which will return on packedSelectors() not only the selector data, but also the target Single Function Facet address

opt D) blueprint reference

  • Similar to opt C “meta-facet”, but this time such meta-facet’s packedSelectors() would return a set of selectors and facet addresses that are logically relevant (ideally using EXTCODECOPY as mentioned in hardening the security).

@mudgen What would you prefer?

2 Likes

@radek Thanks for your post!

Great!

How would the implementation of packedSelectors() in the facet work? How could a developer, who only know Solidity, do this? I am interested to understand this approach better or in more detail. Please give more information.

I want this too.

I appreciate the different options and I think they are very interesting.

What about the single function simply returning a bytes array with the function’s selector when the first four bytes of callback equal the function selector for packedSelectors()?

This single if condition could be added to the beginning of the code for a single function facet:

// check if packedSelectors() is called
if eq(shr(224, calldataload(0)), 0x3e62267c) { // 27 gas
  // return an ABI encoded bytes array containing the 
  // function selector of this function.
  mstore(0, 0x20) 
  mstore(0x20, 0x04) 
  mstore(0x40, shl(224, 0x????????)) // specify the selector of this function
  return(0, 0x60) 
}

The gas cost to execute the if condition is 27 gas. Could that work?

It is still a single function facet, it just returns a bytes array containing its own function selector if the the first four bytes of calldata is 0x3e62267c (which is the selector for packedSelectors() ).

1 Like

I would consider that as

option E) i.e. internal function check, similarly like extra check is common for non-payable function.

IMO the key question is whether this standard should be about facet’s self provided introspections or whether it is about to ease diamond’s config.

In latter case if we use opt C or D (i.e. meta-facet(s) or blueprints), we can enable also facets that do not have selectors introspection embedded, but can be reported to the diamond upgrade in an efficient way.

1 Like

Honestly I am not much in favor of this options as I assume the ecosystem-wide optimisations will rather lead to the stripped code to the core logic - which is also healthier from the audit perspective.

2 Likes

@radek I understand and I have an implementation of adding/replacing/removing facets using an introspection function for facets. It is here: Compose/src/diamond/DiamondUpgradeFacet.sol at 529d11a5db4c501289bdc1789a85d83feafd02d6 · Perfect-Abstractions/Compose · GitHub

I am building a diamond system that balances a number of factors as best as possible. It should be simple enough that a developer who just knows Solidity can understand and use. It should depend on no or few tools and minimum infrastructure. Deployment and upgrades should be more gas efficient than past solutions (for adding/replacing/removing diamond functionality).

I would be very interested at looking at full, complete, descriptions of how great diamond systems could be. I have no full system, fully described, and no complete implementation to look at concerning options C or D (i.e. meta-facet(s) or blueprints). I would need more understanding, details about how diamonds would add/replace/remove facets/functionality using those methods, before doing more with them.

1 Like

@radek

I like the idea of packedSelectors() returning offset and size for EXTCODECOPY, but I don’t see a practical way for a regular Solidity developer to implement that.

How does a regular Solidity developer easily get the offset of function selectors embedded in the bytecode of their contract, which they make packedSelectors() return in the same contract?

The current approach, just return the function selectors, has the advantage that it is easy for a regular Solidity developer to do, requires no tooling, and self-documents within the facet source code which function selectors it exposes to diamonds. As long as the facet is immutable, and the packedSelectors() function just returns the list of function selectors, then this approach is secure and immutable.

1 Like

I also don’t see a way to do it in solidity. I can see how to use data sections in pure yul and in my own assembler, but in solidity-yul there doesn’t seem to be a way to utilize data offsets. I get Function "dataoffset" not found. when I try.

(edit: fixed link)

2 Likes

@wjmelements Thanks for checking that!

1 Like

To be honest, packedSelectors introduces unnecessary operational complexity. Developers must implement it for every facet and re-audit it with each update, which significantly increases the maintenance burden and expands the surface area for errors. The approach is neither simple nor particularly safe. Additionally, the deployment system may also require extra validation logic to ensure correctness, further compounding the complexity.

@Conight Thanks for your input about this.

I see that you implemented off-chain selector management for deployment and upgrades of diamond contracts here: GitHub - Conight/diamond-hardhat-template: A sophisticated, production-ready template for building modular and upgradeable smart contracts using the Diamond Standard (EIP-2535/EIP-8109).

That’s fantastic. Thank you for the credit you gave there.

1 Like

You are right, Solidity does not allow us to point to data segment, e.g. like this:

object "LifeFacet" {
    code {
        // Deployment logic: Copy runtime to memory and return it
        datacopy(0, dataoffset("runtime"), datasize("runtime"))
        return(0, datasize("runtime"))
    }

    object "runtime" {
        code {
            // Extract function selector
            let sig := shr(224, calldataload(0))

            switch sig
            case 0x371edc87 { // lifeMeaning()
                mstore(0, 42)
                return(0, 0x20)
            }
            case 0x9378108c { // packedSelectors()
                // Directly return the offset and size of the data segment
                mstore(0, dataoffset("selectorData"))
                mstore(0x20, datasize("selectorData"))
                return(0, 0x40)
            }
            default {
                revert(0, 0)
            }
        }

        // Data segment strictly appended to the bytecode
        data "selectorData" hex"371edc87"
    }
}

I tried to research some workarounds in Solidity, but they were rather too much gas consuming.

So there seams not to be a way to enforce the immutability via implementation. Standard can define though that the packedSelectors() MUST be a pure function. That would mean one will be using constants.

Note - this was addressing the case of using EXTCODECOPY towards the facet having packedSelectors(). I believe option C) / D) i.e. meta-facet or blueprints can work with EXTCODECOPY.

Wrt blueprints - the idea is like this:

// script/DeployBlueprint.s.sol
// SPDX-License-Identifier: MIT
import "forge-std/Script.sol";

contract DeployBlueprint is Script {
    function run() external {
        // 1. Generate hash for versioned blueprint
        bytes32 rawHash = keccak256("some.facet.blueprint.v1");
        
        // 2. Zero out the first byte (0x00...) to make sure STOP is executed if called
        bytes32 blueprint = rawHash & 0x00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;

        // 3. Define selectors
        bytes4 s1 = bytes4(keccak256("func1()"));
        bytes4 s2 = bytes4(keccak256("func2()"));
        bytes4 s3 = bytes4(keccak256("func3()"));

        // 4. Combine data
        bytes memory data = abi.encodePacked(blueprint, s1, s2, s3);
        
        // 5. Construct Initcode
        bytes memory payload = abi.encodePacked(
            hex"60", uint8(data.length), hex"8060093d393df3", 
            data
        );

        vm.startBroadcast();
        address deployedAddress;
        assembly {
            deployedAddress := create(0, add(payload, 0x20), mload(payload))
        }
        vm.stopBroadcast();

        console.log("Blueprint data contract (with STOP byte):", deployedAddress);
        console.logBytes32(blueprint);
    }
}

This is conceptual, ofc we would have to define the standard (hence versioning string), that would pack the data into the correct form for the immediate consumption by upgrade function.

I wrote up a proposal here. This is the second time I’ve wanted this functionality in solidity.

1 Like