# 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() - 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 # # --- # 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