Storage layout
When using delegatecall
, the implementation contract’s code is executed in the proxy’s storage context. For that to work properly, the storage layout in the proxy must match the storage layout in the implementation.
However, in your StorageProxy
, you’ve inherited from OpenZeppelin’s Ownable
, which adds its own storage variables—specifically _owner
. This shifts around your storage slots in a way that your ImplementationvX
contracts do not account for.
Parent contract
The parent contract extends the Ownable
contract from Openzeplin
1. OpenZeppelin’s Ownable
In the standard (simplified) OpenZeppelin Ownable contract, you typically have something like:
abstract contract Ownable { address private _owner; // ...}
So, for Ownable, the first storage slot (slot 0
) is _owner
.
2. StorageProxy
Your StorageProxy
contract inherits from Ownable
and declares two variables:
contract StorageProxy is Ownable { uint public num; // (1) address implementation; // (2) ...}
So the layout in StorageProxy
is effectively:
- Slot 0 →
_owner
- Slot 1 →
num
- Slot 2 →
implementation
3. Implementation v1 / v2 / v3
Each of your implementation contracts declares:
solidityCopy codecontract ImplementationvX { uint public num; // Only variable ...}
Because there’s just one variable (uint public num
), it sits in slot 0 in each of those contracts:
- Slot 0 →
num