ERC-8153 - Facet-Based Diamonds

Created 2026-02-07
Status Draft
Category ERC
Type Standards Track
Authors
Requires

Abstract

A diamond is a proxy contract that delegatecalls to multiple implementation contracts called facets.

Diagram showing how a diamond contract works

Diamond contracts were originally standardized by ERC-2535. This ERC builds on that foundation by defining a facet-based architecture in which facets self-describe their function selectors through a standardized introspection interface.

By moving selector discovery on-chain, this approach eliminates the need for off-chain selector management. As a result, diamond deployment and upgrades become simpler, more deterministic, and more gas efficient.

This ERC introduces a facet introspection function, exportSelectors(), which every facet MUST implement. This function returns the list of function selectors implemented by the facet, allowing a diamond to discover and register selectors on-chain during deployment or upgrade.

This ERC also defines facet-based events for adding, replacing, and removing facets.

Additionally, the ERC also defines an optional upgradeDiamond function. This function uses exportSelectors() to automatically determine which selectors should be added, replaced, or removed when applying facet changes.

Motivation

Motivation for Diamond Contracts

Obligatory diamondThrough a single contract address, a diamond provides functionality from multiple implementation contracts (facets). Each facet is independent, yet facets can share internal functions and storage. This architecture allows large smart-contract systems to be composed from separate facets and presented as a single contract, simplifying deployment, testing, and integration with other contracts, off-chain software, and user interfaces.

By decomposing large smart contracts into facets, diamonds can reduce complexity and make systems easier to reason about. Distinct areas of functionality can be isolated, organized, tested, and managed independently.

Diamonds combine the single-address convenience of a monolithic contract with the modular flexibility of distinct, integrated contracts.

This architecture is well suited to immutable smart-contract systems, where all functionality is composed from multiple facets at deployment time and permanently fixed thereafter.

For upgradeable systems, diamonds enable incremental development: new functionality can be added, and existing functionality modified, without redeploying unaffected facets.

Additional motivation and background for diamond-based smart-contract systems can be found in ERC-1538 and ERC-2535.

Motivation for this Standard

In the past, deploying and upgrading diamonds suffered 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:

Specification

Terms

  1. A diamond is a smart contract that routes external function calls to one or more implementation contracts, referred to as facets. A diamond is stateful: all persistent data is stored in the diamond’s contract storage. A diamond implements the requirements in the Implementation Requirements section.
  2. A facet is a smart contract that defines one or more external functions. A facet is deployed independently, and one or more of its functions are added to one or more diamonds. A facet’s functions are executed in the diamond’s context via delegatecall, so reads/writes affect the diamond’s storage. The term facet is derived from the diamond industry, referring to a flat surface of a diamond.
  3. An introspection function is a function that returns information about the facets and/or functions used by a diamond or facet.
  4. For the purposes of this specification, a mapping refers to a conceptual association between two items and does not refer to a specific implementation.

Diamond Diagram

This diagram shows the structure of a diamond.

It shows that a diamond has a mapping from function to facet and that facets can access the storage inside a diamond.

Diagram showing structure of a diamond

Fallback

When an external function is called on a diamond, its fallback function is executed. The fallback function determines which facet to call based on the first four bytes of the calldata (known as the function selector) and executes the function from the facet using delegatecall.

A diamond’s fallback function and delegatecall enable a diamond to execute a facet’s function as if it was implemented by the diamond itself. The msg.sender and msg.value values do not change and only the diamond’s storage is read and written to.

Here is an example of how a diamond’s fallback function might be implemented:

error FunctionNotFound(bytes4 _selector);

// Executes function call on facet using `delegatecall`.
// Returns function call return data or revert data.
fallback() external payable {
    // Get facet address from function selector
    address facet = selectorToFacet[msg.sig];
    if (facet == address(0)) {
        revert FunctionNotFound(msg.sig);
    }
    // Execute external function on facet using `delegatecall` and return any value.
    assembly {
        // Copy function selector and any arguments from calldata to memory.
        calldatacopy(0, 0, calldatasize())
        // Execute function call using the facet.
        let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
        // Copy all return data from the previous call into memory.
        returndatacopy(0, 0, returndatasize())
        // Return any return value or error back to the caller.
        switch result
        case 0 {revert(0, returndatasize())}
        default {return (0, returndatasize())}
    }
}

Function Not Found

If the fallback function cannot find a facet for a function selector, and there is no default function or other mechanism to handle the call, the fallback MUST revert with the error FunctionNotFound(bytes4 _selector).

Inspecting Diamonds

An ERC-8153 diamond MUST implement the same introspection functions as defined in ERC-2535. Specifically, these functions MUST be implemented:

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

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

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

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

    /// @notice Gets the facet that supports the given selector.
    /// @dev If facet is not found return address(0).
    /// @param _functionSelector The function selector.
    /// @return facetAddress_ The facet address.
    function facetAddress(bytes4 _functionSelector) external view returns (address facetAddress_);
}

Typically, these functions are implemented in a facet and the facet is added to diamonds.

Inspecting Facets

Each facet MUST implement the following pure introspection function:

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

exportSelectors() 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 function MUST not return the same selector more than once.

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 exportSelectors() 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.

Facet-Based Events

This ERC replaces ERC-2535’s function-based events with facet-based events.

When facets are added, replaced, or removed, the diamond MUST emit the following events:

 /**
  * @notice Emitted when a facet is added to a diamond.
  * @dev The function selectors this facet handles can be retrieved by calling
  *      `IFacet(_facet).exportSelectors()`
  *
  * @param _facet The address of the facet that handles function calls to the diamond.
  */
event FacetAdded(address indexed _facet);

/**
 * @notice Emitted when an existing facet is replaced with a new facet.
 * @dev
 * - Selectors that are present in the new facet but not in the old facet are added to the diamond.
 * - Selectors that are present in both the new and old facet are updated to use the new facet.
 * - Selectors that are not present in the new facet but are present in the old facet are removed from
 *   the diamond.
 *
 * The function selectors handled by these facets can be retrieved by calling:
 * - `IFacet(_oldFacet).exportSelectors()`
 * - `IFacet(_newFacet).exportSelectors()`
 *
 * @param _oldFacet The address of the facet that previously handled function calls to the diamond.
 * @param _newFacet The address of the facet that now handles function calls to the diamond.
 */
event FacetReplaced(address indexed _oldFacet, address indexed _newFacet);

/**
 * @notice Emitted when a facet is removed from a diamond.
 * @dev The function selectors this facet handles can be retrieved by calling
 *      `IFacet(_facet).exportSelectors()`
 *
 * @param _facet The address of the facet that previously handled function calls to the diamond.
 */
event FacetRemoved(address indexed _facet);

Block explorers and other tooling can obtain the function selectors for any of the facets referenced by these events by calling exportSelectors() on the facet address.

Optional Upgrade Events

Recording Non-Fallback delegatecalls

This event is OPTIONAL.

This event can be used to record delegatecalls made by a diamond.

This event MUST NOT be emitted for delegatecalls made by a diamond’s fallback function when routing calls to facets. It is only intended for delegatecalls made by functions in facets or a diamond’s constructor.

This event enables tracking of changes to a diamond’s contract storage caused by delegatecall execution.

/**
* @notice Emitted when a diamond's constructor function or function from a
*         facet makes a `delegatecall`. 
* 
* @param _delegate         The contract that was the target of the `delegatecall`.
* @param _delegateCalldata The function call, including function selector and 
*                          any arguments.
*/
event DiamondDelegateCall(address indexed _delegate, bytes _delegateCalldata);

Diamond Metadata

This event is OPTIONAL.

This event can be used to record versioning or other information about diamonds.

It can be used to record information about diamond upgrades.

/**
* @notice Emitted to record information about a diamond.
* @dev    This event records any arbitrary metadata. 
*         The format of `_tag` and `_data` are not specified by the 
*         standard.
*
* @param _tag   Arbitrary metadata, such as a release version.
* @param _data  Arbitrary metadata.
*/
event DiamondMetadata(bytes32 indexed _tag, bytes _data);

Implementation Requirements

A facet-based diamond MUST implement the following:

  1. Diamond Structure
  2. A fallback() function.
  3. Function Association
  4. It MUST associate function selectors with facet addresses.
  5. Function Execution
  6. When an external function is called on a diamond:
    • The diamond’s fallback function is executed.
    • The fallback function MUST find the facet associated with the function selector.
    • The fallback function MUST execute the function on the facet using delegatecall.
    • If no facet is associated with the function selector, the diamond MAY execute a default function or apply another handling mechanism.
    • If no facet, default function, or other handling mechanism exists, execution MUST revert with the error FunctionNotFound(bytes4 _selector).
  7. Events
  8. The following events MUST be emitted:
    • FacetAdded — when a facet is added to a diamond.
    • FacetReplaced — when a facet is replaced with a different facet.
    • FacetRemoved — when a facet is removed from a diamond.
  9. Diamond Introspection
  10. A diamond MUST implement the following introspection functions:
    • facets()
    • facetFunctionSelectors(address _facet)
    • facetAddresses()
    • facetAddress(bytes4 _functionSelector)
  11. Facet Introspection
  12. Each facet MUST implement the exportSelectors() function, which returns a bytes array.

receive() function

A diamond MAY have a receive() function.

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 exportSelectors() 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 exportSelectors() on the old facet to obtain its packed selectors.
  2. Call exportSelectors() 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 exportSelectors() 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 ExportSelectorsCallFailed(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 facets:
 * - `FacetAdded(address indexed _facet)`
 * - `FacetReplaced(address indexed _oldFacet, address indexed _newFacet)`
 * - `FacetRemoved(address indexed _facet)`
 *
 * 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 events and custom errors referenced below are given earlier in this standard.

  1. Inputs
  2. _addFacets array of facet addresses to add.
  3. _replaceFacets array of (oldFacet, newFacet) pairs.
  4. _removeFacets array of facet addresses to remove.

  5. Execution Order

  6. Add facets
  7. Replace facets
  8. Remove facets

  9. Event Emission

  10. Every change to a facet MUST emit exactly one of:

    • FacetAdded
    • FacetReplaced
    • FacetRemoved
  11. Error Conditions

  12. 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.
  13. Facet Validation

  14. If any facet address contains no contract bytecode, revert with NoBytecodeAtAddress.
  15. If exportSelectors() is missing, reverts, or cannot be called successfully, revert with ExportSelectorsCallFailed.
  16. If exportSelectors() returns zero selectors, revert with NoSelectorsForFacet.

  17. Delegate Validation

  18. If _delegate is non-zero but contains no bytecode, revert with NoBytecodeAtAddress.

  19. Delegatecall Execution

  20. If _delegate is non-zero, the diamond MUST delegatecall _delegate with _delegateCalldata.
  21. If the delegatecall fails and returns revert data, the diamond MUST revert with the same revert data.
  22. If the delegatecall fails and returns no revert data, revert with DelegateCallReverted.
  23. If a delegatecall is performed, the diamond MUST emit the DiamondDelegateCall event.
  24. _delegateCalldata MAY be empty. If empty, the delegatecall executes with no calldata.

  25. Metadata Event

  26. 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 exportSelectors() 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 exportSelectors() on each facet to obtain selectors on-chain, avoiding calldata costs for selector lists. While calling exportSelectors() introduces some overhead, non-facet-based diamonds typically perform code-existence checks (e.g., extcodesize) on facet addresses anyway, incurring the cold account access gas cost.

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.

exportSelectors() Function Return Value

exportSelectors() 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 exportSelectors() concisely using Solidity's built-in function bytes.concat. Example:

function exportSelectors() 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.

Diamond Upgrades

This upgrade function specified by this standard is optional.

This means a couple things:

1. Diamonds Can Be Immutable

A Diamond does not have to have an upgrade function.

2. You Can Create Your Own Upgrade Functions

You can design and create your own upgrade functions and remain compliant with this standard. All that is required is that you emit the appropriate add/replace/remove required events specified in the Facet-Based Events section, that the introspection functions defined in the Inspecting Diamonds section and the Inspecting Facets section continue to exists and accurately return function and facet information.

Runtime Gas Considerations

Routing calls via delegatecall introduces a small amount of gas overhead. In practice, this cost is mitigated by several architectural and tooling advantages enabled by diamonds:

  1. Optional, gas-optimized functionality
    By structuring functionality across multiple facets, diamonds make it straightforward to include specialized, gas-optimized features without increasing the complexity of core logic.
    For example, an ERC-721 diamond may implement batch transfer functions in a dedicated facet, improving both gas efficiency and usability while keeping the base ERC-721 implementation simple and well-scoped.

  2. Reduced external call overhead
    Some contract architectures require multiple external calls within a single transaction. By consolidating related functionality behind a single diamond address, these interactions can execute internally with shared storage and shared authorization, reducing gas costs from external calls and repeated access-control checks.

  3. Selective optimization per facet
    Because facets are compiled and deployed independently, they may be built with different compiler optimizer settings. This allows gas-critical facets to use aggressive optimization configurations to reduce execution costs, without increasing bytecode size or compilation complexity for unrelated functionality.

Storage Layout

Diamonds and facets need to use a storage layout organizational pattern because Solidity’s default storage layout doesn’t support proxy contracts or diamonds. The storage layout technique or pattern to use is not specified in this ERC. However, examples of storage layout patterns that work with diamonds are ERC-8042 Diamond Storage and ERC-7201 Namespaced Storage Layout.

Facets Sharing Storage & Functionality

Facets are separately deployed, independent units, but can share state and functionality in the following ways:

On-chain Facets can be Reused and Composed

A deployed facet can be used by many diamonds.

It is possible to create and deploy a set of facets that are reused by different diamonds.

The ability to use the same deployed facets for many diamonds has the potential to reduce development time, increase reliability and security, and reduce deployment costs.

It is possible to implement facets in a way that makes them usable/composable/compatible with other facets.

Backwards Compatibility

Diamonds implementing this ERC have the same introspection functions as ERC-2535 diamonds, so they are compatible with ERC-2535 tooling that rely on these functions.

Security Considerations

Arbitrary Execution with upgradeDiamond

The upgradeDiamond function allows arbitrary execution with access to the diamond’s storage (through delegatecall). Access to this function must be restricted carefully.

Use Only Trusted and Verified Facets

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

exportSelectors() 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 exportSelectors() 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:

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

Do Not Self Destruct

Use of selfdestruct in a facet is heavily discouraged. Misuse of it can delete a diamond or a facet.

Transparency

A diamond emits an event every time a facet is added, replaced or removed. Source code can be verified. This enables people and software to monitor changes to a diamond.

Security and domain experts can review a diamond's upgrade history.

Copyright

Copyright and related rights waived via CC0.