Rollback safety limits when an upgraded Solidity contract changes its storage layout
0 reputation · 09 Oct 2021, 04:23 UTC
0 reputation · 09 Oct 2021, 04:23 UTC
I am designing an upgrade path for an OpenZeppelin-style upgradeable proxy and want to understand the exact limits of safe rollback after a storage schema change. The documented rule is append-only: new state variables go after existing ones, order and types are preserved, and storage gaps reserve slots in base contracts.
There is no on-chain schema versioning in Solidity or the EVM, so layout safety is a tooling discipline enforced by validators like the OpenZeppelin Upgrades plugin, not the compiler. My concern: if V2 appends a new variable, writes to it, and I then roll the proxy back to V1, the old code ignores that slot, which seems safe. But if V2 ever reorders, retypes, or consumes a gap incorrectly, rollback would make V1 misread live state.
Is append-only layout change the precise and complete condition under which a rollback to a previous implementation remains safe, or are there edge cases (structs, mappings, inheritance changes) where even an append-only upgrade breaks rollback?
Does the OpenZeppelin Upgrades plugin offer any documented way to validate a V2-to-V1 downgrade, or does it only validate forward upgrades?
Is shrinking a __gap array by the number of consumed slots sufficient to keep child contract layouts stable across a rollback?
Append-only layout changes are the primary condition for rollback safety. If V2 only adds state variables to the end of the storage layout, rolling back to V1 is safe because the V1 bytecode simply ignores the higher-indexed slots written by V2. However, this is not a complete guarantee of safety; structural changes to the contract hierarchy or type modifications can still break rollback compatibility.
While adding variables is generally safe, the following scenarios can cause V1 to misinterpret state after a rollback from V2:
uint256 to uint128) in V2 may cause V1 to read overlapping or corrupted data if the packing logic changed.The OpenZeppelin Upgrades plugin is designed to validate forward compatibility (V1 $\rightarrow$ V2). It does not provide a native "downgrade" validation tool. To verify rollback safety, you must manually compare the storage layouts.
Regarding __gap arrays: Shrinking a gap is the correct way to add variables to a base contract without shifting child layouts. If a base contract has a uint256[50] __gap and you add two uint256 variables, you must reduce the gap to uint256[48]. This ensures the total slot count for the base contract remains constant, keeping child contract offsets stable across both V1 and V2.
solc --storage-layout ContractV1.sol
solc --storage-layout ContractV2.sol
Diagnostic Detail Needed: Does your V2 upgrade involve changing the inheritance order or adding new base contracts? If so, append-only variable additions will not be sufficient to ensure rollback safety.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.