Featured image of post Solidity Introduction to language

Solidity Introduction to language

Sample article showcasing basic Solidity syntax and concepts.

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.

ExtVsContractAccount

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
contract Data {

    uint public data;

    function addData(uint data_) {
        if(msg.sender == 0x692a70d2e424a56d2c6c27aa97d1a86395877b3a)
            data = data_;
    }
    
}

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
contract DataOwnerSettable {
    
    uint public data;
    
    address public owner = msg.sender;

    function addData(uint data_) {
        if(msg.sender == owner)
            data = data_;
    }
    
    function setOwner(address owner_) {
        if(msg.sender == owner)
            owner = owner_;
    }
    
}

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.

BankAuthSequence

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.

 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
contract AccountValidator {
    
    address public owner = msg.sender;
    
    function validate(address addr) constant returns (bool) {
        return addr == owner;
    }
    
    function setOwner(address owner_) {
        if(msg.sender == owner)
           owner = owner_;
    }
    
}


contract DataExternalValidation {
    
    uint public data;

    AccountValidator _validator;

    function DataExternalValidation(address validator) {
        _validator = AccountValidator(validator);
    }

    function addData(uint data_) {
        if(_validator.validate(msg.sender))
            data = data_;
    }
    
    function setValidator(address validator) {
        if(_validator.validate(msg.sender))
            _validator = AccountValidator(validator);
    }
}

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
contract AccountValidator {
    function validate(address addr) constant returns (bool);
}


contract SingleAccountValidator is AccountValidator {
    
    address public owner = msg.sender;
    
    function validate(address addr) constant returns (bool) {
        return addr == owner;
    }
    
    function setOwner(address owner_) {
        if(msg.sender == owner)
            owner = owner_;
    }
    
}

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
contract MultiAccountValidator is AccountValidator {
    
    mapping(address => bool) public owners;
    
    function MultiAccountValidator() {
        owners[msg.sender] = true;
    }
    
    function validate(address addr) constant returns (bool) {
        return owners[addr];
    }
    
    function addOwner(address addr) {
        if(owners[msg.sender])
            owners[addr] = true;
    }
}

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.

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy