Rules for Upgrading Diamond Storage
EIP-2535 Diamonds specifies the ‘diamondCut’ function which is used to add/replace/remove any number of facets and functions to a diamond in a single transaction. It can be important to execute an upgrade in a single transaction so that a diamond does not get into an inconsistent state at any time.
The ‘diamondCut’ function can also optionally execute an arbitrary external function with delegatecall during an upgrade. This is to initialize state variables and otherwise make any changes needed for an upgrade.
Here is a link to a test script that shows various examples of upgrading a diamond: https://github.com/mudgen/diamond-3-hardhat/blob/main/test/diamondTest.js
Keep State Variables Safe
It is important not to corrupt state variables during an upgrade. It is easy to handle state variables correctly in upgrades.
Here’s some things that can be done:
To add new state variables to an AppStorage struct or a Diamond Storage struct, add them to the end of the struct. This makes sense because it is not possible for existing facets to overwrite state variables at new storage locations.
New state variables can be added to the ends of structs that are stored in mappings.
The names of state variables can be changed, but that might be confusing if different facets are using different names for the same storage locations.
Do not do the following:
If you are using AppStorage then do not declare and use state variables outside the AppStorage struct. Except Diamond Storage can be used.
Do not add new state variables to the beginning or middle of structs. Doing this makes the new state variable overwrite existing state variable data and all state variables after the new state variable reference the wrong storage location.
Do not put structs directly in structs unless you don’t plan on ever adding more state variables to the inner structs. You won't be able to add new state variables to inner structs in upgrades without overwriting existing state variables.
Do not add new state variables to structs that are used in arrays.
When using Diamond Storage do not use the same namespace string for different structs. This is obvious. Two different structs at the same location will overwrite each other.
Do not allow any facet to be able to call `selfdestruct`. This is easy. Simply don’t allow the `selfdestruct` command to exist in any facet source code and don’t allow that command to be called via a delegatecall. Because `selfdestruct` could delete a facet that is used by a diamond, or `selfdestruct` could be used to delete a diamond proxy contract.
A trick to use inner structs and still enable them to be extended is to put them in mappings. A struct stored in a mapping can be extended in upgrades. You could use a value like 0 defined with a constant like INNER_STRUCT. Put your structs in mappings and then access them with the INNER_STRUCT constant. Example:
uint256 internal constant INNER_STRUCT = 0;
struct AppStorage {
uint256 value1;
mapping(uint256 => Mystruct) mystruct;
uint256 value2;
}
struct MyStruct {
uint256 a;
uint256 b;
}
MyStruct storage mystruct = appstorage.mystruct[INNER_STRUCT];
Note that any Solidity data type can be used in Diamond Storage or AppStorage structs. It is just that structs directly in structs and structs that are used in arrays can’t be extended with more state variables in the future. That could be fine in some cases.
These rules will make sense if you understand how Solidity assigns storage locations to state variables. I highly recommend reading and understanding this section of the Solidity documentation: Layout of State Variables in Storage
Here is a link to an article that shows an example of doing an upgrade on a diamond that adds new state variables:
More detailed information about how diamond upgrades work can be found in this article: How Diamond Upgrades Work