A diamond is a proxy contract that delegatecalls to multiple implementation contracts called facets.
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.
Through 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.
In the past, deploying and upgrading diamonds suffered from:
This standard reduces gas costs and eliminates off-chain selector management:
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.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.
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())}
}
}
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).
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.
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.
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.
delegatecallsThis 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);
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);
A facet-based diamond MUST implement the following:
fallback() function.delegatecall.FunctionNotFound(bytes4 _selector).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.facets()facetFunctionSelectors(address _facet)facetAddresses()facetAddress(bytes4 _functionSelector)exportSelectors() function, which returns a bytes array.receive() functionA diamond MAY have a receive() function.
upgradeDiamond FunctionImplementing 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:
exportSelectors() on the facet to obtain its function selectors. exportSelectors() on the old facet to obtain its packed selectors.exportSelectors() on the new facet to obtain its packed selectors.exportSelectors() on the facet to obtain its packed selectors./**
* @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;
}
/**
* @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.
_addFacets array of facet addresses to add._replaceFacets array of (oldFacet, newFacet) pairs._removeFacets array of facet addresses to remove.
Execution Order
Remove facets
Event Emission
Every change to a facet MUST emit exactly one of:
FacetAddedFacetReplacedFacetRemovedError Conditions
The implementation MUST detect and revert with the specified error when:
CannotAddFunctionToDiamondThatAlreadyExists.CannotRemoveFacetThatDoesNotExist.CannotReplaceFacetWithSameFacet.FacetToReplaceDoesNotExist.CannotReplaceFunctionFromNonReplacementFacet.Facet Validation
NoBytecodeAtAddress.exportSelectors() is missing, reverts, or cannot be called successfully, revert with ExportSelectorsCallFailed.If exportSelectors() returns zero selectors, revert with NoSelectorsForFacet.
Delegate Validation
If _delegate is non-zero but contains no bytecode, revert with NoBytecodeAtAddress.
Delegatecall Execution
_delegate is non-zero, the diamond MUST delegatecall _delegate with _delegateCalldata.delegatecall fails and returns revert data, the diamond MUST revert with the same revert data.delegatecall fails and returns no revert data, revert with DelegateCallReverted.delegatecall is performed, the diamond MUST emit the DiamondDelegateCall event._delegateCalldata MAY be empty. If empty, the delegatecall executes with no calldata.
Metadata Event
_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).
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.
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.
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 ValueexportSelectors() returns bytes rather than bytes4[] for two reasons:
bytes4[] Wastes MemoryEach 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.
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.
This upgrade function specified by this standard is optional.
This means a couple things:
A Diamond does not have to have an upgrade function.
A diamond can be fully constructed within its constructor function without adding any upgrade function, making it immutable upon deployment.
A large immutable diamond can be built using well organized facets.
A diamond can initially be upgradeable, and later made immutable by removing its upgrade function.
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.
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:
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.
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.
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.
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 are separately deployed, independent units, but can share state and functionality in the following ways:
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.
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.
upgradeDiamondThe upgradeDiamond function allows arbitrary execution with access to the diamond’s storage (through delegatecall). Access to this function must be restricted carefully.
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.
The specified upgradeDiamond behavior prevents a number of upgrade mistakes. Upgrades revert when:
exportSelectors() successfully.Selector collisions (two different signatures with the same 4-byte selector) are handled as “selector already exists” and are therefore prevented.
Use of selfdestruct in a facet is heavily discouraged. Misuse of it can delete a diamond or a facet.
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 and related rights waived via CC0.