ERC-8153 Facet-based diamond (Facet-Based Diamonds)
Final · Other · created 2026-02-07
A standard for diamond contracts, which bundle multiple implementation contracts (facets) behind a single address. Each facet reports its own function list, which makes deployment and upgrades simpler and cheaper.
Why it is needed
A diamond is a structure that provides the functionality of multiple implementation contracts through a single contract address, and it was first standardized in ERC-2535. However, the existing approach required collecting the identifier of each function (the function selector) off-chain and passing them in whenever a diamond was deployed or upgraded. General development tools such as Hardhat and Foundry do not support this selector management by default, so developers had to depend on separate scripts or third-party libraries. Passing and storing the selector list as call data also increased gas costs. This standard mandates an exportSelectors() function through which a facet reports its own selectors on-chain. This moves selector collection onto the chain and handles deployment and upgrades per facet. As a result, off-chain selector management disappears, deployment becomes more deterministic, and gas efficiency improves.
What to implement
function facets() external view returns (Facet[] memory facets_);: Returns all facet addresses used by the diamond and the list of 4-byte function selectors for each facet.function facetFunctionSelectors(address _facet) external view returns (bytes4[] memory facetFunctionSelectors_);: Returns all function selectors supported by a given facet.function facetAddresses() external view returns (address[] memory facetAddresses_);: Returns all facet addresses used by the diamond.function facetAddress(bytes4 _functionSelector) external view returns (address facetAddress_);: Returns the address of the facet that handles the given selector. If there is none, it returns the zero address.function exportSelectors() external pure returns (bytes memory selectors);: Returns the function selectors implemented by the facet as bytes, with each 4-byte selector concatenated one after another. Every facet must implement it.event FacetAdded(address indexed _facet);: Emitted when a facet is added to the diamond.event FacetReplaced(address indexed _oldFacet, address indexed _newFacet);: Emitted when an existing facet is replaced with a new facet.event FacetRemoved(address indexed _facet);: Emitted when a facet is removed from the diamond.event DiamondDelegateCall(address indexed _delegate, bytes _delegateCalldata);: An optional event emitted when the diamond's constructor or a facet function performs a delegatecall.event DiamondMetadata(bytes32 indexed _tag, bytes _data);: An optional event that records arbitrary metadata such as a version.
In depth
Diamond structure and fallback routing
A diamond is a proxy contract that routes external function calls to multiple implementation contracts (facets), and all persistent data lives in the diamond's storage. When an external function is called, the fallback function runs, finds the corresponding facet using the first 4 bytes of calldata (the function selector), and executes it with delegatecall. In this process msg.sender and msg.value are unchanged, and only the diamond's storage is read and written. If no facet matches the selector and there is no other handler such as a default function, it must revert with the FunctionNotFound(bytes4 _selector) error.
The exportSelectors() specification
Every facet must implement exportSelectors() as a pure function. The return value is bytes made of concatenated 4-byte selectors. Its length must be a multiple of 4, and it must not contain the same selector more than once. At deployment or upgrade time, the diamond calls this function on each facet to decide on-chain which selectors to add, replace, or remove. The reason for using bytes instead of bytes4[] is memory efficiency. bytes4[] takes 32 bytes per element, which wastes 87.5%, but packing into bytes reduces the overhead, and on the facet side it can be implemented concisely with bytes.concat.
Facet-based events
This standard replaces ERC-2535's function-level events with facet-level events. When a facet is added, replaced, or removed, FacetAdded, FacetReplaced, and FacetRemoved must be emitted respectively, and exactly one event must be emitted for each facet change. Tools such as block explorers call exportSelectors() on the facet address contained in the event to get the function selectors. The optional events are DiamondDelegateCall, which records a delegatecall that is not fallback routing, and DiamondMetadata, which records arbitrary information such as a version. If upgradeDiamond is implemented, these two must be emitted under the conditions set by the standard.
upgradeDiamond behavior rules
upgradeDiamond is optional and is specified for interoperability with tools. It executes in the order of add, replace, and remove. When replacing, it compares exportSelectors() of the old and new facets: selectors present only in the new facet are added, those present in both are replaced with the new facet, and those present only in the old facet are removed. If _delegate is not zero, it performs a delegatecall with _delegateCalldata to initialize or migrate state, and emits the DiamondDelegateCall event. If the delegatecall fails, it reverts with the received revert data as is, or with DelegateCallReverted if there is no data. If _tag is not zero or _metadata is not empty, it emits the DiamondMetadata event. A call that only performs a delegatecall without changing any facets is also valid.
Error conditions that prevent upgrade mistakes
An upgradeDiamond implementation must detect the following conditions and revert with the specified errors. Adding a selector that already exists reverts with CannotAddFunctionToDiamondThatAlreadyExists, removing a facet that does not exist reverts with CannotRemoveFacetThatDoesNotExist, replacing a facet with itself reverts with CannotReplaceFacetWithSameFacet, replacing a facet that does not exist reverts with FacetToReplaceDoesNotExist, and replacing a selector that is mapped to a facet other than the one being replaced reverts with CannotReplaceFunctionFromNonReplacementFacet. It must also revert with NoBytecodeAtAddress if the facet address has no bytecode, with ExportSelectorsCallFailed if the exportSelectors() call fails, and with NoSelectorsForFacet if there are zero selectors. A collision where different function signatures share the same 4-byte selector is also treated as an already existing selector and is blocked.
How the Gas Savings Work
The previous approach passed the selectors to the constructor as a bytes4[] array, which cost calldata fees and also the cost of copying them into memory. A facet-based diamond passes only the facet addresses and obtains the selectors onchain, which avoids these costs. On the storage side, the previous approach stored selectors directly, so the number of slots grew in proportion to the number of functions, but a facet-based diamond can store lookup data per facet rather than per function. Only one representative selector per facet is needed, so a single 32-byte slot can hold up to 8 facets, and if implemented as a linked list of facets, a single slot can support lookups with no limit on the number of facets or selectors.
Immutable Diamonds and Custom Upgrade Functions
An upgrade function is not required, so if a diamond is fully configured inside the constructor and no upgrade function is included, it becomes immutable at the moment of deployment. A diamond can also start out upgradeable and later become immutable by removing its upgrade function. Even if you build your own upgrade function instead of the standard upgradeDiamond, the diamond remains compliant with the standard as long as it emits events for facet additions, replacements and removals and its lookup functions keep returning accurate information. This standard does not specify the storage layout, so patterns such as ERC-8042 diamond storage or ERC-7201 namespaced storage can be used.
Compatibility with ERC-2535
A diamond under this standard implements the same lookup functions as ERC-2535, so it is compatible with ERC-2535 tooling that depends on those functions. However, compatibility breaks for events and for the upgrade method. In addition, an existing facet deployed for ERC-2535 cannot be used in a diamond under this standard if it lacks the exportSelectors() function. The status of this proposal has been finalized as Final.
Things to watch out for
- Access Restriction for upgradeDiamond: upgradeDiamond allows arbitrary execution that accesses the diamond's storage through delegatecall, so access to this function must be restricted carefully.
- Use Only Trusted and Verified Facets: Only trusted and verified facets should be added. exportSelectors() must be pure, must not contain logic that can change its return value, and the facet must be immutable. If the returned selectors change, an upgrade can add, remove or replace the wrong selectors and corrupt the diamond.
- Upgrade Integrity Checks: An upgradeDiamond implemented to the specification uses reverts to block several mistakes, including adding a facet that already exists, replacing or removing a facet that does not exist, adding a selector that already exists, addresses with no bytecode, and zero selectors. Selector collisions are also treated as an already existing selector and are blocked.
- Recommendation Against Using selfdestruct: Using selfdestruct in a facet is strongly discouraged. Misuse can cause the diamond or the facet to be deleted.
- Ensuring Transparency: An event is emitted whenever a facet is added, replaced or removed, and the source code can be verified. This lets people and software monitor changes to a diamond and lets experts review its upgrade history.