Writing Upgradable Contracts in Solidity

Original

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
contract EternalStorage {
mapping(bytes32 => uint) UIntStorage;

function getUIntValue(bytes32 record) constant returns (uint) {
return UIntStorage[record];
}

function setUIntValue(bytes32 record, uint value) {
UIntStorage[record] = value;
}

mapping(bytes32 => string) StringStorage;

function getStringValue(bytes32 record) constant returns (string) {
return StringStorage(record);
}

function setStringValue(bytes32 record, string value) {
StringStorage[record] = value;
}

mapping(bytes32 => address) AddressStorage;

function getAddressValue(bytes32 record) constant returns (address) {
return AddressStorage[record];
}

function setAddressValue(bytes32 record, address value) {
AddressStorage[record] = value;
}

mapping(bytes32 => bytes) BytesStorage;

function getBytesValue(bytes32 record) constant returns (bytes) {
return BytesStorage[record];
}

function setBytesValue(bytes32 record, bytes value) {
BytesStorage[record] = value;
}

mapping (bytes32 => bool) BooleanStorage;

function getBooleanValue(bytes32 record) constant returns (bool) {
return BooleanStorage[record];
}

function setBooleanValue(bytes32 record, bool value) {
BooleanStorage[record] = value;
}

mapping (bytes32 => int) IntStorage;

function getIntValue(bytes32 record) constant returns (int) {
return IntStorage[record];
}

function setIntValue(bytes32 record, int value) {
IntStorage[record] = value;
}
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
import "EternalStorage.sol";

library ProposalsLibrary {
function getProposalCount(address _storageContract) constant returns (uint256) {
return EternalStorage(_storageContract).getUIntValue(sha3("proposalCount"));
}

function addProposal(address _storageContract, bytes32 _name) {
var idx = getProposalCount(_storageContract);
EternalStorage(_storageContract).setBytes32Value(sha3("proposal_name", idx), _name);
EternalStorage(_storageContract).setUIntValue(sha3("proposal_eth", idx), 0);
EternalStorage(_storageContract).setUIntValue(sha3("ProposalCount"), idx + 1);
}
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
import "ProposalsLibrary.sol";

contract Organization {
using ProposalsLibrary for address;
address public eternalStorage;

function Organization(address _eternalStorage) {
eternalStorage = _eternalStorage;
}

function addProposal(bytes32 _name) {
eternalStorage.addProposal(_name);
}
}

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
2
3
4
5
6
7
8
9
10
11
12
13
import "ITokenLedger.sol";

contract Organization {
ITokenLedger public tokenLedger;

function Organization(address _tokenLedger) {
tokenLedger = ITokenLedger(_tokenLedger);
}

function generateToekns(uint256 _amount) {
tokenLedger.generateTokens(_amount);
}
}

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.

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×