Ethereum contracts are immutable – once deployed to the blockchain they cannot be updated, yet the need to change their logic with time is ultimately necessary.
During a contract upgrade the following factors need to be considered:
Block gas limit(4712388 for Homestead)
Upgrade transaction tend to be large due to the amount of processing they have to complete e.g. deploy a contract, move data, move references.
Inter-contract dependencies
when a contract is compiled, all of its imports are compiled into the contract thus leading to a ripple effect when you want to swap out a contract which is being referenced by other contract.
These two are related, as having more dependencies affects the size of your deployed contracts and the overall transaction size of the upgrade. The implementation patterns below work to minimise the upgrade gas costs as well as loosening the coupling of contracts without breaking Solidity type safety.
Note that for the sake of simplying the examples, we have omitted the implementation of security and permission.
Avoiding large data copy operations
Storing data is expensive(SSTORE
operation costs 5000 or 20000 gas) and upgrading contracts containing large storage variables runs the chance of hitting the transaction gas limit during the copying of its data.
You may therefore want to isolate your datastore from the rest of your code, and make it as flexible as possible, so that it is unlikely to need to be upgrade.
Depending on your circumstances, how large of a datastore you need and whether you expect its structure to change often, you may choose a strict definition or a loosely typed flat store. Below is an example of the latter which implements support for storing a sha3 key and value pairs. It is the more flexible and extensible option. This ensures data schema changes can be implemented without requiring upgrades to the storage contract.
1 | contract EternalStorage { |
For upgrades you can then just switch the upgraded contract to point to the new EternalStorage contract instance without having a copy of its data.
Use Libraries to Encapsulate Logic
Libraries are special form of contracts that are singletons and not allowed any storage variables.
The advantage of libraries in the context of upgrades is that they allow encapsulation of business logic or data management logic into singleton instance that cost only upgrading one and not many contracts.
Example below shows a library used for adding a Proposal to storage.
1 | import "EternalStorage.sol"; |
Under the cover, library functions are called using delegatecall
from the calling contract which has the advantage of passing the msg.sender
and msg.value
seamlessly. You can therefore write your library code as if it were just part of your contract, without having to worry about the sender or value chagning.
The example below shows a sample Organization
contract using ProposalsLibrary
to interact with data storage.
1 | import "ProposalsLibrary.sol"; |
With libraries, there is a slight gas overhead on each call. However, it makes deploying a new contract much cheaper.
Use ‘interface’ to decouple inter-contract communication
Abstract Contract implementation behind an interface that only define its function siguatures.
This is a well known pattern in object oriented programming.
1 | import "ITokenLedger.sol"; |
Here instead of importing the entire TokenLedger.sol
contract, we use an interface containing just the function signatures. This eases any possible upgrades to TokenLedger which don’t affect its interface, too, which with this model can be implemented without redeploying the Organization contract.