In this article I am going to explain Diamonds.
What is it?
Let's begin with explaining what is it. It is an upgradeable smart contract architecture. It can have:
- one state (data) contract
- multiple logic contract
So in this way you can upgrade only a part of your system without changing the other parts.
Why you should use it?
If you need/want to upgrade your smart contract in the future, it is an option for you. You can manage your functions easily, and upgrade them.
DiamondCut: The Managing Interface
The diamond storage MUST implement the diamondCut function. With this function one can (un)register functions for a specific logic contract.
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;
}
DiamondStorage: The data keeper
We can store our datas in this contract. But remember, we have a lot of logic contract. How can we be sure about our slots? Here is the solution: Special slot addresses.
library MyStructStorage {
bytes32 constant MYSTRUCT_POSITION =
keccak256("com.mycompany.projectx.mystruct");
struct MyStruct {
uint var1;
bytes memory var2;
mapping (address => uint) var3
}
function myStructStorage()
internal
pure
returns (MyStruct storage mystruct)
{
bytes32 position = MYSTRUCT_POSITION;
assembly {
mystruct.slot := position
}
}
}
In this example we can use FacetData like this:
function diamondUser() external {
MyStructStorage.MyStruct storage = mystruct = MyStructStorage.myStructStorage();
mystruct.var1 = 10;
uint var3 = mystruct.var3[address(this)];
}
Instead of using a hash of a string other schemes can be used to create random positions in contract storage. Here is a scheme that could be used:
bytes32 constant POSITION = keccak256(abi.encodePacked(
ERC1155.interfaceId,
ERC1155.name,
address(this)
))
Don't make these mistakes when you are using Diamond storage:
-
Don't use the same namespace string (com.mycompany.projectx.mystruct or POSITION in our examples) for different structs. Because the two structs will overwrite each other in storage.
-
Be careful when you are trying to add/remove new state variables to your system. We are using delegatecall, because of this state order is very important.
DiamondLoupe: Finding oyt which functions are supported
A loupe is four standard read-only functions that tell you what functions and facets are provided by a diamond.
interface IDiamondLoupe {
struct Facet {
address facetAddress;
bytes4[] functionsSelectors;
}
function facets() external view returns (Facet[] memory facets_);
function facetFunctionSelectors(address _facet) external view returns (bytes4[] memory facetFunctionSelectors_);
function facetAddresses() external view returns (address[] memory facetAddresses_);
function facetAddress(bytes4 _functionSelector) external view returns (address facetAddress_);
}
Any diamond MUST implement those loupe functions to help figuring out which functions are defined on the diamond.
- facetAddresses(): Returns all the facet addresses used by a diamond.
- facetAddress(bytes4 _functionSelector): Returns the facet address that implements functions.
- facetFunctionSelector(address _facet): Returns all the functions used by a diamond that come from a particular facet.
- facets(): Returns an array consisting of all the facet addresses and functions used by a diamond.
The loupe functions make diamonds transparent. This is important for understanding, showing and using exactly the functionality provided by diamonds.
An alternative to DiamondLoupe: Events
The loupe functions are not the only way to find out what functions and facets a diamond has.
Anytime a function/facet is added/replaced/removed on a diamond the DiamondCut event is emitted that records exactly what changed in a diamond.
You might wonder why we have two ways to do the same thing.
First, the DiamondCut event provides something that the loupe functions don't: A historical record of all upgrades done on a diamond. The historical record can be used to verify the state, correctness and security of a diamond over time.
Why we have events and loupe functions?
Pros:
-
More choice: Having events and the loupe functions make diamonds easier to use for more people because they can choose how to get the data.
-
Easier adoption by services and tool: Services such as Etherscan, blockchain explorers, web applications, programming libraries and other software may prefer or require the use of events or external functions. Having both options increases the capability to integrate with tools and services.
-
More transparent, reliable and available: If an event system is not available or up-to-date or is slow or has any other problem, then the loupe functions can be used. If external functions can't be called in a particular context then an event system or database can be used. Having both methods available increases the reliability, availability and transparency of diamonds.
-
Simple and easy: The loupe functions are simple and easy to use. They are regular external contract functions that are called with regular client contract libraries like web3.js or ethers.js. No special custom software or third party software or library or dependency is needed to call the loupe functions, since they are regular contract functions.
-
Secure: The code for the diamondCut function and the diamond loupe functions is carefully looked at by Solidity developers and security experts. These functions are simple enough for a competent Solidity developer to read and understand.
Cons:
- Loupe functions add code to a diamond
- Loupe functions require more gas for adding/replacing/removing functions
How to implement a diamond?
There is currently no reference implementation available via the Openzeppelin contracts. But the EIP author Nick Mudge created three reference implementations which are all audited:
- diamond-1-hardhat (Simple implementation)
- diamond-2-hardhat (Gas-optimized)
- diamond-3-hardhat (Simple loupe functions)
Which one should you choose? They all do the same thing, so it doesn't matter too much. If you plan to do many upgrades and care about gas costs, consider going with diamond-2. It contains some complicated bitwise operations to reduce the storage space and will save you roughly 80,000 gas for every 20 functions added. Otherwise diamond-1 is probably a better choice since the code is much more readable.
Resources
Official proposal page Solidity developer - eip2535 Devto/mudgen - why loupe functions for diamonds