DCIPs/assets/eip-2535/storage-examples/DiamondStorage.sol

157 lines
6.7 KiB
Solidity
Raw Normal View History

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// Diamond storage is a contract storage strategy that is used in proxy contracts and diamonds.
// It greatly simplifies organizing and using state variables in proxy contracts and diamonds.
// Diamond storage relies on Solidity structs that contain sets of state variables.
// A struct can be defined with state variables and then used in a particular position in contract
// storage. The position can be determined by a hash of a unique string or other data. The string
// acts like a namespace for the struct. For example a diamond storage string for a struct could
// be 'com.mycompany.projectx.mystruct'. That will look familiar to you if you have used programming
// languages that use namespaces.
// Namespaces are used in some programming languages to package data and code together as separate
// reusable units. Diamond storage packages sets of state variables as separate, reusable data units
// in contract storage.
// Let's look at a simple example of diamond storage:
library LibERC721 {
bytes32 constant ERC721_POSITION = keccak256("erc721.storage");
// Instead of using a hash of a string other schemes can be used to create positions in contract storage.
// Here is a scheme that could be used:
//
// bytes32 constant ERC721_POSITION =
// keccak256(abi.encodePacked(
// ERC721.interfaceId,
// ERC721.name
// ));
struct ERC721Storage {
// tokenId => owner
mapping (uint256 => address) tokenIdToOwner;
// owner => count of tokens owned
mapping (address => uint256) ownerToNFTokenCount;
string name;
string symbol;
}
// Return ERC721 storage struct for reading and writing
function getStorage() internal pure returns (ERC721Storage storage storageStruct) {
bytes32 position = ERC721_POSITION;
assembly {
storageStruct.slot := position
}
}
event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);
// This is a very simplified implementation.
// It does not include all necessary validation of input.
// It is used to show diamond storage.
function transferFrom(address _from, address _to, uint256 _tokenId) internal {
ERC721Storage storage erc721Storage = LibERC721.getStorage();
address tokenOwner = erc721Storage.tokenIdToOwner[_tokenId];
require(tokenOwner == _from);
erc721Storage.tokenIdToOwner[_tokenId] = _to;
erc721Storage.ownerToNFTokenCount[_from]--;
erc721Storage.ownerToNFTokenCount[_to]++;
emit Transfer(_from, _to, _tokenId);
}
}
// Note that this is not a full or correct ERC721 implementation.
// This is an example of using diamond storage.
// Note that the ERC721.name and ERC721.symbol storage variables would probably be set
// in an `init` function at deployment time or during an upgrade.
// Shows use of LibERC721 and diamond storage
contract ERC721Facet {
function name() external view returns (string memory name_) {
name_ = LibERC721.getStorage().name;
}
function symbol() external view returns (string memory symbol_) {
symbol_ = LibERC721.getStorage().symbol;
}
function transferFrom(address _from, address _to, uint256 _tokenId) external {
LibERC721.transferFrom(_from, _to, _tokenId);
}
}
// Here we show how we can share state variables and internal functions between facets by
// using Solidity libraries. Sharing internal functions between facets can also be done by
// inheriting contracts that contain internal functions.
contract ERC721BatchTransferFacet {
function batchTransferFrom(address _from, address _to, uint256[] calldata _tokenIds) external {
for(uint256 i; i < _tokenIds.length; i++) {
LibERC721.transferFrom(_from, _to, _tokenIds[i]);
}
}
}
// HOW TO UPGRADE DIAMOND STORAGE
//--------------------------------------------
// 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:
// 1. To add new state variables to an AppStorage struct or a Diamond Storage struct, add them
// to the end of the struct.
// 2. New state variables can be added to the ends of structs that are stored in mappings.
// 3. 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:
// 1. If you are using AppStorage then do not declare and use state variables outside the
// AppStorage struct. Except Diamond Storage can be used. Diamond Storage and AppStorage
// can be used together.
// 2. 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.
// 3. Do not put structs directly in structs unless you dont 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.
// 4. Do not add new state variables to structs that are used in arrays.
// 5. 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.
// 6. Do not allow any facet to be able to call `selfdestruct`. This is easy. Simply dont
// allow the `selfdestruct` command to exist in any facet source code and dont 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: MyStruct storage mystruct = storage.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 cant 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 recommend reading and understanding this section of the Solidity documentation:
// 'Layout of State Variables in Storage'