Featured image of post Contract Oriented Programming Part 2 - Solidity Introduction to language

Contract Oriented Programming Part 2 - Solidity Introduction to language

Part 2 of article showcasing basic Solidity syntax and concepts - Ethereum contract oriented programming.

Contract Oriented Programming - Part 2

This is the second part of the Contract Oriented Programming tutorial. Here we will look at post-conditions and a few more complex functions than those in the previous post.

NOTE. As in the first post, these are just a few examples of how to apply some simple contract-oriented techniques in Solidity using custom modifiers. We will talk more about the nuances in the following posts. It is mainly for experiments and research.

Postconditions

Post-conditions can be used to ensure that certain things actually happen as a result of executing a given function. Typically these will be assertions about some state, such as a caller’s balance or a contract field. We do not limit the types of states that can (or should) be checked here.

A very simple example of a post-condition would be this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
contract PostCheck {

    uint public data = 0;

    // Check that the 'data' field was set to the value of '_data'.
    modifier data_is_valid(uint _data) {
        _
        if (_data != data)
            throw;
    }

    function setData(uint _data) data_is_valid(_data) {
        data = _data;
    }

}

Notice the position of the _ inside the post-condition modifier; it will execute the modified function before doing the check, as opposed to in a pre-condition modifier where the check is done first.

It is possible to combine pre- and post-conditions. Here is an example:

 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 PrePostCheck {

    uint public data = 0;

    // Check that the input '_data' value is not the same as the value
    // already stored in 'data'.
    modifier data_is_valid(uint _data) {
        if (_data == data)
           throw;
        _
    }

    // Check that the 'data' field was set to the value of '_data'.
    modifier data_was_updated(uint _data) {
        _
        if (_data != data)
            throw;
    }

    function setData(uint _data) data_is_valid(_data) data_was_updated(_data) {
        data = _data;
    }

}

This is a very secure feature. It not only checks if the input data is valid (which in this case means it doesn’t match the data already stored), but also checks if the data variable was actually changed before returning. We’re not going to unit test this contract as we did in Part 1, since the tests will essentially be the same.

Order of modifiers

When adding modifiers to a function, it is very important to order them correctly. Modifiers should be added from left to right in the order in which they are to be executed. This means that if both preconditions and postconditions are used, we must put the preconditions first. There is no semantic difference between pre- and postcondition modifiers, so it’s best (in my opinion) to use a good naming strategy. One way would be to prefix all modifier names with either pre or post, for example. pre_data_is_zero. There are no guidelines in the official Solidity style guide.

return and throw

It can be difficult to decide how to exit a function if the preconditions or postconditions are not met. Given the way Solidity works at the moment, it comes down to whether you allow “return” in modifiers or should they “throw”.

Personally, I’m not sure this is the best solution. I tend to always quit. This approach treats modifiers as traditional assertions, and if the assertion fails, it means that we guarantee (theoretically) that executing the code will not have unforeseen consequences. What I mean by “theoretically” is that it’s true as long as the modifiers are correctly written and added to the functions in the correct way, and that the execution is normal (i.e. no weird EVM exploits are used). This seems to be the most contract-oriented way of doing things. The downside is that throw does not allow any form of recovery when calling functions, because there is no way to catch, but even if you could, there are still no error types, but as I pointed out in part 1 - also it’s not possible to return error codes when using modifiers, so it doesn’t really matter at the moment.

Separating function logic from conditions

Sometimes it can be difficult to know whether the conditional expression should be placed inside a modifier (as a precondition or postcondition) or be part of the function body itself. Here’s a function contract that’s a bit more complicated than part 1:

 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
contract Token {

    // The balance of everyone
    mapping (address => uint) public balances;

    // Blacklisted accounts
    mapping (address => bool) public blacklisted;


    // Constructor - we're a millionaire!
    function Token() {
        balances[msg.sender] = 1000000;
    }

    // Transfer funds.
    // If the caller is blacklisted, this will fail.
    // If the receiver is not blacklisted and the caller has the funds, 
    // they will be transferred to the receiver, otherwise the caller is blacklisted 
    // and their account is emptied.
    function transfer(uint _amount, address _dest) {
        if (blacklisted[msg.sender])
            return;
        if (!blacklisted[_dest] && balances[msg.sender] >= _amount) {
            balances[msg.sender] -= _amount;
            balances[_dest] += _amount;
        }
        else {
            balances[msg.sender] = 0;
            blacklisted[msg.sender] = true;
        }
    }

}

Here we see a number of conditionals being used, but it is different from the contracts we’ve looked at thus far in that transfer can do two completely different things based on the state of the contract - one is to transfer funds from one account to another, and one is to empty the callers account and blacklist them. So, how do we decompose this?

Let’s start by looking at the conditionals. We have the following three:

  1. blacklisted[msg.sender] == true

  2. blacklisted[_dest] == false

  3. balance[msg.sender] >= _amount

Out of these, I would argue that 1 is the only pre-condition. It asserts that the caller is not blacklisted, and if they are, the rest of the body will not be run. The other two conditions should not be broken out, because they are part of the function’s logic. It doesn’t matter if one or both of them fail; the function will still work as intended.

What we have then is one pre-condition and no post-conditions. With modifiers it should look something like this:

 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
contract Token {

    // The balance of everyone
    mapping (address => uint) public balances;

    // Blacklisted accounts
    mapping (address => bool) public blacklisted;


    // Constructor - we're a millionaire!
    function Token() {
        balances[msg.sender] = 1000000;
    }

    // msg.sender cannot be blacklisted
    modifier not_blacklisted {
        if (blacklisted[msg.sender])
            throw;
        _
    }

    // Transfer funds.
    // If the caller is blacklisted, this will fail.
    // If the receiver is not blacklisted and the caller has the funds, 
    // they will be transferred to the receiver, otherwise the caller is blacklisted 
    // and their account is emptied.
    function transfer(uint _amount, address _dest) not_blacklisted {
        if (!blacklisted[_dest] && balances[msg.sender] >= _amount) {
            balances[msg.sender] -= _amount;
            balances[_dest] += _amount;
        }
        else {
            balances[msg.sender] = 0;
            blacklisted[msg.sender] = true;
        }
    }

}

Now imagine that we change the function so that a low balance does not cause the sender to be blacklisted, but only prevent them from transacting (which honestly seems a bit more sensible). In that case we change the balance check to a pre-condition as well.

(Note that we will also update the documentation)

 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
contract Token {

    // The balance of everyone
    mapping (address => uint) public balances;

    // Blacklisted accounts
    mapping (address => bool) public blacklisted;


    // Constructor - we're a millionaire!
    function Token() {
        balances[msg.sender] = 1000000;
    }

    // msg.sender cannot be blacklisted
    modifier not_blacklisted {
        if (blacklisted[msg.sender])
            throw;
        _
    }

    // msg.sender must have a balance of at least 'x' tokens.
    modifier at_least(uint x) {
        if (balances[msg.sender] < x)
            throw;
        _
    }

    // Transfer funds.
    // If the caller is blacklisted or does not have enough funds,
    // the transfer will fail.
    // If the receiver is not blacklisted, the funds are transferred
    // to the receiver, otherwise the caller is blacklisted and their
    // account emptied.
    function transfer(uint _amount, address _dest) not_blacklisted at_least(_amount) {
        if (!blacklisted[_dest]) {
            balances[msg.sender] -= _amount;
            balances[_dest] += _amount;
        }
        else {
            balances[msg.sender] = 0;
            blacklisted[msg.sender] = true;
        }
    }

}

This looks a lot neater, but we can do more. We have actually separated the balance adjustment logic from the blacklisting logic, which means we can put that code in a different function, and add the at_least modifier to the new function instead. The balance adjustment function would be private, so that only the transfer function can access it. Also, to be extra neat, we can break out the blacklisting code as well (the code inside the else block).

 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
contract Token {

    // The balance of everyone
    mapping (address => uint) public balances;

    // Blacklisted accounts
    mapping (address => bool) public blacklisted;


    // Constructor - we're a millionaire!
    function Token() {
        balances[msg.sender] = 1000000;
    }

    // msg.sender cannot be blacklisted
    modifier not_blacklisted {
        if (blacklisted[msg.sender])
            throw;
        _
    }

    // msg.sender must have a balance of at least 'x' tokens.
    modifier at_least(uint x) {
        if (balances[msg.sender] < x)
            throw;
        _
    }

    // Transfer funds. This fails if the caller does not have enough funds.
    function __transfer(uint _amount, address _dest) private at_least(_amount) {
        balances[msg.sender] -= _amount;
        balances[_dest] += _amount;
    }

    // Blacklist an account.
    function __blacklist() private {
        balances[msg.sender] = 0;
        blacklisted[msg.sender] = true;
    }

    // Transfer funds.
    // If the caller is blacklisted or does not have enough funds,
    // the transfer will fail.
    // If the receiver is not blacklisted, the funds are transferred
    // to the receiver, otherwise the caller is blacklisted and their
    // account emptied.
    function transfer(uint _amount, address _dest) not_blacklisted {
        if(!blacklisted[_dest])
            __transfer(_amount, _dest);
        else
            __blacklist();
    }

}

Note that we could just use a ternary operator in the body of transfer for now, but that makes the code harder to read, so we won’t do that.

Also note that we have separated the interface. The “transfer” function no longer has a balance check; instead, it has been moved to a private __transfer function. However, if we look a little closer, we can see that this actually makes sense, because if the target account is blacklisted, the “transfer” function will not even invoke the balance transfer logic. This is an important detail.

Contracts and smart contracts

These tutorials use two different types of contracts and the differences between them should be clear.

  • A “contract” here - in terms of contract-oriented programming - is a definition of functionality, that is, what the function does, and the conditions that must be met in order for the function to do its job. It’s informal, meaning it doesn’t have full language support.

  • The balance criterion in this case is a precondition in the definition of an (informal) transactional contract.

  • The at_least modifier is used to check the balance in the code.

  • The “transfer” function is where the functions described in the contract are performed. It is part of a “smart contract”, the name used for the code that runs in the EVM (Ethereum Virtual Machine).

  • The “transfer” function is an entry point, but it defers some of the work to other functions.

Note that “contract” (in this context) is also not the JSON ABI of a smart contract, nor is it the JSON ABI of the “pass” function, although the JSON ABI is part of the definition of the contract because it contains specifications for inputs and outputs and some others. things. There is no way in Solidity to formally specify these types of contracts. Much of the contract-related validation work has to be done manually, but it’s still good to work this way because (as Gavin points out) it makes the code safer and makes it easier to validate and test.

Finally, I will also not be unit testing these contracts. The example in Part 1 is sufficient for now. I’ll probably add a part dedicated to just that once the basics are sorted.

Conclusion

This part shows how post-conditions can be created as custom modifiers and how more complex code can be structured. The point of the advanced example was to show that contract-oriented programming cannot be applied simply by moving conditional statements into custom modifiers, but this requires some thought.

If someone wants to learn more about contract-oriented programming in general, a good document can be found here. He explains several concepts and how they can be applied.

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