solidity icon indicating copy to clipboard operation
solidity copied to clipboard

Builtin for base slot of storage layout

Open cameel opened this issue 10 months ago • 3 comments

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).layoutBase that 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.

cameel avatar Feb 28 '25 22:02 cameel

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?

frangio avatar Feb 28 '25 23:02 frangio

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?

cameel avatar Feb 28 '25 23:02 cameel

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;

frangio avatar Mar 01 '25 17:03 frangio