Description
🧐 Motivation
OpenZeppelin’s upgradeable contracts are widely used in standard proxy-based deployments. However, when used in a modular delegatecall architecture (such as the Diamond Standard or similar plugin-style routers), fixed storage slot constants across multiple modules can cause critical storage collisions.
Each module is executed in the same storage context (the proxy/Router), so repeated use of hardcoded slot identifiers like INITIALIZABLE_STORAGE
leads to overlapping writes between otherwise isolated modules. This makes current upgradeable contracts unsafe in modular delegatecall
setups — an increasingly common pattern in modern contract systems.
📝 Details
I propose a backward-compatible enhancement to support diamond-compatible usage by deriving storage slot positions uniquely per module based on their deployed address.
This can be done using the following pattern:
address private immutable __self = address(this);
bytes32 private immutable INITIALIZABLE_STORAGE =
keccak256(
abi.encode(
uint256(keccak256(abi.encodePacked(__self, "openzeppelin.storage.Initializable"))) - 1
)
) & ~bytes32(uint256(0xff));
Why this works:
- Safe: Each module gets a deterministic, unique slot namespace.
- Aligned: The bitmask aligns with EIP-1967 slot layout expectations.
- Minimal Overhead: Only requires switching some
pure
functions toview
due to__self
, with no storage reads and negligible gas impact. - Compatible: Initialization checks are typically only called once, so runtime cost is not a concern.
Suggested Implementation
Introduce a new base contract or modifier version (e.g. DiamondCompatibleInitializable
) that mirrors Initializable
but uses per-module slot derivation. This could live in an experimental or diamond
subdirectory to avoid impacting the existing suite.
Let me know if you want this as a full PR — happy to help.