-
Notifications
You must be signed in to change notification settings - Fork 12k
Storage Slot Collisions in Diamond-Compatible Delegatecall Architecture (Upgradeable Contracts) #5681
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Update: Upon further inspection, I realized that although Additionally, the private helper function The ProblemIn delegatecall-based modular architectures (e.g., Diamond Standard), each module needs a unique storage slot to avoid clashing. The pattern I'm looking at would be: address internal immutable __self = address(this);
bytes32 private immutable INITIALIZABLE_STORAGE =
keccak256(abi.encode(uint256(keccak256(abi.encodePacked(__self, "openzeppelin.storage.Initializable"))) - 1)) &
~bytes32(uint256(0xff));
function _initializableStorageSlot() internal view override returns (bytes32) {
return INITIALIZABLE_STORAGE;
} However, this is not allowed because:
Proposed FixUpdate the mutability of following two functions in function _initializableStorageSlot() internal view virtual returns (bytes32) {
return INITIALIZABLE_STORAGE;
}
function _getInitializableStorage() private view returns (InitializableStorage storage $) {
bytes32 slot = _initializableStorageSlot();
assembly {
$.slot := slot
}
} This would:
Broader ContextThis problem pattern may exist in other OpenZeppelin upgradeable contracts that expose Let me know if this is of interest — I'm happy to open a PR. |
Hi @EricForgy All upgradeable contracts are designed to avoid storage collision via EIP-7201, and we have built a whole suite of tools to prevent storage collisions: https://docs.openzeppelin.com/upgrades-plugins/ Although the collision scenarios you describe are potentially true, it is intended for upgradeable contracts to keep using the same store locations given the library maintains Backwards Compatibility between major versions. Adding the address to the storage root calculation would (for example) clean everyone's balances after upgrading. We're considering adding |
Hi @ernestognw, You're right — just making the storage slot getters If the team is open to it, I’d be happy to submit PRs to add Thanks again. |
Hi again, After realizing the upgradeable contracts are transpiled from the base repo, I looked into adjusting the storage slot logic more directly. The specific problem I’m addressing is in REENTRANCY_GUARD_STORAGE In a setup where calls may be delegated to multiple contracts, this leads to slot collisions. In my fork, I made the following change:
This allows consumers like me to override just the slot position in a safe way without touching any core logic. Here's the commit for reference: The actual fix would need to be implemented in the transpiler. Unfortunately, I don’t know how to do that. If there is interest, I could try to figure it ouot, but would probably need some help. |
While investigating how the transpiler works, I realized that Thank you for your consideration. |
Hey @EricForgy, We’ll move forward with making the storage slot getters However, just to clarify, this doesn’t imply we’ll be officially supporting the Diamond Pattern in our libraries — at least not at this point. Our focus remains on production-ready support for UUPS, Transparent, and Beacon patterns via @openzeppelin/upgrades for performing upgrades in a safe manner, which continue to be our recommended upgrade paths. Be aware that the |
Thanks so much - I really appreciate the thoughtful response and your willingness to move forward with this change 🙏 |
Hello, @gonzaotc mentionned this issue during our weekly call yesterday, particularly in the context of "diamond proxy" that use multiple implementation. The point was made that these multiple implementations (facets) may want to use independant reentrancy guard, and that collision of the transiant slot used for that would "couple" the modifier. While I understand this usecase, I personally believe that making this slot virtual here is probably not the best approach.
IMO the better option is to keep the slot "location" of the reentrancy guard(s) constant across facets, like it is today, but allow the dev to derive multiple independant guards from it. It would look like abstract contract ReentrancyGuardTransient {
using SlotDerivation for bytes32;
using TransientSlot for *;
// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.ReentrancyGuard")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant REENTRANCY_GUARD_STORAGE =
0x9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00;
error ReentrancyGuardReentrantCall(bytes32 key);
modifier nonReentrant() {
_nonReentrantBefore(bytes32(0));
_;
_nonReentrantAfter(bytes32(0));
}
modifier nonReentrantWithKey(bytes32 key) {
_nonReentrantBefore(key);
_;
_nonReentrantAfter(key);
}
function _nonReentrantBefore(bytes32 key) private {
// On the first call to nonReentrant, REENTRANCY_GUARD_STORAGE.deriveMapping(key).asBoolean().tload() will be false
if (_reentrancyGuardEntered(key)) {
revert ReentrancyGuardReentrantCall(key);
}
// Any calls to nonReentrant after this point will fail
REENTRANCY_GUARD_STORAGE.deriveMapping(key).asBoolean().tstore(true);
}
function _nonReentrantAfter(bytes32 key) private {
REENTRANCY_GUARD_STORAGE.deriveMapping(key).asBoolean().tstore(false);
}
function _reentrancyGuardEntered(bytes32 key) internal view returns (bool) {
return REENTRANCY_GUARD_STORAGE.deriveMapping(key).asBoolean().tload();
}
function _reentrancyGuardEntered() internal view returns (bool) {
return _reentrancyGuardEntered(bytes32(0));
}
} That would allow the devs that want independant reentrancy guard in their facets to use a "per-facet" key. @EricForgy wdyt ? In your case, |
Hi @Amxx, Thank you for the thoughtful response and proposed alternative. Just to clarify upfront: I’ve moved away from the For example, here's how I use it in practice (for By overriding the slot getter, developers can assign a unique slot (i.e., key) per module or context. This is sufficient to avoid collisions in modular systems like diamond routers while maintaining compatibility with existing tooling. Regarding the possible merging of facets: while I understand the concern, I’d argue that in practice, the need to merge independently guarded modules is rare, and there’s no added gas cost or maintenance burden in keeping them separate. Prioritizing simplicity and efficiency in the common case seems like a good tradeoff here. Compared to the alternative proposal - which introduces additional indirection and dependency on Best regards, Edit: I do like the key idea too. I can see it being useful if your want fine control even at a function level within a given facet. What if we keep this PR with pure virtual slot getter and separately add |
🧐 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 modulardelegatecall
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:
Why this works:
pure
functions toview
due to__self
, with no storage reads and negligible gas impact.Suggested Implementation
Introduce a new base contract or modifier version (e.g.
DiamondCompatibleInitializable
) that mirrorsInitializable
but uses per-module slot derivation. This could live in an experimental ordiamond
subdirectory to avoid impacting the existing suite.Let me know if you want this as a full PR — happy to help.
The text was updated successfully, but these errors were encountered: