Why storage layout matters

In Solidity, state variables are stored in contract storage slots in declaration order, with packing rules applied for smaller types. In a non-upgradeable contract, the layout is fixed for the lifetime of the deployed bytecode. In an upgradeable proxy architecture, however, the proxy keeps the storage while the implementation logic can change.

That means the new implementation must interpret the proxy’s existing storage exactly the same way as the old one. If it does not, the contract may read the wrong slot or write over unrelated data.

A storage collision can happen when you:

  • insert a new variable in the middle of existing state variables,
  • change the type of an existing variable,
  • reorder inherited base contracts,
  • remove a variable without reserving space,
  • or introduce a new parent contract that shifts the linearized layout.

The result is often subtle: the contract still compiles and upgrades successfully, but the state becomes inconsistent.


How collisions happen in practice

Consider a simple upgradeable vault:

// V1
contract VaultV1 {
    address public owner;
    uint256 public totalDeposits;
    mapping(address => uint256) public balances;
}

If you later deploy:

// V2 - unsafe
contract VaultV2 {
    address public owner;
    bool public paused;          // new variable inserted
    uint256 public totalDeposits;
    mapping(address => uint256) public balances;
}

The new paused variable occupies a storage slot that previously held part of totalDeposits or another variable depending on packing and ordering. The contract may now interpret old data incorrectly.

A more dangerous case is changing a type:

// V1
uint256 public feeBps;

// V2 - unsafe
uint128 public feeBps;

Even if the variable name stays the same, the storage interpretation changes. Your code may read only part of the original value or pack adjacent variables differently.

Inheritance can also shift storage

Solidity lays out storage according to the C3 linearization of inherited contracts. If you add a new base contract before an existing one, the full layout can shift.

contract A {
    uint256 internal x;
}

contract B {
    uint256 internal y;
}

// V1
contract V1 is A, B {}

// V2 - unsafe if it changes linearization or adds state in a parent
contract V2 is NewBase, A, B {}

Even if V2 does not add obvious state variables, inherited state can still move.


The safest pattern: append-only storage

The core rule for upgradeable contracts is simple:

Never change the meaning of an existing storage slot.

In practice, that means:

  • keep existing variables in the same order,
  • only append new variables at the end,
  • never change a variable’s type,
  • never remove variables from the middle,
  • and preserve inheritance order.

Example of a safe upgrade

// V1
contract VaultV1 {
    address public owner;
    uint256 public totalDeposits;
    mapping(address => uint256) public balances;
}
// V2 - safe
contract VaultV2 {
    address public owner;
    uint256 public totalDeposits;
    mapping(address => uint256) public balances;

    bool public paused; // appended at the end
}

Appending paused preserves all previous slots. Existing storage remains intact, and the new variable uses the next available slot.


Use storage gaps for future expansion

A common pattern in upgradeable contracts is reserving unused storage slots in advance. This gives you room to add variables later without shifting inherited layouts.

contract VaultV1 {
    address public owner;
    uint256 public totalDeposits;
    mapping(address => uint256) public balances;

    uint256[47] private __gap;
}

The gap is a fixed-size array of unused slots. In a later version, you can consume part of that gap:

contract VaultV2 {
    address public owner;
    uint256 public totalDeposits;
    mapping(address => uint256) public balances;

    bool public paused;
    uint256[46] private __gap;
}

Why gaps help

  • They make future upgrades easier to plan.
  • They reduce the risk of accidental layout shifts in inherited contracts.
  • They are widely used in production upgradeable libraries.

Important caveat

A gap is not a license to reorder variables. It only provides reserved space at the end. You still need to preserve the original layout exactly.


Storage layout rules you should never violate

ChangeSafe?Why
Append a new variable at the endYesExisting slots remain unchanged
Reorder existing variablesNoSlot assignments change
Change uint256 to uint128NoType interpretation changes
Remove a variable from the middleNoLater variables shift slots
Add state to a base contract before existing basesNoInheritance linearization changes
Append to a reserved storage gapYesUses preallocated space
Rename a variable without changing type/orderYesName does not affect storage layout

Practical example: upgrading a proxy safely

Suppose you are using a transparent proxy or UUPS proxy. The proxy stores state, and the implementation contract provides logic.

Version 1

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract TreasuryV1 {
    address public owner;
    uint256 public treasuryBalance;

    function initialize(address _owner) external {
        require(owner == address(0), "already initialized");
        owner = _owner;
    }

    function deposit() external payable {
        treasuryBalance += msg.value;
    }
}

Version 2 with a safe extension

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract TreasuryV2 {
    address public owner;
    uint256 public treasuryBalance;
    bool public paused;

    function initialize(address _owner) external {
        require(owner == address(0), "already initialized");
        owner = _owner;
    }

    function deposit() external payable {
        require(!paused, "paused");
        treasuryBalance += msg.value;
    }

    function setPaused(bool value) external {
        require(msg.sender == owner, "not owner");
        paused = value;
    }
}

This upgrade is safe because paused is appended after the original variables. The proxy’s existing owner and treasuryBalance values remain valid.

What would break it?

If paused were inserted before treasuryBalance, the old balance could be misread. If owner changed from address to bytes32, the stored owner address would no longer decode correctly.


Common collision sources in real projects

1. Adding variables to inherited parents

A base contract used by multiple implementations can be a hidden source of layout drift. If you modify the base, every child contract may be affected.

Best practice:

  • treat base contracts as part of the storage ABI,
  • version them carefully,
  • and review the full inheritance tree before upgrading.

2. Changing library patterns

Libraries that rely on fixed storage slots, such as diamond storage or namespaced storage, can be safe when used correctly. But if you mix patterns inconsistently, you can still create collisions.

Best practice:

  • use one storage strategy per subsystem,
  • document the slot namespace,
  • and avoid ad hoc storage writes.

3. Upgrading from packed variables

Solidity packs smaller types into a single slot when possible. If you add or reorder small types, you may unintentionally change packing.

Example:

contract PackedV1 {
    uint128 public a;
    uint128 public b;
}

If you change it to:

contract PackedV2 {
    uint128 public a;
    bool public paused;
    uint128 public b;
}

the packing arrangement changes. Even though the types are small, the slot boundaries are no longer the same.


Best practices for safe upgradeable storage

Keep an explicit storage policy

Document a rule such as:

  • append-only state variables,
  • no type changes,
  • no base contract reordering,
  • no storage writes outside approved patterns.

This makes code review easier and reduces accidental regressions.

Use automated storage layout checks

Modern Solidity toolchains can output storage layout metadata. Compare the layout of the old and new implementation before upgrading.

Look for:

  • slot number changes,
  • offset changes,
  • type changes,
  • inherited layout changes.

If the layout differs unexpectedly, stop the upgrade.

Separate logic from state assumptions

Avoid writing code that depends on “current slot order” or low-level storage tricks unless absolutely necessary. The more your logic assumes a specific layout, the harder upgrades become.

Reserve space early

If you expect future features, add a storage gap from the beginning. It is much easier to reserve space than to retrofit it later.

Test upgrades on a fork or local simulation

Before mainnet deployment:

  1. deploy V1,
  2. initialize and populate state,
  3. upgrade to V2,
  4. verify all critical variables retain their values,
  5. run functional tests against the upgraded contract.

This catches layout mistakes that compilation alone will not reveal.


When to prefer namespaced storage

For complex systems, especially modular protocols, namespaced storage can reduce collision risk. Instead of relying on inherited slot order, each module stores its data in a dedicated struct anchored at a fixed slot.

This approach is useful when:

  • multiple teams maintain different modules,
  • you expect frequent upgrades,
  • or you want to isolate storage for plugins.

However, namespaced storage must still be implemented consistently. A wrong namespace constant can be just as dangerous as a bad slot order.


Upgrade review checklist

Before approving an implementation upgrade, verify the following:

  • Existing state variables appear in the same order.
  • No variable types changed.
  • No inherited base contracts were reordered.
  • Any new variables were appended only.
  • Storage gaps were reduced correctly, if used.
  • Initialization logic does not overwrite existing state.
  • Automated storage layout comparison shows no unexpected drift.
  • A fork or simulation confirms state preservation.

If any item is unclear, treat the upgrade as unsafe until proven otherwise.


Conclusion

Storage layout collisions are one of the most damaging upgradeability bugs in Solidity because they often compile cleanly and fail silently at runtime. The safest defense is disciplined storage design: append-only variables, stable inheritance, reserved gaps, and automated layout checks before every upgrade.

If you build upgradeable contracts, treat storage layout as part of your public interface. Once deployed, it must remain stable just like an external function signature.

Learn more with useful resources