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

157 lines
6.7 KiB
Solidity
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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'