Signed-off-by: Marcel Ribeiro-Dantas <mribeirodantas@seqera.io>
26 KiB
language | filename | contributors | |||
---|---|---|---|---|---|
Vyper | learnVyper.vy |
|
The content of this document is largely inspired by "Learn Solidity in Y Minutes"
Vyper lets you program on Ethereum, a blockchain-based virtual machine that allows the creation and execution of smart contracts, without requiring centralized or trusted parties. It was designed to improve upon Solidity, another smart contract language for Ethereum, by limiting unsafe practices and enhancing readability; Vyper seeks to optimize the security and auditability of smart contracts.
Vyper is an experimental, statically typed, contract programming language meant to resemble Python. Like objects in OOP, each contract contains state variables, functions, and common data types. Contract-specific features include event notifiers for listeners, and custom global variables, global constants.
Some Ethereum contract examples include crowdfunding, voting, and blind auctions.
Table of Contents
- Intro
- Example
- Data types and associated methods
- Data structures
- Simple operators
- Global variables of note
- Functions and more a. functions b. events
- Branching and loops
- Objects/contracts a. calling external contracts b. ERC20 built-in c. following an interface
- Other keywords a. selfdestruct
- Contract design notes a. obfuscation b. storage optimization c. data access in blockchain d. cron job e. observer pattern
- Security
- Style notes
- Natspec comments
- Other documents
Intro
From the docs the foundational tenants of Vyper are:
- Security
- Language and compiler simplicity
- Auditability
This allows for the following features:
- Bounds and overflow checking
- On the arithmetic and array level
- There are no dynamic arrays in Vyper
- Support for signed integers and decimal fixed point numbers
- Decidability - You can always compute precise upper bound on gas cost
- Strong typing - for built-in and custom types
- Small and understandable compiler code
- Limited support for pure functions
- Anything marked
@constant
is not allowed to change the state
- Anything marked
Following the principles and goals, Vyper does not provide the following features:
- Modifiers (defining parts of functions elsewhere)
- Class inheritance
- Inline assembly
- Function overloading
- Operator overloading
- Recursive calling
- Infinite-length loops
- Binary fixed point (decimal fixed point is used for its exactness)
WITH THE RAPID CHANGES IN ETHEREUM, THIS DOCUMENT IS UNLIKELY TO STAY UP TO DATE, SO YOU SHOULD FOLLOW THE LATEST VYPER DOCS AND ETHEREUM BLOG FOR THE LATEST. ALL CODE HERE IS PROVIDED AS IS, WITH SUBSTANTIAL RISK OF ERRORS OR DEPRECATED CODE PATTERNS.
This document primarily discusses syntax, and so excludes many popular design patterns.
As Vyper and Ethereum are under active development, experimental or beta features are typically marked, and subject to change. Pull requests welcome.
This document describes Vyper version 0.1.0-beta.8
.
All of the following code exists for educational purposes only! None of the following code should be used in production as-is!
Example
# First, a simple todo list contract
# Implements CRUD operations for tasks
# todo.vy (note .vy extension)
### **** START EXAMPLE **** ###
# Start with Natspec comment
# used for documentation
# @title SimpleBank v1
# @author kennyp
# @notice This is a simple bank.
# Vyper contracts must obey a particular order:
# struct -> interface -> events -> globals and constants -> functions
# Additionally, like Python, Vyper functions must be defined in the file
# before they're called.
# Structs
struct Task:
done: bool
deleted: bool
task: string[100]
metadata: bytes32
# Interfaces
contract AnotherContract():
def fetch() -> bytes32: constant
def inform(_taskId: uint256, _status: uint256) -> bool: modifying
# Events
# Events - publicize actions to external listeners
# `indexed` means that it's easier to search/filter on this field
TaskStatus: event({_taskId: indexed(uint256), _status: uint256})
# Global Variables
# State variables are values which are permanently stored in contract storage
# State vars consist of any value persisting beyond any function's scope
# and are permanently stored in contract storage
# You can define your own, custom, unmutable constants
CREATED: constant(uint256) = 0
COMPLETED: constant(uint256) = 1
DELETED: constant(uint256) = 2
# The `public` built-in allows for this address to be read externally
# without defining a `get()` constant function
owner: public(address)
other: public(address)
# uint256 means "unsigned positive integer between 0 and 2^256 - 1"
# Overflow protection is built-in to Vyper
taskCount: uint256
tasks: map(uint256, Task) # dictionary: key=uint256, value: Task struct
# Private Functions
# Start each function with Pythonic decorators
# These decorators resemble Natspec but are actually enforced by Vyper's compiler
# These decorators are:
# @public XOR @private (either one or the other)
# @public (if any contract/user can call it)
# @private (if only internal functions can call it)
# @payable (if the function is payable i.e. accepting ETH)
# @constant (if the function is not modifying anything on-chain)
@private
def _changeTaskStatus( \
_sender: address, \
_taskId: uint256, \
_status: uint256, \
):
# backslashes (\) allow for multi-line code
# Natspec comments are particularly helpful for documentation and readability
# Natspec can be included using familiar Pythonic docstring syntax
"""
@notice
@dev `_sender` MUST be `self.owner`
@param _sender Who is triggering this function
@param _task The description of the task (only useful when task added)
"""
# NOTE: Private functions do not have access to `msg.sender`
# SIDE NOTE: `msg.sender` refers to whoever immediately called the function of
# immediate scope. In other words, if I call a function that calls another
# in-contract, public function, then `msg.sender` turns from my address to
# the address of the current contract.
assert _sender == self.owner # failed assertions cause calls/transactions to fail
# Note that unlike Solidity, `self.` is required to query the contract's state
# Control flow is Pythonic, as is much of Vyper:
_task: string[100] # initialized to default value
_data: bytes32 = sha3(convert(_sender, bytes32)) # owner is obfuscated (but still visible in logs)
if _status == CREATED: # control flow mimics python
# How a new struct is instantiated:
self.tasks[_taskId] = Task({ \
done: False, deleted: False, task: _task, metadata: _data \
})
elif _status == COMPLETED:
# Modifying an existing struct:
self.tasks[_taskId].done = True
elif _status == DELETED:
self.tasks[_taskId].deleted = True
AnotherContract(self.other).inform(_taskId, _status) # modifying external call
log.TaskStatus(_taskId, _status) # emit an event
# Public Functions
# Pythonic constructor - can receive none or many arguments
@public
def __init__(_owner: address, _other_contract: address):
"""
@dev Called once and only upon contract depoyment
"""
self.owner = _owner
self.other = _other_contract
# NOTE: Pythonic whitespace rules are mandated in Vyper
@public
def addTask(_task: string[100]) -> uint256:
"""
@notice Adds a task to contract
@param _task Description of task
@return Id of newly minted task
"""
# msg.sender gives the address of who/what contract is calling this function
self._changeTaskStatus(msg.sender, self.taskCount, CREATED)
self.tasks[self.taskCount].task = _task
self.taskCount += 1
return self.taskCount - 1
@public
def addSpecialTask(_task: string[100]) -> uint256:
"""
@notice Adds a task with metadata pulled from elsewhere
@param _task Description of task
@return Id of newly minted task
"""
self._changeTaskStatus(msg.sender, self.taskCount, CREATED)
self.tasks[self.taskCount].task = _task
self.tasks[self.taskCount].metadata = AnotherContract(self.other).fetch()
self.taskCount += 1
return self.taskCount - 1
@public
def completeTask(_taskId: uint256):
"""
@notice Marks a task as "completed"
@param _taskId Id of task to complete
"""
self._changeTaskStatus(msg.sender, _taskId, COMPLETED)
@public
def deleteTask(_taskId: uint256):
"""
@notice Adds a task to contract
@param _taskId Id of task to delete
"""
self._changeTaskStatus(msg.sender, _taskId, DELETED)
@public
@constant # allows function to run locally/off blockchain
def getTask(_taskId: uint256) -> string[100]:
"""
@notice Getter for a task's description
@param _taskId Id of task with desired description
@return Description of task
"""
return self.tasks[_taskId].task
### **** END EXAMPLE **** ###
# Now, the basics of Vyper
# ---
# 1. DATA TYPES AND ASSOCIATED METHODS
# uint256 used for currency amount and for dates (in unix time)
x: uint256
# int of 128 bits, cannot be changed after contract deployment
# with 'constant', compiler replaces each occurrence with actual value
a: constant(int128) = 5
# All state variables (those outside a function)
# are by default 'internal' and accessible inside contract
# Need to explicitly set to 'public' to allow external contracts to access
# A getter is automatically created, but NOT a setter
# Can only be called in the contract's scope (not within functions)
# Add 'public' field to indicate publicly/externally accessible
a: public(int128)
# No random functions built in, use other contracts for randomness
# Type casting is limited but exists
b: int128 = 5
x: uint256 = convert(b, uint256)
# Types of accounts:
# Contract Account: f(creator_addr, num_transactions)=address set on contract creation
# External Account: (person/external entity): f(public_key)=address
# Addresses - An address type can hold an Ethereum address which
# equates to 20 bytes or 160 bits. It returns in hexadecimal notation
# with a leading 0x. No arithmetic allowed
owner: public(address)
# Members can be invoked on all addresses:
owner.balance # returns balance of address as `wei_value`
owner.codesize # returns code size of address as `int128`
owner.is_contract # `True` if Contract Account
# All addresses can be sent ether via `send()` built-in
@public
@payable
def sendWei(any_addr: address):
send(any_addr, msg.value)
# Bytes available
a: bytes[2]
b: bytes[32]
c: bytes32
# `b` and `c` are 2 different types
# Bytes are preferable to strings since Vyper currently offers better
# support for bytes i.e. more built-ins to deal with `bytes32`, `bytes32`
# can be returned from functions and strings[] can't be, UTF8 (string encoding)
# uses more storage, etc.
# There are no dynamically sized bytes, similar to how there are no
# dynamic arrays
# Fixed-size byte arrays (Strings)
a: string[100]
b: string[8]
c: string[108] = concat(a, b) # check the latest docs for more built-ins
# Time
t1: timedelta
t2: timestamp
# Both types are built-in "custom type" variants of `uint256`
# `timedelta` values can be added but not `timestamp` values
# Money
m: wei_value
# Also has the base type `uint256` like `timestamp` and `timedelta`
# 1 unit of WEI (a small amount of ETH i.e. ether)
# Custom types
# specify units used in the contract:
units: {
cm: "centimeter",
km: "kilometer"
}
# usage:
a: int128(cm)
b: uint256(km)
# BY DEFAULT: all values are set to 0 on instantiation
# `clear()` can be called on most types
# Does NOT destroy value, but sets value to 0, the initial value
# ---
# 2. DATA STRUCTURES
# Arrays
bytes32[5] nicknames; # static array
bytes32[] names; # dynamic array
uint newLength = names.push("John"); # adding returns new length of the array
# Length
names.length; # get length
names.length = 1; # lengths can be set (for dynamic arrays in storage only)
# Multidimensional Arrays
# At initialization, array dimensions must be hard-coded or constants
# Initialize a 10-column by 3-row, multidimensional fixed array
ls: (uint256[10])[3] # parentheses are optional
@public
def setToThree():
# Multidimensional Array Access and Write
# access indices are reversed
# set element in row 2 (3rd row) column 5 (6th column) to 3
self.ls[2][5] = 3
# Dictionaries (any simple type to any other type including structs)
theMap: map(uint256, bytes32)
theMap[5] = sha3("charles")
# theMap[255] result is 0, all non-set key values return zeroes
# To make read public, make a getter that accesses the mapping
@public
def getMap(_idx: uint256) -> bytes32:
"""
@notice Get the value of `theMap` at `_idx`
"""
return self.theMap[_idx]
self.getMap(5) # returns sha3("charles") in bytes32
# Nested mappings
aMap: map(address, map(address, uint256))
# NOTE: Mappings are only allowed as state variables
# NOTE: Mappings are not iterable; can only be accessed
# To delete (reset the mapping's value to default at a key)
clear(balances["John"])
clear(balances); # sets all elements to 0
# Unlike other languages, CANNOT iterate through all elements in
# mapping, without knowing source keys - can build data structure
# on top to do this
# Structs
struct Struct:
owner: address
_balance: uint256 # balance is a reserved keyword, is a member for addresses
exampleStruct: Struct
@public
def foo() -> uint256:
self.exampleStruct = Struct({owner: msg.sender, _balance: 5})
self.exampleStruct._balance = 10
self.exampleStruct._balance = 5 # set to new value
clear(self.exampleStruct._balance)
clear(self.exampleStruct)
return self.exampleStruct._balance
# Data locations: Memory vs. storage vs. calldata - all complex types (arrays,
# structs) have a data location
# 'memory' does not persist, 'storage' does
# Default is 'storage' for local and state variables; 'memory' for func params
# stack holds small local variables
# for most types, can explicitly set which data location to use
# ---
# 3. SIMPLE OPERATORS
# Comparisons, bit operators and arithmetic operators are provided
# exponentiation: **
# modulo: %
# maximum: max(x, y)
# AND: bitwise_and(x, y)
# bitwise shift: shift(x, _shift)
# where x,y are uint256
# _shift is int128
# 4. GLOBAL VARIABLES OF NOTE
# ** self **
self # address of contract
# often used at end of contract life to transfer remaining balance to party:
self.balance # balance of current contract
self.someFunction() # calls func externally via call, not via internal jump
# ** msg - Current message received by the contract **
# Ethereum programmers take NOTE: this `msg` object is smaller than elsewhere
msg.sender # address of sender
msg.value # amount of ether provided to this contract in wei, the function should be marked `@payable`
msg.gas # remaining gas
# ** tx - This transaction **
# Ethereum programmers take NOTE: this `tx` object is smaller than elsewhere
tx.origin # address of sender of the transaction
# ** block - Information about current block **
block.timestamp # time at current block (uses Unix time)
# Note that `block.timestamp` can be manipulated by miners, so be careful
block.number # current block number
block.difficulty # current block difficulty
# ** storage - Persistent storage hash **
storage['abc'] = 'def'; # maps 256 bit words to 256 bit words
# ---
# 5. FUNCTIONS AND MORE
# A. FUNCTIONS
# Simple function
function increment(uint x) returns (uint) {
x += 1;
return x;
}
# Functions can return many arguments
@public
@constant
def increment(x: uint256, y: uint256) -> (uint256, uint256):
x += 1
y += 1
return (x, y)
# Call previous function
@public
@constant
def willCall() -> (uint256, uint256):
return self.increment(1,1)
# One should never have to call a function / hold any logic outside
# outside the scope of a function in Vyper
# '@constant'
# indicates that function does not/cannot change persistent vars
# Constant function execute locally, not on blockchain
y: uint256
@public
@constant
def increment(x: uint256) -> uint256:
x += 1
y += 1 # this line would fail
# y is a state variable => can't be changed in a constant function
# 'Function Decorators'
# Used like python decorators but are REQUIRED by Vyper
# @public - visible externally and internally (default for function)
# @private - only visible in the current contract
# @constant - doesn't change state
# @payable - receive ether/ETH
# @nonrentant(<unique_key>) - Function can only be called once, both externally
# and internally. Used to prevent reentrancy attacks
# Functions hare not hoisted
# Functions cannot be assigned to a variable
# Functions cannot be recursive
# All functions that receive ether must be marked 'payable'
@public
@payable
def depositEther():
self.balances[msg.sender] += msg.value
# B. EVENTS
# Events are notify external parties; easy to search and
# access events from outside blockchain (with lightweight clients)
# typically declare after contract parameters
# Declare
LogSent: event({_from: indexed(address), address: indexed(_to), _amount: uint256})
# Call
log.LogSent(from, to, amount)
/**
For an external party (a contract or external entity), to watch using
the Web3 Javascript library:
# The following is Javascript code, not Vyper code
Coin.LogSent().watch({}, '', function(error, result) {
if (!error) {
console.log("Coin transfer: " + result.args.amount +
" coins were sent from " + result.args.from +
" to " + result.args.to + ".");
console.log("Balances now:\n" +
"Sender: " + Coin.balances.call(result.args.from) +
"Receiver: " + Coin.balances.call(result.args.to));
}
}
**/
# Common paradigm for one contract to depend on another (e.g., a
# contract that depends on current exchange rate provided by another)
# ---
# 6. BRANCHING AND LOOPS
# All basic logic blocks from Python work - including if/elif/else, for,
# while, break, continue, return - but no switch
# Syntax same as Python, but no type conversion from non-boolean
# to boolean (comparison operators must be used to get the boolean val)
# REMEMBER: Vyper does not allow resursive calls or infinite loops
# ---
# 7. OBJECTS/CONTRACTS
# REMEMBER: Vyper does not allow for inheritance or imports
# A. CALLING EXTERNAL CONTRACTS
# You must define an interface to an external contract in the current contract
contract InfoFeed():
def getInfo() -> uint256: constant
info: uint256
@public
def __init__(_source: address):
self.info = InfoFeed(_source).getInfo()
# B. ERC20 BUILT-IN
# Using the `ERC20` keyword implies that the contract at the address
# follows the ERC20 token standard, allowing you to safely call
# functions like `transfer()`, etc.
tokenAddress: address(ERC20)
@public
def transferIt(_to: address, _amt: uint256(wei)):
self.tokenAddress.transfer(_to, _amt)
# C. FOLLOWING AN INTERFACE
# Vyper is experimenting with using the following syntax at the top of
# a `.vy` file to specify what interfaces are followed by the contract
# This allows interfaces to be better organized, registered, and recognized
import interfaces.some_interface as SomeInterface
implements: SomeInterface
# <rest of contract>
# ---
# 8. OTHER KEYWORDS
# A. selfdestruct()
# selfdestruct current contract, sending funds to address (often creator)
selfdestruct(SOME_ADDRESS);
# removes storage/code from current/future blocks
# helps thin clients, but previous data persists in blockchain
# Common pattern, lets owner end the contract and receive remaining funds
@public
def endItAll() {
assert msg.sender == self.creator # Only let the contract creator do this
selfdestruct(self.creator) # Makes contract inactive, returns funds
# May want to deactivate contract manually, rather than selfdestruct
# (ether sent to selfdestructed contract is lost)
# B. sha3()
# Encrypts strings and other data
# Very important on the blockchain
# Takes 1 argument, `concat()` can be called beforehand
# All strings passed are concatenated before hash action
sha3(concat("ab", "cd")) # returns bytes32
# ---
# 9. CONTRACT DESIGN NOTES
# A. Obfuscation
# All variables are publicly viewable on blockchain, so anything
# that is private needs to be obfuscated (e.g., hashed w/secret)
# Oftentimes, a "commit-reveal" scheme is employed
# Step 1. Commit
# Place a commitment by sending output of `sha3()`
sha3("a secret"); # bytes32 commit
sha3(concat("secret", "other secret", "salt")); # commit multiple things
# The `sha3()` calculation should occur off-chain, only the bytes32
# output should be inputted into some `commit()` function
commits: map(address, bytes32)
@public
def commit(commitment: bytes32):
self.commits[msg.sender] = commitment
# Step 2. Reveal
# Send your previously committed data so the contract can check
# if your commitment was honest
@public
def reveal(_secret: string[100], _salt: string[100]) -> bool:
return sha3(concat(_secret, _salt)) == self.commits[msg.sender]
# B. Storage optimization
# Writing to blockchain can be expensive, as data stored forever; encourages
# smart ways to use memory (eventually, compilation will be better, but for now
# benefits to planning data structures - and storing min amount in blockchain)
# Cost can often be high for items like multidimensional arrays
# (cost is for storing data - not declaring unfilled variables)
# C. Data access in blockchain
# Cannot restrict human or computer from reading contents of
# transaction or transaction's state
# While 'private' prevents other *contracts* from reading data
# directly - any other party can still read data in blockchain
# All data to start of time is stored in blockchain, so
# anyone can observe all previous data and changes
# D. Cron Job
# Contracts must be manually called to handle time-based scheduling;
# can create external code to regularly ping or provide incentives
# (ether) for others to ping
# E. Observer Pattern
# An Observer Pattern lets you register as a subscriber and
# register a function which is called by the oracle (note, the oracle
# pays for this action to be run)
# Some similarities to subscription in Pub/sub
# This is an abstract contract, both client and server classes import,
# the client should implement
### **** START EXAMPLE **** ###
contract SomeOracleCallback():
def oracleCallback(_value: uint256, _time: timestamp, _info: bytes32): modifying
MAX_SUBS: constant(uint256) = 100
numSubs: public(uint256) # number of subscribers
subs: map(uint256, address) # enumerates subscribers
@public
def addSub(_sub: address) -> uint256:
"""
@notice Add subscriber
@param _sub Address to add
@return Id of newly added subscriber
"""
self.subs[self.numSubs] = _sub
self.numSubs += 1
return self.numSubs - 1
@private
def notify(_value: uint256, _time: timestamp, _info: bytes32) -> bool:
"""
@notice Notify all subscribers
@dev Check `numSubs` first; Watch out for gas costs!
@param _value whatever
@param _time what have you
@param _info what else
@return True upon successful completion
"""
j: uint256
for i in range(MAX_SUBS):
j = convert(i, uint256) # `i` is int128 by default
if j == self.numSubs:
return True
SomeOracleCallback(self.subs[j]).oracleCallback(_value, _time, _info)
@public
def doSomething():
"""
@notice Do something and notify subscribers
"""
# ...something...
whatever: uint256 = 6
what_have_you: timestamp
what_else: bytes32 = sha3("6")
self.notify(whatever, what_have_you, what_else)
# Now, your client contract can addSubscriber by importing SomeOracleCallback
# and registering with Some Oracle
### **** END EXAMPLE **** ###
# ---
# 10. SECURITY
# Bugs can be disastrous in Ethereum contracts - and even popular patterns in
# Vyper may be found to be antipatterns
# See security links at the end of this doc
# ---
# 11. STYLE NOTES
# Based on Python's PEP8 style guide
# Full Style guide: http:#solidity.readthedocs.io/en/develop/style-guide.html
# Quick summary:
# 4 spaces for indentation
# Two lines separate contract declarations (and other top level declarations)
# Avoid extraneous spaces in parentheses
# Can omit curly braces for one line statement (if, for, etc)
# else should be placed on own line
# Specific to Vyper:
# arguments: snake_case
# events, interfaces, structs: PascalCase
# public functions: camelCase
# private functions: _prefaceWithUnderscore
# ---
# 12. NATSPEC COMMENTS
# used for documentation, commenting, and external UIs
# Contract natspec - always above contract definition
# @title Contract title
# @author Author name
# Function natspec
# Should include in docstring of functions in typical Pythonic fashion
# @notice Information about what function does; shown when function to execute
# @dev Function documentation for developer
# Function parameter/return value natspec
# @param someParam Some description of what the param does
# @return Description of the return value
Additional resources
Sample contracts
Security
Vyper is secure by design, but it may be helpful to understand what Vyper is protecting you from.
Style
- Vyper Style Guide WIP
- Heavily derived from Solidity's style guide ...
- ... which, in turn, is heavily derived from Python's PEP 8 style guide.
Editors
Future To Dos
- Update to current Vyper release
- List of common design patterns
Feel free to send a pull request with any edits - or email pelusoken -/at-/ gmail