Programming proxy contracts with OpenZeppelin | Summary
- Introduction
- General points
- To avoid
- Potentially Unsafe Operations
- Deployment & Test
- Specificity
- Checklist
- Reference
Introduction
Proxy contracts are an architecture widely used within the Ethereum ecosystem. Basically, in this architecture, there are two main contracts:
- The proxy contract, which stores the memory and delegate its calls to another contract called logic or implementation contracts
- This second contract contains all the smart contract logic.
The proxy will delegate the calls from the sender to the implementation.

Proxy contracts can generally be upgraded to a new implementation.

For example, the implementation of an ERC-20 proxy will contain all the logic to perform a transfer while the balance of each address is stored in the proxy memory.
To change the behavior of the proxy contract, the proxy admin can deploy a new logic contract and points the proxy to this new one.
But this kind of architecture is not easy to implement, and it is the origin of several bugs:
- The platform Audius, which was hacked in 2022 for $6 million due to a vulnerability in their proxy contract (storage conflict). Reference: [SharkTeam 2022], [Reutov 2022], [Toulas 2022].
- Wormhole, who offered a bug bounty record of $10 million for a bug affecting their proxy contract (Uninitialized Proxy), also in 2022. Reference: [Immunefi 2022].
The majority of bug related to proxy concerns:
- Storage collision between the proxy and the implementation contract
- Storage collision between an implementation and the new one when performing an upgrade
- Uninitialized implementation but the severity of this has reduced with the EIP-6780 (deactivate selfdestruct)
OpenZeppelin did a fantastic job of offering proper tools to manage and create proxy contracts.
If you want to use a proxy architecture, this article is a summary of the most important points to think about and check to build proxy contracts on Ethereum/EVM, with a focus on the OpenZeppelin library. For each topic, you can find more information by reading the provided links.
The Solidity interviews Questions by RareSkills contains also several questions related to proxy architecture. You can find my answers for them in two article: Medium and Hard levels
[TOC]
General points
Storage collision
A storage collision can appear in two situations:
a) Proxy and the implementation contract
Storage collision between the proxy and the implementation contract
A storage collision happens when a proxy and its implementation use the same slot to store a value. As a result, when the implementation contract writes to update its variable, it will overwrite in reality the variable used by the proxy.
A solution, used by OpenZeppelin, is to “randomize” slot positions in the proxy’s storage
Reference: [d. OpenZeppelin - Unstructured Storage Proxies]
b) Two implementations
Storage collision between two implementations
This collision happens when a proxy is upgraded to point to a new implementation
In this case, the new implementation overwrites a variable from the previous implementation.
It is very important to keep in mind that you cannot change the order in which the contract state variables are declared, nor their type.
A first solution put in place by OpenZeppelin was to use a state variable named __gap. This is empty reserved space allows to freely add new state variables in the future without compromising the storage compatibility with existing deployments. See OpenZeppelin doc - storage gaps.
Since their version v5.0.0, OpenZeppelin proposed and uses the ERC-7201 - Namespaced Storage Layout. In short, in this case, the storage of each variable is computed with the following formula: \(keccak256(id) - 1\) Example from the ERC: \(keccak256(abi.encode(uint256(keccak256("example.main")) - 1))\;\& ~bytes32(uint256(0xff));\)
bytes32 private constant MAIN_STORAGE_LOCATION =
0x183a6125c38840424c4a85fa12bab2ab606c4b6d0e7cc73c0c06ba5300eab500;
Reference: [h. OpenZeppelin - Modifying your contracts], p. OpenZeppelin - Storage Collisions Between Implementation Versions, eip-1967, Introducing OpenZeppelin Contracts 5.0
Constructor / initializer
In principle, you can not have a constructor in your implementation contract
If variables are initialized inside the constructor, the proxy has no way to see these values since :
- The constructor is not stored in the runtime bytecode, but only in the creation bytecode.
- The implementation contract is not deployed in the context of the proxy.
The solution is to use a public initialize function to initialize the proxy with the different values for each variable.
One exception to this is for immutable variables. Since this value is stored in the contract bytecode instead of the contract storage, you can use and initialize an immutable inside the constructor of the implementation contract.
For example, OpenZeppelin, for the upgradeable implementation of ERC2771 by OpenZeppelin, choose to set the forwarder in the constructor
-
File: ERC2771ContextMockUpgradeable.sol [OpenZeppelin 2022j]
-
Issue: issues/175 , issues/156, issues/154
Reference : c. OpenZeppelin - proxies#the constructor caveat, g. OpenZeppelin - initializers
Initializer / OnlyInitializing
initializer
Since proxied contracts do not make use of a constructor, it’s common to move constructor logic to an external initializer function, usually called initialize. It then becomes necessary to protect this initializer function so it can only be called once. The initializer modifier provided by this contract will have this effect.
onlyInitializing
Modifier to protect an initialization function so that it can only be invoked by functions with the initializer and reinitializer modifiers, directly or indirectly.
In short
-
initializeris used for public-facing functions ; -
onlyInitializingis used for internal functions.
Reference: frangio 2022, g. OpenZeppelin - initializers
To avoid
Avoiding Initial Values
Variables must be initialized inside the function initialize.
contract MyContract is Initializable {
uint256 public hasInitialValue;
function initialize() public initializer {
hasInitialValue = 42; // set initial value in initializer
}
}
Exception
It is possible to initialize value with the constant keyword
uint256 public constant hasInitialValue = 42;
Reference: [f. OpenZeppelin - Avoid initial values in field declarations]
Avoid leaving a contract uninitialized
As seen in the section “Initializer / OnlyInitializing”, a proxy contract calls a specific function from the implementation contract to initialize its variables.
But what happens if this function is called on the implementation contract directly by an attacker?
This can be very bad since this function is not protected by default and can be used to take over the implementation contract.
To prevent the implementation contract from being used, you should invoke the {_disableInitializers} function in the constructor to automatically lock it when it is deployed
Reference : n. OpenZeppelin- Initializable, o. OpenZeppelin - initializing_the_implementation_contract
Potentially Unsafe Operations
The main reference for this section is this article from OpenZeppelin: [Potentially Unsafe Operations]
Delegatecall
As indicated in the OpenZeppelin documentation, use a delegatecallin your implementation contract is an advanced technique and can put funds at risk of permanent loss.
- For example, before EIP-6780, if the contract can be made to delegatecall into a malicious contract that contains a selfdestruct, then the calling contract will be destroyed.
- perform a
callor adelegatecallcan lead to several vulnerabilities if not properly implemented, as seen for example in the furucombo hack in 2021.
Reference: [a.OpenZeppelin - delegatecall selfdestruct], [i.OpenZeppelin - potentially unsafe operations]
Modifying Your Contracts
When you modify your implementation contract to create a new version, you cannot change the order in which the contract state variables are declared, nor their type.
If you don’t respect this, this will lead to a collision between the storage of the previous implementation and the new one.
Reference: [h.OpenZeppelin - modifying your contracts]
Selfdestruct
Since the Dencun Upgrade and the EIP-6780, selfdestructcan no longer destruct the contract bytecode.
More information in my article.
Before this update, if a direct call, without passing through the proxy, to the logic contract triggers a self destruct, then the logic contract will be destroyed.
The result is catastrophic since all your contract instances will end up delegating all calls to an address without any code.
- With a transparent proxy, it was still possible to upgrade to a new implementation
- but with a UUPS proxy, the consequence is much more serious since the logic to upgrade the proxy is coded in the logic contract. Therefore, if the logic contract is destroyed, it is not possible to upgrade the proxy and all the proxy point to the logic contracts are broken forever.
Reference: [a.OpenZeppelin - delegatecall-selfdestruct], [i.OpenZeppelin - potentially unsafe operations]
Deployment & Test
It is important to perform tests on your proxy architecture. For this, you can use the Upgrades Plugins. Plugins are available for Hardhat, Foundry and Truffle
This plugin validates that the contract respects the necessary rules for upgradeability and that the storage layout is correctly preserved.
These plugins are also available to manage deployment and upgrade.
Reference: [OpenZeppelin - staying safe with smart contract upgrades/]
Standalone
If you want to have the possibility to deploy with a proxy or also in standalone mode (without proxy), you should call the initializefunction directly in the constructor for the standalone version like this:
constructor(<constructor argument>) {
initialize(< function argument>);
}
If you do not this, an attacker could front-run you to call the function initialize before you.
However, this remains very unlikely.
Specificity
UUPS proxy
In a UUPS proxy, the logic to upgrade the proxy is coded in the implementation contract. Thus, it is important that this function is indeed implemented in the implementation and correctly protected by an access control.
With OpenZeppelin, your contract must inherit from the contract UUPSUpgradeable and overrides the function _authorizeUpgrade.
Here the function is protected by the modifier onlyOwner.
function _authorizeUpgrade(address) internal onlyOwner {}
This function is internal because it is called by the public function upgradeToAndCall..
Transparent proxy
In a transparent proxy, since the function to upgrade is in the proxy, not in the implementation, it is better to use a dedicated contract ProxyAdminto manage the proxy.
- If the admin calls the proxy, it can call the
upgradeToAndCallfunction but any other call won’t be forwarded to the implementation. - If the admin tries to call a function on the implementation it will fail with an error indicating the proxy admin cannot fallback to the target implementation.
See Openzeppelin doc - ProxyAdmin
Checklist
List of main points to check on a proxy contract based on OpenZeppelin architecture:
-
constructor
- call to
_disableInitializers()to disable the possibility to initialize the implementation
- call to
-
Storage gap in all base contracts if ERC-7201 is not used
-
Public initialize function
- Add
initializerto the signature to prevent its initializer function from being invoked twice.
- Add
-
Internal
_initfunction- Add
internal onlyInitializingto the signature - Call all the
initfunctions of the contracts parents - An
_initfunction does not set any variable, it is role of theunchained initfunction
- Add
-
internal
__init_unchainedfunction- No call to the parent
initfunction of the contracts parents, it is role of the_initfunction - Set the different variables
- No call to the parent
-
If UUPS proxy, the function to upgrade the proxy is public/external and protected by access control. With OpenZeppelin, the function
_authorizeUpgradeis correctly overriden.
Reference
OpenZeppelin
Documentation
a) FAQ - Can I safely use delegatecall and selfdestruct?
b) OpenZeppelin Hardhat Upgrades AP
c)Proxy Upgrade Pattern - The Constructor Caveat
d) Proxy Upgrade Pattern - Unstructured Storage Proxies4
e) Upgrades Plugins - Test usage
f) Writing Upgradeable Contracts - Avoiding Initial Values in Field Declarations
g) Writing Upgradeable Contracts - Initializers
h) Writing Upgradeable Contracts - Modifying Your Contracts
i) Writing Upgradeable Contracts - Potentially Unsafe Operations
k) OpenZeppelin Truffle Upgrades API
o) Writing Upgradeable Contracts - Initializing the Implementation Contract
p) Storage Collisions Between Implementation Versions
q) blog - OpenZeppelin - staying safe with smart contract upgrades/
Truffle - A Sweet Upgradeable Contract Experience with OpenZeppelin and Truffle
Github
j) github.com/OpenZeppelin - mocks/ERC2771ContextMockUpgradeable.sol
n) github.com/OpenZeppelin - contracts/proxy/utils/Initializable.sol
r) npmjs.com - openzeppelin/hardhat-upgrades
Others
- OpenZeppelin forum - What’s the difference between onlyInitializing and initialzer
- immunefi - Wormhole Uninitialized Proxy Bugfix Review
- Certik - Upgradeable Proxy Contract Security Best Practices
- EIP-1967: Proxy Storage Slots
- Audius hack:
- Audius Governance Takeover Post-Mortem 7/23/22
- sharkteam.org - report/analysis audius hack
- bleepingcomputer.com - Hackers steal $6 million from blockchain music platform Audius
- offzone moscow - Upgradable Contract Vulnerability—Analysis on the Hack of Web3 Music Platform Audius