Challenges with Object Oriented Smart Contracts
The battle between upgradeability and trust
Working with smart contracts on a platform like Ethereum is an interesting challenge if you’ve spent much time working in a more conventional software development setting. Once deployed, a smart contract can never be removed from the network, and thus the default behavior is that it is long living.
Early on things like kill switches emerged to essentially shut down a contract in the event that something went wrong. These kill switches couldn’t be activated by just anyone, but typically needed to be activated by the contract creator or creators (i.e. they are centralized control functions).
These simple kill switches highlight an extremely important tradeoff in decentralized computing: upgradeability vs. trust.
Upgradeability can be defined as the ability for programmers to modify the behavior of software at some point in the future.
Kill switches clearly fit into this definition, even if in a limited sense. Kill switches would typically need to be preprogrammed into the contract, and would thus be transparent to an astute user (but likely not an average user). A broader class of upgradeability would include the ability to release additional or modified code which represents changes or enhancements to the system.
The ability to upgrade smart contracts is desirable in the broader sense for a number of reasons:
- Fixing defects in the code when they are found
- Upgrading the codebase when new standards emerge or more efficient methodologies are found
- Changing the behavior of the code as requirements evolve
It’s important to note that these aspects are both desirable during development and after deployment. A completely inflexible system becomes unwieldy to develop when it reaches a certain size. It’s common to see smart contracts that are hundreds and hundreds of lines long. They typically contain their own data storage which is manipulated by business rules defined within the same contract.
Something we started experimenting with a while back is splitting up code that represents business rules into separate contracts from code that simply manages data. In order to be effective, the business rules need full ability to modify the state of the data. Since much of the security of a Blockchain programming environment like Solidity revolves around restricting behavior based on msg.sender, separating the logic from the contract where data is being manipulated means that you need protected contracts — contracts which have no exposure to the outside world. These contracts can only be called by other contracts or addresses which have been whitelisted.
Splitting the data and behavior into separate contracts introduces several interesting properties.
First of all, migrating data on the Blockchain is problematic for a number of reasons. Encapsulating the data storage with simple CRUD functionality within its own contract decreases the likelihood that the containing contract will need to change. It’s still not zero. Think of the number of times database tables are changed. However, moving the business rules into their own, separate contracts allows us to upgrade the logic of the system without needing to recreate the state of the data.
Further, splitting up data storage and business logic allows us to define stable interfaces for specific topics while maintaining flexibility of implementation.
This is great for a couple of reasons.
First, it’s a good software practice to not depend on things you don’t use. So, if an external system is depending on a specific set of functionality from our system, it’s useful if they can just depend on that subset of functionality and not everything within our system.
As an example, suppose we have a smart contract system for car rentals. We decide to put all of the code in One Big Contract™. Our contract contains logic regarding a number of things, but a third party called CarRep is just concerned with looking at people’s car rental history for a new reputation system they are working on.
Lo and Behold, we need to upgrade how car cleanings are recorded within our contract, and so we fix the issue and deploy a new contract. Of course we fix all of our internal apps to point to the new contract and notify the people that (we know) are using our contract of the change. Now CarRep has to change their code because we decided to change how we deal with car cleanings. However, there is a non-zero possibility that we don’t even know about CarRep, and if we don’t communicate well, we’ve just broken their system.
If we’d split up our logic into different contracts, CarRep wouldn’t have had to change anything.
Second, if we define high level interfaces which point to implementations of business logic, CarRep would only need to change their code in the event that we change the high level public API (Proxy contract). If we are careful up front with our definitions and naming, these changes can be rare. This is essentially the same as how we’ve come to expect major releases of public APIs like Stripe or Twitter to work.
Finally, following the pattern we’ve been laying out allows our system to follow the Open-Closed Principle — we are able to add new behavior to the system without needing to modifying the source code. We can simply deploy new components and whitelist them where needed so that they have the authorization to interact with our system.
One of the early ideas promoted with regard to smart contracts is the fact that they are binding — hence the word “contract”. Descriptions went something like: “Joe and I enter into a bet regarding who will win an election. Once the agreement is made, it can’t be undone. Once the election is over, the smart contract will reward the winner.” In this sense, smart contracts are also “trustless”, because I can completely depend on the code to execute as specified without intermediation.
This stands in stark contrast to the previous arguments regarding the need to upgrade contracts. If I can single-handedly upgrade a contract, then I can do all sorts of malicious or biased things, and I have reintroduced the need for people to trust me.
“Software must be soft — that is, it must be easy to change.”
If I can’t change the code, all sorts of problems can emerge. On the other hand, if a contract can change, is it a contract?
Part of the problem may be that the term “smart contract” is poorly chosen. Some things will certainly fall under the category of contracts, but a general purpose programming environment like Solidity on Ethereum enables all manner of things to be created — some of which will be less contractual in nature.
In my opinion, this is a far-from-solved problem. However, I’ll offer a few thoughts:
First, at scale I’d expect a very small number of users to actually be reading code and understanding what they are signing off on. So, in that sense they will very often be trusting that the company, website, platform, etc. that they are interacting with is not malicious.
Second, in many contexts this problem can be resolved by requiring multiple developers to sign off on changes that are being deployed. This will reduce the likelihood of a developer going rouge and robbing people.
Third, for very sensitive systems, external auditors could be required to sign off on changes in addition to multiple developers before changes are pushed out. TCRs could be used to manage lists of approved developers and auditors in situations where full decentralization is required.
This topic is deeply related to a number of projects that I’m working on, and I’ll be continuing to investigate and write about it as I learn new things.