CertiK: Best Security Practices for Diamond Proxy Contracts
Author: CertiK
Proxy contracts are an important tool for smart contract developers. Nowadays, there are various proxy patterns and corresponding usage rules in contract systems. We have previously outlined best practices for the security of upgradeable proxy contracts.
In this article, we will introduce another proxy pattern that is quite popular in the developer community, known as the Diamond Proxy Pattern.
The Diamond Proxy Contract, also known as "Diamond," is a design pattern for Ethereum smart contracts introduced by Ethereum Improvement Proposal (EIP) 2535.
The Diamond Pattern allows contracts to have unlimited functionality by splitting the contract's functions into smaller contracts (also vividly referred to as "facets"). The Diamond acts as a proxy, routing function calls to the appropriate facet.
The design of the Diamond Pattern can address the maximum contract size limitation on the Ethereum network. By breaking a large contract into smaller facets, the Diamond Pattern enables developers to build more complex and feature-rich smart contracts without being affected by size constraints.
Compared to traditional upgradeable contracts, Diamond Proxies offer tremendous flexibility. They allow parts of the contract to be upgraded, adding, replacing, or removing selected functions without touching other parts.
This article provides an overview of EIP-2535, including comparisons with widely used Transparent Proxy and UUPS Proxy patterns, as well as its security considerations for the developer community.
In the context of EIP-2535, a "Diamond" is a proxy contract whose functionality is implemented by different logic contracts, referred to as "facets."
Imagine that a real diamond has different sides, called facets; similarly, the corresponding Ethereum Diamond contract has different facets. Each contract that borrows functionality from the diamond is a different side or facet.
The Diamond Standard extends the functionality of "diamond cutting" in an analogical way, allowing for the addition, replacement, or removal of facets and functions.
Additionally, the Diamond Standard provides a feature called "Diamond Loupe," which returns information about the facets and the functions present in the diamond. Compared to traditional proxy patterns, the "Diamond" is equivalent to a proxy contract, while different "facets" correspond to implementation contracts. Different facets of a diamond proxy can share internal functions, libraries, and state variables. The key components of a diamond are as follows:
As the central contract acting as a proxy, it routes function calls to the appropriate facets. It contains a mapping of function selectors to facet addresses.
A single contract that implements specific functionality. Each facet contains a set of functions that can be called by the diamond.
A set of standard functions defined in EIP-2535 that provides information about the facets and function selectors used in the diamond. The Diamond Loupe allows developers and users to inspect and understand the structure of the diamond.
Functions used to add, replace, or remove facets and their corresponding function selectors in the diamond. Only authorized addresses (e.g., the diamond's owner or a multi-signature contract) can perform diamond cutting.
Similar to traditional proxies, when a function call is made on the diamond proxy, the proxy's fallback function is triggered. The main difference with the diamond proxy is that in the fallback function, there is a selectorToFacet mapping that stores and determines which logic contract address has the implementation of the called function. It then uses delegatecall to execute that function, just like a traditional proxy.
All proxies use the fallback() function to delegate function calls to external addresses. Below is the implementation of the diamond proxy and the traditional proxy.
It is worth noting that their assembly code blocks are very similar, so the only difference is the facet address in the diamond proxy's delegate call and the impl address in the traditional proxy's delegate call.
The main difference lies in that in the diamond proxy, the facet address is determined by a hashmap from the caller's msg.sig (function selector) to the facet address, while in the traditional proxy, the impl address does not depend on the caller's input.
Diamond proxy fallback function
Traditional proxy fallback function The SelectorToFacet mapping determines which contract contains the implementation for each function selector. Project staff often need to add, replace, or remove this mapping of function selectors to implementation contracts. EIP-2535 stipulates that to achieve this, there must be a diamondCut() function. Below is an example interface.
Each FacetCut structure contains a facet address and an array of four-byte function selectors for updating in the diamond proxy contract. FacetCutAction allows people to add, replace, and delete function selectors. The implementation of the diamondCut() function should include sufficient access control to prevent storage slot collisions, recover on failure, etc. To query what functions a diamond proxy has and which facets are used, we use the "Diamond Loupe." The "Diamond Loupe" is a special facet that implements the following interface defined in EIP-2535:
The facets() function should return the addresses of all facets and their four-byte function selectors. The facetFunctionSelectors() function should return all function selectors supported by a specific facet. The facetAddresses() function should return all facet addresses used by a diamond.
The facetAddress() function should return the facet that supports a given selector, or address(0) if not found. Note that there should not be more than one facet address with the same function selector.
Given that the diamond proxy delegates different function calls to different implementation contracts, it is crucial to manage storage slots correctly to prevent conflicts. EIP-2535 mentions several methods for storage slot management.
This facet can declare state variables in the structure. This facet can use any number of structures, each with different storage locations. Each structure has a specific location in the contract storage. Facets can declare their own state variables but cannot conflict with the storage locations of state variables declared by other facets. EIP-2535 provides a sample library and diamond storage contract, as shown below: App storage is a more specialized version of diamond storage. This pattern is used to more conveniently and easily share state variables of facets. An AppStorage structure is defined to contain any number and type of state variables required by an application. A facet always declares the AppStorage structure as the first and only state variable, located at storage slot 0. Different facets can then access variables from that structure. Additionally, there are other storage slot management strategies, including a mix of diamond storage and AppStorage. For example, some structures are shared between different facets, while others are specific to certain facets. In all cases, preventing accidental storage slot collisions is very important.
Comparison with Transparent Proxy and UUPS Proxy
Currently, the two main proxy patterns used in the Web3 developer community are the Transparent Proxy Pattern and the UUPS Proxy Pattern. In this section, we briefly compare the Diamond Proxy Pattern with the Transparent Proxy and UUPS Proxy patterns. 1. EIP-2535: https://eips.ethereum.org/EIPS/eip-2535#Facets,%20State%20Variables%20and%20Diamond%20Storage
2. EIP-1967: https://eips.ethereum.org/EIPS/eip-1967
3. Diamond proxy reference implementation: https://github.com/mudgen/Diamond
4. OpenZeppelin implementation: https://github.com/OpenZeppelin/openzeppelin-contracts/tree/v4.7.0/contracts/proxy
Proxies and upgradeable solutions are complex systems, and OpenZeppelin provides libraries and comprehensive documentation for UUPS, Transparent, and Beacon upgradeable proxies. However, for the Diamond Proxy Pattern, while OpenZeppelin acknowledges its benefits, they have still decided not to include the implementation of EIP-2535 diamonds in their library.
Therefore, developers using existing third-party libraries or implementing the solution themselves must exercise extra caution during implementation. Here, we have compiled a checklist of security best practices for the developer community's reference. By breaking down contract logic into smaller, more manageable modules, developers can more easily test and audit their code.
Additionally, this approach allows developers to focus on building and maintaining specific aspects of the contract rather than managing a complex, monolithic codebase. The end result is a more flexible and modular codebase that can be easily updated and modified without affecting other parts of the contract. Source: Aavegotchi Github
When a diamond proxy contract is deployed, it must add the address of the DiamondCutFacet contract to the diamond proxy contract and implement the diamondCut() function. The diamondCut() function is used to add, remove, or replace facets and functions; without DiamondCutFacet and diamondCut(), the diamond proxy cannot function properly. Source: Mugen's Diamond-3-Hardhat When adding new state variables to the storage structure in a smart contract, they must be added to the end of the structure. Adding new state variables at the beginning or in the middle of the structure can lead to new state variables overwriting existing state variable data, and any state variables after the new state variable may reference incorrect storage locations.
The AppStorage pattern requires declaring one and only one structure for the diamond proxy, and that structure is shared by all facets. If multiple structures are needed, the DiamondStorage pattern should be used. Do not place structures directly inside another structure unless you are sure you do not intend to add more state variables to the inner structure. If you do not overwrite the storage slots declared after the structure, you cannot add new state variables to the inner structure during upgrades.
The solution is to add new state variables to the storage mapping structure instead of placing a "structure" inside another "structure." The way variables in the mapping are calculated is different, and they are not contiguous in storage.
The size of an array will be affected by the size of the structure. When a new state variable is added to a structure, it changes the size and layout of that structure.
If that structure is used as an element in an array, it may cause issues. If the size and layout of the structure change, then the size and layout of the array will also change, which may lead to problems with indexing or other operations that depend on the consistency of the structure's size and layout.
Like other proxy patterns, each variable should have a unique storage slot. Otherwise, two different structures at the same location will overwrite each other.
The initialize() function is typically used to set important variables, such as the addresses of privileged roles. If not initialized at contract deployment, malicious actors can call and take control of the contract.
It is recommended to add appropriate access control to the initialization/setup function or ensure that this function is called during contract deployment and cannot be called again.
If any facet in the contract can call the selfdestruct() function, it has the potential to destroy the entire contract, leading to loss of funds or data. This is extremely dangerous in the diamond proxy pattern, as multiple facets can access the storage and data of the proxy contract. Currently, we see an increasing number of projects adopting the diamond proxy pattern in their smart contracts. Compared to traditional proxies, it offers flexibility and other advantages.
However, the additional flexibility may also mean providing attackers with a broader attack surface. We hope this article helps the developer community understand the mechanisms of the diamond proxy pattern and its security considerations.
At the same time, project teams should conduct rigorous testing and third-party audits to reduce the risk of vulnerabilities associated with implementing diamond proxy contracts.