Summary
This article delves into the ERC-2535 diamond contract pattern and introduces a decision tree to aid in determining whether the diamond pattern may be an appropriate design choice for a given contract or a system of contracts. It also explores diamond storage and offers guidance on when to consider employing it.
The ERC-2535 Diamond Standard
The ERC-2535 diamond contract pattern is a design pattern that facilitates access to an almost unlimited number of functions from a single contract. These functions, known as “facets,” can be customized to be upgradeable (upgradeable diamond), immutable (single-cut diamond), or locked (finished diamond), depending on the contract’s specific logic. Facets can be defined in separate contracts or libraries and are accessed via delegatecall from the base diamond contract.
The diamond standard presents several advantages over traditional contracts:
- No size constraints: Solidity contracts are limited to 24kb in size. The diamond contract, by accessing facets in various external contracts or libraries through delegatecall, allows the bytecode logic to expand across an unlimited number of contracts, enabling arbitrary sizing.
- Fine-grained upgrades: Privileged accounts can individually add, upgrade, or remove functions. This is particularly useful for contracts designed to evolve over time and can provide a simpler alternative to the transparent upgradeable proxy pattern.
- Other benefits, such as atomic upgrades and enhanced interoperability, are detailed on the EIP 2535 page.
Less frequently discussed are potential drawbacks of diamond contracts compared to traditional contracts:
- Deviation from EVM’s native contract and library abstractions: The diamond pattern introduces complexity to a contract system and should only be used when specific advantages are sufficiently beneficial (as listed above).
- Explicit upgradeability logic required: Unlike some patterns, the diamond pattern does not inherently offer upgradeability or immutability. Instead, these behaviors must be programmed into the base contract. While highly flexible, users must inspect this logic to understand the potential future behavior of the smart contract.
- Deployment-specific logic: Base diamond contracts do not fully encapsulate their logic in one place. Their behavior depends significantly on how the contract was configured during deployment or through subsequent transactions. While all contracts’ behavior is influenced by post-deployment actions, the highly flexible diamond pattern can further obscure how a deployed contract will behave when a function is called.
- Interface conformance: Compiler-level interface conformance is no longer guaranteed with the diamond pattern and typically necessitates workarounds. Interface-driven design remains a powerful tool for smart contract systems, particularly for downstream indexing systems (e.g., subgraphs, custom event indexing, alert monitoring), and frontend designs.
Diamond Storage
Diamond storage represents a contract storage style that departs from Solidity’s default storage layout. Instead, it specifies that variables should be stored in structs located at deterministic slots in contract storage. Diamond storage can be employed by any smart contract, including those that don’t adhere to the ERC-2535 diamond pattern.
A straightforward example of diamond storage is presented below:
library DiamondStorage {
bytes32 constant STRUCT_POSITION =
keccak256("lib.examplelibrary.examplestruct");
struct ExampleStruct {
uint var1;
bytes var2;
mapping (address => uint) var3;
}
function exampleStructStorage()
internal
pure
returns (ExampleStruct storage exampleStruct)
{
bytes32 position = STRUCT_POSITION;
assembly {
examplestruct.slot := position
}
}
}
This diamond storage can then be utilized as follows:
function exampleFunction()
external
{
// Load example struct pointer from storage
DiamondStorage.ExampleStruct storage exampleStruct =
DiamondStorage.exampleStructStorage();
// Access and modify variables in storage
exampleStruct.var1 = 10;
uint var3 = exampleStruct.var3[address(this)];
// ...
}
While diamond storage does introduce complexity to a contract’s design, it can be particularly beneficial in the following scenarios:
- Avoiding gap storage: When employing diamond storage, parent contracts of an upgradeable contract can bypass the need for “gap storage.” Gap storage is a problematic and overly complex design pattern that also limits the upgradeability of parent contracts. OpenZeppelin, for instance, transitioned to diamond storage to eliminate their use of gap storage, as described here.
- Enabling native storage sharing and access: Libraries are inherently stateless, but they can access and manipulate storage during a delegatecall. Diamond storage allows libraries to access storage natively without requiring the passing of storage references. Additionally, any contract within a system can directly access an appropriate storage slot based on a struct’s position identifier, which can be simpler than managing storage references.
Lastly, a variant of diamond storage referred to as “app storage” may be appropriate in some cases to simplify the logic around diamond storage. While not discussed in detail here, it should be considered a subset of diamond storage and explored if diamond storage is under consideration.
When to Use Diamond Contracts and/or Diamond Storage
The choice to employ the diamond pattern and/or diamond storage should align with the goals of a specific contract. It is akin to other design decisions, such as opting for an upgradeable versus immutable contract or choosing between a contract and a proxy.
In general, the following decision chart can serve as a guide to determine whether to consider the diamond pattern and/or diamond storage. This simplified diagram makes inherent design decisions based on the benefits and drawbacks of these patterns while aiming to guide designers toward contracts with minimal complexity that still fulfill the intended objectives.
Prerequisites
- Determine whether the contract should be upgradeable or immutable, a decision rooted in philosophy and security goals.
- Decide whether the contract will use inheritance, a library-based design, or be a standalone contract.
- Determine whether the contract will approach or exceed the contract size limit (>~20kb) or remain relatively small.
- Assess whether multiple copies of the same contract will be deployed.
Decision Tree: Diamond Pattern
- Is the contract upgradeable?
- Is the contract nearing or exceeding the contract size limit?
- Consider employing the diamond pattern to evade size limitations.
- The contract is comfortably below the size limit
- Is fine-grained upgradeability desired?
- Contemplate the use of the diamond pattern to provide fine-grained upgradeability.
- Fine-grained upgradeability is unnecessary
- Consider using the slightly more straightforward transparent upgradeable proxy pattern.
- Is fine-grained upgradeability desired?
- Is the contract nearing or exceeding the contract size limit?
- The contract is immutable?
- Is the contract nearing or exceeding the contract size limit?
- Consider offloading logic to external libraries, external contracts, or using a diamond pattern to access specific function logic.
- The contract is comfortably below the size limit
- Will there be many duplicate deployments with identical bytecode?
- Evaluate the transparent proxy pattern to reduce deployment costs compared to traditional contracts.
- Will the contract have few instances or prioritize clear bytecode?
- Consider employing traditional contract design for simplicity and clarity.
- Will there be many duplicate deployments with identical bytecode?
- Is the contract nearing or exceeding the contract size limit?
Decision Tree: Diamond Storage
- Is the contract employing the diamond pattern?
- Consider implementing diamond storage.
- Is the contract upgradeable?
- Is the contract utilizing inheritance?
- Consider implementing diamond storage to avoid problematic gap storage.
- The contract does not use inheritance
- Does the contract employ libraries that access storage?
- Consider using diamond storage to avoid the need to pass storage references, compared to the native Solidity storage design. Both options are valid, and there may be no clear winner.
- The contract does not use libraries that access storage
- Consider using native Solidity storage.
- Does the contract employ libraries that access storage?
- Is the contract utilizing inheritance?
- The contract is immutable
- Does the contract use libraries that access storage?
- Consider using diamond storage to avoid the need to pass storage references, compared to the native Solidity storage design. Both options are valid, and there may be no clear winner.
- The contract does not use libraries that access storage
- Consider using native Solidity storage.
- Does the contract use libraries that access storage?
The above “uses libraries” reference pertains to any operation executing logic within the base contract’s context, including internal functions of imported libraries or delegate call operations to external contracts.
Examples on Ethereum
Below is a list of real-world examples that align with the decision tree outlined in the previous section:
- Aavegotche
- This contract is highly interactive, upgradeable, and likely to approach or exceed the single-contract size limit. It utilizes the ERC-2535 diamond standard to support fine-grain upgrades and avoid exceeding the contract size limit.
- Gnosis Safe
- The contract is immutable, not near the size limit, and will experience numerous duplicate deployments. It employs the transparent proxy standard (non-upgradeable variant) to reduce the cost of creating new Safe contracts.
- OpenZeppelin upgradeable parent contracts
- OpenZeppelin contracts that are both upgradeable and employ inheritance have recently transitioned to using diamond storage to eliminate problematic gap storage.
- Art Blocks Shared Minter Suite
- These contracts are immutable and employ a library-based design. These contracts originally opted to pass storage references to libraries, but were redesigned to use diamond storage due to some favorable patterns that emerged when a couple other changes were made. This serves as an example of a contract architecture with multiple valid technical approaches to managing storage.