157 lines
6.7 KiB
Solidity
157 lines
6.7 KiB
Solidity
|
// 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 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.
|
|||
|
|
|||
|
// 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 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: 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 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 recommend reading and understanding this section of the Solidity documentation:
|
|||
|
// 'Layout of State Variables in Storage'
|