ERC-2535 Diamond Multi-Facet Proxy (Diamonds, Multi-Facet Proxy)

Final · Other · created 2020-02-22

Defines a modular contract structure in which multiple facets, pieces of functionality, are attached behind a single contract address, so functionality can be added, replaced, and removed even after deployment. There is effectively no size limit either.

Why it is needed

A single Ethereum contract cannot exceed 24KB. An app with many features has to split its code across several contracts, which means multiple addresses and makes deployment, testing, and integration with other software cumbersome. Things like error messages are sometimes removed to fit the size limit. To fix a bug or attach a new standard after deployment, existing functionality has to be redeployed as well, and upgrading across several transactions risks corrupting data midway. Also, for an upgradeable contract, verified source code alone does not reveal which functions it currently contains, so users and tools had no common way to look inside. This standard solves these problems with a structure for attaching and detaching functionality under one address, plus a common interface for recording and querying those changes.

What to implement

In depth

Definitions of diamond, facet, loupe, and immutable function

A diamond is a facade contract that receives external calls and delegatecalls them to facets. It has state, and its data accumulates in the diamond's contract storage. A facet is a stateless contract or Solidity library with external functions. Some or all functions of a facet, once deployed, are added to one or more diamonds. A facet does not keep data in its own storage, but it can define state and read and write the diamond's storage. A loupe facet is a facet that provides lookup functions for inspecting the inside. An immutable function is an external function that is defined directly in the diamond or is blocked by logic from modification, so it cannot be replaced or removed. In this document, a function on its own means an external function, and what applies to external functions also applies to public functions. A mapping only means a link between two things and does not refer to any specific implementation.

The fallback function finds the facet by selector and delegatecalls it

When an external function is called on a diamond, an immutable function defined directly on the diamond runs immediately. Otherwise the fallback function runs, finds the facet linked to the function selector (the first 4 bytes of the call data), and executes that function with delegatecall. Because it is a delegatecall, msg.sender and msg.value do not change, and the only storage that is read and written is the diamond's. In other words, the facet's function behaves as if the diamond itself had implemented it. If no facet is linked to the selector, a default function can optionally be executed. If there is no default function and no other way to handle the call, execution reverts. This standard does not specify how to implement the default function. The fallback example in the original text gets the facet address from selectorTofacet[msg.sig], copies the call data with calldatacopy, then receives the delegatecall result with returndatacopy and either returns it as is or reverts.

Storage layout and Solidity library facets

Solidity's default storage layout does not support proxies or diamonds, so a separate state variable layout pattern is needed. This EIP does not specify a concrete layout, and a later proposal may define one. The source names Diamond Storage and AppStorage as examples that can be used together. Facets share state variables by placing the same struct at the same storage location, and share internal functions by inheriting the same contract or using the same library. All data lives in the diamond's storage, but the access scope can differ from facet to facet. A deployed Solidity library can also be a facet, but only a library with at least one external function is deployed and becomes a facet. A library with only internal functions cannot be deployed and cannot be a facet. Its code is included in the bytecode of the facets that use it and serves to share internal functions between facets. A library facet cannot be deleted and has no state, and by syntax it cannot declare state variables outside Diamond Storage.

The IDiamond interface and the DiamondCut event

Every diamond must implement the IDiamond interface. The FacetCutAction enum is Add=0, Replace=1, Remove=2. The FacetCut struct has three fields, facetAddress, action and functionSelectors (a bytes4 array), which describe which selectors of which facet are changed and how. The DiamondCut event records _diamondCut (a FacetCut array), _init and _calldata. A DiamondCut event must be emitted whenever an external function is added, replaced or removed. This applies to every upgrade of any kind, whether or not it goes through the diamondCut function. At deployment, the immutable functions and all added external functions must also be emitted in a DiamondCut event. The events accumulated this way form the full history of function changes for the diamond.

The Add, Replace and Remove rules of diamondCut and the execution of _init

A diamond whose function selector mappings can be changed after deployment should implement IDiamondCut. diamondCut updates multiple functions of multiple facets in a single transaction. This is to prevent data corruption that can occur in upgrades spread over multiple transactions. For each FacetCut, if the action is Add, each entry in functionSelectors is mapped to facetAddress, and the call reverts if any selector is already mapped. If it is Replace, the mapping is changed to facetAddress, and the call reverts if the existing value equals facetAddress or the selector is empty. If it is Remove, the mapping is deleted, and the call reverts if any selector was empty. An attempt to replace or remove an immutable function must revert. Requiring the intent to be explicit in this way catches upgrade mistakes. After the function changes are done, _calldata is executed on the _init address with delegatecall to initialize or clean up data. If _init is address(0), the execution is skipped, and _calldata may then be zero bytes or arbitrary information. For interoperability, tools and UIs should expect and use this standard diamondCut function.

Four loupe functions to inspect facets and functions

A diamond must implement IDiamondLoupe so that its facets and functions can be inspected. facets() returns all facet addresses and their 4-byte selectors as an array of Facet structs. facetFunctionSelectors(_facet) returns the list of selectors a specific facet supports, facetAddresses() returns the list of facet addresses in use, and facetAddress(_functionSelector) returns the address of the facet responsible for that selector, or address(0) if there is none. If there are immutable functions, the loupe functions must return that information too, and the facet address in that case is the diamond's own address. With an ordinary contract, you can learn its functions just by looking at the verified source code, but a diamond's source code does not list which functions it has, so this means of inspection is needed. The source lists these uses: showing diamond information in a UI and letting users call functions, getting source code and ABIs from services such as Etherscan, verifying that a transaction adding, replacing or removing functions succeeded, and use in deployment and upgrade tools and tests. Checking with facetAddress which facet address implements a function tells you the version of the function.

The EIP-165 interface ID and the address that does not change

Implementing supportsInterface from EIP-165 is optional. If a diamond has a diamondCut function, its interface ID is IDiamondCut.diamondCut.selector. The loupe interface ID is IDiamondLoupe.facets.selector ^ IDiamondLoupe.facetFunctionSelectors.selector ^ IDiamondLoupe.facetAddresses.selector ^ IDiamondLoupe.facetAddress.selector, the XOR of the four selectors. The address users interact with is the diamond address, and this address does not change. Only facet addresses change, through diamondCut or other functions. Two ways of implementing an interface are also allowed. Implementing it directly, as in contract Contract is Interface, or implementing it by adding functions from one or more facets are both treated by this proposal as implementing the interface.

Gas considerations and how to share functions between facets

Delegating with delegatecall adds a small gas overhead, but the source lists ways to mitigate it. Because there is no size limit, gas-optimized functions for specific uses can be added, such as putting a batch transfer function in an EIP-721 implementation. Merging a structure that called several contracts in one transaction into a single diamond and accessing storage directly saves gas. A contract with few functions has cheaper call gas than one with many, so a facet can hold few external functions, and a high Solidity optimizer setting can reduce execution gas. Ways to use a function from another facet include copying the internal function code, putting a common internal function in a contract or library that several facets inherit, calling the external function in a type-safe way such as MyOtherFacet(address(this)).myFunction(arg1, arg2), looking up the facet address in selectorToFacet in storage and calling it with delegatecall, and creating an internal version of the external function and including it in the facets that need it. Two external functions with the same function signature cannot be added to one diamond at the same time. Not every function of a facet needs to be added to the diamond, and one deployed facet can be used by several diamonds.

Things to watch out for

Original ERC · ← ERCs