Structs with gap in Upgradeable smart-contract

Hello Everyone, I have a question related to a struct that I have in my project, at first glance I was planning to fill the storage of struct with data, but turn back and leave it like this:

struct MyStruct {
        uint16 val1;
        uint16 val2;
        uint64 val3;
        // Technically there is a 160bits gap here
        string name;
}

In this case it would be secure to upgrade/replace this smart contract struct on a version 2 to look like this?

struct MyStruct {
        uint16 val1;
        uint16 val2;
        uint64 val3;
        uint160 val4;
        string name;
}

It would be a must if I could use that space.
Thanks in advance.

Hi, very interesting question, the answer is yes you could do that, but I wouldnt consider it safe since you would have to be very careful not to accidentally overlap storages.

The storage layout for MyStruct version 1 would be, ignoring the astIds:

"members": [
                  {
                    "astId": 486,
                    "contract": "",
                    "label": "val1",
                    "offset": 0,
                    "slot": "0",
                    "type": "t_uint16"
                  },
                  {
                    "astId": 488,
                    "contract": "",
                    "label": "val2",
                    "offset": 2,
                    "slot": "0",
                    "type": "t_uint16"
                  },
                  {
                    "astId": 490,
                    "contract": "",
                    "label": "val3",
                    "offset": 4,
                    "slot": "0",
                    "type": "t_uint64"
                  },
                  {
                    "astId": 492,
                    "contract": "",
                    "label": "name",
                    "offset": 0,
                    "slot": "1",
                    "type": "t_string_storage"
                  }
                ],

And for the second version:

"members": [
                  {
                    "astId": 577,
                    "contract": "",
                    "label": "val1",
                    "offset": 0,
                    "slot": "0",
                    "type": "t_uint16"
                  },
                  {
                    "astId": 579,
                    "contract": "",
                    "label": "val2",
                    "offset": 2,
                    "slot": "0",
                    "type": "t_uint16"
                  },
                  {
                    "astId": 581,
                    "contract": "",
                    "label": "val3",
                    "offset": 4,
                    "slot": "0",
                    "type": "t_uint64"
                  },
                  {
                    "astId": 583,
                    "contract": "",
                    "label": "val4",
                    "offset": 12,
                    "slot": "0",
                    "type": "t_uint160"
                  },
                  {
                    "astId": 585,
                    "contract": "",
                    "label": "name",
                    "offset": 0,
                    "slot": "1",
                    "type": "t_string_storage"
                  }
                ],

As you can see all uint values will belong to the same slot on different offsets, so in theory it can be possible to use this gap in structs when upgrading contracts.

Note: For this you would need to manually implement the upgrades since the OZ tool for upgrades wont allow this.

2 Likes

Thank you so much Julissa :smile:. I think I don't have the necessary skill to execute this change by myself. Any other approach that you suggest to use this gap using OZ tools? Or it would be better to forget that gap and create another structure to hold the new values?

I think the second option would be ideal, since our tools try to avoid things like this in order to protect the users from accidentally shifting the storage if they don't know how much storage is left in mind. If the feature interest you, and you know others that might be wanting this too, you could propose it in the repo.

Thanks Julissa. Really appreciate your help. Totally understand the reasons OZ not opt to fulfill this gaps cause it sounds really tricky to solve it on any possible structures.

Hi, just to provide an update here, this scenario is supported in the latest versions of the OpenZeppelin Upgrades Plugins which includes checks for storage gaps. The scenario that you described is similar to the last example in the storage gaps link, where an additional variable can use some of the remaining space in a storage slot (as kind of an implied gap).

As Julissa mentioned, accidentally shifting the storage would be unsafe so you would need to ensure that the storage layout remains compatible. The Upgrades Plugins help to perform this type of validation for you.

1 Like

Oh.... let me get this straight then, it is possible for me create this "virtual" gap after deployed the contract? For example... In the case I mention above

struct Base {
        uint16 val1;
        uint16 val2;
        uint64 val3;
        // Technically there is a 160bits gap here
        string name;
}

If I replace the version 1.0 of the file(already deployed) for:

struct Base {
        uint16 val1;
        uint16 val2;
        uint64 val3;
        uint160[1] __gap;
        string name;
}

Then I can use this reserved slot on a version 2? like this?

struct Child {
        uint16 val1;
        uint16 val2;
        uint64 val3;
        uint160 val4;
        string name;
}

Do you think this could work? Obviously I'll make a lot of tests before deploy this, but I'm really curious as technically that empty slot exists.

No need to add uint160[1] __gap;. (That won't work anyways, since the array starts a new slot)

You can just go from:

struct MyStruct {
        uint16 val1;
        uint16 val2;
        uint64 val3;
        // Technically there is a 160bits gap here
        string name;
}

to

struct MyStruct {
        uint16 val1;
        uint16 val2;
        uint64 val3;
        uint160 val4;
        string name;
}
2 Likes

Sure. Will try and return in case of succeeded. Thanks!