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
event DiamondCut(FacetCut[] _diamondCut, address _init, bytes _calldata);: Each time functions are added, replaced, or removed, it records the changed facets and selectors, the initialization target, and the call data.function facets() external view returns (Facet[] memory facets_);: Returns all facet addresses and each facet's 4-byte function selectors.function facetFunctionSelectors(address _facet) external view returns (bytes4[] memory facetFunctionSelectors_);: Returns the list of function selectors supported by a specific 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 responsible for the given selector, or address(0) if there is none.
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
- Ownership and authentication design is not part of the standard: How to design and implement ownership and authentication for a diamond is not part of this standard. The examples in the source and the reference implementation are only examples of possible approaches. Everything is possible, from a simple scheme where one account has the right to add, replace and remove functions, to one where a decentralized autonomous organization (DAO) can change only specific functions, to a consensus scheme where diamondCut is executed only after several people have called approval functions. The source encourages the development of standards and implementations for ownership, control and authentication.
- diamondCut allows arbitrary execution with access to storage: The diamondCut function allows arbitrary code execution with access to the diamond's storage through delegatecall. Access to this function must be restricted carefully.
- Do not use selfdestruct in facets: It is strongly recommended not to use selfdestruct in facets. Misuse can cause the diamond or a facet to be deleted.
- Function selector collisions: If a selector collision occurs, where the 4-byte hashes of different function signatures are the same, adding a new function could overwrite an existing one. A properly implemented diamondCut prevents adding a selector that already exists, so this scenario does not happen.
- All changes are visible through events: A diamond emits an event whenever a function is added, replaced or removed, and all source code can be verified. People and software can monitor changes to the contract, so a malicious function being added would be noticed. Security and domain experts can review the change history to look for traces of misconduct.