Gm, sers.
I’ve read the ERC and seen that it’s in the last call.
With the way Solc currently works, storageLayout
will be empty when ERC7201 structs are used. This output is often used by tools for parsing Solidity files.
This can be easily fixed by adding that ERC7201_MOCK
MUST be defined, as follows:
ContractStorage private ERC7201_MOCK;
Where ContractStorage
is the ERC7201 struct.
This will be the only ‘regular’ variable, and will never be used. It won’t have effect on the contract, since namespaces are used.
With the mock in place, the complete storage layout can be parsed from Solc’s output by adding storage[i].slot + namespace
.
This was the main thing I wanted to suggest.
What follows are a few others ideas on how the template for contracts with storage contracts can be standardized:
contract Profile {
/// @custom:storage-location erc7201:zeroekkusu.magicians.Profile
struct ProfileStorage {
string _username;
string name;
}
ProfileStorage private ERC7201_MOCK;
// keccak256(abi.encode(uint256(keccak256("zeroekkusu.magicians.Profile")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant ERC7201_NAMESPACE = 0x5b0d49f4bb702a14af81a298d6304c57802c4493960a722fb612a039d9e77100;
function $Profile() internal pure returns (ProfileStorage storage $) {
assembly {
$.slot := ERC7201_NAMESPACE
}
}
function name() external view returns (string memory) {
return $Profile().name;
}
}
contract ExtendedProfile is Profile {
/// @custom:storage-location erc7201:zeroekkusu.magicians.ExtendedProfile
struct ExtendedProfileStorage {
string bio;
}
ExtendedProfileStorage private ERC7201_MOCK;
// keccak256(abi.encode(uint256(keccak256("zeroekkusu.magicians.ExtendedProfile")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant ERC7201_NAMESPACE = 0xb544e00821034a47c6890dc236e787d1935ceb592b881d8a41d96b1b7a311e00;
function $ExtendedProfile() internal pure returns (ExtendedProfileStorage storage $) {
assembly {
$.slot := ERC7201_NAMESPACE
}
}
function bio() external view returns (string memory) {
return $ExtendedProfile().bio;
}
}
Context: Usernames are usually unique, while the (display) names and bios can be altered freely.
The above approach introduces two things:
- Standardizing a function for reading from and writing to storage
- Distinguishing between ‘private’ and ‘internal’ variables in the ERC7201 struct
First, let’s address the elephant in the room.
The ‘private’ variables are prepended with _
and can (but should not) be directly accessed from the inheriting contract - much like in Python.
The approach is not completely fool-proof, but should be safe enough, because the developer must specify the contract the variable is from verbatim ($Contract()
) and then break the rule by writing to the ‘private’ variable (._variable =
). This assumes the basic familiarity with the ERC.
Contracts may choose not to make their $Contract
function internal
- e.g. OpenZeppelin - in which case they will need to add setters and getters for ‘internal’ variables.
The function name is standardized as $Contract
, where Contract
is the same label used in the contract name and ERC7201 struct. It is concise, persistent, and $
implies ‘storage’.
There are no other requirements.
ERC7201_NAMESPACE
is just an arbitrary name, and any can be reused indefinitely, as long as it’s private
.
Libraries such as Forge-Std can provide scripts for quickly generating an ERC7201 template. No base contract required, since Solidity doesn’t support generics anyway.
Let me know what you think!
Edit: Something’s been brought to my attention that I think should be clarified here. OpenZeppelin already makes a distinction between their regular and upgradable contracts. All the upgradeable ones use ERC-7201. Adding the mock requirement to the ERC would automatically rule out mixing layouts (using both ‘regular’ variables and namespaces). This means all upgradeable contracts would need to use ERC-7201. If this is undesirable, another solution for storageLayout
can be explored, such as relying on toolchains (Foundry, Hardhat, etc) to generate the output.