Storage Layouts
Solidity Storage (Default)
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.
Solidity Default Layout
Facets of a diamond share the same storage address space because they read and write state variables to the same Diamond. As a result, 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. If this is not done properly, issues such as storage clashes could occur.
Problem Example:
- Diamond has two facets
- FacetA and FacetB
- FacetA declares state variables ‘uint first;’ and ‘bytes32 second;’
- FacetB declares state variables ‘uint first;’ and ‘string name;’.
- Both facets are storing
uint first
at position 0, both facets read and write that variable without problem.- Both facets are writing and reading different variables at position 1 — ‘bytes32 second’ and ‘string name’.
Diamond Storage
- Unlike Solidity storage layout, Diamond storage allows you to choose the address space
- Facets specify different locations to start storing data, preventing different facets with different state variables from clashing storage locations.
- This is what Diamond Storage does -
- Hash of unique string gets random storage position to store a struct
- struct contains all of the state variables
- ‘unique string’ acts as a namespace for particular functionality
- example: ERC721Facet implementation
- Facet stores a struct called ‘ERC721Storage’ at position ‘keccak256("com.myproject.erc721");’.
- struct could contain all the state variables related to ERC721
- ERC721Facet reads/writes and nothing else
- Hash of unique string gets random storage position to store a struct
External functions with the same function signature can’t be added to the same diamond.
Advantages
- ERC721Facet is reusable.
- ERC721Facet not cluttered with declarations of variables it does not use
App Storage
- AppStorage enforces a naming/access convention makes code more readable and prevents name clashes of state variables.
The pattern works in the following way:
- Define a struct called AppStorage that contains all the state variables specific to your application and that you plan to share with different facets. Store AppStorage in a file.
Any of your facets can now import this file to access the state variables.
// AppStorage.solstruct AppStorage {uint256 secondVar;uint256 firstVar;uint256 lastVar;...}
- In a facet that imports the AppStorage struct declare an AppStorage state variable called s.
This should be the only state variable declared in the facet. Here's an example:
import "./AppStorage.sol"contract StakingFacet {AppStorage internal s;...
Now in your facet you can access all the state variables in AppStorage by prepending state variables with s.. Here's a simple example:
import "./AppStorage.sol" contract StakingFacet { AppStorage internal s; function myFacetFunction() external { s.lastVar = s.firstVar + s.secondVar; }
This is a very nice convention because it makes state variables concise, easy to access, and it distinguishes state variables from local variables and prevents name clashes/shadowing with local variables and function names. It helps identify and make explicit state variables in a convenient and concise way. It is also a little more gas efficient than diamond storage access.
Since AppStorage s is the first and only state variable declared in facets its position in contract storage is 0. This fact can be used to access AppStorage in Solidity libraries using diamond storage access. Here's an example of that:
library MyLib { function appStorage() internal pure returns (AppStorage storage ds) { assembly { ds.slot := 0 } } function someFunction() internal { AppStorage storage s = appStorage(); ... do stuff } }
AppStorage s
can be declared as the one and only state variable in facets or it can be declared in a contract that facets inherit.
AppStorage won't work if state variables are declared outside of AppStorage and outside of Diamond Storage.
It is a common error for a facet to inherit a contract that declares state variables outside AppStorage and Diamond Storage.
This causes a misalignment of state variables.
One downside is that state variables can't be declared public in structs so getter functions can't automatically be created this way. But I think it is better anyway to make your own getter functions for state variables because its more explicit.