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:
|
|
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:
|
|
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:
|
|
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:
blacklisted[msg.sender] == true
blacklisted[_dest] == false
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:
|
|
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)
|
|
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).
|
|
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.