A Beginner’s Guide to The Diamond Standard Proxy
Table of Content
- History
- Introduction
- How Diamond Standard Works
- Storage Types — Diamond Storage and AppStorage
- Diamond Standard Implementation
- Pros and Cons
- Conclusion
HISTORY
The Diamond Standard also known as an EIP-2535 is a form of an Upgradeable Proxy Pattern which was created by Nick Mudge.
It was birthed out of the problem Nick faced as he was implementing an ERC721 token that could control other ERC721 tokens and ERC20 tokens but he was limited by the fact that a smart contract can only accommodate 24 KB Size of data. Once this limit is reached, nothing can be added to the smart contract code.
Nick created EIP-2535 Diamond to solve the 24KB contract limit, but it turned out this solution is applicable beyond the Data size scope. EIP-2535 also provides a framework for building larger smart contract systems that can grow as time develops without data limits.
INTRODUCTION
Just like the name implies, a Diamond is a single item with different faces/cuts. In our case, it can be translated as a Single Smart Contract that contains different internal contracts called Facets. You can think of the Diamond as the mother contract which contains only the state variables which is where data is stored while the facets contain only the functionalities/logic.
HOW DIAMOND STANDARD WORKS
- A diamond is a smart contract. It has a single Ethereum address which users interact with.
- All state variables are stored in the diamond proxy(mother contract). The diamond proxy keeps track of the storage data.
- A diamond contains a set of internal contracts called facets(the different cuts of the diamond).
- A Proxy contract doesn’t have any external functions of its own — it uses facets for external functions which read/write its data. This makes facets easy to write and gas efficient.
- Each facet has its own address and contains the logic and functionalities which the diamond calls.
- Each facet or library used by it also declares structs and state variables that it reads and writes.
- State variables and structs are defined in facets but the actual data is stored in the Proxy contract.
- The Proxy contract contains a fallback function that uses a Delegatecall to route the external function calls to facets.
Here is another diagram to visualize how the Diamond standard works:
In the diagram above, a diamond stores within it a mapping of function selector to facet address. You can see that functions func1 and func2 are associated with FacetA. Functions func3, func4, func5 are associated with FacetB. Functions func6 and func7 are associated with FacetC.
Also in the diagram, you see that different structs within the diamond are used by different facets.
- FacetA uses DiamondStorage3.
- FacetB uses DiamondStorage3 and DiamondStorage2.
- FacetC uses DiamondStorage2 and DiamondStorage1.
The diagram above shows the DiamondStorage structs in the diamond. It is true that all contract storage data is stored in the diamond, not in its facets. But the struct definitions exist in the facet source code.
STORAGE TYPES IN DIAMOND STANDARD
A key part of the Diamond Standard is how it helps you with contract storage. Diamond Storage and AppStorage are two-state variable storage patterns that have been successfully used in diamonds.
Diamond Storage
By default, when you create new state variables like unsigned integers, structs, mappings, etc. Solidity automatically takes care of where exactly these things are stored within the contract storage. But this default functionality becomes a problem when upgrading diamonds with new facets. New facets declaring new state variables clashes with existing state variables i.e data for new state variables gets written to where existing state variables exist.
Diamond Storage solves this problem by enabling you to specify where your data gets stored within the contract storage. This might sound risky but it is not if you use a hash of a string that applies to your application or by being specific to your application by using the hash as the starting location of where to store your data in contract storage.
Doing that might seem risky to you too, but note that this is how Solidity’s storage location mechanism works for maps and arrays. Solidity uses hashes of data for starting locations of data stored in contract storage.
When an external function is called on a diamond, its fallback function is executed. The fallback function finds the facet through the mapping which has the function that has been called and then executes that function from the facet using delegatecall.
The image above shows how functions are mapped to the facets that hold the function code: A diamond’s fallback function and delegatecall
enable a diamond to execute a facet’s external function as its own external function. The msg.sender
and msg.value
values do not change and only the diamond’s contract storage is read and written to.
How Diamond Storage achieves Modularity by Decoupling Facets
In the past, it was common for a facet to contain within its source code every single state variable that was ever used by the diamond in the order they were first declared. Sometimes these facets contained state variables in their source code that they were not in use. This was done to avoid the problem of a new facet clobbering existing state variables.
With Diamond Storage, the source code of a facet can just contain the state variables that it actually needs and there is no concern about overwriting existing state variables. This means that facets that use Diamond Storage are independent of each other and these facets can be reused by different diamonds, therefore, becoming reusable libraries for diamonds.
Here is a simple example that shows Diamond Storage and its use in a facet:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// A contract that implements Diamond Storage.
library LibA {
// This struct contains state variables we care about.
struct DiamondStorage {
address owner;
bytes32 dataA;
}
// Returns the struct from a specified position in contract storage
// ds is short for DiamondStorage
function diamondStorage() internal pure returns(DiamondStorage storage ds) {
// Specifies a random position in contract storage
// This can be done with a kaccak256 hash of a unique string as is
// done here or other schemes can be used such as this:
// bytes32 storagePosition = keccak256(abi.encodePacked(ERC1155.interfaceId, ERC1155.name, address(this)));
bytes32 storagePosition = keccak256("diamond.storage.LibA");
// Set the position of our struct in contract storage
assembly {ds.slot := storagePosition}
}
}
// Our facet uses the Diamond Storage defined above.
contract FacetA {
function setDataA(bytes32 _dataA) external {
LibA.DiamondStorage storage ds = LibA.diamondStorage();
require(ds.owner == msg.sender, "Must be owner.");
ds.dataA = _dataA;
}
function getDataA() external view returns (bytes32) {
return LibA.diamondStorage().dataA;
}
}
By using Diamond Storage, facets can be developed independently, without connection or concern for other facets.
AppStorage
The AppStorage pattern is used to conveniently and easily share state variables between facets. It is implemented by defining a struct called AppStorage that contains the state variables of your application. It can contain any number of state variables of any type including arrays, mappings, and more can be added in upgrades.
The AppStorage struct is defined and declared as the first and only state variable directly or through Inheritance
in any facet that uses it. This means that AppStorage is always located at position 0 in the contract storage.
The AppStorage state variable is often named s
to provide easy access to it and to distinguish state variables from function arguments, function names, and local variables.
Here is a simple example of a contract that uses AppStorage:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./AppStorage.sol"
contract StakingFacet {
AppStorage internal s;
function myFacetFunction(uint256 _nextVar) external {
s.total = s.firstVar + _nextVar;
}
...
The above example accesses the s.firstVar
state variable and stores a computation in the s.total
state variable.
DIAMOND STANDARD IMPLEMENTATION
Nick Mudge created 3 diamond templates which are all audited and can be used to set up your own Diamond. They are:
- diamond-1-hardhat (Simple implementation)
- diamond-2-hardhat (Gas-optimized)
- diamond-3-hardhat (Simple loupe functions)
The Diamond templates come with two prewritten facets that help you manage your Diamond:
- The Diamond Loupe
- The DiamondCut
The Diamond Loupe
A Diamond loupe tells you what functions are in a faucet and which are supported by the diamond. louper.dev is a website that is used to display information about diamonds, their facets, and also function selectors.
Loupes are important for security in order to verify that diamonds are used correctly and not mistakenly or maliciously. They are also important for testing to ensure that diamonds contain the correct functions and facets and to test code that uses diamonds. To learn more about loupe, read this
DiamondCut Facet
The DiamondCut facet enables you to add, replace and remove any number of functions from a diamond in a single transaction. Here is how:
interface IDiamondCut {
enum FacetCutAction {Add, Replace, Remove}struct FacetCut {
address facetAddress;
FacetCutAction action;
bytes4[] functionSelectors;
}function diamondCut(
FacetCut[] calldata _diamondCut,
address _init,
bytes calldata _calldata
) external;
}contract FacetA {
struct FacetData {
address owner;
bytes32 dataA;
}
function facetData()
internal
pure
returns(FacetData storage facetData) {
bytes32 storagePosition = keccak256("diamond.storage.FacetA");
assembly {facetData.slot := storagePosition}
}
function setDataA(bytes32 _dataA) external {
FacetData storage facetData = facetData();
require(facetData.owner == msg.sender, "Must be owner.");
facetData.dataA = _dataA;
}
function getDataA() external view returns (bytes32) {
return facetData().dataA;
}
}
I recommend using Appstorage because it's easier to implement and saves time.
PROS AND CONS OF USING THE DIAMOND STANDARD PROXY
Pros:
- It provides a framework for building larger smart contract systems and contract systems that can grow in production.
- Facet reusability -These facets are deployed once and can be reused by lots of different diamonds.
- Avoiding max contract size issues — Through the facet structure, a facet can store 24kb max because each facet is a different contract on its own that has logic, therefore there is an infinite number of facets that can be deployed which will all be connected to one Diamond proxy.
- Modular upgradability — Most Traditional Smart contracts cannot be modular because they always depend on each other, especially in larger systems. When a system is not modular, it makes it hard to isolate and nullify problems. The Diamond Proxy employs the use of facets (these are just contracts connected to the Diamond Proxy itself but only contain logic and do not have any storage of their own)
- Storage slot management
Cons:
- Complexity — at first, you may not understand it, but over time and with constant use, you would be a Pro.
- It’s not supported by tools like Etherscan, but an alternative for Etherscan exists which is Louper.
CONCLUSION
This is an extensive guide into the world of Diamond Standard. To feed your curiosity more, you can check out the following links for a deep dive:
- Diamond Standard
- Check out some projects that have implemented the Diamond Standard here.
Follow Nick Mudge on the following platforms :
Kindly follow me on the following platforms:
Check out projects I have built on my Github.
Thank you for reading.