Vyper: Ethereum's Contract-Oriented Python-Like Scripting Language for the EVM
Jan 23, 2019, 2:36PMA general outline of Ethereum's upcoming contracts language, Vyper. Purpose, design philosophy, features, adoption, research and development.
Smart contracts are a micro-service with a set of callable functions for storing and reading on a block-chained ledger in a decentralized environment. It is, technically speaking, a contract between a machine ("world computer") and the rest of the world - an arrangement that it would work in a certain way for as long as the network lives. These defining aspects/features considered, smart contracts ought to be easy to read, write and audit. In general, programming languages differ in multiple dimensions (such as paradigm, type system, etc.). Due to the unusual execution environment of contract code, smart contract languages (SCLs) have different sets of trade-offs and have spawned a number of attempts at creating secure and expressive SCLs.
Vyper is a complementary Ethereum language that aims for such read and write simplicity, usability, and trivial use case scenarios (rather than complex Web 3.0 dApps, as is the case with Solidity). Loosely following Python 3 conventions, it is a contract-oriented scripting language (where Solidity is more object-oriented) targeting the Ethereum Virtual Machine (Serpent was an early Python-like high-level language compiling to EVM bytecode that was deprecated due to security issues with the compiler). Following these design principles and in order to avoid increasing complexity and security risks, Vyper is a fairly stripped down language whose reductive approach does away with features like class inheritance, function and operator overloading, recursive calling, infinite-length loops and other constructs unnecessary for "code as law" contractual agreements and in line with reader auditability, making it maximally difficult to write misleading code.
This bias towards "correctness" at the expense of some flexibility is a desired property in a contracts programming paradigm and allows for avoiding certain pitfalls that have caused serious vulnerabilities in the past. Getting rid of inheritance, for example, is intended to help keep things “on the same page” rather than getting lost in jumping between multiple contract files in the hierarchy of precedence in order to piece together the scattered fragments of what a program is doing under the hood. This minimal and clean composition of contracts and enforcing of self-explanatory code patterns also make Vyper appealing to the more conservative Ethereum Classic community and its "code is law" ethos.
Intended to be fully leveraged with the transition to sharded validation of what came to be referred to as Ethereum 2.0, the sharding and PoS implementations are themselves specified and written in Vyper.
An Ethereum smart contract usually consists of state variables and functions. State variables are values which are permanently stored in contract storage and functions are the executable units of code within a contract.
State Variable Types
State variables are the variables used to describe the "state" of the dynamic system and what defines smart contracts as stateful objects. Global variables (e.g., contracts, which Solidity treats as objects) in Vyper are declared as follows:
token: adress(ERC20)
Or:
beneficiary: public(address)
address
datatype and makes that public
. Mappings
Mappings are what Ethereum contracts usually begin with. They initialize the contract storage fields (such as a token balance mapping). In Vyper, mappings can be seen as virtually initialized hash tables whose keys (stored as its keccak256 hash) are used to look up the corresponding values, whose default byte-representation is all zeros. In Solidity, mappings are declared as mapping(_KeyType => _ValueType).
Vyper uses the following syntax: _ValueType[_KeyType]
. Where _KeyType
can be almost any type except for mappings, a contract, or a struct, while _ValueType
can be any type, including mappings.
Example:
#Defining a mapping
exampleMapping: decimal[int128]
#Accessing a value in hash table mapping
exampleMapping[0] = 10.1
Vyper has just two integer types: uint256 (for non-negative integers) and int128 (for signed integers):
Signed Integers (128 bit)
int128
is a type to store positive and negative integers with values between -2 to the 127 and (2 to the 127 - 1).
Arithmetic operators for integers are +
, -
, *
, /
, **
, %, min()
, max()
.
Unsigned Integers (256 bit)
uint256
is a type to store non-negative integers between 0 and (2to the 256 - 1). Bitwise operators include bitwise_and
, bitwise_not
, bitwise_or
, bitwise_xor
, shift
(respectively AND, NOT, OR, XOR and Bitwise Shift).
Decimals
decimal
is a type to store a decimal fixed point value with a precision of 10 decimal places between -2 to the 127 and (2 to the 127 - 1). Comparisons (e.g., less, less or equal, greater than, etc.) return a boolean value.
Addresses
address
type holds an Ethereum address (a wallet with an external owner or a contract) which has hexadecimal notation with a leading 0x. It has the syntax of _address
. where is either balance (querying the balance of an address, returned in wei) or codesize (querying the code size of an address returned in int128).
Booleans
Designated as bool
, a boolean is a type storing a logical/truth value. Possible values are True or False. Boolean operators are not
, and
, or
, ==
and !=
, describing respectively logical negation, conjunction, disjunction, equivalence, and inequality.
Unit Types
Vyper allows the definition of types with discrete units e.g. meters, seconds, wei, … These types may only be based on either, uint256
int128
or decimal
. Vyper has two unit types built in: time (timestamp
and timedelta
) and wei (wei_value
, an uint256 giving the amount of Ether in wei). Additionally, custom unit types can be defined (as uint256
, int128
or decimal
).
For example:
# specify units used in contract.
units: {
mm: "milimeter",
cm: "centimeter"
}
32-bit-wide Byte Arrays
bytes32
is a 32-bit-wide byte array.
Operators include length (len(x)
), sha3 hash (sha3(x)
), concatenating multiple inputs (concat(x, ...)
), and slicing of length from a point (slice(x, start=_start, len=_len)
).
Fixed-Size Byte Arrays
bytes
denotes a byte array with a fixed size. Fixed-size byte arrays can hold strings (with equal or fewer characters than the maximum length of the byte array defined in the syntax bytes[maxLen]). Operators are the same as with bytes32. Example: exampleString = "String".
Reference Types (that do not fit into 32 bytes)
These include fixed-size lists that hold a finite number of elements that belong to a specified type, declared as _name:
_ValueType[_Integer], and structs which are custom defined types that can group several variables.
Functions
Functions (or methods) are defined the same way as in Python (prefaced with def).
Function calls can happen internally or externally relative to the contract object and have different levels of visibility towards other contracts, decorated as either @public or @private.
A contract can have a default function (a construct functioning same as fallback functions in Solidity) which is executed on a call to the contract if no other functions match the identifier (or if none is supplied at all, as when sending ETH). This function is always named __default__ and must be annotated with the @public decorator and cannot have arguments or return anything.
If the function is annotated as @payable, this function is executed whenever the contract sends Ether (without data).
def bid(): // Defining a function
Events
Events may be logged in indexes and data structured allowing clients to efficiently search and register them. Events must be declared before global declarations and function definitions. The basic flow of event logging (as taken from the sample ERC-20 contract) as described in Vyper contract code looks as follows:
# Events of the token.
Transfer: event({_from: indexed(address), _to: indexed(address), _value: num256})
Approval: event({_owner: indexed(address), _spender: indexed(address), _value: num256})
Ethereum-Specific Decorators
@public
gives a function public visibility and allows external entities to call it.
@private
is the default.
@constant
is used to decorate methods that only read a state.
@payable
makes any method able to be called with a payment, enabling it to handle transactions.
Other Logical and Syntactical Specifics
self.
is used to assert instance variables, adding clarity to the form of how a code is structured and minimizing possible errors.
msg.value
and msg.sender
are members of the msg (message) object when sending (state transitioning) transactions on the Ethereum network. msg.sender
, as in Solidity, designates the owner of the contract, while msg.value
contains the amount of wei sent in the transaction.
assert
takes a boolean expression, asserting the specified condition - if the condition evaluates it to be true, the code will continue to run. Otherwise, the code will stop operation, reverting to the state before.
The constructor function in Vyper takes the form of the familiar initialize function in Python:
def __init__():
self.owner = msg.sender
Contract Example
We'll provide an illustrative example using the Vyper port of the basic ERC-20 token contract.
At the start of the contract (usually designated by the .vy
filename extension, or sometimes to preserve Python syntax highlighting, .v.py
), the event logs are declared:
# Events of the token.
Transfer: event({_from: indexed(address), _to: indexed(address), _value: uint256})
Approval: event({_owner: indexed(address), _spender: indexed(address), _value: uint256})
Then the usual ERC-20 token variables (name, ticker, supply, decimal points of divisibility, etc.):
# Variables of the token.
name: public(bytes32)
symbol: public(bytes32)
totalSupply: public(uint256)
decimals: public(int128)
balances: int128[address]
allowed: int128[address][address]
The following is the token contract initialization:
def __init__(_name: bytes32, _symbol: bytes32, _decimals: uint256, _initialSupply: uint256):
self.name = _name
self.symbol = _symbol
self.decimals = _decimals
self.totalSupply =_initialSupply * convert(10, 'uint256') ** _decimals
self.balances[msg.sender] = convert(self.totalSupply, 'int128')
The function for checking the balance of a particular account:
def balanceOf(_owner: address) -> uint256:
return convert(self.balances[_owner], 'uint256')
The function for sending _amount
tokens to _to
from your account:
def transfer(_to: address, _amount: int128(uint256)) -> bool:
if self.balances[msg.sender] >= _amount and \
self.balances[_to] + _amount >= self.balances[_to]:
self.balances[msg.sender] -= _amount # Subtract from the sender
self.balances[_to] += _amount # Add the same to the recipient
log.Transfer(msg.sender, _to, convert(_amount, 'uint256')) # log transfer event.
return True
else:
return False
Move tokens allowed from a particular account to another (the trivial by now transferFrom
function):
def transferFrom(_from: address, _to: address, _value: int128(uint256)) -> bool:
if _value <= self.allowed[_from][msg.sender] and \
_value <= self.balances[_from]:
self.balances[_from] -= _value # decrease balance of from address.
self.allowed[_from][msg.sender] -= _value # decrease allowance.
self.balances[_to] += _value # incease balance of to address.
log.Transfer(_from, _to, convert(_value, 'uint256')) # log transfer event.
return True
else:
return False
Allow _spender
to withdraw from your account up to the specified _value
amount:
def approve(_spender: address, _amount: int128(uint256)) -> bool:
self.allowed[msg.sender][_spender] = _amount
log.Approval(msg.sender, _spender, convert(_amount, 'uint256'))
return True
Get the allowance an address has to spend of a token from another address:
def allowance(_owner: address, _spender: address) -> uint256:
return convert(self.allowed[_owner][_spender], 'uint256')
Deployment and Compilation to Bytecode
Vyper scripts directly compile to EVM bytecode (rather than getting interpreted, as with Python). Under the hood, both Vyper and Solidity compile to bytecode in the same fashion, following the same sequence of steps, so they are largely inter-operable (and able to make external calls between each other’s contracts). The Vyper compiler itself is written in Python.
In brief, the high-level code is taken up by a parser which parses it into an abstract syntax tree representation of the OP code instructions (see the yellow paper EVM specification), and from there a type checking process iterates through the tree, assigning their corresponding types. After performing static analysis checks the bytecode is generated.
Vyper contracts can be manually deployed by pasting the generated bytecode, although there are various wrappers and scripts available as well, and integration with the Populus smart contract development framework.
Research and Development. Tooling and Formalizations.
Runtime Verification, Inc. (a well-established software company using runtime verification-based techniques to improve on the safety, reliability, and correctness of software systems) have undertaken the full formalization of Vyper using the K Framework, having announced in December 2017:
Runtime Verification, Inc. (RV) along with the Formal Systems Lab at the University of Illinois (FSL) have announced a joint initiative targeting the full formalization of the Viper [sic] smart contract programming language, using the K Framework to create a full formal definition of this research-oriented smart contract programming language. This effort is intended to yield a number of useful tools and artifacts, and to lay the foundation for the future of principled and formally rigorous smart contract development.
The Ethereum Commonwealth (which maintains and develops Ethereum Classic) is also supporting Vyper development and has stated their desire to adopt Vyper as a default smart contract language. Vitalik Buterin himself makes occasional contributions to Vyper, given his general affinity for Python.
Importantly, as mentioned at the outset of this article, Ethereum’s sharding and Casper implementations are specified and written in Vyper, indicating its importance and likely mainstream adoption with Ethereum's base protocol upgrades and maturation towards Ethereum 2.0.
Resources and Links
A curated list of resources on specialized contracts languages.
Official documentation.
A list of Vyper tools and resources.
ChainShot's interactive introduction to Vyper.
Community Gitter.
In-browser IDE.
Disclaimer: information contained herein is provided without considering your personal circumstances, therefore should not be construed as financial advice, investment recommendation or an offer of, or solicitation for, any transactions in cryptocurrencies.