EIP-2535: A Technical Overview of the Diamond Smart Contract Standard
Introduction
If you're familiar with Ethereum, you'll know that smart contracts are the network's backbone. They enable developers to create decentralized applications (dapps) that are secure, transparent, and trustless.
However, traditional smart contracts have a limitation: they're immutable and unchangeable once deployed. The immutable nature of smart contracts can be a problem when developers need to add new functionalities or fix bugs in smart contracts. That's where the Diamond Standard comes in.
This article will provide a technical overview of the EIP-2535 Diamond Standard, explore its key features and benefits, and build a Diamond Standard smart contract.
Prerequisites
Before diving into the technical details of how the Diamond Standard works, it is important to have a basic understanding of the following concepts:
Smart contracts and their implementation.
The Solidity programming language and its syntax.
Fallback
anddelegatecall
functions in Solidity.Smart contract storage layouts.
Smart contract proxy design patterns.
Understanding these concepts will help you better understand the technical details of the EIP-2535 Diamond Standard.
Requirements
We'll build a Diamond Standard smart contract, so ensure you have the following.
Git; alternatively, you get it from here.
Node.js version 16 or higher.
A code editor such as Visual Studio Code or your preferred editor.
Knowing the command line is essential.
An Overview
What is the Diamond Standard?
The Diamond Standard is a proxy design pattern developed by Nick Mudge. It enables the modularity and upgradeability of smart contracts. The Diamond Standard tackles the immutable nature of smart contracts and allows deployed contracts to be upgraded by developers.
With the Diamond Standard, developers can add, replace or remove smart contract functionalities and can also split these functionalities into different contract parts.
A smart contract that implements this standard is known as a "Diamond", and the contracts that provide different functionalities to the diamond are known as "Facets."
The Diamond Standard works like other proxy standards; they store the data of the smart contract and use the Solidity "fallback" function to make "delegate calls" to facets that contain the actual logic code. Solidity's "delegatecall" function is a low-level function that allows one smart contract to interact with or call another smart contract. It uses the logic of the called contract while preserving and utilizing the caller contract's storage. You can learn more about how it works here.
Now that we've covered more of the Diamond Standard's introduction let's look at some of its key features.
Key features of the Diamond Standard
The EIP-2535 Diamond Standard was designed to address some of the issues that smart contracts face and thus provides useful features to address them, which are as follows:
Smart contract upgradeability: Developers can upgrade smart contracts that implement the Diamond Standard after deployments.
Unlimited functionality smart contracts(Facets): There is no limit to how many facets a diamond can have.
No smart contract code size limit: Since functionalities are shared and split into different facets of a diamond, the diamond does not reach the smart contract code size limit of 24 kilobytes.
Modular and structural arrangement of code and data: Functionalities are separated into various facets, allowing for a modular and structural code arrangement.
One fixed diamond smart contract address supporting different facets: Developers can add multiple facets to a diamond.
Support for already deployed smart contracts: Developers can deploy smart contracts and use them as facets of a diamond if they do not interfere with its internal storage.
Reusability of facets: Diamonds do not restrict facets to themselves. Other diamonds can also reuse them.
Unlike other proxy standards, which support just one implementation contract (facet), the Diamond Standard allows for multiple facets. This characteristic makes the Diamond Standard more flexible than existing proxy standards.
The Diamond Standard Specification
The Diamond Standard comprises contracts and interfaces that interact to form a modular and upgradeable smart contract system.
Diamonds have the following:
- The diamond: It is the proxy contract, and all function calls to the diamond are forwarded to a specific facet with the function. How does the diamond know which facet has that specific function? Well, there is a mapping of function selectors to a facet address. Here is what it looks like in the code:
// struct for facets addresses in the diamond and their function selector position
struct FacetAddressAndPosition {
address facetAddress;
uint96 functionSelectorPosition;
}
// mapping of bytes4 (function selector) to its facet address
mapping(bytes4 => FacetAddressAndPosition) selectorToFacetAndPosition;
The "FacetAddressAndPosition" struct is the struct that stores a facet address and its function selector position. The diamond uses this in its fallback
function to make a delegatecall
to the facet with the particular function selector sent to it.
Let's take a look at how the fallback
function works.
fallback() external payable {
LibDiamond.DiamondStorage storage ds;
bytes32 position = LibDiamond.DIAMOND_STORAGE_POSITION;
// get diamond storage
assembly {
ds.slot := position
}
// this is where the diamond gets the facet for a particular function selector by using the message signature (msg.sig global variable)
address facet = ds.selectorToFacetAndPosition[msg.sig].facetAddress;
require(facet!= address(0), "Diamond: Function does not exist");
// Execute external function from facet using delegatecall and return any value.
assembly {
// copy function selector and any arguments
calldatacopy(0, 0, calldatasize())
// execute function call using the facet
let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
// get any return value
returndatacopy(0, 0, returndatasize())
// return any return value or error to the caller
switch result
case 0 {
revert(0, returndatasize())
}
default {
return(0, returndatasize())
}
}
}
We can see in the above code snippet how the diamond makes this proxy call to different facets. This proxy call is exactly how all diamonds work. Note that even if the diamond makes calls to its facets, the contract storage of those facets is not used or affected. Any state changes affect the diamond itself due to the nature of the delegatecall
function.
- The
IDiamondCut
interface: It defines a function for adding, removing, and updating facets of a diamond. A facet can inherit this interface and has the sole purpose of upgrading the diamond (an upgrade means adding, removing, or replacing facets and their functions), as demonstrated in an implementation by Nick Mudge. When developers use the "diamondCut" function or any other function to upgrade the diamond, the process will emit the "DiamondCut" event, which is mandatory for this standard to keep track of upgrades that have happened on the diamond.
The 'IDiamondCut' interface looks like this.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IDiamondCut {
enum FacetCutAction {Add, Replace, Remove}
struct FacetCut {
address facetAddress;
FacetCutAction action;
bytes4[] functionSelectors;
}
// used for adding, removing, and updating facets in the diamond
function diamondCut(
FacetCut[] calldata _diamondCut,
address _init,
bytes calldata _calldata
) external;
event DiamondCut(FacetCut[] _diamondCut, address _init, bytes _calldata);
}
// the DiamondCut event
event DiamondCut(IDiamondCut.FacetCut[] _diamondCut, address _init, bytes _calldata);
The "FacetCut" struct defines the structure for adding, removing, or replacing facets and their function selectors. The "FacetCutAction" enum specifies what upgrade action is to be performed on the diamond, where "Add" is 0, "Replace" is 1, and "Remove" is 2. When carrying out an upgrade, all of these parameters must be passed. We will see how this is done later in this article.
- The
IDiamondLoupe
interface: contains some read-only functions used to get the details of a diamond, such as its facets and their function selectors (a function selector is the first 4 bytes of a function signature).
interface IDiamondLoupe {
struct Facet {
address facetAddress;
bytes4[] functionSelectors;
}
/// gets all facet addresses and their four-byte function selectors.
function facets() external view returns (Facet[] memory facets_);
/// gets all the function selectors supported by a specific facet
function facetFunctionSelectors(address _facet) external view returns (bytes4[] memory facetFunctionSelectors_);
/// get all the facet addresses used by the diamond
function facetAddresses() external view returns (address[] memory facetAddresses_);
/// gets a facet that has the given function selector
function facetAddress(bytes4 _functionSelector) external view returns (address facetAddress_);
}
- The Diamond Storage: Facets do not store the state variables and data of the diamond. Facets cannot have a state since the diamond use them through proxy calls. Therefore, there must be a way for a diamond to share state variables with its facets. For this, diamonds use a more advanced and structured approach to store their state variables, allowing facets to access them. There is a method for achieving this, and it is known as "Diamond Storage."
library LibDiamond {
// 32 bytes keccak hash to use as a diamond storage location.
bytes32 constant DIAMOND_STORAGE_POSITION = keccak256("diamond.standard.diamond.storage");
// the diamond storage is a struct storage mechanism
struct DiamondStorage {
address owner;
string name;
uint256 age;
}
// this function is used to access the contents of the diamond storage
function diamondStorage() internal pure returns (DiamondStorage storage ds) {
bytes32 position = DIAMOND_STORAGE_POSITION;
assembly {
ds.slot := position
}
}
"Diamond Storage" is a struct storage mechanism where state variables are defined in a struct inside a library. The "diamondStorage" function uses a storage pointer of bytes32 constant DIAMOND_STORAGE_POSITION = keccak256("
diamond.standard.diamond.storage
");
to store the contents of the "DiamondStorage" struct.
This example is a simple example of how Diamond Storage is implemented. For a real implementation and how it is used in a diamond, see here.
Building a Diamond Standard smart contract
Now that we understand the EIP-2535 Diamond Standard and its key features let's explore how to create a smart contract that implements this standard.
In this section, we will walk through the steps of creating a Diamond Standard smart contract and how to upgrade it. We will also cover deploying and interacting with the contract on the local hardhat node.
By the end of this section, you'll have a solid understanding of creating a modular and upgradeable smart contract using the Diamond Standard.
We will use the reference implementation of the Diamond Standard by Nick Mudge, which aids in creating diamonds. It has everything we need to set up a diamond, so we don't have to build the whole structure from scratch. The major thing we will be doing is creating our facet and adding it to the diamond.
We'll create a simple storage facet that updates and returns a greeting string, which will help us understand how diamonds work and how state variables are shared between the diamond and its facet.
So, let's dive in!
- First, run the following commands in the terminal to clone the GitHub repository of the reference implementation contract.
git clone https://github.com/mudgen/diamond-3-hardhat.git
We have now cloned the repository; the next step is to open up the cloned repository folder in our preferred code editor. Visual Studio Code will be used throughout this example.
We see something like this when we open the folder.
Let’s carefully go through the “DiamondCutFacet.sol” (highlighted in the image above), which is used to upgrade the diamond, and the “DiamondLoupeFacet.sol,” which is used to inspect the diamond, found inside the facets folder. We see it implements the IDiamondCut
and IDiamondLoupe
interfaces, as discussed in the previous section.
They are already implemented as facets for the diamond and come with this reference implementation.
Also, notice the "LibDiamond.sol" library. This library is where the Diamond Storage, functionalities to upgrade the diamond, and other functionalities the diamond needs are defined.
Almost everything needed to create a diamond is ready for us, and the diamond contract is already implemented in the "Diamond.sol" file.
- After opening the folder, install the packages needed with npm.
npm install
- Next, delete the "Test1Facet.sol" and "Test2Facet.sol" files in the facets folder and create a "SimpleStorage.sol" file there. It will be the file for our facet.
Remember, we previously said that state variables could not be declared in facets since the diamond does not work with the built-in solidity storage mechanism. Our SimpleStorage facets need to have a string variable that it can update and also return. How do we do this? Well, remember Diamond Storage? Yes, we are using Diamond Storage.
We can declare our variable in the diamondStorage
struct, which can be found inside the .library/LibDiamond.sol
file, or we declare our diamond storage.
For this example, we will declare our diamond storage inside the SimpleStorage.sol
file.
We will start by defining a solidity library that will hold the internal functions of our facet and the diamond storage of our facet.
- Paste the code below to the “SimpleStorage.sol” file.
library libSimpleStorage {
bytes32 constant SIMPLE_STORAGE_POSITION = keccak256("diamond.standard.simple.storage");
struct simpleStorage {
string greeting;
}
// we declare this function in this library to be used in our "SImpleStorage" facet
function getGreeting() internal view returns (string memory) {
return diamondStorage().greeting;
}
// we declare this function in this library to be used in our "SImpleStorage" facet
function setGreeting(string memory _newGreeting) internal {
diamondStorage().greeting = _newGreeting;
}
function diamondStorage() internal pure returns (simpleStorage storage ds) {
bytes32 position = SIMPLE_STORAGE_POSITION;
assembly {
ds.slot := position
}
}
}
Congratulations, we have our state variable and functions ready to update and access it!
- In the "SimpleStorage.sol" file, paste this code above the library we pasted earlier.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleStorage {
// get the greeting string declared in the DiamondStorage
function getGreeting() public view returns (string memory) {
return libSimpleStorage.getGreeting();
}
// update greeting string declared in the DiamondStorage
function setGreeting(string memory _newGreeting) public {
libSimpleStorage.setGreeting(_newGreeting);
}
}
Solidity libraries' internal functions are injected into any contract that uses them, making the function behave as if it were originally declared in the contract. This nature of "solidity libraries" is how the facet shares state variables and internal functions with the diamond if needed because it also refers to the same library and its contents.
We now have everything set up; the next step is to deploy our diamond and add our "SimpleStorage" facet. Remember, this implementation has everything set for us. There is already a deployment script. Open the "deploy.js" file inside the script folder; the code to deploy the diamond is implemented there.
There is a "FacetNames" array declared on lines 33--36 in the "deploy.js" file (depending on the code editor used) that looks like this.
Add the name of our facet to the end of the array; it should now look like this:
All that is needed to deploy our diamond and add our facet to the diamond is done. The code that specifies what upgrade is to be done on the diamond is declared immediately after the "FacetNames" array, and the code that does the upgrade is declared next in the deploy script.
They look like this.
// this defines the upgrade structure and the diamond cut upgrade action; in this case, the FacetCutAction (the enum in IDiamondCut interface) is equal to add
const cut = []
for (const FacetName of FacetNames) {
const facet = await ethers.getContractFactory(FacetName)
const facet = await Facet.deploy()
await facet.deployed()
console.log(`${FacetName} deployed: ${facet.address}`)
cut.push({
facetAddress: facet.address,
action: FacetCutAction.Add, //facet cut action is the add action; therefore, all facets and their function selectors will be added to the diamond
functionSelectors: getSelectors(facet)
})
}
// this is used to perform an upgrade in the diamond
console.log('')
console.log('Diamond Cut:', cut)
const diamondCut = await ethers.getContractAt('IDiamondCut', diamond.address)
let tx
let receipt
let functionCall = diamondInit.interface.encodeFunctionData('init')
tx = await diamondCut.diamondCut(cut, diamondInit.address, functionCall) // the upgrade - this adds the facets to the diamond
console.log('Diamond cut tx: ', tx.hash)
receipt = await tx.wait()
if (!receipt.status) {
throw Error(`Diamond upgrade failed: ${tx.hash}`)
}
console.log('Completed diamond cut')
return diamond.address
}
Now that we have this all setup run this command in the terminal to run the deploy script.
npx hardhat run scripts/deploy.js
Boom! Our diamond is deployed on the local hardhat node. We should see something like this in the terminal if that is successful. Some parts are highlighted for better understanding.
- It's time to interact with the diamond. We will call the functions in the SimpleStorage facet with the address of the diamond and see if this will work. Copy and paste the code below to line 66 or 67 in the deploy.js file, right before the
console.log('Completed diamond cut')
line.
// interacting with the diamond by calling the SimpleStorage facet functions
const simpleStorage = await ethers.getContractAt(
"SimpleStorage",
diamond.address
); // load the deployed diamond with the abi of the SimpleStorage facet.
const setGreeting = await simpleStorage.setGreeting(
"Hello, Diamond Standard!!!"
);
const greeting = await simpleStorage.getGreeting();
console.log(`Greeting: ${greeting}`);
We used ethersJs to load the deployed diamond with the abi of the SimpleStorage facet. This method is valid because the diamond uses this abi to make a delegatecall
to the facet.
Now run the deploy script command again.
npx hardhat run scripts/deploy.js
We got the greeting string!
Congratulations on getting this far! So far, we have learned about the EIP-2535 Diamond Standard and how it works, and we deployed one.
Conclusion
The EIP-2535 Diamond Standard enables developers to build modular and upgradeable smart contracts. It separates contract functionalities into different implementation contract parts called "facets." New functionalities can be added to diamonds without deploying new smart contracts. With the reference implementation by Nick Mudge, creating a Diamond Standard smart contract is straightforward. This article has given you a good understanding of how diamonds work and how they are upgraded.
Next Steps
I recommend reading further about the standard and different implementations of it. A great place to start is the blog dedicated to the Diamond Standard. Also, join the discord server, where experienced developers discuss topics on diamonds. Check out a list of projects that implemented the Diamond Standard.
References
Original EIP-2535 Specification: https://eips.ethereum.org/EIPS/eip-2535
Diamond Hardhat-3 implementation: https://github.com/mudgen/diamond-3-hardhat
Solidity Libraries Can't Have State Variables -- Oh Yes They Can!: https://dev.to/mudgen/solidity-libraries-can-t-have-state-variables-oh-yes-they-can-3ke9
Keep Your Data Right in EIP2535 Diamonds: https://eip2535diamonds.substack.com/p/keep-your-data-right-in-eip2535-diamonds
Simplicity of EIP-2535 Diamonds Standard: https://eip2535diamonds.substack.com/p/simplicity-of-eip-2535-diamonds-standard
Subscribe to my newsletter
Read articles from Jesse Raymond directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Jesse Raymond
Jesse Raymond
Blockchain Developer/Researcher