Solidity - The basics
This is a new series of tutorials on how to create modular smart contract systems and how to reliably replace contracts. That, along with some other lifecycle management, is basically what this DAO framework does.
Readers should have a basic understanding of how Ethereum and Solidity work.
Introduction
Most DApp contracts become obsolete at some point and need to be updated. Just like in other applications. This may be because new features need to be added, a bug has been discovered, or a better, more optimized version has been made. Upgrading can, of course, cause problems, so this should be done with care. Some of the things that need to be ensured are that:
- the new contract works as intended.
- all calls made during the replacement procedure were completed successfully.
- contract replacement has no side effects in other parts of the system.
However, the first thing to check is that an upgrade is possible at all. This is not always the case due to the way Ethereum accounts work.
Accounts, Code and Storage
A very important property of Etheruem contracts is that once the contract is uploaded to the chain, the code can no longer be changed. Contracts are stored in special account objects that contain references to the contract (byte) code, database, and some other things. The database is a key and value store, also known as a “store”, and this is where data such as contract field values is stored.
When contracts are uploaded to the chain, the first thing that happens is the creation of a new account. The contract code is then loaded into the virtual machine, which runs the constructor part, initializes the fields, etc., and then adds the executable part (or body) of the contract to the account. Once the account is created, there is no way to change the code, and there is no other way to update the database other than with this code.
Let’s look at a very simple contract example:
|
|
This contract allows the user to add and read an unsigned integer. The only account allowed to add data is the account with the address 0x692a...
. This address is a hexadecimal literal, so it is added to the bytecode when the contract is compiled.
But what if we want to replace this address later?
The thing is, it’s impossible. If we want this functionality, we need to prepare a contract in advance. An easy way to make it more flexible is to instead put the owner’s current address in a storage variable and make it changeable.
|
|
This contract has an “owner” field (mapped to the store). It is initialized with the address of the account that creates the contract, and can later be changed by the current owner by calling setOwner. The security inside addData remains the same; the only thing that has changed is that the owner address is not hardcoded.
Delegation
But what if the set owner is not enough? What if we want to be able to update not only the owner’s address, but the entire verification process?
In fact, there is a way to replace the code, namely to connect several contract calls to form one call chain. Contract C
can call another contract D
as part of its functionality, which means that a transaction to C
will execute code not only in C
but also in D
. In addition, we could also make the address of D
settable, that is, we could change the instance of D
pointed to by C
. An example of this would be a banking contract that calls another contract to authenticate.
In this case, each time deposit
is called on Bank
, it calls Authenticator
using the caller’s address as an argument and checks the return value to decide whether to complete the deposit. “Authenticator” is just another contract and it is possible to change the address to “Authenticator” which means calls will be routed to another contract.
We are now going to update the data contract to work this way, starting by moving the account validation to another contract.
|
|
To use this contract on the chain we would first create an AccountValidator
contract, then create a DataExternalValidation
-contract and inject the address of the validator through the contract constructor. When someone tries to write to data
it will call validate
on the current validator contract to do the owner check.
This is very nice, because it is now possible to replace the contract where the owner check is. Also, since the AccountValidator
is its own contract we could potentially use that instance to do authentication for more contracts then just one.
One thing remains though. We still can’t replace the code! All we have done is move the validation code out of the contract. The code of the AccountValidator
contract can’t be changed anymore then that of the data contract. Fortunately, Solidity provides a very simple and powerful solution - abstract functions.
Abstract functions
Using abstract functions, the validator contract could be changed into this:
|
|
With these contracts, the data contract no longer works with a concrete validator contract but an abstract (interface) representation instead, meaning we can choose which implementation we want to provide. This works in pretty much the same way as it does in languages like Java and C++. Let’s say we want to allow more owner accounts then just one. We could then provide it with this contract:
|
|
Finally, it is worth noticing that you can actually pass in a contract that is not an AccountValidator
. There is no type check when you convert an address to a contract type, so it would only show up when the contract is actually called; and in fact, so long as the contract has the required method the call will work - even if it does not actually actually extend AccountValidator
.
It is of course not recommended to use contracts in that way.
Summary
Proper delegation is an important part of smart-contract systems, because it means contracts can be replaced. There are some things to keep in mind, though:
Delegation requires type-unsafe conversion, which means one must be careful when setting/changing a contract reference.
The more contracts that are in the system the harder they become to manage, and a strategy that makes a small system work may not be suitable for a medium-sized or large one.
Modularity comes with a cost, because it requires more code, storage variables and calls. On the public chain, where the gas limitations are quite severe (for obvious reasons), even a small modular system could be hard to deploy and run. Generally, when it comes to scalability vs. efficiency I tend to go with scalability. The large, expensive contracts in an excessively modular system can after all be improved and replaced, but if the contracts are locked down that may not be an option.