Proxy contracts are an important tool for smart contract developers. Today, there are many proxy models and corresponding usage rules in the contract system. We have previously outlined the best practices for scalable proxy contract security.

In this article, we will introduce another agency model that is popular in the developer community, namely the diamond agency model.

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 functionality of a contract into smaller contracts (also figuratively called "facets"). Diamonds act as proxies, routing function calls to the appropriate facets.

The Diamond Mode is designed to solve the maximum contract size limit of the Ethereum network. By breaking down a large contract into smaller aspects, the Diamond Mode allows developers to build more complex and feature-rich smart contracts without being affected by the size limit.

Compared to traditional upgradeable contracts, Diamond Proxies offer tremendous flexibility. They allow partial upgrades of contracts, adding, replacing, or removing selected parts of functions without touching other parts.

This article provides an overview of EIP-2535, including a comparison to the widely used transparent proxy mode and UUPS proxy mode, and its security considerations for the developer community.

In the context of EIP-2535, a “diamond” is a proxy contract whose functionality implementation is provided by different logic contracts, called “facets”.

Imagine that a real diamond has different sides, called facets, and the corresponding Ethereum diamond contract also has different facets. Each contract that borrows functions from a diamond is a different side or facet.

The Diamond Standard uses an analogy to expand the capabilities of a "diamond cut" to add, replace, or remove facets and features.

Additionally, the Diamond Standard provides a function called "Diamond Loupe" that returns information about the facets and presence of a diamond.

Compared to the traditional proxy model, a "diamond" is equivalent to a proxy contract, and different "facets" correspond to the implementation contract. Different facets of a diamond proxy can share internal functions, libraries, and state variables. The key components of a diamond are as follows:

A central contract that acts as a proxy, routing function calls to the appropriate aspect. It contains a mapping of function selectors to "aspect" addresses.

A single contract that implements a specific functionality. Each facet contains a set of functions that can be called by a diamond.

is a set of standard functions defined in EIP-2535 that provide information about the facets and function selectors used in diamonds. The Diamond Magnifier allows developers and users to inspect and understand the structure of diamonds.

Functions for adding, replacing, or removing facets in a diamond and their corresponding feature selectors. Only authorized addresses (e.g. the owner of the diamond or a multi-signature contract) can perform diamond cutting.

Similar to traditional proxies, when there is a function call on a Diamond Proxy, the proxy's fallback function is triggered. The main difference from 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 called function. It then uses delegatecall to execute the 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 diamond proxies, the facet address is determined by a hashmap from the caller's msg.sig (function selector) to the facet address, while in traditional proxies, 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 of each function selector. Project staff often need to add, replace, or delete this mapping of function selectors to implementation contracts. EIP-2535 stipulates that: To achieve this purpose, there must be a diamondCut() function. Below is an example interface.

Each FacetCut structure contains a facet address and a four-byte array of feature selectors to be updated in the diamond proxy contract. FaceCutAction allows one to add, replace, and remove feature selectors. The implementation of the diamondCut() function should include adequate access control, prevent collisions of storage slots, recover on failure, etc.

In order to query what functions a diamond agent has and which facets it uses, we use the "diamond magnifying glass". The "diamond magnifying glass" 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 particular 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, or address( 0) if not found. Note that there should not be more than one facet address with the same feature selector.

Given that Diamond Proxy delegates different function calls to different implementation contracts, it is crucial to properly manage storage slots to prevent conflicts. EIP-2535 mentions several storage slot management methods.

This aspect can declare state variables in structures. This aspect can use any number of structures, each with different storage locations. Each structure has a specific location in the contract storage. Aspects can declare their own state variables, but they cannot conflict with the storage locations of state variables declared by other aspects. A sample library and diamond storage contract are provided in EIP-2535, as shown in the figure below:

App storage is a more specialized version of diamond storage. This pattern is used to make sharing state variables between aspects more convenient and easier. An App storage structure is defined to contain any number and type of state variables required by an application. An aspect always declares the AppStorage structure as the first and only state variable, located in storage slot 0. Different aspects can then access variables from this structure.

There are other slot management strategies, including hybrids of Diamond and AppStorage. Some structures are shared between facets, while others are facet-specific. In all cases, it is important to prevent accidental slot collisions.

Comparison with Transparent Proxy and UUPS Proxy

The two main proxy modes currently used by the Web3 developer community are the transparent proxy mode and the UUPS proxy mode. In this section, we briefly compare the diamond proxy mode with the transparent proxy and UUPS proxy modes.

1.EPI-2535:https://eips.ethereum.org/EIPS/eip-2535 #Facets,% 20 State% 20 Variables% 20 and% 20 Diamond% 20 Storage

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

Proxy 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 model, although OpenZeppelin recognizes 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 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 a 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, delete or replace facets and functions. Without DiamondCutFacet and diamondCut(), the Diamond Proxy cannot 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 the existing state variable data, and any state variables after the new state variable may reference the wrong storage location.

The AppStorage pattern requires that one and only one structure be declared for the Diamond proxy and that this structure is shared by all facets. If multiple structures are required, the DiamondStorage pattern should be used.

Do not place a structure directly inside another structure unless you are sure you do not intend to add more state variables to the inner structure. You cannot add new state variables to the inner structure in an upgrade without overwriting variable storage slots declared after the structure.

The solution is to add the new state variables to the storage map structure instead of placing the struct directly in the struct. The storage slots for variables in the map are calculated differently and are not contiguous in the storage.

The size of the 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.

This can cause problems 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 rely on the structure's size and layout being consistent.

Similar to 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 often used to set important variables, such as the addresses of privileged roles. If it is not initialized when the contract is deployed, malicious actors can call and control the contract.

It is recommended to add appropriate access control to the initialization/setup function, or ensure that the function is called when the contract is deployed and cannot be called again.

If any facet in the contract is able to call the selfdestruct() function, it could potentially corrupt the entire contract, leading to loss of funds or data. This is extremely dangerous in the Diamond Proxy model, as multiple facets can access the proxy contract's storage and data.

Currently, we see more and more projects adopting the diamond proxy model in their smart contracts. Compared with traditional proxies, it has flexibility and other advantages.

However, additional flexibility may also mean providing a wider attack surface for attackers. We hope that this article will help the developer community understand the mechanism of the diamond proxy model and its security considerations.

At the same time, the project team should conduct rigorous testing and third-party audits to reduce the risk of vulnerabilities associated with the implementation of the diamond proxy contract.

CertiK will continue to publish technical articles like this to help more developers develop securely. Follow us to get more similar information and news!