Keep Your Data Right in EIP2535 Diamonds
Solidity stores data in contracts using a numeric address space. The first state variable is stored at position 0, the next state variable is stored at position 1, the next state variable is stored at position 2, etc.
Facets of a diamond share the same storage address space because they have the same diamond and facets only read and write state variables in diamonds, not in themselves. If you don’t understand this then you need to understand how delegatecall works because all facet external functions are called with delegatecall from a diamond.
This is a problem if not handled correctly. For example let’s say that a diamond has two facets: FacetA and FacetB. Let’s say FacetA declares state variables ‘uint first;’ and ‘bytes32 second;’ and FacetB declares state variables ‘uint first;’ and ‘string name;’.
Both facets are storing `uint first` at position 0, so both facets can read and write that variable without problem.
But both facets are writing and reading something different at position 1 — ‘bytes32 second’ and ‘string name’. They are clobbering, or messing up each other’s data because they are interpreting and writing different data at position 1. This is why facets of the same diamond need to declare the same state variables in the same order if they are reading and writing to the same locations.
Strategies need to exist to make it easy for facets to declare the same state variables in the same order. This really isn’t a problem, even with upgrades, if you have a good strategy.
Inherited Storage
One simple strategy is to create a contract that declares all state variables used by all the facets of a diamond. It could be called ‘Storage’ or something. It could then be inherited by every facet. This strategy works and has been used successfully in production. But it has limitations and in my opinion I’ve found a similar but better strategy.
A limitation Inherited Storage has is that it prevents facets from being reusable. If you deploy a facet that uses Inherited Storage then you likely won’t be able to reuse that deployed facet with a different diamond that has different state variables.
Another limitation, in my opinion, is that it is too easy to accidentally name something like an internal function or local variable the same name as a state variable and have a name clash. Especially with a large diamond with 100 or more state variables. But perhaps this could be overcome by using code naming conventions that prevent such name clashes.
Diamond Storage
Different facets of the same diamond do not actually have to declare the same state variables in the same order if facets are storing data at different locations.
As mentioned earlier, Solidity automatically stores state variables at storage locations starting from 0 and incrementing by one. But we don’t have to use Solidity’s default storage layout mechanism. We don’t have to store data starting at location 0. We can specify where to start storing data in the address space. For different facets we can specify different locations to start storing data, therefore preventing different facets with different state variables from clashing storage locations. This is what Diamond Storage does.
We can hash a unique string to get a random storage position and store a struct there. The struct can contain all the state variables that we want. The unique string can act like a namespace for particular functionality.
For example we could implement an ERC721Facet. This facet could store a struct called ‘ERC721Storage’ at position ‘keccak256("com.myproject.erc721");’. The struct could contain all the state variables related to ERC721 that ERC721Facet reads and writes and nothing else. There are a couple nice advantages to this. One is that ERC721Facet is reusable. ERC721Facet can be deployed only once, and the deployed ERC721Facet can be used with multiple different diamonds that are using facets with different state variables. Another nice thing is that ERC721Facet is not cluttered with state variable declarations of variables it doesn’t use.
Another nice advantage to Diamond Storage is that it is possible for the internal functions of Solidity libraries to access Diamond Storage just like any facet function. Solidity libraries are a great way to share internal functions between different facets. I wrote a blog post about using Solidity libraries with Diamond Storage here: Solidity Libraries Can't Have State Variables -- Oh Yes They Can!
Diamond Storage is particularly useful for creating reusable facets. These facets are deployed once and can be reused by lots of different diamonds. The current reference implementations of diamonds use Diamond Storage for the DiamondCutFacet, DiamondLoupeFacet and OwnershipFacet facets. These can be deployed once and used by lots of different diamonds.
Checkout the example of Diamond Storage in EIP-2535 Diamonds.
For more information about Diamond Storage and another code example, see this blog post: How Diamond Storage Works. I also recommend reading Understanding Diamonds on Ethereum.
AppStorage
AppStorage is similar to Inherited Storage but it solves the name clash problem where it is too easy to accidentally name something like an internal function or local variable the same name as a state variable. This might seem a trivial matter but I found in practice it is very nice because AppStorage also distinguishes code in a way that makes it easier to scan and read. If you care about code readability then you will like AppStorage.
AppStorage enforces a naming or access convention that makes it impossible to clash names of state variables with something else.
A struct called AppStorage is written in a Solidity file. The AppStorage struct contains state variables that will be shared between facets. To use it a facet imports the AppStorage struct and declares `AppStorage internal s;` as the first and only state variable in the facet. The facet then accesses all state variables in functions via the struct like this: `s.myFirstVariable`, `s.mySecondVariable`, etc. Here is an example:
// AppStorage.sol
struct AppStorage {
uint256 secondVar;
uint256 firstVar;
uint256 lastVar;
...
}
// StakingFacet.sol
import "./AppStorage.sol"
contract StakingFacet {
AppStorage internal s;
function myFacetFunction() external {
s.lastVar = s.firstVar + s.secondVar;
}
}
It is important that ‘AppStorage internal s;’ is declared as the first and only state variable in all facets that use it. That puts it at position 0 in the storage address space. So if all facets declare it as the first and only state variable then the storage data between facets will line up correctly. Don’t add more state variables directly to a facet because that will clash with the state variables declared in the AppStorage struct. To add more state variables add them to the end of the AppStorage struct or use Diamond Storage.
AppStorage is more convenient to use than Diamond Storage because in every function Diamond Storage requires getting a pointer to a struct, whereas with AppStorage the `s` struct pointer is automatically available throughout a facet.
Another advantage that AppStorage has over Inherited Storage is that AppStorage can be accessed by Solidity libraries in the same way that Diamond Storage can. An AppStorage struct is always stored at location 0 so internal functions in Solidity libraries can use this to initialize the ‘s’ storage pointer to point to the AppStorage struct. Here is an example of that:
//LibAppStorage.sol
struct AppStorage {
uint256 secondVar;
uint256 firstVar;
uint256 lastVar;
...
}
library LibAppStorage {
function diamondStorage()
internal
pure
returns (AppStorage storage ds) {
assembly {
ds.slot := 0
}
}
function myLibraryFunction() internal {
AppStorage storage s = LibAppStorage.diamondStorage();
s.lastVar = s.firstVar + s.secondVar;
}
function myLibraryFunction2(AppStorage storage s) internal {
s.lastVar = s.firstVar + s.secondVar;
}
}
A storage pointer to an AppStorage struct can also be passed into library functions as an argument, as can be seen in the `myLibraryFunction2` function above.
AppStorage can be used with contract inheritance. This is done by declaring ‘AppStorage internal s;’ in a contract. Then all facets that use AppStorage inherit that contract.
AppStorage is particularly useful for application or project specific facets that won’t be reused across different diamonds that use different storage. AppStorage can be used with Diamond Storage in the same facet, and this is common to do.
AppStorage is also useful in normal smart contracts because it makes code more readable and prevents name clashes.
Checkout this blog post about AppStorage for more information: AppStorage Pattern for State Variables in Solidity