-
Notifications
You must be signed in to change notification settings - Fork 12.4k
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
purefunctions toviewdue 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.