Why you need to be careful when upgrading your proxy contracts

A storage collision occurs when an upgrade to a proxy contract's implementation changes its storage layout in a way that misaligns with the storage layout of the proxy contract.

Since proxy contracts are designed to delegate calls to implementation contracts, maintaining a consistent storage layout across upgrades is critical.

The proxy contract itself holds the state, while the implementation contract contains the logic. If the storage layout in the implementation contract changes, it can lead to mismatches when the proxy contract tries to access or modify state variables.

To illustrate, consider the following scenario:

Previous Implementation Storage Layout:

address USDC public (slot[0])
address USDT public (slot[1])
address DAI public (slot[2])

New Implementation Storage Layout:

address ETH public (slot[0])
address USDC public (slot[1])
address USDT public (slot[2])
address DAI public (slot[3])

In this example, by introducing a new storage variable ETH at the beginning of the contract, we've inadvertently shifted the original variables down one slot. This means the ETH variable would now occupy what was previously the USDC storage slot (slot[0]), and so forth.

As a result, the contract would try to read ETH's address as USDC, USDC as USDT, and so on.

This misalignment effectively breaks the storage pattern and, by extension, the whole contract's functionality.

How can you prevent this?

To prevent such storage collisions, developers should follow these guidelines:

Always add new state variables to the end of the storage layout in the implementation contract. This approach ensures that the original storage slots are not altered, preserving the alignment with the proxy contract.

What to do for contracts which are meant to be inherited by implementations?

For contracts designed to be inherited and potentially extended, include a "gap" array. This array acts as a buffer of reserved storage slots. When a new variable is added in a derived contract, reduce the size of the gap array correspondingly. This technique maintains the storage layout's integrity across different versions.

Notably, @OpenZeppelin added a specific custom storage slot for their newer versions, effectively mitigating this issue.

Proxy collisions present a significant risk to the upgradeability and functionality of smart contracts. By understanding and employing best practices, such as careful planning of storage layouts and utilizing gap arrays, developers can mitigate these risks. This ensures that smart contracts remain robust, secure, and functional across upgrades, safeguarding the interests of all stakeholders involved.