Builtin for base slot of storage layout
Abstract
The base slot of the storage layout should be available as a constant usable in both compile-time and runtime expressions.
Motivation
From https://github.com/ethereum/solidity/issues/597#issuecomment-2568003933:
A potential issue is that contracts that manually implement custom storage locations will be unaffected by Solidity's native layout rerooting.
For example: https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/dfe0973e94e137277cae220ef54eb66df60cbf92/contracts/access/OwnableUpgradeable.sol#L27-L34
In a sense this should be expected because it's a very low level operation. But it would be good if Solidity provided some way for this kind of logic to adapt to a potentially rerooted layout... For example, if the base of the layout was available as a constant. Maybe something like
type(Contract).layoutBasethat could be used in constants.
Specification
Define type(C).storageBase that returns the value of contact's base slot expression.
- The type is
uint256. - The value is zero if the contract does not have a layout specifier.
- The value is undefined (compilation error) for contracts which cannot have a layout specifier:
- ones not at the top level of the inheritance hierarchy
- abstract contracts
- interfaces
- libraries
The value should be a compile-time constant (via isPure annotation) and usable in:
- Constant initializers:
uint constant X = type(C).storageBase + 0x1000; - Layout specifiers:
contract D layout at type(C).storageBase + 0x1000 {} - Array sizes:
uint[type(B).storageBase - type(A).storageBase] array;
Backwards Compatibility
Fully backwards compatible.
Replying to https://github.com/ethereum/solidity/issues/597#issuecomment-2691649251.
Maybe this helps clarify what I was expecting: given contract A { uint x; } I'd expect type(A).storageBase to be the slot at which x is stored. If A is on its own that would be 0, but if you have something like contract X is Y, A {} then it would be the slot after Y's storage. That is, the value of type(A).storageBase depends on the inheritance context.
I don't think the specification in this issue does that, does it?
It does not. The way it is here, you'll get a compilation error if you try to use type(A).storageBase.
But I'd like to understand how you expect it to work in your version, because to me it seems unresolvable without some pretty arbitrary assumptions. The problem is that you don't necessarily know the inheritance context at the point where you use storageBase.
Say, we have code like this:
contract X { uint x; }
contract Y is X layout at 42 {}
contract Z is X layout at 24 {}
uint constant BASE = type(X).storageBase;
How should the compiler decide which value to initialize BASE with?
Ah, right, I'm not interested in reading this value outside of a contract.
I can see how that's incompatible with type(X).storageBase.
I actually want something like thisContract.storageBase.
In terms of the use case here, more specifically what I think is needed is some expression E so we can write something like the following and have the value move together with layout at annotations in the context of use:
bytes32 private constant OwnableStorageLocation =
E ^ 0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300;