This blog post talks about two problems common to all Ethereum smart contracts — upgradeability and block gas limit. The idea presented here builds upon this excellent blog post by Elena Dimitrova of Colony. Please read it before proceeding further.
The EternalStorage idea presented by Elena works very well because you can have newer versions of your contract talk to the same EternalStorage. However, upgrading the organisation requires upgrading the parent contract as well. This is due to the fact that the Parent contract (reproduced below) is coded to a specific implementation of the Organisation 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 |
import "Organisation.sol"; import "TokenLedger.sol"; import "EternalStorage.sol"; contract Parent { event OrganisationCreated(address organisation, uint now); event OrganisationUpgraded(address organisation, uint now); mapping(bytes32 => address) public organisations; function createOrganisation(bytes32 key_) { var tokenLedger = new TokenLedger(); var eternalStorage = new EternalStorage(); var organisation = new Organisation(tokenLedger, eternalStorage); organisations[key_] = organisation; OrganisationCreated(organisation, now); } function getOrganisation(bytes32 key_) constant returns (address) { return organisations[key_]; } function upgradeOrganisation(bytes32 key_) { address organisationAddress = organisations[key_]; var organisation = Organisation(organisationAddress); var tokenLedger = organisation.tokenLedger(); var eternalStorage = organisation.eternalStorage(); Organisation organisationNew = new Organisation(tokenLedger, eternalStorage); organisation.kill(organisationNew); organisations[key_] = organisationNew; OrganisationUpgraded(organisationNew, now); } } |
https://gist.github.com/elenadimitrova/4fb2f43d6f7c6141db54ab14ff3f87d7.js
Shortcomings of the original solution
The “upgradeOrganisation” function doesn’t work since any new instance of organisation will have the exact same blueprint as the original organisation. The bytecode of the Organisation contract was deployed along with the Parent contract, so the Parent contract knows of only that specific implementation of the Organisation contract. This implies that in order to upgrade the contract, you need to upgrade the Parent contract as well. In the above implementation, the Parent contract is not upgradeable but it can be easily made so by storing the organisation mapping in the EternalStorage as well. Redeploying the Parent contract would obviously change the contract address with which your users interact. Hence, this upgrade is not transparent to the users.
This approach also does not address the block gas limit issue during deployment since all components need to be deployed in the same transaction.
Revised Solution
An alternative solution is to create an Organisation interface and code the Parent to the interface instead.
The Parent contract can then be deployed independent of the Organisation contract. Hence, these two contracts can be deployed in separate transactions, each with a lower gas requirement than the gas required for deploying the entire set of contracts. This alleviates the block gas limit issue issue.
Newer implementations of the Organisation contract can be instantiated and passed on to the modified “upgradeOrganisation” function to retain the original data. The Parent contract does not need to be redeployed in order to upgrade an organisation. This upgrade procedure is transparent to the users of the contract.
Caveats
A side-effect of this separation is that the Parent contract can no longer instantiate the Organisation contract. The owners or the admins of the contracts are responsible for instantiating the organisation and using Parent to register or upgrade it. This seems to be a fair tradeoff for the benefits it provides — transparent upgradeability and lower gas requirement per block.
Summary
While there are a few other, probably better, approaches to make your contracts upgradeable, this one is simple to understand and easy to implement, and addresses both upgradeability and block gas limit issues. We have used this solution in an internal app.
The updated code is embedded below. To see how these contracts are used with Truffle, see this GitHub repo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
contract OrganisationInterface { function setDataStore(address _tokenLedger, address _eternalStorage); function addProposal(bytes32 _name); function proposalsCount() constant returns (uint256); function getProposal(uint256 _id) constant returns (bytes32 _name, uint256 _eth); function updateProposal(uint256 _id, bytes32 _name); function fundProposal(uint256 _id); function setProposalFund(uint256 _id, uint256 _eth); function generateTokens(uint256 _amount); function getBalance(address _account) constant returns (uint256); function setTokenLedgerAddress(address _tokenLedger); function kill(address upgradedOrganisation_); } |
https://gist.github.com/nrchandan/dae0d980fcb83c50b273193b54672f9d.js
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 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
import "ITokenLedger.sol"; import "ProposalsLibrary.sol"; import "SecurityLibrary.sol"; import "DataVerifiable.sol"; contract Organisation is DataVerifiable { ITokenLedger public tokenLedger; using ProposalsLibrary for address; using SecurityLibrary for address; address public eternalStorage; modifier onlyAdmins { if (!eternalStorage.isUserAdmin(msg.sender)) throw; _ } function setDataStore(address _tokenLedger, address _eternalStorage) { tokenLedger = ITokenLedger(_tokenLedger); eternalStorage = _eternalStorage; } function addProposal(bytes32 _name) onlyAdmins refundEtherSentByAccident throwIfIsEmptyBytes32(_name) { eternalStorage.addProposal(_name); } function proposalsCount() constant returns (uint256) { return eternalStorage.getProposalCount(); } function getProposal(uint256 _id) constant returns (bytes32 _name, uint256 _eth) { return eternalStorage.getProposal(_id); } function updateProposal(uint256 _id, bytes32 _name) { eternalStorage.updateProposal(_id, _name); } function fundProposal(uint256 _id) { eternalStorage.fundProposal(_id); } function setProposalFund(uint256 _id, uint256 _eth) { eternalStorage.setProposalFund(_id, _eth); } function generateTokens(uint256 _amount) { tokenLedger.generateTokens(_amount); } function getBalance(address _account) constant returns (uint256) { return tokenLedger.balanceOf(_account); } function setTokenLedgerAddress(address _tokenLedger) { tokenLedger = ITokenLedger(_tokenLedger); } function kill(address upgradedOrganisation_) { var tokenBalance = tokenLedger.balanceOf(this); tokenLedger.transfer(upgradedOrganisation_, tokenBalance); selfdestruct(upgradedOrganisation_); } } |
https://gist.github.com/nrchandan/1e9bb18cf7b19e89eff7c08df0be88b3.js
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 |
import "OrganisationInterface.sol"; import "TokenLedger.sol"; import "EternalStorage.sol"; import "SecurityLibrary.sol"; contract Parent { event OrganisationCreated(address organisation, uint now); event OrganisationUpgraded(address organisation, uint now); using SecurityLibrary for EternalStorage; mapping(bytes32 => address) public organisations; function registerOrganisation(bytes32 key_, address orgAddress) { var tokenLedger = new TokenLedger(); var eternalStorage = new EternalStorage(); // Set the calling user as the first colony admin eternalStorage.addAdmin(msg.sender); OrganisationInterface(orgAddress).setDataStore(tokenLedger, eternalStorage); // Set the organisation as the storage owner eternalStorage.changeOwner(orgAddress); organisations[key_] = orgAddress; OrganisationCreated(organisation, now); } function getOrganisation(bytes32 key_) constant returns (address) { return organisations[key_]; } function upgradeOrganisation(bytes32 key_, address newOrgAddress) { address organisationAddress = organisations[key_]; var organisation = Organisation(organisationAddress); var tokenLedger = organisation.tokenLedger(); var eternalStorage = organisation.eternalStorage(); OrganisationInterface(newOrgAddress).setDataStore(tokenLedger, eternalStorage); organisation.kill(newOrgAddress); organisations[key_] = newOrgAddress; OrganisationUpgraded(newOrgAddress, now); } } |
https://gist.github.com/nrchandan/6c75bc76498b5fabd998f6ff486558d9.js
(Article cross-posted on Medium)