EIP 2535: Diamond standard explained – Part 3: Understanding storage patterns in diamonds

Okay, from Part 1 and Part 2 of this series, we know: 

  • Why EIP 2535 is required / used
  • What is diamond standard and how the architecture looks like

In this part, let’s try to explore what different storage patterns can be used in diamond standard. Storage patterns define rules for declaring state variables in main diamond as well as facets. We know from the previous articles that diamonds use proxy and “delegateCall” architecture, where all facets share the context and data storage of the main diamond contract.

Why are storage techniques important?

Solidity stores data in contract storage sequentially. For example, the variables are stored at position 0, position 1etc. Now consider two facet contracts which declares variables as follows:
In Facet 1:

Uint firstVar;
String secondVar;

And in Facet 2:

Uint firstVar
Uint secondVar

Since facets are going to use the same execution context and storage ( proxy pattern ), in this case it will result in a conflict because facet 1 is trying to read/write uint at position 1 whereas facet 2 is trying to read/write string at that location. 

Let’s discuss few strategies that can be used with diamonds for managing the storage: 

  • Inherited storage
  • Diamond storage
  • App storage

Inherited storage

This is a relatively simple strategy to manage storage for diamonds and proxies. All you have to do is separate out your storage in a separate contract and then all the facets can inherit this “storage” contract to make sure that they are using the same order and data declaration is consistent. Compound finance uses a similar approach.

However, this arrangement is less flexible. First of all, you are enforcing all the facets to strictly use pre decided contract storage and variables. In case there is a need to add a variable, you need to deploy a new storage contract, deploy new facets again which import the new storage contract and then update the diamond’s function to facet mapping by using diamondCut function. Also, the facets are hardly reusable in this case.

Following code snippet will help you understand inherited storage technique:

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract Storage {
uint firstVar;
uint secondVar;
string thirdVar;
string fourthVar;
}
contract FacetA is Storage {
function getVars() public view returns(uint, uint) {
return (firstVar, secondVar);
}
}
contract FacetB is Storage {
function getVars() public returns(string memory, string memory) {
thirdVar = “Ashwin”;
fourthVar = “Yardi”;
return (thirdVar, fourthVar);
}
}

App Storage:

This is very similar to inherited storage because we still declare all the state variables in a separate contract. The difference being, instead of directly declaring the variable, we declare them inside a struct. When this “storage” contract with structs is now inherited in facets, facets always need just one on declaration to make the state accessible:  Struct internal s; 

And specific variables can be accessed like s.firstVar, s.secondVar.

This particular approach is more useful incase we have a “storage” contract with a lot of variables. In such a case, we can accidentally name a variable in the storage contract and a method in one of the facets, resulting in errors and unwanted scenarios. Once we have all of our variables packed inside a struct, it avoids this collision.

Diamond storage

We know that solidity stores data in contract storage sequentially. But we don’t have to use the default storage locations. We can decide which particular slot we want to use. Great, now suddenly it’s just a matter of coming up a strategy for assigning unique storage slots to each facet. With this arrangement, facets don’t have to declare the data in the same order. Each facet is using a different slot, hence there are no storage collisions. 

Generally followed strategy for randomly getting a storage slot is to hash a string which represents facet logically and its function. For example, let’s say we have a calculator diamond contract with four facets: add, subtract, divide and multiply. We can get unique storage slots by:

‘keccak256(“com.calculator.add”);’

Now that we have a slot, we can define a struct which has all the required variables and store the struct at a calculated slot.

This storage approach is more flexible. Our facets are independent and can be used with multiple diamonds. Also facets can manage their own data very effectively without over-replicating the data which is used by other facets.

Following code snippet will help you understand diamond storage technique:

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract FacetA {
struct DiamondStorage {
uint firstVar;
uint secondVar;
}
function getStorage() internal pure returns(DiamondStorage storage ds) {
// Specifies a random position in contract storage
// This can be done with a keccak256 hash of a unique string as is
// done here or other schemes can be used
bytes32 storagePosition = keccak256(“diamond.storage.facetA”);
// Set the position of our struct in contract storage
assembly {ds.slot := storagePosition}
}
function getVars() public view returns(uint, uint) {
DiamondStorage storage s = getStorage();
return (s.firstVar, s.secondVar);
}
}
contract FacetB {
struct DiamondStorage {
string thirdVar;
string fourthVar;
}
function getStorage() internal pure returns(DiamondStorage storage ds) {
bytes32 storagePosition = keccak256(“diamond.storage.facetB”);
assembly {ds.slot := storagePosition}
}
function setVars() public {
DiamondStorage storage s = getStorage();
s.thirdVar = “Ashwin”;
s.fourthVar = “Yardi”;
}
function getVars() public view returns(string memory, string memory) {
DiamondStorage storage s = getStorage();
return (s.thirdVar, s.fourthVar);
}
}

There is no clear winner between App storage or Diamond storage techniques. If we are dealing with very large state data, app storage might prove to be more efficient, given we are structuring the logic in such a way that unused variables per facets are minimum. Reason being, in diamond storage technique, there is an overhead of creating a pointer every time a read/write operation is to be performed.

In the next and final part of this series, let’s create a calculator using diamond standard. 

%d bloggers like this: