Skip to content

Latest commit

 

History

History
245 lines (165 loc) · 18 KB

vyper.asciidoc

File metadata and controls

245 lines (165 loc) · 18 KB

Vyper: A contract-oriented programming language

A recent study analyzed nearly a million deployed smart contracts, and found that many contained serious vulnerabilities. The researchers outline three basic categories of so-called "trace vulnerabilities" that have already resulted in the catastrophic loss of funds for Ethereum users, including:

Suicidal contracts

Can be killed by arbitrary addresses

Greedy contracts

Can reach a state in which they cannot release Ether

Prodigal contracts

Can be made to release Ether to arbitrary addresses

Vyper is an experimental, contract-oriented programming language for the Ethereum Virtual Machine (EVM) which strives to provide superior auditability by making it easier for to write readable code. One of the principles of Vyper is to make it virtually impossible for developers to write misleading code.

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. In this section we explain what those features are and why they’re omitted, for those considering developing smart contracts in Vyper.

Modifiers

In Solidity, you can write a function using modifiers. For example, the following function called changeOwner will run the code in a modifier, called onlyBy, as part of its execution.

function changeOwner(address _newOwner)
    public
    onlyBy(owner)
{
    owner = _newOwner;
}

As we can see below, the modifier called onlyBy enforces a rule in relation to ownership. Whilst modifiers are powerful, they can also be misleading: they change the environment of the function they modify, because they wrap the modified function’s code, and their effects may not be obvious.

modifier onlyBy(address _account)
{
    require(msg.sender == _account);
    _;
}

Modifiers are typically used to perform a single check before a function’s execution. Hence, Vyper’s recommendation is to do away with modifiers altogether and simply use in-line checks and asserts as part of a 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 pre-written code by acquiring pre-existing 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 (EVM), allowing Solidity programs to perform operations by directly accessing EVM instructions. For example, the following inline assembly code adds 3 to memory location 0x80:

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:

function f(uint _in) public pure returns (uint out) {
    out = 1;
}

function f(uint _in, bytes32 _key) public pure returns (uint out) {
    out = 2;
}

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 argument can be confusing, so Vyper does not support function overloading.

Variable typecasting

There are two sorts of typecasting.

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 below.

uint32 a = 0x12345678;
uint16 b = uint16(a);
//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) is as follows:

def convert(expr, context):
    output_type = expr.args[1].s
    if output_type in conversion_table:
        return conversion_table[output_type](expr, context)
    else:
        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:

conversion_table = {
    'int128': to_int128,
    'uint256': to_unint256,
    'decimal': to_decimal,
    'bytes32': to_bytes32,
}

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:

@signature(('int128', 'uint256', 'bytes32', 'bytes'), 'str_literal')
def to_int128(expr, args, kwargs, context):
    in_node = args[0]
    typ, len = get_type(in_node)
    if typ in ('int128', 'uint256', 'bytes32'):
        if in_node.typ.is_literal and not SizeLimits.MINNUM <= in_node.value <= SizeLimits.MAXNUM:
            raise InvalidLiteralException("Number out of range: {}".format(in_node.value), expr)
        return LLLnode.from_list(
            ['clamp', ['mload', MemoryPositions.MINNUM], in_node, ['mload', MemoryPositions.MAXNUM]], typ=BaseType('int128'), pos=getpos(expr)
        )
    else:
        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 seen above) as well as other anomalies which 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.

Pre-conditions and post-conditions

Vyper handles pre-conditions, post-conditions and state changes explicitly. Whilst 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 3 points. Ideally, each of the 3 points should be carefully considered and then thoroughly documented in the code. Doing so will improve the design of the code, ultimately making code more readable and auditable.

  • 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 i.e. what will be affected, and what will not be affected? Are these effects congruent with the smart contract’s intentions?

  • Interaction - Now that the first two steps 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.

A new programming paradigm

Vyper’s creation opens the door to a new programming paradigm. By removing class inheritance, as well as other functionality, it can be said that Vyper is leaning away from the traditional Object Oriented Programming (OOP) paradigm.

Historically, OOP has provided a mechanism for representing real world objects. For example, OOP allows the instantiation of an employee object which can inherit from a person class. However, it is arguably ill-suited to smart contracts, where the most important aspect is value transfer; what one might call "transactional programming". Put simply, transactional computations are worlds apart from real world objects. For example, when was the last time you held a transaction or a forward chaining business rule in your hand?

Vyper is neither an OOP language nor a functional language (the full list of reasons is beyond the scope of this chapter). For this reason, we might boldly suggest, even at this early stage of development, to describe it as a new software development paradigm, one which endeavors to future-proof blockchain executable code. One which prevents the catastrophic loss of funds. Past disasters have created opportunities for further research and development in this space, which could result in a new paradigm for software development.

Decorators

Decorators like @private @public @constant and @payable may be used at the start of each function.

@private decorator

The @private decorator makes the function inaccessible from outside the contract.

@public decorator

The @public decorator makes the function both visible and executable publicly. For example, even the Ethereum wallet will display such functions when viewing the contract.

@constant decorator

Functions with the @constant decorator are not allowed to change state variables. In fact, the compiler will reject the entire program (with an appropriate error) if the function tries to change a state variable.

@payable decorator

Only functions with the @payable decorator are allowed to transfer value.

Vyper implements the logic of decorators explicitly. For example, the Vyper compilation process will fail if a function has both a @payable decorator and a @constant decorator. This makes sense because a function that transfers value has by definition updated the state, so cannot be @constant. Each Vyper function must be decorated with either @public or @private (but not both!).

Online code editor and compiler

Vyper has its own online code editor and compiler, which allows you to write and then compile your smart contracts into Bytecode, ABI and LLL using only your web browser. The Vyper online compiler has a variety of prewritten smart contracts for your convenience. These include a simple open auction, safe remote purchases, ERC20 token and more.

Compiling using the command line

Each Vyper contract is saved in a single file with the .vy extension. Once installed Vyper can compile and provide bytecode by running the following command:

vyper ~/hello_world.vy

The human-readable ABI description (in JSON format) can then be obtained by running the following command:

vyper -f json ~/hello_world.v.py

Protecting against overflow errors at the compiler level

Overflow errors in software can be catastrophic when dealing with real value. This transaction shows the malicious transfer of over 57,896,044,618,658,100,000,000,000,000,000,000,000,000,000,000,000,000,000,000 BEC tokens. The transaction, which occured in mid April of 2018, is the result of an integer overflow issue in BeautyChain’s ERC20 token contract (BecToken.sol). Solidity developers do have libraries like SafeMath as well as Ethereum smart contract security analysis tools like Mythril. However, unfortunately in cases such as this, developers are not forced to use the safety tools. Put simply, if safety is not enforced by the language, developers can write unsafe code which will successfully compile and later on "successfully" execute.

Vyper has built-in overflow protection, implemented in a two-pronged approach. Firstly, Vyper provides a SafeMath equivalent which includes the necessary exception cases for integer arithmetic. Secondly, Vyper uses clamps whenever a literal constant is loaded, a value is passed to a function, or a variable is assigned. Clamps are implemented via custom functions in the Low-level Lisp-like Language (LLL) compiler, and cannot be disabled. (The Vyper compiler outputs LLL rather than EVM bytecode; this simplifies the development of Vyper itself.)

Reading and writing data

Smart contracts can write data to two places: Ethereum’s global state trie and Ethereum’s chain data. While it is costly to store, read and modify data, these storage operations are a necessary component of most smart contracts.

Global state

The state variables in a given smart contract are stored in Ethereum’s global state trie; a given smart contract can only store, read and modify data specifically in relation to that contract’s address (i.e. smart contracts can not read or write to other smart contracts).

Log

As previously mentioned, a smart contract can also write to Ethereum’s chain data through log events. While Vyper initially employed the __log__ syntax for declaring these events, an update has been made which brings Vyper’s event declaration more in line with Solidity’s original syntax. For example, Vyper’s declaration of an event called MyLog was originally MyLog: __log__({arg1: indexed(bytes[3])}) Vyper’s syntax has now become MyLog: event({arg1: indexed(bytes[3])}). It is important to note that the execution of the log event in Vyper was, and still is, as follows: log.MyLog("123").

While smart contracts can write to Ethereum’s chain data (through log events), smart contracts are unable to read the on-chain log events which they created. Notwithstanding, one of the advantages of writing to Ethereum’s chain data via log events is that logs can be discovered and read, on the public chain, by light clients. For example, the logsBloom value in a mined block can indicate whether or not a log event is present. Once the existence of log events has been established, the log data can be obtained from a given transaction receipt.

ERC20 token interface implementation

Vyper implements ERC20 as a precompiled contract, allowing these smart contracts to be easily used out of the box. Contracts in Vyper must be declared as global variables. An example for declaring the ERC20 variable is as follows.

token: address(ERC20)

Opcodes

The code for smart contracts is mainly written in high level languages like Solidity or Vyper. The compiler is responsible for compiling high level code to EVM bytecode, which is directly implemented by the EVM. The EVM opcodes are of course defined in the Ethereum Yellow Paper.