Open pipermerriam opened 8 years ago
not so!
See Nick Johnson's Library on upgradeability :)
https://gist.github.com/Arachnid/4ca9da48d51e23e5cfe0f0e14dd6318f#file-upgradeable-sol
Especially with contract upgrades in mind, wouldn't it be better to copy the storage layout and "disable" unused state variables by e.g. prefixing them? Otherwise I don't see how you would practically verify that the storage layout is consistent between upgrades.
Is there documentation on how storage layout is determined?
On Wed, May 25, 2016, 1:54 AM chriseth notifications@github.com wrote:
Especially with contract upgrades in mind, wouldn't it be better to copy the storage layout and "disable" unused state variables by e.g. prefixing them? Otherwise I don't see how you would practically verify that the storage layout is consistent between upgrades.
— You are receiving this because you authored the thread. Reply to this email directly or view it on GitHub https://github.com/ethereum/solidity/issues/597#issuecomment-221499635
Ok, so after reading up on storage layouts...
contract MyContractV1 {
uint a;
bytse32 b;
}
In this example, a
should be stored in slot 0 and b
in slot 1.
Now, consider I upgrade it to the following.
contract MyContractV2 {
int c;
uint a;
bytes32 b;
}
This would end up with c
stored in slot 0, a
in 1, and b
in 2 which would break things.
So, instead, I propose being able to do the following.
contract MyContractV2 {
int c;
uint a @ 0x0;
bytes32 b @ 0x1;
}
The solidity compiler would see that a
and b
are designated for storage slots 0 and 1 respectively, and would then place c
at the next available location, slot 2.
Does that make sense? Is this possible?
I was looking for a complementary/similar feature: the ability to disable packing. (i.e. currently if two storage parameters are each < 256 bits and together they fit into one slot, they are packed together.)
Ultimately the compiler could optimise the packing based on the frequency of changes to one ore more variables within.
With your suggestion this is a given, each marked variable gets its own slot. I would use a different markup though:
storage(0) int a;
storage(1) bytes32 b;
I would use a different markup though
the @
was just the first thing that came to mind. I like storage(...)
better.
I think the tradeoff between introducing errors and decreasing readability is much better when just adding int c
at the end. If you want, you can also use inheritance (let the upgraded contract inherit from the old contract).
^ 👍 for the inheritance structure...it overall is cheaper and more cost effective to do it that way. I envision a lot of modularity around dapps in the future in regards to storage to better handle updates and save gas.
This came up again as a discussion with @federicobond and I think a good middle ground could be to have an annotation (as proposed in https://github.com/ethereum/solidity/issues/597#issuecomment-221611370), but instead of marking a storage slot, it would rather have a string literal as a key, which is hashed to produce a 256-bit key for storage.
This would be more expensive (due to the fact of using 32-byte long constants and one couldn't combine multiple variables into a single slot), but might be justified for some.
When this annotation is missing, it would default to the current behaviour.
For syntax I propose:
int256 a storage_key("this_is_my_variable");
bytes32 b storage_key("and_this_too");
@axic
I really don't think solidity should have such low-level impact on the storage location. If you want to dislocate storage variables, why not use structs or a mapping to structs?
One more possible other solution:
contract MyContract {
storage("some-collection") {
uint foo;
uint bar;
}
storage("other-collection") {
mapping (uint => bool) qux;
MyStruct baz;
}
}
The advantage of this is that contracts could define blocks of variables that are colocated in storage, but providing gaps, to extend structs later, etc.
Just throwing this as an idea: given that this need arises from avoiding clashes when working with upgradeability, wouldn't it make sense to just avoid clashing by storing all variables in a hashed location, similar to how a mapping works? We could either store all variables from the same contract/struct together (the hash being a contract identifier, and variables are stored at offsets of that hash), or all individual variables in sparse hashed locations.
The issue remains on how to generate an identifier for a contact, to ensure there are no clashes between different contracts, but that identifier is more robust than a simple name. Maybe requiring a special constant with a random value for every contract that will use this approach, similar to old Java's serialVersionUID
?
There was also a lengthy related discussion in #4017.
This came up again with #7891 .
If we want to expose really general control we need three components:
Natural restrictions would apply (violating would result in compile time errors):
offset + sizeOfType <= 32
numberOfBytesReserved >= sizeOfType
(offset + numberOfBytesReserved) % 32 == 0
and only later decide whether to lift that
I would suggest to make all such specifiers optional. Variables without specifiers before any variables with specifiers will be assigned slots as before. For variables without specifiers after any variables with specifiers there are two options:
For the purpose of inheritance: locations are assigned just as if it was one flat contract containing all variables in the order of C3 linearization.
Example (we can always decide on a different syntax):
contract A {
uint256 a; // will occuply full slot 0
// slots 1 and 2 will remain unused
storage{slot: 3, offset: 0, reserved: 32} bool b; // will occupy full slot 3
storage{slot: 4, offset: 1} bool c; // will occupy the second byte in slot 4
storage{slot: 4, offset: 0} bool d; // will occupy the first byte in slot 4
storage{slot: 4, offset: 16} uint128 d; // will occupy the second half of slot 4
uint128 e; // will occupy the first half of slot 5
storage{slot: 5, offset: 16} uint128 f; // will occupy the second half of slot 5
storage{slot: 6, offset: 0} bool g; // will occupy first byte in slot 6
bool h; // will occupy second byte in slot 6
storage{slot: 6, offset: 2, reserved: 2} bool i; // will occupy third byte in slot 6
bool j; // will occupy fifth byte in slot 6
storage{slot: 6, offset: 16, reserved: 48} uint128 k; // will occupy second half of slot 6
// slot 7 will remain unused
uint128 l; // will use the first half of slot 8
}
An alternative notation-wise would be to merge slot and offset into a single byte offset that is then split into slot = byteOffset/32
and offset = byteOffset%32
(to which the same restrictions would apply). A copy of the example above using this notation:
contract A {
uint256 a; // will occuply full slot 0
// slots 1 and 2 will remain unused
storage{offset: 96, reserved: 32} bool b; // will occupy full slot 3
storage{offset: 129} bool c; // will occupy the second byte in slot 4
storage{offset: 128} bool d; // will occupy the first byte in slot 4
storage{offset: 144} uint128 d; // will occupy the second half of slot 4
uint128 e; // will occupy the first half of slot 5
storage{offset: 160} uint128 f; // will occupy the second half of slot 5
storage{offset: 192} bool g; // will occupy first byte in slot 6
bool h; // will occupy second byte in slot 6
storage{offset: 194, reserved: 2} bool i; // will occupy third byte in slot 6
bool j; // will occupy fifth byte in slot 6
storage{offset: 208, reserved: 48} uint128 k; // will occupy second half of slot 6
// slot 7 will remain unused
uint128 l; // will use the first half of slot 8
}
Another alternative would be to require specifying the location for all variables, if the location is specified for any variable.
Also we could at a later point allow compile time evaluated expressions in the specifier, i.e.:
storage{slot: keccak256("some_key")} uint256 some_key;
Although we'd need to consider that one could construct those to specifically collide with some mapping key, so this would be dangerous.
Although that's also true for choosing some specific value for slot:
that happens to be the location of some mapping element.
Maybe we should gather some data about how this feature would be used. One use is avoiding clashes during upgrades, another is having more efficient use of storage by combining small variables in a certain way. I think just providing full flexibility all the time might not be the way to go as it is too easy to get wrong. So it could already be enough to only allow hashed locations and another way to specify which variables to combine (without specifying the offset exactly) or when to insert "start a new slot here".
What can "go wrong"? Or in particular, what can go wrong that we can't easily detect at compile time?
I'd argue that it makes more sense to provide a general solution and, if deemed necessary, restrict it to simple cases (as in restrict to some particular kinds of values for slot
, etc. - e.g. restricting to only supporting "start a new slot here" would be to require slot
to be the "current slot" plus one and require offset
to be zero).
That way we can always extend the very same solution to support more cases, instead of needing breaking changes and new language features...
One use is avoiding clashes during upgrades
For the sake of upgrades, it'd seem that the only requirement is to be able to assign an immutable id
to a variable, which should be deterministically mapped to a slot (like the storage{slot: keccak256("some_key")}
proposed above). It's not really important where in the storage the variable is kept.
As for EIP2330 linked above, the requirements are pretty much the same. As long as there is a deterministic process for calculating the storage slot, the actual slot can then be just exposed in the ABI for any consumers.
After reading this issue, and thinking about the issue, this is my proposed solution.
deterministic
uint256 deterministic myNumber;
is keccak256('myNumber')
immutable
which doesn't use storage, deterministic
variables will be removed when calculating the sequential slots of each variable, the keccak256 is calculated at compile time and used for all instances.variable name
in multiple inherited contracts with mixed deterministic
states it should throw an error at compile time saying "can't use deterministically declared variable non-deterministically"It seems simple enough to remove complexity but accomplish some of the major goals of this thread.
However, it seems like this thread has grown with a list of reasons and use cases which can only be satisfied by increasingly complex low level access which is difficult to implement without adding a foot cannon.
Edit: Thinking about it more, it might be sufficient to commit only to the variable name, as someone using this feature would probably make their variable names more descriptive address deterministic openZepplinProxyImplementation;
which should probably throw an error if an inherited contract tries to use uint256 deterministic openZepplinProxyImplementation;
which could be another foot cannon.
Edit2: I have a low-deploy-gas-cost proxy contract that I optimized the bytecode for, and it would be great to use single byte storage slots (ie. 0xff) without needing to create 255 dummies... I just tried upping my storage slot for the proxy to PUSH32 with a random hash instead of the 0 I'm using right now, it bumped my deploy cost from 80k to 100k (since I have 1 PUSH in the deploy code and 2 PUSHes of it in the contract code.)... so I would definitely also enjoy the ability to specify an arbitrary value for the slot as well... that said, my use case is extremely niche so I understand not accommodating it.
Copying the suggestion from @dominicletz from #7593:
A new keyword
fixed(@N)
is proposed that can be used to define fixed slot position in interfaces.interface ContractAddressMap { public fixed(@5) mapping(bytes32=>address) addr; }
Here's my spin on specifying storage slots, based on existing EIPs that could benefit from this:
uint256
/bytes32
(see examples)at (<slot>)
(or at(<slot>, <offset>)
) seems more intuitive and readable than using e.g. @
at <slot>
storage_key("key")
works toofixed(@N)
syntax from #7593 could work too, if N
can be more than just an integerEXTSLOAD
opcode from their EIP on the EVM level doesn't care how Solidity compiles their fixed(@N)
syntaxbyte32
s over just integersuint256
/bytes32
s. This is to support e.g. EIP-1967 where we substract 1 after hashing the keyuint256
/bytes32
s, but again, accepting to-be-hashed strings on top of that seems finesomeVarN
in 1st example)
referencing(<state variable>)
(see NewFacet.sol
in 2nd example)This is partly with my (although limited) experience working with Solidity and proxy contracts, the feedback in this issue and the mentioned EIPs. It's both an attempt at covering as much of the use cases and requirements, while also seeing if there's any (planned) progress on this issue. I've noticed now that I'm rereading it that this is quite a big comment, hopefully that isn't too much of an issue.
Example showing how it behaves on its own and how it interacts with non-specified slots:
contract SomeProxy {
uint256 private someVar1; // storage slot 0x0 (in this case starting at 0x0 because there are no inherited fields)
uint256 private someVar2 = 5; // storage slot 0x1
uint256 public something1 at(0xAA112233) = 99; // storage slot 0xAA112233
// This would produce an error/warning because it uses the same storage slot as `something1`
uint256 public something2 at(0xAA112233) = 123; // storage slot 0xAA112233
uint256 public someVar3; // storage slot 0x3 (right behind `someVar2`, thus ignoring `something1` and `something2`)
bytes32 constant IMPLEMENTATION_SLOT = bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1);
// ^ 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
// Not specifying the offset, so can use the syntactic sugar version without the brackets
address private _implementation at IMPLEMENTATION_SLOT; // storage slot 0x360894...
// ....
}
_the _implementation
part is based on the storage slots specified in EIP-1967_
This could also work in interfaces, which helps a lot with diamond storage (EIP-2535), related to #9551. In this example:
OldFacet.sol
specifies a standalone diamond facet with a storage struct at keccak256('diamond.storage.OldFacet')
DiamondStorages
from Storage.sol
for whatever reason. Might be an extern facet, or developed earlierStorage.sol
specifies an interface DiamondStorages
with all known diamond storages, at their proper storage slotsNewFacet.sol
specifies a new diamond facet that also makes use of OldFacet's storage by extending DiamondStorages
DiamondStorages
needs to define the storages your new facet uses, it could instead define all storages part of the diamond. In that case, should "overlapping storage slots" warnings be enabled, your diamond gets checked for this issue// OldFacet.sol
bytes32 constant OLD_FACET_STORAGE_SLOT = keccak256("diamond.storage.OldFacet");
struct OldFacetStorage {
// ...
}
contract OldFacet {
// Directly registers the storage field/slot here, e.g. an externally developed or pre-Storage.sol contract
OldFacetStorage storage at OLD_FACET_STORAGE_SLOT;
// ....
}
// Storage.sol
import "./OldFacet.sol";
import "./NewFacet.sol";
interface DiamondStorages {
OldFacetStorage oldFacetStorage at OLD_FACET_STORAGE_SLOT;
NewFacetStorage newFacetStorage at NEW_FACET_STORAGE_SLOT;
// ....
}
// NewFacet.sol
import "./DiamondStorages";
bytes32 constant NEW_FACET_STORAGE_SLOT = keccak256("diamond.storage.NewFacet");
struct NewFacetStorage {
// ...
}
// Makes use of Storage.sol instead of specifying the storage field/slot for every (known/used) faucet's storage
contract NewFacet is DiamondStorages {
// Optional "aliasing storage fields" which can be handy for shortening long names to shorter ones
NewFacetStorage private s referencing(newFacetStorage);
function banana() public returns (uint256) {
// Using OldFacet's storage as declared in `DiamondStorages` in `Storage.sol`
// Using NewFacet's storage as declared by `s`, using the same slot as `newFacetStorage` in `DiamondStorages`
return oldFacetStorage.apple + s.durian;
}
}
Since all the used/known diamond storages are all defined and known at compile-time, the compiler could (as part of later feature requests) check whether any storage slots overlap each other.
Example of how this would benefit #7593 should non-integer values for N
be allowed:
interface ContractAddressMap {
public mapping(bytes32=>address) addr at keccak256('ENS.addr');
// or different versions of their syntax that allows non-integer `N` values:
public fixed(@keccak256('ENS.addr')) mapping(bytes32=>address) addr; // keep `@` to differentiate from e.g. `map_v1`
public fixed(keccak256('ENS.addr')) mapping(bytes32=>address) addr; // get rid of `@`, force 1st argument to be location
public fixed('ENS.addr') mapping(bytes32=>address) addr; // assuming using a direct string implies `keccak256`
}
contract Resolver is ContractAddressMap {
// ...
}
Mind that this also more or less solves their issue regarding conflicts, as compared to using fixed(@5)
, it's a lot harder to have clashes when using e.g. keccak256('ENS.addr')
. Suggesting these kind of modifications to the EIP-2330 draft seems doable and beneficial for everyone.
Should #3157 be implemented, this can help a lot with readability with complex keys later on, e.g. for our first example:
function proxy_slot(string key) public pure returns(bytes32) {
return bytes32(uint256(keccak256(abi.encodePacked('eip1967.proxy.'), key)) - 1);
}
contract SomeProxy {
// Points to the slot at `bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)`
// aka `0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc`
address private _implementation at proxy_slot("implementation");
}
This feature request is purely to use statically assigned storage slots, as specified by those EIPs or used in the diamond (storage) pattern. The whole "deterministic storage slot" idea is also interesting, but more of a separate feature from (or on top of) specifying specific storage locations. It's worth mentioning that the latter could still be relatively easily done should (the static part of) this feature be implemented:
contract DeterministicFields {
address private _owner at keccak256('_owner');
// or again, if we have #3157 and support pure functions in the contract itself during compile-time:
function storage_slot(string key) internal pure returns(bytes32) {
return bytes32(uint256(keccak256(abi.encodePacked('DeterministicFields.'), key)) - 1);
}
address private _admin at storage_slot('_admin');
// ^ Would use storage slot `keccak256('DeterministicFields._admin')`, making it less likely to clash
// with storage slots from other contracts using an `_admin` field (e.g. in base contracts or other facets in a diamond)
}
In https://github.com/ethereum/solidity/issues/8353#issuecomment-932474378 I have described another solution, similar to what @gnidan and @spalladino suggested above (see https://github.com/ethereum/solidity/issues/597#issuecomment-406582568, https://github.com/ethereum/solidity/issues/597#issuecomment-418798622).
In short, you can already use a mapping of structs to give your contract a set of variables at a storage location deterministically computed from some key:
contract MyContract {
struct State {
uint variable1;
address variable2;
}
mapping (string => State) states;
function f() public returns (uint) {
return states["MyContract"].variable1;
}
}
We could just add a bit of syntax sugar on top of that and have a simple solution that might be good enough in most cases. I.e. this would generate bytecode identical to the above:
contract mapping MyContract {
uint variable1;
address variable2;
function f() public returns (uint) {
return variable1;
}
}
Since it would be a new contract variety, it would be fully backwards-compatible.
This issue has been marked as stale due to inactivity for the last 90 days. It will be automatically closed in 7 days.
We have multiple issues along this (see e.g. the list in https://github.com/ethereum/solidity/issues/13466#issuecomment-1419319182) - I'm marking this one here unstale and as must have eventually
, since it's the most generic and most likely version to be implemented eventually.
This issue has become more critical with the growing popularity of account abstraction, so I would like to resume this discussion.
Account abstraction adds a new challenge that, to the best of my knowledge, no other contract has faced before.
The user owns the account, typically a proxy, and may occasionally wish to switch to a different implementation as the needs change. E.g. switch between Safe and Argent. Unlike other proxy situations where new implementations use a compatible storage layout, or at least the new implementation is aware of the old one and can perform a conversion, now we're dealing with implementations that are not aware of each other.
Currently, most account implementations start at storage slot zero. When switching implementations, the user may inadvertently end up with things like shadow signers, e.g. this Safe which seems to have one valid signer, but in fact has 20 invisible signers who can actually transact in the Safe.
There is no safe way for users to migrate between account implementations, unless each account uses a different storage base. I would like to propose a pragma for doing so:
pragma storage_base <value>;
When present, the compiler calculates keccak(value) and uses it as a hardcoded offset for storage access. A typical value would be
The pragma only sets the offset of next items, and following items are incremented by one, as usual. Therefore if the pragma appears multiple times, each pragma forms a "logical group" of member fields.
A buggy/malicious account would still be able to bypass it and access other storage offsets, but it'll require asm code and therefore attract auditors' attention. Pure solidity code will only access storage determined by the storage_base
value.
@yoavw Typically what people do to get this is to add a state variable uint[1000] junk
.
// Inherit this for leaving out the first 100 slots
abstract contract storageBase {
uint[100] junk;
}
Would that be a reasonable way to achieve the same?
Would that be a reasonable way to achieve the same?
I'm not sure, since the storage base has to be at a random location in the storage space, typically keccak(project_name)
to avoid conflicts between different account implementations. solc would complain if contracts allocate an array half the size of the storage space.
Another issue is that a contract may need to access the storage base of another contract. If they allocate these slots for an array, they won't be able to. At least two cases require this:
S1
and a module starts at S2
, with S1 < S2
. The module will have the S1
space inside its junk
array and won't be able to access these variables.With the pragma I suggested, it would be accessed like this:
contract SafeStorage {
#pragma storagebase "safe.core"
uint threshold;
mapping (address=>address) owners;
#pragma storagebase "safe.modules.niftymodule"
mapping (address=>address) someModuleMapping;
uint someModuleVariable;
}
With the pragma I suggested, a Safe migration module could be something like this:
contract MigrateSimpleAccountToSafe is OwnerManager {
#pragma storagebase "safe.core"
uint threshold;
mapping (address=>address) owners;
#pragma storagebase "simpleaccount"
address public simpleAccountOwner;
function migrate() public {
addOwnerWithThreshold(simpleAccountOwner, 1);
simpleAccountOwner = address(0);
}
}
Pragmas are a mechanism for modifying file-based compiler settings - I wouldn't consider them a good match for this purpose. My preferred approach would still be to have specific syntax for specifying the storage location of any specific storage slot, which would result in the following slots to continue using storage after the previous slot as usual, until the next slot has specified a specific storage location.
So in your example something like
contract MigrateSimpleAccountToSafe is OwnerManager {
storage{slot: keccak256("safe.core")} uint threshold;
mapping (address=>address) owners;
storage{slot: keccak256("simpleaccount")} address public simpleAccountOwner;
function migrate() public {
addOwnerWithThreshold(simpleAccountOwner, 1);
simpleAccountOwner = address(0);
}
}
(potentially with different syntax, but with regular language keywords decorating the slots instead of pragmas)
Pragmas are a mechanism for modifying file-based compiler settings
It's mostly a file-based property and should affect everything in that file. Except for rare cases like a migration contract or a module accessing the account's core storage, the pragma is set at the beginning of the file and affects all variables defined in that file.
We could also have a syntax for variable-level setting, but I think we need a way to do it for the entire file. Otherwise we risk missing some variable and it gets based at slot 0 rather than the account's storage base. None of the variables should be based at zero if the implementation might change.
In the above example, threshold
is based at keccak256("safe.core")
, but wouldn't owners
be based at zero rather than keccak256("safe.core")+1
? If it's based at zero, it would conflict with any account implementation that also happens to have something at zero due to making the same mistake?
The intent we'd like to signal to the compiler is "from here on, variables start at slot S and grow from there". If pragma is not the appropriate way to signal something like this, what would be the right way?
It's not a file-based property at all - it's a property at most of a contract, but really of its state variables - pragmas are definitely the wrong tool for this. I mean, even in your example, it's a property that changes more than once within one contract in a file :-).
In the above example, threshold is based at keccak256("safe.core"), but wouldn't owners be based at zero rather than keccak256("safe.core")+1? If it's based at zero, it would conflict with any account implementation that also happens to have something at zero due to making the same mistake?
In my last comment I said "specifying the storage location of any specific storage slot, which would result in the following slots to continue using storage after the previous slot as usual" - storage slots are generally not assigned globally but in relation to the last slot (since there may be packing involved). So whenever we'd specify the specific slot for any state variables, the next variable would continue continuously from there, and in particular in the example owners
would live at keccak256("safe.core")+1
. That's the only sane choice, I'd argue, since interleaving different continuous storage regions would be horribly confusing.
The intent we'd like to signal to the compiler is "from here on, variables start at slot S and grow from there". If pragma is not the appropriate way to signal something like this, what would be the right way?
I'd argue that the right way to do this is specifying the slot directly for the first variable that's supposed to live at slot S - and then let the compiler append the next slot after the previous one like it always does.
ok, no pragma then :)
Let's go with the syntax you're suggesting.
One case I'm unsure of, is inheritance. A pragma would have been placed before the contract definition, affecting everything that comes after it. With the above syntax, how would the following be handled?
abstract contract Ownable { // OpenZeppelin Ownable.sol - imported library, not modified locally.
address private _owner;
...
}
contract SomeAccount is Ownable {
storage{slot: keccak256("someaccount")} mapping(address => address) internal modules;
...
}
My understanding is that Ownable._owner
gets allocated before SomeAccount.modules
. Wouldn't _owner
end up in slot 0 then?
Over 7 (SEVEN!) years into this thread about something obviously useful and desirable, what legitimate arguments go against just rolling with something simple, stupid, and safe:
contract Contract {
mapping (address=>uint256) stuff at keccak256('stuff');
// ...
}
or
contract Contract {
mapping (address=>uint256) stuff @ keccak256('stuff');
// ...
}
Not having this feature was a design flaw right from the beginning, probably due to the mistaken belief held at the time that "code is law, code is immutable," which allowed for abstracting storage address away since nobody would look at it again.
Over 7 (SEVEN!) years into this thread about something obviously useful and desirable, what legitimate arguments go against just rolling with something simple, stupid, and safe
Yes please! Something simple and stupid is better than nothing at all. We need this.
As for this specific syntax, it'll be a bit error-prone since the developer needs to manually handle the storage counter or use different strings for each variable:
contract Contract {
mapping(address=>uint256) mapping1 at keccak256('stuff');
uint256 array1[10] at keccak256('stuff')+1;
uint256 num at keccak('stuff')+11;
// ...
}
or
contract Contract {
mapping(address=>uint256) mapping1 at keccak256('Contract.mapping');
uint256 array1[10] at keccak256('Contract.array1');
uint256 num at keccak('Contract.num');
// ...
}
I think being able to set the storage base, and then all subsequent declarations continue from the new base, is more readable and less error prone. But at this point anything would be better than waiting another 7 years.
I saw the talk by openzeppelin at solidity summit. They have mentioned that their v5 uses storage locations as here.
Problem: this does not include the info in the storage interface in the solc output.
It would be great if solidity supports specifying the storage locations so that the storage interface will contain all these values so that tools can utilize this info. Is there any active issue regarding this?
It would be great if solidity supports specifying the storage locations so that the storage interface will contain all these values so that tools can utilize this info.
Right. It should be a compiler level feature, not a hack by the contract itself.
Is there any active issue regarding this?
This issue is still open :) but it's been here since 2016. Can we increase its priority somehow?
@zemse asks if there is an open issue discussing the problem that struct types as used in ERC-7201 will be ignored by solc when producing the storageLayout
compiler output. This is because the struct type is not used in any state variable that solc is aware of, the type is only used as the type of a storage pointer returned by a function.
I've discussed this with the Solidity team in the past on Gitter, but did not open an issue. I do think it should be fixed. We had to implement a pretty complex workaround because of it, for the storage safety checks implemented in OpenZeppelin Upgrades.
EIP-7702 was here.
Happy 8th birthday to this issue! 🎉🥳
And more seriously, with EIP-7702 this becomes critical. EOAs will be delegating to different contracts, all of which may write to storage slots in the EOA itself. Conflicts are pretty much guaranteed if we don't make it easy to use different storage bases.
We discussed this with @yoavw on the design call today. The biggest hangup here is really that none of the solutions proposed so far seem to both be simple and address all the relevant use cases. One that would be the most appealing is letting the user specify an offset for a particular variable and have all the variables defined after that just follow it. The problem is that contract inheritance severely limits its usefulness since the variables of the base contracts are laid out first.
Still, we're aware that EIP-7702 is making the issue more pressing and we need to address it quickly. We decided that we're going to introduce a simple solution that will at least address this particular use case without conflicting with possible extensions in the future.
We'll make it possible to specify a base storage offset for a contract as a whole (including its inheritance hierarchy). For now it will only be possible to specify it for the most derived contract, but we may consider relaxing that restriction if there are good reasons for it. The offset may be specified indirectly (e.g. be a hash of some identifier, which is the common way to do it), but must still be computable at compilation time.
Optionally, we may consider including a mechanism for moving the variables of a specific contract out of that flow, to give them an independent location, which might mesh better with upgradeable contracts. But then there's the question if the location should be completely fixed or depend on the base offset?
That's the general outline, but we still need a concrete syntax proposal.
We're also interested in feedback on that solution. Is there anything it's missing in the context of EIP-7702?
Decoupling storage location from inheritance linearization is important. What is the main challenge to going in that direction rather than just adding a global offset?
In the case of 7702, wouldn't it make more sense to set it globally at the contract level? For example instead of declaring the pragma on a per variable scope, we can set it globally
storagePragma
contract Contract {}
Where the contract code is keccak hashed, and thus every variable downstream is namespaced based on the contract that is currently deployed?
In the case of 7702, wouldn't it make more sense to set it globally at the contract level?
Yes. I believe that's what the solidity team has in mind. Set it globally in the most-inherited contract.
@cameel @yoavw really glad to see this is making progress. I think this will be a big help for the EIP 7702 and for AA wallet implementations in general.
Curious to know if we have a draft or idea on how the syntax will work for setting the base storage slot from the latest comment?
Regarding the below comment:
We'll make it possible to specify a base storage offset for a contract as a whole (including its inheritance hierarchy)
Does it imply that we'll use the final contract's base storage as a whole? keccak256("ModularAccount")
in this case or would the storage be allocated in each contract's declared base storage slot?
basestorage{slot: keccak256("Storage")} // Just assuming a possible syntax
contract Storage {
address entryPoint;
// ...
}
basestorage{slot: keccak256("ModularAccount")} // Just assuming a possible syntax
contract ModularAccount is Storage {
mapping (bytes4=>address) selectorTomodules;
// ...
}
We discussed this with @yoavw on the design call today. The biggest hangup here is really that none of the solutions proposed so far seem to both be simple and address all the relevant use cases.
Is there a summary of what those proposals are? This GitHub issue seems to mostly describe proposals around specifying slots for specific fields, but not necessarily what the currently considered proposals for setting a storage namespace for entire contracts in the context of 7702.
Some random thoughts (although I don't have a concrete syntax proposal).
contract Foo is Bar{storage: 1337} {
}
pragma storage namespace "MyNamespace";
And just assign slots starting at keccak256("foo") - 1
(instead of 0) and "grow downwards" in order to prevent issues around storage slots being at locations with known preimages (if that is a concern). This does, however, get a bit tricky with inheritance (what happens if two files use pragmas to different namespaces?) and I don't know if there are some ugly edge cases that I didn't think about.
As a follow up suggestion, another possible solution is to have something like:
storage namespace Foo is Bar {
slot: 1337
}
Which would output bytecode for a "contract" named Foo whose implementation is Bar with different field storage slot assignment as described above (where the field that is assigned storage slot 0 is instead assigned storage slot 1337, etc.). This eliminates ambiguity around what happens when base and derived contracts start specifying this, since the "storage namepace" is not a contract that can be inherited from.
All syntax suggestions are just placeholders
Inline assembly has now made fully up upgradable contracts possible. One of the main hangups with this is that the storage locations have to stay the same across upgrades. Would it be possible to introduce support for specifying the storage locations for storage variables?