Best Security Practices for Diamond Agent Contracts
Top Security Tips for Diamond Agent Contracts
Proxy contracts are an important tool for smart contract developers. Today, there are various proxy modes and corresponding usage rules in the contract system. We have previously outlined the best security practices for upgradeable proxy contracts.
In this article, we will introduce another popular proxy mode in the developer community, 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 a contract’s functionality into smaller contracts, also known as “facets,” and allowing the proxy to route function calls to the appropriate facet.
- Include “familiarity with AI application software” in j...
- Opside Whitepaper V2 Released: Introducing Multi-Chain ZK-PoW Mecha...
- Stablecoin protocol Reserve invests $20 million in Convex, Curve, a...
The diamond pattern’s design solves the Ethereum network’s maximum contract size limit problem. By breaking down a large contract into smaller facets, the diamond pattern allows developers to build more complex and feature-rich smart contracts without being affected by size limitations.
Compared to traditional upgradeable contracts, diamond proxies provide tremendous flexibility. They allow contracts to be partially upgraded, adding, replacing, or removing selected portions of functions without touching other parts.
This article provides an overview of EIP-2535, including a comparison with the widely used transparent proxy pattern and UUPS proxy pattern, as well as its security considerations for the developer community.
With EIP-2535, a “diamond” is a proxy contract whose functionality is provided by different logical contracts, called “facets.”
Imagine a real diamond with different facets, called facets, and the corresponding Ethereum diamond contract has different facets. Each contract that borrows functionality from a diamond is a different facet or facet.
The diamond standard extends the functionality of “diamond cutting” by analogy, to add, replace, or delete facets and functions.
In addition, the diamond standard provides a function called “Diamond Loupe,” which returns information about facets and the functions that exist in the diamond.
Compared with traditional proxy patterns, “diamond” is equivalent to a proxy contract, and different “facets” correspond to the 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:
The central contract acting as a proxy, routing function calls to the appropriate facet. It contains a function selector to facet address mapping.
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.
A function for adding, replacing, or removing facets and their corresponding function selectors in the diamond. Only authorized addresses (e.g., the owner of the diamond or a multisig contract) can perform diamond cutting.
Similar to traditional proxies, when a function call is made on a diamond proxy, the fallback function of the proxy 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 logical contract address has the implementation of the function being called. It then uses delegatecall to execute that function, just like a traditional proxy.
All proxies use the fallback() function to delegate function calls to an external address. 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 delegate call and the impl address in the traditional proxy delegate call.
The main difference is that in the diamond proxy, the facet address is determined by a hashmap of the caller’s msg.sig (function selector) to the facet’s address, while in the traditional proxy, the impl address is independent of the caller’s input.
Diamond Proxy Fallback Function
Traditional fallback function for proxies
The SelectorToFacet mapping determines which contract contains the implementation of each function selector. Project staff often need to add, replace, or remove this function selector-to-implementation-contract mapping. EIP-2535 requires that there be a diamondCut() function for this purpose. Here is an example interface.
Each FacetCut structure includes 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 remove function selectors. The implementation of diamondCut() should include sufficient access control, collision prevention for storage slots, and recovery on failure, among other things.
To query what functions a diamond proxy has and which facets they use, we use a “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 the given selector and address(0) if none is found. Note that there should not be more than one facet address with the same function selector.
Given that a diamond proxy delegates different function calls to different implementation contracts, properly managing storage slots to prevent conflicts is crucial. EIP-2535 mentions several storage slot management methods.
This facet can declare state variables in its structure. This facet can use any number of structures, each with a different storage location. Each structure has a specific location in the contract storage. Facets can declare their own state variables, but they cannot conflict with the storage location 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 mode is used to share the state variables of facets more conveniently and easily. An App Storage structure is defined as containing 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 in slot 0 of the storage. Different facets can then access variables from this structure.
There are also other storage slot management strategies, including a combination of Diamond Storage and App Storage. For example, some structures are shared among different facets, while others are specific to certain facets. In all cases, preventing accidental storage slot collisions is crucial.
Comparison with Transparent Proxy and UUPS Proxy
The two main proxy patterns used by the Web3 developer community are the Transparent Proxy pattern and the UUPS (Upgradeable) Proxy pattern. In this section, we briefly compare the Diamond Proxy pattern with the Transparent and UUPS Proxy patterns.
1. EPI-2535: https://eips.ethereum.org/EIPS/eip-2535#Facets,%20State%20Variables%20and%20Diamond%20Storage
2. EPI-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 more complex systems, and OpenZeppelin provides code libraries and comprehensive documentation for UUPS, Transparent, and Beacon upgradeable proxies. However, for the Diamond Proxy pattern, although OpenZeppelin acknowledges its benefits, they still decided not to include the implementation of EIP-2535 Diamond in their library.
Therefore, developers who use existing third-party libraries or implement this solution themselves must be extra cautious when implementing it. Here, we have compiled a list of security best practices for the developer community to refer to.
By breaking contract logic into smaller, more easily 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 the 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, and without DiamondCutFacet and diamondCut(), the Diamond Proxy will not work properly.
Source: Mugen’s Diamond-3-Hardhat
When adding a new state variable to a storage structure in a smart contract, it must be added to the end of the structure. Adding a new state variable at the beginning or middle of the structure will cause the new state variable to overwrite 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 that is shared by all facets. If multiple structures are needed, the DiamondStorage pattern should be used.
Do not directly nest structures within one another unless it is certain that no more state variables will be added to the internal structure in the future. It will not be possible to add new state variables to the internal structure during an upgrade if the struct is not overwriting the declared variable storage slot.
The solution is to add the new state variable to a storage mapping structure instead of directly placing the “struct” within the “struct”. Variables slots in a mapping are calculated differently and 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 will change the size and layout of that structure.
This can cause issues if the structure is used as an element in an array. If the size and layout of the structure changes, the size and layout of the array will also change, which can cause problems with indexing or other operations that depend on the structure size and layout being consistent.
Like other proxy patterns, each variable should have a unique storage slot. Otherwise, two different structures at the same position will overwrite each other.
The initialize() function is usually used to set important variables, such as the address of a privileged role. If it is not initialized during contract deployment, a malicious actor can call it and take control of the contract.
It is recommended to add appropriate access controls on initialization/setup functions, or ensure that the function is called during contract deployment and cannot be called again.
If any aspect of the contract is able to call the selfdestruct() function, it has the potential to destroy the entire contract, leading to loss of funds or data. This is particularly dangerous in the diamond proxy pattern, as multiple aspects can access the storage and data of the proxy contract.
Currently, we are seeing more and more projects adopting the diamond proxy pattern in their smart contracts. It has more flexibility and other advantages compared to traditional proxies.
However, the extra flexibility can also mean a wider attack surface for attackers. We hope this article helps the developer community understand the mechanics of the diamond proxy pattern and its security considerations.
Meanwhile, project teams should conduct rigorous testing and third-party audits to minimize vulnerability risks associated with implementing diamond proxy contracts.