-
-
owned this note
-
Published
Linked with GitHub
###### tags: `Technical Notes`
# Syntax proposal for explicit storage locations
## Summary of requirements from the issue
Only includes discussion after [my comment that restarted it recently](https://github.com/ethereum/solidity/issues/597#issuecomment-2137917945). Note that some of these are in conflict with each other:
- Keeping same location when upgrading an existing contract.
- Keeping an inherited contract at a constant location across all versions (raised by Gnosis Safe).
- Safely using the same library/base contract with fixed storage in multiple implementations.
- Decoupling storage from inheritance linearization (ERC-7201).
- ERC-1967 (Proxy storage slots): putting different members at different locations.
- Preventing attacks that rely on non-standard storage layout resulting in collisions and try to obfuscate that fact.
- EIP-7702: take a contract off the shelf and deploy it at a storage offset other than 0 without changes to the base contract source code.
- Authors of reusable contracts should be able to ship changes to their code, including to storage variables, without breaking downstream code.
- Per-state-variable control of layout should not be applicable to private state variables.
- Guarantee that the layout is collision-free (potentially by disallowing arbitrary expressions for locations).
## Base definitions
Contract definitions that examples below refer to.
These contain no new syntax.
```solidity
interface ICommonBase {}
contract BaseOfCommonBase is ICommonBase {
uint256 a;
}
contract CommonBase is BaseOfCommonBase {
uint256 b;
}
contract BaseA is CommonBase {
uint256 x;
uint256 y;
uint256 z;
}
contract BaseB is CommonBase {}
```
ERC-7201
```solidity
function erc7201(string memory namespace_id) pure returns (uint) {
return keccak256(abi.encode(uint256(keccak256(namespace_id)) - 1)) & ~bytes32(uint256(0xff));
}
contract Base {
function foo() public {}
}
contract Example is Base {
struct MainStorage {
uint256 x;
uint256 y;
}
MainStorage private _mainStorage;
function _getMainStorage() private pure returns (MainStorage storage) {
return _mainStorage;
}
function _getXTimesY() internal view returns (uint256) {
MainStorage storage $ = _getMainStorage();
return $.x * $.y;
}
}
```
*Note: This is not the exact reproduction of ERC-7201, it's only meant to show how a the same effect of decoupling storage from inheritance hierarchy could be achieved using the new feature. Most notably the `@custom:storage-location` annotation is not included since it would become redundant with the new syntax.*
ERC-1967
```solidity
contract CustomProxy is ERC1967Proxy {
bool private _rollback;
address private _implementation;
}
```
## Variant 1: Header, non-extensible
A syntax marker in the contract definition header (anywhere before the contract body brackets) that *only* accounts for specifying a global storage base for the entire inheritance graph, without considering extensibility to anything else.
Must play well with inheritance specifiers.
```solidity
contract FinalContract
at keccak256(abi.encode(uint256(keccak256("FinalContract")) - 1)) & ~bytes32(uint256(0xff))
is BaseA, BaseB
{
mapping (address => bool) public m;
bool private flag;
}
```
### ERC-7201
Not supported by the syntax.
### ERC-1967
Not supported by the syntax.
## Variant 2: Header, extensible
A syntactic construct in the contract definition header that allows specifying a global storage base, but also is extensible to specify storage bases for individual base contracts or inheritance subtrees and maybe even for individual state variables
Must play well with inheritance specifiers.
```solidity
contract FinalContract is BaseA, BaseB
layout
FinalContract at keccak256(abi.encode(uint256(keccak256("FinalContract")) - 1)) & ~bytes32(uint256(0xff)),
FinalContract.flag at keccak256("flag"),
CommonBase+ at 0,
BaseA.x after CommonBase,
y, z after BaseA.x
{
mapping (address => bool) public m;
bool private flag;
}
```
Shorthand syntax:
```solidity
contract C is A, B layout at 0x1234 {}
```
### ERC-7201
```solidity
contract Derived is Example
layout
Example at erc7201("example.main"),
Derived after Base
{
uint256 x;
}
```
### ERC-1967
```solidity
contract CustomProxy is ERC1967Proxy
layout
ERC1967Proxy._rollback at bytes32(uint256(keccak256("eip1967.proxy.rollback")) - 1)),
ERC1967Proxy._implementation at bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1))
{}
```
## Variant 3: Body, extensible
A syntactic construct within the contract body that allows specifying a global storage base, but also is extensible to specify storage bases for individual base contracts or inheritance subtrees and maybe even for individual state variables
```solidity
contract FinalContract1 is BaseA, BaseB {
mapping (address => bool) public m;
bool private flag;
layout {
FinalContract at keccak256(abi.encode(uint256(keccak256("FinalContract")) - 1)) & ~bytes32(uint256(0xff)),
FinalContract.flag at keccak256("flag"),
CommonBase+ at 0,
BaseA.x after CommonBase,
y, z after BaseA.x
}
}
```
Shorthand syntax:
```solidity
contract C is A, B {
layout at 0x1234;
}
```
### ERC-7201
```solidity
contract Derived is Example {
layout {
Example at erc7201("example.main");
Derived after Base;
}
uint256 x;
}
```
### ERC-1967
```solidity
contract CustomProxy is ERC1967Proxy {
layout {
ERC1967Proxy._rollback at bytes32(uint256(keccak256("eip1967.proxy.rollback")) - 1)),
ERC1967Proxy._implementation at bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1))
}
}
```
## Detailed rules for the extensible variants
1. Storage layout can only be specified in the most derived contract and no more than once.
1. Layout can be specified for:
1. The whole hierarchy: using the shorthand `layout at <offset>` syntax.
2. Whole inheritance subtrees: using the shorthand `<contract>+` syntax.
3. Individual contracts: within a `layout` block.
4. Individual state variables: within a `layout` block.
1. `layout` can be appear at any position in the header, i.e. either before or after the inheritance list (that starts with `is`). In the body it can be at any position allowed for top-level definitions within a contract.
1. The layout must be exhaustive, covering the whole hierarchy.
Every contract from the linearized inheritance hierarchy must be listed unless:
- The contract has no storage variables.
- Location is provided for the whole inheritance hierarchy.
- Location is provided for a subtree the contract is a part of.
- Location is provided for all the storage variables of the contract individually.
1. A subtree can be given a single location only if it is self-contained, i.e. if none of its contracts appears elsewhere in the whole inheritance hierarchy.
1. Variable packing:
1. Specifying offsets explicitly is not possible.
1. When location is provided using `at`, the offset is `0`.
Multiple variables cannot be packed into the same slot, other than as a part of an aggregate type (e.g. struct or array).
1. When location is specified using `after`, normal packing rules apply.
- `after` can be used with whole contracts as well.
1. Overriding individual variable locations:
1. Location can be specified for individual variables of a contract even if it was already given for the whole contract.
- In that case location must also be explicitly specified for all variables that follow to avoid ambiguity. The `after` keyword allows selecting the default location.
1. Variables from the most derived contract have to be prefixed with the contract name as well, to avoid ambiguities (i.e. `FinalContract.m at ...` rather than `m at ...`).
- Prefix can be omitted when using `after` and the variable belongs to the same contract as what's listed on the right-hand side.
1. The location may be specified as any integer expression that can be evaluated at compilation time.
- As of Solidity 0.8.26 there are very few such expressions, mostly literals, constants and arithmetic operations on them.
With time this set will be extended to include enough things to cover ERC-7201 (keccak, conversions, some subset of ABI encoding).
Eventually (though probably only on top of the new system) the language will have extensive compile-time evaluation support allowing the use of user-defined functions and possibly even inline assembly.
- Common formulas for locations could be considered for inclusion in the standard library when it becomes available.
- The compiler does not allow overlap between statically-sized storage variables within a single inheritance hierarchy.
Preventing overlap between different contracts or dynamically-sized variables is left up to the user.
The use of conventions designed to minimize risk of such collisions, like ERC-7201, is encouraged but not enforced.