Comparison to Solidity

One of the ways in which Vyper tries to make unsafe code harder to write is by deliberately omitting some of Solidity’s features. It is important for those considering developing smart contracts in Vyper to understand what features Vyper does not have, and why. Therefore, in this section, we will explore those features and provide justification for why they have been omitted.

Modifiers

As we saw in the previous chapter, in Solidity you can write a function using modifiers. For example, the following function, changeOwner, will run the code in a modifier called onlyBy as part of its execution:

  1. function changeOwner(address _newOwner)
  2. public
  3. onlyBy(owner)
  4. {
  5. owner = _newOwner;
  6. }

This modifier enforces a rule in relation to ownership. As you can see, this particular modifier acts as a mechanism to perform a pre-check on behalf of the changeOwner function:

  1. modifier onlyBy(address _account)
  2. {
  3. require(msg.sender == _account);
  4. _;
  5. }

But modifiers are not just there to perform checks, as shown here. In fact, as modifiers, they can significantly change a smart contract’s environment, in the context of the calling function. Put simply, modifiers are pervasive.

Let’s look at another Solidity-style example:

  1. enum Stages {
  2. SafeStage,
  3. DangerStage,
  4. FinalStage
  5. }
  6. uint public creationTime = now;
  7. Stages public stage = Stages.SafeStage;
  8. function nextStage() internal {
  9. stage = Stages(uint(stage) + 1);
  10. }
  11. modifier stageTimeConfirmation() {
  12. if (stage == Stages.SafeStage &&
  13. now >= creationTime + 10 days)
  14. nextStage();
  15. _;
  16. }
  17. function a()
  18. public
  19. stageTimeConfirmation
  20. // More code goes here
  21. {
  22. }

On the one hand, developers should always check any other code that their own code is calling. However, it is possible that in certain situations (like when time constraints or exhaustion result in lack of concentration) a developer may overlook a single line of code. This is even more likely if the developer has to jump around inside a large file while mentally keeping track of the function call hierarchy and committing the state of smart contract variables to memory.

Let’s look at the preceding example in a bit more depth. Imagine that a developer is writing a public function called a. The developer is new to this contract and is utilizing a modifier written by someone else. At a glance, it appears that the stageTimeConfirmation modifier is simply performing some checks regarding the age of the contract in relation to the calling function. What the developer may not realize is that the modifier is also calling another function, nextStage. In this simplistic demonstration scenario, simply calling the public function a results in the smart contract’s stage variable moving from SafeStage to DangerStage.

Vyper has done away with modifiers altogether. The recommendations from Vyper are as follows: if only performing assertions with modifiers, then simply use inline checks and asserts as part of the function; if modifying smart contract state and so forth, again make these changes explicitly part of the function. Doing this improves auditability and readability, as the reader doesn’t have to mentally (or manually) “wrap” the modifier code around the function to see what it does.

Class Inheritance

Inheritance allows programmers to harness the power of prewritten code by acquiring preexisting functionality, properties, and behaviors from existing software libraries. Inheritance is powerful and promotes the reuse of code. Solidity supports multiple inheritance as well as polymorphism, but while these are key features of object-oriented programming, Vyper does not support them. Vyper maintains that the implementation of inheritance requires coders and auditors to jump between multiple files in order to understand what the program is doing. Vyper also takes the view that multiple inheritance can make code too complicated to understand—a view tacitly admitted by the Solidity documentation, which gives an example of how multiple inheritance can be problematic.

Inline Assembly

Inline assembly gives developers low-level access to the Ethereum Virtual Machine, allowing Solidity programs to perform operations by directly accessing EVM instructions. For example, the following inline assembly code adds 3 to memory location 0x80:

  1. 3 0x80 mload add 0x80 mstore

Vyper considers the loss of readability to be too high a price to pay for the extra power, and thus does not support inline assembly.

Function Overloading

Function overloading allows developers to write multiple functions of the same name. Which function is used on a given occasion depends on the types of the arguments supplied. Take the following two functions, for example:

  1. function f(uint _in) public pure returns (uint out) {
  2. out = 1;
  3. }
  4. function f(uint _in, bytes32 _key) public pure returns (uint out) {
  5. out = 2;
  6. }

The first function (named f) accepts an input argument of type uint; the second function (also named f) accepts two arguments, one of type uint and one of type bytes32. Having multiple function definitions with the same name taking different arguments can be confusing, so Vyper does not support function overloading.

Variable Typecasting

There are two sorts of typecasting: implicit and explicit

Implicit typecasting is often performed at compile time. For example, if a type conversion is semantically sound and no information is likely to be lost, the compiler can perform an implicit conversion, such as converting a variable of type uint8 to uint16. The earliest versions of Vyper allowed implicit typecasting of variables, but recent versions do not.

Explicit typecasts can be inserted in Solidity. Unfortunately, they can lead to unexpected behavior. For example, casting a uint32 to the smaller type uint16 simply removes the higher-order bits, as demonstrated here:

  1. uint32 a = 0x12345678;
  2. uint16 b = uint16(a);
  3. // Variable b is 0x5678 now

Vyper instead has a convert function to perform explicit casts. The convert function (found on line 82 of convert.py):

  1. def convert(expr, context):
  2. output_type = expr.args[1].s
  3. if output_type in conversion_table:
  4. return conversion_table[output_type](expr, context)
  5. else:
  6. raise Exception("Conversion to {} is invalid.".format(output_type))

Note the use of conversion_table (found on line 90 of the same file), which looks like this:

  1. conversion_table = {
  2. 'int128': to_int128,
  3. 'uint256': to_unint256,
  4. 'decimal': to_decimal,
  5. 'bytes32': to_bytes32,
  6. }

When a developer calls convert, it references conversion_table, which ensures that the appropriate conversion is performed. For example, if a developer passes an int128 to the convert function, the to_int128 function on line 26 of the same (convert.py) file will be executed. The to_int128 function is as follows:

  1. @signature(('int128', 'uint256', 'bytes32', 'bytes'), 'str_literal')
  2. def to_int128(expr, args, kwargs, context):
  3. in_node = args[0]
  4. typ, len = get_type(in_node)
  5. if typ in ('int128', 'uint256', 'bytes32'):
  6. if in_node.typ.is_literal
  7. and not SizeLimits.MINNUM <= in_node.value <= SizeLimits.MAXNUM:
  8. raise InvalidLiteralException(
  9. "Number out of range: {}".format(in_node.value), expr
  10. )
  11. return LLLnode.from_list(
  12. ['clamp', ['mload', MemoryPositions.MINNUM], in_node,
  13. ['mload', MemoryPositions.MAXNUM]], typ=BaseType('int128'),
  14. pos=getpos(expr)
  15. )
  16. else:
  17. return byte_array_to_num(in_node, expr, 'int128')

As you can see, the conversion process ensures that no information can be lost; if it could be, an exception is raised. The conversion code prevents truncation as well as other anomalies that would ordinarily be allowed by implicit typecasting.

Choosing explicit over implicit typecasting means that the developer is responsible for performing all casts. While this approach does produce more verbose code, it also improves the safety and auditability of smart contracts.

Preconditions and Postconditions

Vyper handles preconditions, postconditions, and state changes explicitly. While this produces redundant code, it also allows for maximal readability and safety. When writing a smart contract in Vyper, a developer should observe the following three points:

Condition

What is the current state/condition of the Ethereum state variables?

Effects

What effects will this smart contract code have on the condition of the state variables upon execution? That is, what will be affected, and what will not be affected? Are these effects congruent with the smart contract’s intentions?

Interaction

After the first two considerations have been exhaustively dealt with, it is time to run the code. Before deployment, logically step through the code and consider all of the possible permanent outcomes, consequences, and scenarios of executing the code, including interactions with other contracts.

Ideally, each of these points should be carefully considered and then thoroughly documented in the code. Doing so will improve the design of the code, ultimately making it more readable and auditable.