ADVANCED TOPICS

Writing upgradeable contracts

When working with upgradeable contracts in ZeppelinOS, 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 ZeppelinOS.

Initializers

You can use your Solidity contracts in ZeppelinOS 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. You can read in-depth about the reasons behind this restriction in the ZeppelinOS Upgrades Pattern page.

This means that, when using a contract within ZeppelinOS, you need to change its constructor into a regular function, typically named initialize, where you run all the setup logic:

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:

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:

Use upgradeable packages

Keep in mind that this restriction affects not only your contracts, but also the contracts you import from a library. For instance, if you use the ERC20Detailed token implementation from OpenZeppelin, the contract initializes the token's name, symbol and decimals in its constructor:

This means that you should not be using these contracts in your ZeppelinOS project. Instead, make sure to use openzeppelin-eth, which is an official fork of OpenZeppelin, which has been modified to use initializers instead of constructors. For instance, an ERC20 implementation provided by openzeppelin-eth is the ERC20Mintable:

Whether it is OpenZeppelin or another EVM package, always make sure that the package is set up to handle upgradeable contracts.

Avoid initial values in field declarations

Solidity allows defining initial values for fields when declaring them in a contract.

contract MyContract {
uint256 public hasInitialValue = 42;
}

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 above; otherwise, any upgradeable instances will not have these fields set.

The easiest way around this issue is to avoid creating contracts on your own altogether: instead of creating a contract in an initialize function, simply accept an instance of that contract as a parameter, and inject it after creating it from ZeppelinOS:

import "zos-lib/contracts/Initializable.sol";import "openzeppelin-eth/contracts/token/ERC20/ERC20.sol";
contract MyContract is Initializable {
ERC20 public token;
functioninitialize(ERC20 _token)initializerpublic{
token = _token; // This contract will be upgradeable if it was created via ZeppelinOS
}
}

An advanced alternative, if you need to create upgradeable contracts on the fly, is to keep an instance of your ZeppelinOS App in your contracts. The App is a contract that acts as the entrypoint for your ZeppelinOS project, which has references to your logic implementations, and can create new contract instances:

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 strongly recommended to avoid any usage of either selfdestruct or delegatecall in your contracts. If you need to include them, make absolutely sure they cannot be called by an attacker on an uninitialized logic contract.

Modifying your contracts

When writing new versions of your contracts, either due to new features or bugfixing, 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 in the pattern section.

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.

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 on 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.

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.