Writing Upgradeable Contracts
When working with upgradeable contracts using OpenZeppelin Upgrades, there are a few minor caveats to keep in mind when writing your Solidity code.
It’s worth mentioning that these restrictions have their roots in how the Ethereum VM works, and apply to all projects that work with upgradeable contracts, not just OpenZeppelin Upgrades.
Initializers
You can use your Solidity contracts with OpenZeppelin Upgrades without any modifications, except for their constructors. Due to a requirement of the proxy-based upgradeability system, no constructors can be used in upgradeable contracts. To learn about the reasons behind this restriction, head to Proxies.
This means that, when using a contract with the OpenZeppelin Upgrades, you need to change its constructor into a regular function, typically named initialize
, where you run all the setup logic:
// NOTE: Do not use this code snippet, it's incomplete and has a critical vulnerability!
pragma solidity ^0.6.0;
contract MyContract {
uint256 public x;
function initialize(uint256 _x) public {
x = _x;
}
}
However, while Solidity ensures that a constructor
is called only once in the lifetime of a contract, a regular function can be called many times. To prevent a contract from being initialized multiple times, you need to add a check to ensure the initialize
function is called only once:
// contracts/MyContract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract MyContract {
uint256 public x;
bool private initialized;
function initialize(uint256 _x) public {
require(!initialized, "Contract instance has already been initialized");
initialized = true;
x = _x;
}
}
Since this pattern is very common when writing upgradeable contracts, OpenZeppelin Contracts provides an Initializable
base contract that has an initializer
modifier that takes care of this:
// contracts/MyContract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract MyContract is Initializable {
uint256 public x;
function initialize(uint256 _x) public initializer {
x = _x;
}
}
Another difference between a constructor
and a regular function is that Solidity takes care of automatically invoking the constructors of all ancestors of a contract. When writing an initializer, you need to take special care to manually call the initializers of all parent contracts. Note that the initializer
modifier can only be called once even when using inheritance, so parent contracts should use the onlyInitializing
modifier:
// contracts/MyContract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract BaseContract is Initializable {
uint256 public y;
function initialize() public onlyInitializing {
y = 42;
}
}
contract MyContract is BaseContract {
uint256 public x;
function initialize(uint256 _x) public initializer {
BaseContract.initialize(); // Do not forget this call!
x = _x;
}
}
Using Upgradeable Smart Contract Libraries
Keep in mind that this restriction affects not only your contracts, but also the contracts you import from a library. Consider for example ERC20
from OpenZeppelin Contracts: the contract initializes the token’s name and symbol in its constructor.
// @openzeppelin/contracts/token/ERC20/ERC20.sol
pragma solidity ^0.8.0;
...
contract ERC20 is Context, IERC20 {
...
string private _name;
string private _symbol;
constructor(string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
}
...
}
This means you should not be using these contracts in your OpenZeppelin Upgrades project. Instead, make sure to use @openzeppelin/contracts-upgradeable
, which is an official fork of OpenZeppelin Contracts that has been modified to use initializers instead of constructors. Take a look at what ERC20Upgradeable looks like in @openzeppelin/contracts-upgradeable
:
// @openzeppelin/contracts-upgradeable/contracts/token/ERC20/ERC20Upgradeable.sol
pragma solidity ^0.8.0;
...
contract ERC20Upgradeable is Initializable, ContextUpgradeable, IERC20Upgradeable {
...
string private _name;
string private _symbol;
function __ERC20_init(string memory name_, string memory symbol_) internal onlyInitializing {
__ERC20_init_unchained(name_, symbol_);
}
function __ERC20_init_unchained(string memory name_, string memory symbol_) internal onlyInitializing {
_name = name_;
_symbol = symbol_;
}
...
}
Whether using OpenZeppelin Contracts or another smart contract library, always make sure that the package is set up to handle upgradeable contracts.
Learn more about OpenZeppelin Contracts Upgradeable in Contracts: Using with Upgrades.
Avoiding Initial Values in Field Declarations
Solidity allows defining initial values for fields when declaring them in a contract.
contract MyContract {
uint256 public hasInitialValue = 42; // equivalent to setting in the constructor
}
This is equivalent to setting these values in the constructor, and as such, will not work for upgradeable contracts. Make sure that all initial values are set in an initializer function as shown below; otherwise, any upgradeable instances will not have these fields set.
contract MyContract is Initializable {
uint256 public hasInitialValue;
function initialize() public initializer {
hasInitialValue = 42; // set initial value in initializer
}
}
It is still ok to define constant state variables, because the compiler does not reserve a storage slot for these variables, and every occurrence is replaced by the respective constant expression. So the following still works with OpenZeppelin Upgrades: |
contract MyContract {
uint256 public constant hasInitialValue = 42; // define as constant
}
Initializing the Implementation Contract
Do not leave an implementation contract uninitialized. An uninitialized implementation contract can be taken over by an attacker, which may impact the proxy. 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:
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
Creating New Instances From Your Contract Code
When creating a new instance of a contract from your contract’s code, these creations are handled directly by Solidity and not by OpenZeppelin Upgrades, which means that these contracts will not be upgradeable.
For instance, in the following example, even if MyContract
is deployed as upgradeable, the token
contract created is not:
// contracts/MyContract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyContract is Initializable {
ERC20 public token;
function initialize() public initializer {
token = new ERC20("Test", "TST"); // This contract will not be upgradeable
}
}
If you would like the ERC20
instance to be upgradeable, the easiest way to achieve that is to simply accept an instance of that contract as a parameter, and inject it after creating it:
// contracts/MyContract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol";
contract MyContract is Initializable {
IERC20Upgradeable public token;
function initialize(IERC20Upgradeable _token) public initializer {
token = _token;
}
}
Potentially Unsafe Operations
When working with upgradeable smart contracts, you will always interact with the contract instance, and never with the underlying logic contract. However, nothing prevents a malicious actor from sending transactions to the logic contract directly. This does not pose a threat, since any changes to the state of the logic contracts do not affect your contract instances, as the storage of the logic contracts is never used in your project.
There is, however, an exception. If the direct call to the logic contract triggers a selfdestruct
operation, then the logic contract will be destroyed, and all your contract instances will end up delegating all calls to an address without any code. This would effectively break all contract instances in your project.
A similar effect can be achieved if the logic contract contains a delegatecall
operation. If the contract can be made to delegatecall
into a malicious contract that contains a selfdestruct
, then the calling contract will be destroyed.
As such, it is not allowed to use either selfdestruct
or delegatecall
in your contracts.
Modifying Your Contracts
When writing new versions of your contracts, either due to new features or bug fixing, there is an additional restriction to observe: you cannot change the order in which the contract state variables are declared, nor their type. You can read more about the reasons behind this restriction by learning about our Proxies.
Violating any of these storage layout restrictions will cause the upgraded version of the contract to have its storage values mixed up, and can lead to critical errors in your application. |
This means that if you have an initial contract that looks like this:
contract MyContract {
uint256 private x;
string private y;
}
Then you cannot change the type of a variable:
contract MyContract {
string private x;
string private y;
}
Or change the order in which they are declared:
contract MyContract {
string private y;
uint256 private x;
}
Or introduce a new variable before existing ones:
contract MyContract {
bytes private a;
uint256 private x;
string private y;
}
Or remove an existing variable:
contract MyContract {
string private y;
}
If you need to introduce a new variable, make sure you always do so at the end:
contract MyContract {
uint256 private x;
string private y;
bytes private z;
}
Keep in mind that if you rename a variable, then it will keep the same value as before after upgrading. This may be the desired behavior if the new variable is semantically the same as the old one:
contract MyContract {
uint256 private x;
string private z; // starts with the value from `y`
}
And if you remove a variable from the end of the contract, note that the storage will not be cleared. A subsequent update that adds a new variable will cause that variable to read the leftover value from the deleted one.
contract MyContract {
uint256 private x;
}
Then upgraded to:
contract MyContract {
uint256 private x;
string private z; // starts with the value from `y`
}
Note that you may also be inadvertently changing the storage variables of your contract by changing its parent contracts. For instance, if you have the following contracts:
contract A {
uint256 a;
}
contract B {
uint256 b;
}
contract MyContract is A, B {}
Then modifying MyContract
by swapping the order in which the base contracts are declared, or introducing new base contracts, will change how the variables are actually stored:
contract MyContract is B, A {}
You also cannot add new variables to base contracts, if the child has any variables of its own. Given the following scenario:
contract Base {
uint256 base1;
}
contract Child is Base {
uint256 child;
}
If Base
is modified to add an extra variable:
contract Base {
uint256 base1;
uint256 base2;
}
Then the variable base2
would be assigned the slot that child
had in the previous version. A workaround for this is to declare unused variables or storage gaps in base contracts that you may want to extend in the future, as a means of "reserving" those slots. Note that this trick does not involve increased gas usage.
Storage Gaps
Storage gaps are a convention for reserving storage slots in a base contract, allowing future versions of that contract to use up those slots without affecting the storage layout of child contracts.
To create a storage gap, declare a fixed-size array in the base contract with an initial number of slots. This can be an array of uint256
so that each element reserves a 32 byte slot. Use the name __gap
or a name starting with __gap_
for the array so that OpenZeppelin Upgrades will recognize the gap:
contract Base {
uint256 base1;
uint256[49] __gap;
}
contract Child is Base {
uint256 child;
}
If Base
is later modified to add extra variable(s), reduce the appropriate number of slots from the storage gap, keeping in mind Solidity’s rules on how contiguous items are packed. For example:
contract Base {
uint256 base1;
uint256 base2; // 32 bytes
uint256[48] __gap;
}
Or:
contract Base {
uint256 base1;
address base2; // 20 bytes
uint256[48] __gap; // array always starts at a new slot
}
Or:
contract Base {
uint256 base1;
uint128 base2a; // 16 bytes
uint128 base2b; // 16 bytes - continues from the same slot as above
uint256[48] __gap;
}
To help determine the proper storage gap size in the new version of your contract, you can simply attempt an upgrade using upgradeProxy
or just run the validations with validateUpgrade
(see docs for Hardhat Upgrades or Foundry Upgrades). If a storage gap is not being reduced properly, you will see an error message indicating the expected size of the storage gap.
Namespaced Storage Layout
ERC-7201: Namespaced Storage Layout is another convention that can be used to avoid storage layout errors when modifying base contracts or when changing the inheritance order of contracts. This convention is used in the upgradeable variant of OpenZeppelin Contracts starting with version 5.0.
This convention involves placing all storage variables of a contract into one or more structs and annotating those structs with @custom:storage-location erc7201:<NAMESPACE_ID>
. A namespace id is a string that uniquely identifies each namespace in a contract, so the same id must not be defined more than once in a contract or any of its base contracts.
When using namespaced storage layouts, the OpenZeppelin Upgrades plugins will automatically detect the namespace ids and validate that each change within a namespace during an upgrade is safe according to the same rules as described in Modifying Your Contracts.
Solidity version 0.8.20 or higher is required in order to use the Upgrades plugins with namespaced storage layouts. The plugins will give an error if they detect @custom:storage-location annotations with an older version of Solidity, because older versions of the compiler do not produce sufficient information for validation of namespaced storage layouts.
|