3

I want to establish a pattern that ensures no storage slot collisions between current and future (compliant) versions of a contract behind a transparent proxy. In particular, contracts that are meant to be inherited.

I'm aware of the reserved slots pattern:

contract InheritMeImSafe {

  address public foo;
  bool public bar;
  bytes32[50] reservation; // reserve 50 slots for future variables

}

I'm also aware of the storage layout of structs and mappings but I'm struggling to find confirmation of how I think about mapped structs.

Consider:

contract InheritMe {

  struct ThingStruct {
    bool foo;
    uint bar;
  }

  mapping(address => ThingStruct) public things;
...

Given that it would be inherited by an upgradeable contract, I want to be sure that the struct is extendable without causing storage collisions (provided one declares new variables at the end, of course). I also don't want superfluous code for no reason. I think it is not necessary to reserve slots in the struct. Is it?

We could, for example:

struct ThingStruct {
  bool foo;
  uint bar;
  bytes32[50]; // reserve 50 slots for future struct members. Needed?
}

I have not found a lot of documentation about the layout of structs, mappings but nothing to confirm my interpretation of how they work together in mapped structs. I don't want to be wrong about this.

Declare a reservation or not?

Thanks! :-)

1 Answer 1

2

I have found a lot of documentation about the layout of structs, mappings but nothing to confirm my interpretation of how they work together in mapped structs.

Structs work exactly the same with or without a mapping. See the relevant part of the documentation.

Given that it would be inherited by an upgradeable contract, I want to be sure that the struct is extendable without causing storage collisions (provided one declares new variables at the end, of course).

I'm assuming that the struct would only be stored "in" the mapping. Then, there is no way to ensure that no mapping storage collisions happen with or without reserved space. The reserved slot pattern only really makes sense to ensure that state variable slots defined at compilation time stay fixed even through upgrades.

For a mapping, the entry slots will be defined at runtime... It's a different problem. Any entry has a probability of creating a collision when its storage slot is computed through keccak256..

Declare a reservation or not ?

I would argue against it for several reasons :

  1. All non in place modification will be much more costly in gas, you can get around it by using only in place updates but it makes updates in the mapping heavy, especially if your struct ends up with many members...
pragma solidity ^0.8.0;

contract Test {

    struct WithoutReservedSlots {
        bool foo;
        uint bar;
    }

    struct WithReservedSlots {
        bool foo;
        uint bar;
        bytes32[50] _reserved;
    }

    mapping(address => WithReservedSlots) reservedMapping;
    mapping(address => WithoutReservedSlots) notReservedMapping;

    // With reserved slots (in place modification) : 45,801 gas
    function addReservedInPlace() public {
        reservedMapping[msg.sender].foo = true;
        reservedMapping[msg.sender].bar = 0;
    }

    // With reserved slots (full update): 140,113 gas
    function addReservedNotInPlace() public {
        WithReservedSlots memory entry;

        entry.foo = true;
        entry.bar = 0;

        reservedMapping[msg.sender] = entry;
    }

    // Without reserved slots (full update) : 45,853 gas
    function addNotReserved() public {
        WithoutReservedSlots memory entry;

        entry.foo = true;
        entry.bar = 0;
        
        notReservedMapping[msg.sender] = entry;
    }
}
  1. This still doesn't solve nor lower the collision probability "issue" and nothing in the current solidity specs will. If you are really concerned about mapping slots collisions, you shouldn't use a mapping or a storage array in the first place.

See this answer to get a rough idea of the level of risk you are currently considering, and the bigger your struct is, the more you increase the storage collision probability as you are taking bigger chunks of the fixed size storage space for each entry.

I hope that answers your question.

2
  • 1
    I appreciate that. The thing that has me puzzled is what happens to structs that need more than one slot. I understand the first slot is "in the mapping", but where does next one go? If it's something like hash(mappingSlot, secondStructSlot), then I would understand what's actually going on. Or is it more like mappingSlot+1? I'm not sure the difference increases or decreases risk of a collision if the struct is ever extended in the future. Thoughts? Commented May 20, 2022 at 20:51
  • 2
    They are just stored contiguously starting from the storage slot computed with a given key. It increases the risk of collision since each element is bigger. Let's assume you have 10 storage slots availalble, if you are to store 2 structs of 1 slot each, the first one take any slot (X) , and the next one has 1/10 chances of colliding (Y = X). If each struct takes 2 slots, then the second one actually has 3/10 chances of colliding (Y = X - 1, Y = X, Y = X + 1) because in that case, storage collisions happens even with there is no storage slot (hash) collision.
    – hroussille
    Commented May 23, 2022 at 15:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.