Vyper

Vyper logo

Vyper는 Ethereum Virtual Machine (EVM) 을 타겟으로 한 컨트랙트 기반의 파이써닉한 언어입니다.

원칙과 목표

  • 보안: Vyper를 통해 안전한 스마트컨트랙트를 자연스럽게 만들 수 있어야 한다.
  • 언어와 컴파일러에 대한 단순함: 언어와 컴파일러 임플리멘테이션은 간단해야만 한다.
  • 감사가능성: Vyper 코드는 최대한 인간친화적이어야한다. 또한, 최대한 오해 할만한 코드를 쓰는 것을 최대한 막을 수 있어야한다. 코드 해석의 간결성은 코드 작성의 간셜성보다 중요하다. 또한 Vyper 저숙련자(그리고 일반적인 프로그래밍에 대한 저숙련자)에 대한 코드 해석의 간결성은 어느정도 중요하다.

왜냐하면 Vyper는 다음과 같은 기능을 제공하는 것이 목적이기 때문이다.

  • 경계 및 오버플로우 검사: 대수 계산 뿐만 아니라 배열 접근에도 검사를 함
  • 부호를 지닌 정수와 고정 소수점에 대한 지원
  • 결정 가능성: 어떠한 함수에 대해서도 가스 소비량의 적절한 상한치를 계산 가능해야만한다.
  • 강 타입: 단위계의 도입 (e.g. 타임스탬프, 타임델타, 초, wei, 초당 wei, 평방미터)
  • 작고 이해가 가능한 컴파일러 코드
  • 순수 함수의 제한된 지원: 상수라고 적힌 어떠한 것들도 상태의 변경을 일으킬 수 없다.

원칙와 목표에 따라, Vyper는 다음과 같은 기능을 제공하지 않을 것입니다.

  • 수식어(Modifiers): 예를 들어 솔리디티에서는 실행 전이나 후에 검사를 포함시키는 코드를 추가하거나, 상태를 변경하거나 등의 일을 할 수 있는 mod1 같은 것을 포함하는 function foo() mod1 { ... } 를 정의할 수 있습니다. Vyper는 코드를 잘못 쓸 가능성이 너무 크기에 수식어를 지원하지 않습니다. mod1 은 상태 변화나, 임의의 이전 상태나, 이후 상태들을 추가할 수 있는 것에 비해 너무 무해하게 보입니다. 또한, 수식어는 파일 이리저리를 뛰어다니면서 실행되는 부분을 찾아야하기 떄문에, 감사가능성을 떨어뜨립니다. 수식어의 용례는 대부분 실행 전에 단순 검증에 국한되기에, 그냥 asserts를 추가하여 인라인에서 검사하도록 코드를 짜는 것을 권장합니다.
  • 클래스 상속: 클래스 상속은 사람들에게 여러 파일들을 확인하면서 이 프로그램이 무엇을 하는지를 이해하도록 요구를 하며, (어떤 클래스의 'X'함수가 실제로 사용되는거지? 등의) 충돌하는 함수의 우선 순위 규칙을 파악하는데 시간을 쓰게 합니다. 그러므로, 코드는 너무 복잡해서 이해하기 어려워지고, 이는 감사가능성을 떨어뜨립니다.
  • 인라인 어셈블리: 인라인 어셈블리는 추가하는 것은 변수명을 추적하지 못하게 하여, 어떤 부분이 읽히고 쓰이는지를 알 수 없게 합니다.
  • 함수 오버로딩 - 이것은 주어진 시간에 어떤 함수가 호출되는지에 대한 엄청난 혼란을 가져옵니다. 그러므로 좀 더 코드를 잘못 쓰게 됩니다. ( foo("hello") 가 "hello"를 로깅하고, foo("hello","world") 가 당신의 자금을 훔치는 코드 라던지) 또 다른 문제는 오버로딩이 있는 함수들은 어떤 함수를 호출하는지에 대해서 추적을 해야할 때 이를 찾아내는 것을 더 어렵게 만듭니다.
  • 연산자 오버로딩: 연산자 오버로딩은 잘못된 코드를 도출합니다. 예를 들어 "+"가 원치 않는 사용자에게 자금을 보내기 등의 한 눈에 봐서는 알 수 없는 명령을 실행 시키도록 오버로딩 될 수 있다는 것입니다.
  • 재귀 호출: 재귀호출은 가스 제한의 상한치를 알 수 없게 만드므로, 가스 제한 공격의 원인을 제공합니다.
  • 무한한 길이의 루프: 재귀 호출과 비슷하게, 무한한 길이의 루프는 상한치를 측정 불가능하게 하므로, 가스 제한 공격의 원인을 제공합니다.
  • 이진 고정 소수점: 십진 고정 소수점이 더 낫습니다. 그 이유는 코드에 쓰인 그대로의 값을 지니기 때문입니다. 이진 고정 소수점의 경우 버림이 종종 필요한데 (e.g. (0.2) 10 = (0.001100110011...)2, 언젠가는 짤림) 이는 파이썬에서 0.3 + 0.3 + 0.3 + 0.1 != 1과 같은 형태의 영양가 없는 결과를 내 놓습니다.

일부 변경점들은 메트로폴리스 (패치) 이후 STATICCALL 이 사용가능할 때 고려될 것입니다.

  • 특히 "trusted"라고 표시되지 않은 비-스태틱 호출되어지는 어드레스를 제외한 비-스태틱 콜들에 의한 상태 변화 방지. 재진입(re-entrancy) 공격에 대한 위험을 줄여줍니다.
  • "인라인" 비-스태틱 콜들의 방지. e.g. send(some_address, contract.do_something_and_return_a_weivalue()), "호출이 무엇을 할 수 있다"와 "호출로 응답을 얻는다"를 분명하게 나누도록 합니다.

Vpyer는 솔리디티의 100% 대체제가 되려고 하지 않습니다. 이러한 행동은 보안성을 강화한다는 목표를 달성하기 힘들게 만들거나, 불가능하게 만들 수 있기 떄문입니다.

용어 사전

Vyper 설치하기

설치가 실패한다고 해도 놀라지 마세요. Vyper는 아직 개발 중이고 지속적인 변화를 겪고 있습니다. 설치는 스테이블 버전 이후부터는 최적화 되고 단순화 될 것입니다.

깊은 숨을 한 번 들이쉬고, 다음의 설명을 따르십시오, 그리고 에러를 마딱드리게 된다면 이슈를 생성해 주세요.

주석

언어를 사용해보는 제일 쉬운 방법은, 예제를 통해 배우는 것이고, https://vyper.online/ 에서 온라인 컴파일러로 LLL 이나 bytecode 로 코드를 컴파일 해 보는 것입니다.

선행 요구 조건

Python 3.6 설치하기

Vyper can only be built using Python 3.6 and higher. If you are already running Python 3.6, skip to the next section, else follow the instructions here to make sure you have the correct Python version installed, and are using that version. Vyper는 Python 3.6 혹은 그 이후 버전을 통해서만 빌드 될 수 있습니다. Python 3.6을 사용가능하다면, 다음 세션을 건너 뛰십시오. 그렇지 않다면, 다음의 설명을 따라 어떤 버전의 Python이 설치 되어있고, 사용하고 있는지 확실히 하십시오.

Ubuntu
16.04 혹은 예전 버전

당신의 팩키지들이 최신 버전이도록 하는 것부터 시작합니다.

sudo apt-get update
sudo apt-get -y upgrade

Python 3.6과 필요한 팩키지들을 설치합니다.

sudo apt-get install build-essential libssl-dev libffi-dev
wget https://www.python.org/ftp/python/3.6.2/Python-3.6.2.tgz
tar xfz Python-3.6.2.tgz
cd Python-3.6.2/
./configure --prefix /usr/local/lib/python3.6
sudo make
sudo make install
16.10 혹은 최신 버전

Python 3.6은 universe 레포지토리에 포함되어있습니다.

다음의 명령어를 넣어 설치를 하십시오.

sudo apt-get update
sudo apt-get install python3.6

주석

만약 Python.h: No such file or directory 과 같은 에러를 얻는다면, 다음의 명령어로 Python C API를 위한 파이썬 헤더 파일을 설치해야합니다.

sudo apt-get install python3-dev
Bash 스크립트의 사용

Vyper는 Bash 스크립트를 통해 설치가 될 수 있습니다.

https://github.com/balajipachai/Scripts/blob/master/install_vyper/install_vyper_ubuntu.sh

Reminder: bash 스크립트를 사용하여 무언가를 할 때에는 정확히 그 스크립트가 무엇을 하는지 알아야합니다. 특히, sudo 를 사용할 때에는요

아치

(이 예제의 yay 처럼) 선택한 헬퍼를 사용합니다.

yay -S vyper
MacOS

Homebrew가 설치 되어있는지 확실히 하십시오. brew 명령어가 터미널에서 실해오디지 않는다면, 이 설명 을 통해 Homebrew를 설치하십시오.

Python 3.6을 설치하기 위해서는 다음의 설명을 따르십시오. Installing Python 3 on Mac OS X

그리고, brew 명령어를 통해 다음의 라이브러리가 설치 되도록 하십시오:

brew install gmp leveldb
Windows

윈도우 유저는 처음에 install Windows Subsystem for Linux 를 진행하시고 Ubuntu에 나온 설명을 그대로 따라하시던지, install Docker for Windows 를 따라하신 후 Docker 설치하기를 따라하십시오.

주석

  • Windows Subsystem for Linux (WSL)은 윈도우 10에서만 지원합니다.
  • 10 미만의 버전을 사용하는 윈도우에서는 약간 오래되었지만, Docker Toolbox 를 따라서 Docker를 설치하시고 Docker로 설치하기를 따라하십시오.
가상환경 구축하기

Vyper를 가상 Python 환경 안에 설치하는 것을 강력하게 권장합니다. 이를 통하여, 새롭게 설치된 팩키지들이나 빌드 의존성이 Vyper 프로젝트에 포함되게 하고, 다른 개발 환경 설정에 영향을 미치지 못하게 할 수 있습니다.

새로운 가상 환경을 구축하기 위해서는 다음과 같은 명령어를 씁니다.

sudo apt install virtualenv
virtualenv -p python3.6 --no-site-packages ~/vyper-venv
source ~/vyper-venv/bin/activate

가상 환경에 대해서 좀 더 많은 정보를 얻고 싶다면 다음의 글을 보십시오 virtualenv guide.

virtualenv 없이 가상환경을 구축 할 수도 있습니다.

python3.6 -m venv ~/vyper-env
source ~/vyper-env/bin/activate

설치

다시 강조하지만, Vyper를 가상 Python 환경 안에 설치하는 것을 강력하게 권장합니다. 이 가이드는 Python 3.6이 설치된 가상환경에서 작업한다고 가정합니다.

깃헙 레포지토리에서 최신의 Vyper를 받으시고, 명령어와 테스트를 실행시킵니다.

git clone https://github.com/vyperlang/vyper.git
cd vyper
make
make dev-deps
make test

추가적으로 다음의 명령어로 테스트 컨트랙트를 컴파일 해 볼 수 있습니다.

vyper examples/crowdfund.vy

모든게 정상적으로 작동된다면, Vyper로 쓰여진 스마트컨트랙트를 컴파일 할 수 있게 되었습니다. 만약 예상치 못한 에러나 예외가 발생하였다면, 이슈를 열어주세요.

주석

만약 make 를 사용했을 떄 fatal error: openssl/aes.h: No such file or directory 와 같은 에러가 나왔다면, sudo apt-get install libssl-dev1 를 실행 시킨 뒤 다시 make 를 실행키십시오.

MacOS 유저:

Apple has deprecated use of OpenSSL in favor of its own TLS and crypto libraries. This means that you will need to export some OpenSSL settings yourself, before you can install Vyper. 애플은 디프리케이트 된 TLS 및 암호화 라이브러리에 대한 OpenSSL을 사용하고 있습니다. 이것은 Vyper를 설치하기 전에 일부 OpenSSL 설정을 익스포트 해야할 필요가 있다는 것입니다.

다음의 명령어를 사용하십시오.

export CFLAGS="-I$(brew --prefix openssl)/include"
export LDFLAGS="-L$(brew --prefix openssl)/lib"
pip install scrypt

다시 다음의 명령어와 테스트 명령어를 실행하십시오.

make
make dev-deps
make test

만약 make 를 사용했을 때 ld: library not found for -lyaml 와 같은 에러가 나왔다면, brew info libyaml 을 통해 libyaml 을 설치했는지 확인해보십시오. 만약 설치되었다면 다음의 로케이션 플래그를 설정하여 시도를 하십시오.

   export CFLAGS="-I$(brew --prefix openssl)/include -I$(brew --prefix libyaml)/include"
   export LDFLAGS="-L$(brew --prefix openssl)/lib -L$(brew --prefix libyaml)/lib"

``make`` 와 ``make test`` 다시 할 수 있을 것입니다.

PIP

모든 태그가 붙은 Vyper 버전들은 pypi 을 통해 얻을 수 있으며, pip 을 통해 설치 될 수 있습니다.

pip install vyper

특정 버전을 설치하기 위해서는 다음과 같이 합니다.

pip install vyper==0.1.0b2

Docker

Dockerhub

Vyper는 dockerhub에서 도커 이미지 형태로 다운로드 가능합니다.

docker pull vyperlang/vyper

docker run 커맨드를 이용하여 컴파일러를 실행 시킬 수 있습니다.

docker run -v $(pwd):/code vyperlang/vyper /code/<contract_file.vy>

또한, 도커 이미지에 로그인한 뒤, 프롬프트에서 Vyper를 실행 시킬 수 있습니다.

docker run -v $(pwd):/code/ -it --entrypoint /bin/bash vyperlang/vyper
root@d35252d1fb1b:/code# vyper <contract_file.vy>

일반적인 파라미터도 지원됩니다. 다음과 같습니다.

docker run -v $(pwd):/code vyperlang/vyper -f abi /code/<contract_file.vy>
[{'name': 'test1', 'outputs': [], 'inputs': [{'type': 'uint256', 'name': 'a'}, {'type': 'bytes', 'name': 'b'}], 'constant': False, 'payable': False, 'type': 'function', 'gas': 441}, {'name': 'test2', 'outputs': [], 'inputs': [{'type': 'uint256', 'name': 'a'}], 'constant': False, 'payable': False, 'type': 'function', 'gas': 316}]
Dockerfile

레포지토리의 마스터 브랜치에서 Dockerfile도 제공됩니다. 도커 이미지를 빌드하기 위해서는 다음의 명령어를 실행하십시오.

docker build https://github.com/vyperlang/vyper.git -t vyper:1
docker run -it --entrypoint /bin/bash vyper:1

설치 이후에 모든 것들이 정상적으로 작동되는 것을 담보하기 위해서는 테스트 명령어를 실행하십시오. 그리고 컨트랙트를 컴파일 해 보십시오

python setup.py test
vyper examples/crowdfund.vy

Snap

Snap 스토어에 퍼블리싱 되어있습니다. supported Linux distros, 에 적힌 모든 배포판이 지원됩니다. (snap을 통해 설치하면 최신 master에서 가져옵니다.):

sudo snap install vyper --edge --devmode

베타 버전을 다운받고 싶다면 다음 명령어를 쓰십시오

sudo snap install vyper --beta --devmode

Vyper by Example

Simple Open Auction

As an introductory example of a smart contract written in Vyper, we will begin with a simple open auction contract. As we dive into the code, it is important to remember that all Vyper syntax is valid Python3 syntax, however not all Python3 functionality is available in Vyper.

In this contract, we will be looking at a simple open auction contract where participants can submit bids during a limited time period. When the auction period ends, a predetermined beneficiary will receive the amount of the highest bid.

As you can see, this example only has a constructor, two methods to call, and a few variables to manage the contract state. Believe it or not, this is all we need for a basic implementation of an auction smart contract.

Let's get started!

We begin by declaring a few variables to keep track of our contract state. We initialize a global variable beneficiary by calling public on the datatype address. The beneficiary will be the receiver of money from the highest bidder. We also initialize the variables auctionStart and auctionEnd with the datatype timestamp to manage the open auction period and highestBid with datatype wei_value, the smallest denomination of ether, to manage auction state. The variable ended is a boolean to determine whether the auction is officially over. The variable pendingReturns is a map which enables the use of key-value pairs to keep proper track of the auctions withdrawal pattern.

You may notice all of the variables being passed into the public function. By declaring the variable public, the variable is callable by external contracts. Initializing the variables without the public function defaults to a private declaration and thus only accessible to methods within the same contract. The public function additionally creates a ‘getter’ function for the variable, accessible through an external call such as contract.beneficiary().

Now, the constructor.

The contract is initialized with two arguments: _beneficiary of type address and _bidding_time with type timedelta, the time difference between the start and end of the auction. We then store these two pieces of information into the contract variables self.beneficiary and self.auctionEnd. Notice that we have access to the current time by calling block.timestamp. block is an object available within any Vyper contract and provides information about the block at the time of calling. Similar to block, another important object available to us within the contract is msg, which provides information on the method caller as we will soon see.

With initial setup out of the way, lets look at how our users can make bids.

The @payable decorator will allow a user to send some ether to the contract in order to call the decorated method. In this case, a user wanting to make a bid would call the bid() method while sending an amount equal to their desired bid (not including gas fees). When calling any method within a contract, we are provided with a built-in variable msg and we can access the public address of any method caller with msg.sender. Similarly, the amount of ether a user sends can be accessed by calling msg.value.

주석

msg.sender and msg.value can only be accessed from public functions. If you require these values within a private function they must be passed as parameters.

Here, we first check whether the current time is before the auction's end time using the assert function which takes any boolean statement. We also check to see if the new bid is greater than the highest bid. If the two assert statements pass, we can safely continue to the next lines; otherwise, the bid() method will throw an error and revert the transaction. If the two assert statements and the check that the previous bid is not equal to zero pass, we can safely conclude that we have a valid new highest bid. We will send back the previous highestBid to the previous highestBidder and set our new highestBid and highestBidder.

With the endAuction() method, we check whether our current time is past the auctionEnd time we set upon initialization of the contract. We also check that self.ended had not previously been set to True. We do this to prevent any calls to the method if the auction had already ended, which could potentially be malicious if the check had not been made. We then officially end the auction by setting self.ended to True and sending the highest bid amount to the beneficiary.

And there you have it - an open auction contract. Of course, this is a simplified example with barebones functionality and can be improved. Hopefully, this has provided some insight into the possibilities of Vyper. As we move on to exploring more complex examples, we will encounter more design patterns and features of the Vyper language.

And of course, no smart contract tutorial is complete without a note on security.

주석

It's always important to keep security in mind when designing a smart contract. As any application becomes more complex, the greater the potential for introducing new risks. Thus, it's always good practice to keep contracts as readable and simple as possible.

Whenever you're ready, let's turn it up a notch in the next example.

Blind Auction

Before we dive into our other examples, let's briefly explore another type of auction that you can build with Vyper. Similar to its counterpart written in Solidity, this blind auction allows for an auction where there is no time pressure towards the end of the bidding period.

While this blind auction is almost functionally identical to the blind auction implemented in Solidity, the differences in their implementations help illustrate the differences between Solidity and Vyper.

One key difference is that, because Vyper does not allow for dynamic arrays, we have limited the number of bids that can be placed by one address to 128 in this example. Bidders who want to make more than this maximum number of bids would need to do so from multiple addresses.

Safe Remote Purchases

In this example, we have an escrow contract implementing a system for a trustless transaction between a buyer and a seller. In this system, a seller posts an item for sale and makes a deposit to the contract of twice the item's value. At this moment, the contract has a balance of 2 * value. The seller can reclaim the deposit and close the sale as long as a buyer has not yet made a purchase. If a buyer is interested in making a purchase, they would make a payment and submit an equal amount for deposit (totaling 2 * value) into the contract and locking the contract from further modification. At this moment, the contract has a balance of 4 * value and the seller would send the item to buyer. Upon the buyer's receipt of the item, the buyer will mark the item as received in the contract, thereby returning the buyer's deposit (not payment), releasing the remaining funds to the seller, and completing the transaction.

There are certainly others ways of designing a secure escrow system with less overhead for both the buyer and seller, but for the purpose of this example, we want to explore one way how an escrow system can be implemented trustlessly.

Let's go!

This is also a moderately short contract, however a little more complex in logic. Let's break down this contract bit by bit.

Like the other contracts, we begin by declaring our global variables public with their respective data types. Remember that the public function allows the variables to be readable by an external caller, but not writeable.

With a @payable decorator on the constructor, the contract creator will be required to make an initial deposit equal to twice the item's value to initialize the contract, which will be later returned. This is in addition to the gas fees needed to deploy the contract on the blockchain, which is not returned. We assert that the deposit is divisible by 2 to ensure that the seller deposited a valid amount. The constructor stores the item's value in the contract variable self.value and saves the contract creator into self.seller. The contract variable self.unlocked is initialized to True.

The abort() method is a method only callable by the seller and while the contract is still unlocked—meaning it is callable only prior to any buyer making a purchase. As we will see in the purchase() method that when a buyer calls the purchase() method and sends a valid amount to the contract, the contract will be locked and the seller will no longer be able to call abort().

When the seller calls abort() and if the assert statements pass, the contract will call the selfdestruct() function and refunds the seller and subsequently destroys the contract.

Like the constructor, the purchase() method has a @payable decorator, meaning it can be called with a payment. For the buyer to make a valid purchase, we must first assert that the contract's unlocked property is True and that the amount sent is equal to twice the item's value. We then set the buyer to the msg.sender and lock the contract. At this point, the contract has a balance equal to 4 times the item value and the seller must send the item to the buyer.

Finally, upon the buyer's receipt of the item, the buyer can confirm their receipt by calling the received() method to distribute the funds as intended—where the seller receives 3/4 of the contract balance and the buyer receives 1/4.

By calling received(), we begin by checking that the contract is indeed locked, ensuring that a buyer had previously paid. We also ensure that this method is only callable by the buyer. If these two assert statements pass, we refund the buyer their initial deposit and send the seller the remaining funds. The contract is finally destroyed and the transaction is complete.

Whenever we’re ready, let’s move on to the next example.

Crowdfund

Now, let's explore a straightforward example for a crowdfunding contract where prospective participants can contribute funds to a campaign. If the total contribution to the campaign reaches or surpasses a predetermined funding goal, the funds will be sent to the beneficiary at the end of the campaign deadline. Participants will be refunded their respective contributions if the total funding does not reach its target goal.

Most of this code should be relatively straightforward after going through our previous examples. Let's dive right in.

Like other examples, we begin by initiating our variables - except this time, we're not calling them with the public function. Variables initiated this way are, by default, private.

주석

Unlike the existence of the function public(), there is no equivalent private() function. Variables simply default to private if initiated without the public() function.

The funders variable is initiated as a mapping where the key is a number, and the value is a struct representing the contribution of each participant. This struct contains each participant's public address and their respective value contributed to the fund. The key corresponding to each struct in the mapping will be represented by the variable nextFunderIndex which is incremented with each additional contributing participant. Variables initialized with the int128 type without an explicit value, such as nextFunderIndex, defaults to 0. The beneficiary will be the final receiver of the funds once the crowdfunding period is over—as determined by the deadline and timelimit variables. The goal variable is the target total contribution of all participants. refundIndex is a variable for bookkeeping purposes in order to avoid gas limit issues in the scenario of a refund.

Our constructor function takes 3 arguments: the beneficiary's address, the goal in wei value, and the difference in time from start to finish of the crowdfunding. We initialize the arguments as contract variables with their corresponding names. Additionally, a self.deadline is initialized to set a definitive end time for the crowdfunding period.

Now lets take a look at how a person can participate in the crowdfund.

Once again, we see the @payable decorator on a method, which allows a person to send some ether along with a call to the method. In this case, the participate() method accesses the sender's address with msg.sender and the corresponding amount sent with msg.value. This information is stored into a struct and then saved into the funders mapping with self.nextFunderIndex as the key. As more participants are added to the mapping, self.nextFunderIndex increments appropriately to properly index each participant.

The finalize() method is used to complete the crowdfunding process. However, to complete the crowdfunding, the method first checks to see if the crowdfunding period is over and that the balance has reached/passed its set goal. If those two conditions pass, the contract calls the selfdestruct() function and sends the collected funds to the beneficiary.

주석

Notice that we have access to the total amount sent to the contract by calling self.balance, a variable we never explicitly set. Similar to msg and block, self.balance is a built-in variable that's available in all Vyper contracts.

We can finalize the campaign if all goes well, but what happens if the crowdfunding campaign isn't successful? We're going to need a way to refund all the participants.

In the refund() method, we first check that the crowdfunding period is indeed over and that the total collected balance is less than the goal with the assert statement . If those two conditions pass, we then loop through every participant and call send() to send each participant their respective contribution. For the sake of gas limits, we group the number of contributors in batches of 30 and refund them one at a time. Unfortunately, if there's a large number of participants, multiple calls to refund() may be necessary.

Voting

In this contract, we will implement a system for participants to vote on a list of proposals. The chairperson of the contract will be able to give each participant the right to vote, and each participant may choose to vote, or delegate their vote to another voter. Finally, a winning proposal will be determined upon calling the winningProposals() method, which iterates through all the proposals and returns the one with the greatest number of votes.

As we can see, this is the contract of moderate length which we will dissect section by section. Let’s begin!

The variable voters is initialized as a mapping where the key is the voter’s public address and the value is a struct describing the voter’s properties: weight, voted, delegate, and vote, along with their respective data types.

Similarly, the proposals variable is initialized as a public mapping with int128 as the key’s datatype and a struct to represent each proposal with the properties name and vote_count. Like our last example, we can access any value by key’ing into the mapping with a number just as one would with an index in an array.

Then, voterCount and chairperson are initialized as public with their respective datatypes.

Let’s move onto the constructor.

주석

msg.sender and msg.value can only be accessed from public functions. If you require these values within a private function they must be passed as parameters.

In the constructor, we hard-coded the contract to accept an array argument of exactly two proposal names of type bytes32 for the contracts initialization. Because upon initialization, the __init__() method is called by the contract creator, we have access to the contract creator’s address with msg.sender and store it in the contract variable self.chairperson. We also initialize the contract variable self.voter_count to zero to initially represent the number of votes allowed. This value will be incremented as each participant in the contract is given the right to vote by the method giveRightToVote(), which we will explore next. We loop through the two proposals from the argument and insert them into proposals mapping with their respective index in the original array as its key.

Now that the initial setup is done, lets take a look at the functionality.

주석

Throughout this contract, we use a pattern where @public functions return data from @private functions that have the same name prepended with an underscore. This is because Vyper does not allow calls between public functions within the same contract. The private function handles the logic and allows internal access, while the public function acts as a getter to allow external viewing.

We need a way to control who has the ability to vote. The method giveRightToVote() is a method callable by only the chairperson by taking a voter address and granting it the right to vote by incrementing the voter's weight property. We sequentially check for 3 conditions using assert. The assert not function will check for falsy boolean values - in this case, we want to know that the voter has not already voted. To represent voting power, we will set their weight to 1 and we will keep track of the total number of voters by incrementing voterCount.

In the method delegate, firstly, we check to see that msg.sender has not already voted and secondly, that the target delegate and the msg.sender are not the same. Voters shouldn’t be able to delegate votes to themselves. We, then, loop through all the voters to determine whether the person delegate to had further delegated their vote to someone else in order to follow the chain of delegation. We then mark the msg.sender as having voted if they delegated their vote. We increment the proposal’s voterCount directly if the delegate had already voted or increase the delegate’s vote weight if the delegate has not yet voted.

Now, let’s take a look at the logic inside the vote() method, which is surprisingly simple. The method takes the key of the proposal in the proposals mapping as an argument, check that the method caller had not already voted, sets the voter’s vote property to the proposal key, and increments the proposals voteCount by the voter’s weight.

With all the basic functionality complete, what’s left is simply returning the winning proposal. To do this, we have two methods: winningProposal(), which returns the key of the proposal, and winnerName(), returning the name of the proposal. Notice the @constant decorator on these two methods. We do this because the two methods only read the blockchain state and do not modify it. Remember, reading the blockchain state is free; modifying the state costs gas. By having the @constant decorator, we let the EVM know that this is a read-only function and we benefit by saving gas fees.

The _winningProposal() method returns the key of proposal in the proposals mapping. We will keep track of greatest number of votes and the winning proposal with the variables winningVoteCount and winningProposal, respectively by looping through all the proposals.

winningProposal() is a public function allowing external access to _winningProposal().

And finally, the winnerName() method returns the name of the proposal by key’ing into the proposals mapping with the return result of the winningProposal() method.

And there you have it - a voting contract. Currently, many transactions are needed to assign the rights to vote to all participants. As an exercise, can we try to optimize this?

Now that we're familiar with basic contracts. Let's step up the difficulty.

Company Stock

This contract is just a tad bit more thorough than the ones we've previously encountered. In this example, we are going to look at a comprehensive contract that manages the holdings of all shares of a company. The contract allows for a person to buy, sell and transfer shares of a company as well as allowing for the company to pay a person in ether. The company, upon initialization of the contract, holds all shares of the company at first but can sell them all.

Let's get started.

주석

Throughout this contract, we use a pattern where @public functions return data from @private functions that have the same name prepended with an underscore. This is because Vyper does not allow calls between public functions within the same contract. The private function handles the logic and allows internal access, while the public function acts as a getter to allow external viewing.

The contract contains a number of methods that modify the contract state as well as a few 'getter' methods to read it. We first declare several events that the contract logs. We then declare our global variables, followed by function definitions.

We initiate the company variable to be of type address that's public. The totalShares variable is of type currency_value, which in this case represents the total available shares of the company. The price variable represents the wei value of a share and holdings is a mapping that maps an address to the number of shares the address owns.

In the constructor, we set up the contract to check for valid inputs during the initialization of the contract via the two assert statements. If the inputs are valid, the contract variables are set accordingly and the company's address is initialized to hold all shares of the company in the holdings mapping.

We will be seeing a few @constant decorators in this contract—which is used to decorate methods that simply read the contract state or return a simple calculation on the contract state without modifying it. Remember, reading the blockchain is free, writing on it is not. Since Vyper is a statically typed language, we see an arrow following the definition of the _stockAvailable() method, which simply represents the data type which the function is expected to return. In the method, we simply key into self.holdings with the company's address and check it's holdings. Because _stockAvailable() is a private method, we also include the public stockAvailable() method to allow external access.

Now, lets take a look at a method that lets a person buy stock from the company's holding.

The buyStock() method is a @payable method which takes an amount of ether sent and calculates the buyOrder (the stock value equivalence at the time of call). The number of shares is deducted from the company's holdings and transferred to the sender's in the holdings mapping.

Now that people can buy shares, how do we check someone's holdings?

The _getHolding() is another @constant method that takes an address and returns its corresponding stock holdings by keying into self.holdings. Again, a public function getHolding() is included to allow external access.

To check the ether balance of the company, we can simply call the getter method cash().

To sell a stock, we have the sellStock() method which takes a number of stocks a person wishes to sell, and sends the equivalent value in ether to the seller's address. We first assert that the number of stocks the person wishes to sell is a value greater than 0. We also assert to see that the user can only sell as much as the user owns and that the company has enough ether to complete the sale. If all conditions are met, the holdings are deducted from the seller and given to the company. The ethers are then sent to the seller.

A stockholder can also transfer their stock to another stockholder with the transferStock() method. The method takes a receiver address and the number of shares to send. It first asserts that the amount being sent is greater than 0 and asserts whether the sender has enough stocks to send. If both conditions are satisfied, the transfer is made.

The company is also allowed to pay out an amount in ether to an address by calling the payBill() method. This method should only be callable by the company and thus first checks whether the method caller's address matches that of the company. Another important condition to check is that the company has enough funds to pay the amount. If both conditions satisfy, the contract sends its ether to an address.

We can also check how much the company has raised by multiplying the number of shares the company has sold and the price of each share. Internally, we get this value by calling the _debt() method. Externally it is accessed via debt().

Finally, in this worth() method, we can check the worth of a company by subtracting its debt from its ether balance.

This contract has been the most thorough example so far in terms of its functionality and features. Yet despite the thoroughness of such a contract, the logic remained simple. Hopefully, by now, the Vyper language has convinced you of its capabilities and readability in writing smart contracts.

Structure of a Contract

Contracts in Vyper are contained within files, with each file being one smart-contract. Files in Vyper are similar to classes in object-oriented languages. Each file can contain declarations of State Variables and Functions.

Versions

Vyper supports version pragma which is used to reject being compiled with future compiler versions that might introduce incompatible changes.

# @version 0.1.0b13

The version pragma checks that the compiler version is not a major version.

State Variables

State variables are values which are permanently stored in contract storage.

storedData: int128

See the Types section for valid state variable types.

Functions

Functions are the executable units of code within a contract.

@public
@payable
def bid(): // Function
    // ...

Function calls can happen internally or externally and have different levels of visibility (see Non-reentrant Functions) towards other contracts. Functions must be explicitely declared as public or private.

Public Functions

Public functions (decorated with @public) are a part of the contract interface and may be called via transactions or from other contracts. They cannot be called internally.

Public functions in Vyper are equivalent to external functions in Solidity.

Private Functions

Private functions (decorated with @private) are only accessible from other functions within the same contract. They are called via the self variable:

@private
def _times_two(amount: uint256) -> uint256:
    return amount * 2

@public
def calculate(amount: uint256) -> uint256:
    return self._times_two(amount)

Private functions do not have access to msg.sender or msg.value. If you require these values within a private function they must be passed as parameters.

Non-reentrant Functions

The @nonreentrant(<key>) decorator places a lock on the current function, and all functions with the same <key> value. An attempt by an external contract to call back into any of these functions will cause a REVERT call.

Decorators

The following decorators are available:

Decorator Description
@public Can only be called externally.
@private Can only be called within current contract.
@constant Does not alter contract state.
@payable The contract is open to receive Ether.
@nonreentrant(<unique_key>) Function can only be called once, both externally and internally. Used to prevent reentrancy attacks.

The visibility decorators @public or @private are mandatory on function declarations, whilst the other decorators(@constant, @payable, @nonreentrant) are optional.

Default function

A contract can also have a default function, which is executed on a call to the contract if no other functions match the given function identifier (or if none was supplied at all, such as through someone sending it Eth). It is the same construct as fallback functions in Solidity.

This function is always named __default__ and must be annotated with @public. It cannot have arguments and cannot return anything.

If the function is annotated as @payable, this function is executed whenever the contract is sent Ether (without data). This is why the default function cannot accept arguments and return values - it is a design decision of Ethereum to make no differentiation between sending ether to a contract or a user address.

Example:

Payment: event({amount: int128, from: indexed(address)})

@public
@payable
def __default__():
    log.Payment(msg.value, msg.sender)
Considerations

Just as in Solidity, Vyper generates a default function if one isn't found, in the form of a REVERT call. Note that this still generates an exception and thus will not succeed in receiving funds.

Ethereum specifies that the operations will be rolled back if the contract runs out of gas in execution. send calls to the contract come with a free stipend of 2300 gas, which does not leave much room to perform other operations except basic logging. However, if the sender includes a higher gas amount through a call instead of send, then more complex functionality can be run.

It is considered a best practice to ensure your payable default function is compatible with this stipend. The following operations will consume more than 2300 gas:

  • Writing to storage
  • Creating a contract
  • Calling an external function which consumes a large amount of gas
  • Sending Ether

Lastly, although the default function receives no arguments, it can still access the msg global, including:

  • the address of who is interacting with the contract (msg.sender)
  • the amount of ETH sent (msg.value)
  • the gas provided (msg.gas).

Events

Events may be logged in specially indexed data structures that allow clients, including light clients, to efficiently search for them.

Payment: event({amount: int128, arg2: indexed(address)})

total_paid: int128

@public
@payable
def pay():
    self.total_paid += msg.value
    log.Payment(msg.value, msg.sender)

Events must be declared before global declarations and function definitions.

NatSpec Metadata

Vyper supports structured documentation for state variables and functions and events.

carrotsEaten: int128
"""
@author Bob Clampett
@notice Number of carrots eaten
@dev Chewing does not count, carrots must pass the throat to be "eaten"
"""
@public
@payable
def doesEat(food: string):
    """
    @author Bob Clampett
    @notice Determine if Bugs will accept `food` to eat
    @dev Compares the entire string and does not rely on a hash
    @param food The name of a food to evaluate (in English)
    @return true if Bugs will eat it, false otherwise
    """

    // ...
Ate: event({food: string})
"""
@author Bob Clampett
@notice Bugs did eat `food`
@dev Chewing does not count, carrots must pass the throat to be "eaten"
@param food The name of a food that was eaten (in English)
"""

Additional information about Ethereum Natural Specification (NatSpec) can be found here.

Contract Interfaces

An interface is a set of function definitions used to enable communication between smart contracts. A contract interface defines all of that contract's publicly available functions. By importing the interface, your contract now knows how to call these functions in other contracts.

Defining Interfaces and Making External Calls

Interfaces can be added to contracts either through inline definition, or by importing them from a seperate file.

The contract keyword is used to define an inline external interface:

contract FooBar:
    def calculate() -> uint256: constant
    def test1(): modifying

The defined interface can then be use to make external calls, given a contract address:

@public
def test(some_address: address):
    FooBar(some_address).calculate()

The interface name can also be used as a type annotation for storage variables. You then assign an address value to the variable to access that interface. Note that assignment of an address requires the value to be cast using the contract type e.g. FooBar(<address_var>):

foobar_contract: FooBar

@public
def __init__(foobar_address: address):
    self.foobar_contract = FooBar(foobar_address)

@public
def test():
    self.foobar_contract.calculate()

Specifying modifying annotation indicates that the call made to the external contract will be able to alter storage, whereas the constant call will use a STATICCALL ensuring no storage can be altered during execution.

contract FooBar:
    def calculate() -> uint256: constant
    def test1(): modifying

@public
def test(some_address: address):
    FooBar(some_address).calculate()  # cannot change storage
    FooBar(some_address).test1()  # storage can be altered
Importing Interfaces

Interfaces are imported with import or from ... import statements.

Imported interfaces are written using standard Vyper syntax, with the body of each function replaced by a pass statement:

@public
def test1():
    pass

@public
def calculate() -> uint256:
    pass

You can also import a fully implemented contract and Vyper will automatically convert it to an interface.

Imports via import

With absolute import statements, you must include an alias as a name for the imported package. In the following example, failing to include as Foo will raise a compile error:

import contract.foo as Foo
Imports via from ... import

Using from you can perform both absolute and relative imports. With from import statements you cannot use an alias - the name of the interface will always be that of the file:

from contract import foo

Relative imports are possible by prepending dots to the contract name. A single leading dot indicates a relative import starting with the current package. Two leading dots indicate a relative import from the parent of the current package:

from . import foo
from ..interfaces import baz
Searching For Interface Files

When looking for a file to import Vyper will first search relative to the same folder as the contract being compiled. For absolute imports, it also searches relative to the root path for the project. Vyper checks for the file name with a .vy suffix first, then .json.

When using the command line compiler, the root path defaults to to the current working directory. You can change it with the -p flag:

$ vyper my_project/contracts/my_contract.vy -p my_project

In the above example, the my_project folder is set as the root path. A contract cannot perform a relative import that goes beyond the top-level folder.

Built-in Interfaces

Vyper includes common built-in interfaces such as ERC20 and ERC721. These are imported from vyper.interfaces:

from vyper.interfaces import ERC20

implements: ERC20

You can see all the available built-in interfaces in the Vyper GitHub repo.

Implementing an Interface

You can define an interface for your contract with the implements statement:

import an_interface as FooBarInterface

implements: FooBarInterface

This imports the defined interface from the vyper file at an_interface.vy (or an_interface.json if using ABI json interface type) and ensures your current contract implements all the necessary public functions. If any interface functions are not included in the contract, it will fail to compile. This is especially useful when developing contracts around well-defined standards such as ERC20.

Extracting Interfaces

Vyper has a built-in format option to allow you to make your own vyper interfaces easily.

$ vyper -f interface examples/voting/ballot.vy

# Functions

@constant
@public
def delegated(addr: address) -> bool:
    pass

# ...

If you want to do an external call to another contract, vyper provides an external contract extract utility as well.

$ vyper -f external_interface examples/voting/ballot.vy

# External Contracts
contract Ballot:
    def delegated(addr: address) -> bool: constant
    def directlyVoted(addr: address) -> bool: constant
    def giveRightToVote(voter: address): modifying
    def forwardWeight(delegate_with_weight_to_forward: address): modifying
    # ...

The output can then easily be copy-pasted to be consumed.

Built in Functions

Vyper provides a collection of built in functions available in the global namespace of all contracts.

floor(value: decimal) → int128

Rounds a decimal down to the nearest integer.

  • value: Decimal value to round down
ceil(value: decimal) → int128

Rounds a decimal up to the nearest integer.

  • value: Decimal value to round up
convert(value, type_) → Any

Converts a variable or literal from one type to another.

  • value: Value to convert
  • type_: The destination type to convert to (bool, decimal, int128, uint256 or bytes32)

Returns a value of the type specified by type_.

For more details on available type conversions, see Type Conversions.

clear(var: Any) → None

Clears a variable's contents to the default value of its type.

  • var: Variable to clear
as_wei_value(value: int, unit: str) → wei_value

Takes an amount of ether currency specified by a number and a unit and returns the integer quantity of wei equivalent to that amount.

  • value: Value for the ether unit
  • unit: Ether unit name (e.g. "wei", "ether", "gwei", etc.)
as_unitless_number(value) → int

Converts a int128, uint256, or decimal value with units into one without units (used for assignment and math).

slice(b: bytes, start: int128, length: int128) → bytes

Copies a list of bytes and returns a specified slice.

  • b: bytes or bytes32 to be sliced
  • start: start position of the slice
  • length: length of the slice
len(b: bytes) → int128

Returns the length of a given bytes list.

concat(a, b, *args) → bytes

Takes 2 or more bytes arrays of type bytes32 or bytes and combines them into a single bytes list.

keccak256(value) → bytes32

Returns a keccak256 hash of the given value.

  • value: Value to hash. Can be str_literal, bytes, or bytes32.
sha256(value) → bytes32

Returns a sha256 (SHA2 256bit output) hash of the given value.

  • value: Value to hash. Can be str_literal, bytes, or bytes32.
sqrt(d: decimal) → decimal

Returns the square root of the provided decimal number, using the Babylonian square root algorithm.

method_id(method, type_) → Union[bytes32, bytes[4]]

Takes a function declaration and returns its method_id (used in data field to call it).

  • method: Method declaration as str_literal
  • type_: Type of output (bytes32 or bytes[4])

Returns a value of the type specified by type_.

ecrecover(hash: bytes32, v: uint256, r: uint256, s: uint256) → address

Takes a signed hash and vrs and returns the public key of the signer.

ecadd(a: uint256[2], b: uint256[2]) → uint256[2]

Takes two points on the Alt-BN128 curve and adds them together.

ecmul(point: uint256[2], scalar: uint256) → uint256[2]

Takes a point on the Alt-BN128 curve (p) and a scalar value (s), and returns the result of adding the point to itself s times, i.e. p * s.

  • point: Point to be multiplied
  • scalar: Scalar value
extract32(b: bytes, start: int128, type_=bytes32) → Union[bytes32, int128, address]

Extracts a value from a bytes list.

  • b: bytes list to extract from
  • start: Start point to extract from
  • type_: Type of output (bytes32, int128, or address). Defaults to bytes32.

Returns a value of the type specified by type_.

RLPList(b: bytes, types_list: List) → LLLnode

Takes encoded RLP data and an unencoded list of types.

  • b: Encoded data
  • types_list: List of types

Example usage:

vote_msg: bytes <= 1024 = ...

values = RLPList(vote_msg, [int128, int128, bytes32, bytes, bytes])

var1: int128 = values[0]
var2: int128 = values[1]
var3: bytes32 = values[2]
var4: bytes <= 1024 = values[3]
var5: bytes <= 1024 = values[4]

RLP decoder needs to be deployed if one wishes to use it outside of the Vyper test suite. Eventually, the decoder will be available on mainnet at a fixed address. But for now, here's how to create RLP decoder on other chains:

1. send 6270960000000000 wei to 0xd2c560282c9C02465C2dAcdEF3E859E730848761

2. Publish this tx to create the contract

0xf90237808506fc23ac00830330888080b902246102128061000e60003961022056600060007f010000000000000000000000000000000000000000000000000000000000000060003504600060c082121515585760f882121561004d5760bf820336141558576001905061006e565b600181013560f783036020035260005160f6830301361415585760f6820390505b5b368112156101c2577f010000000000000000000000000000000000000000000000000000000000000081350483602086026040015260018501945060808112156100d55760018461044001526001828561046001376001820191506021840193506101bc565b60b881121561014357608081038461044001526080810360018301856104600137608181141561012e5760807f010000000000000000000000000000000000000000000000000000000000000060018401350412151558575b607f81038201915060608103840193506101bb565b60c08112156101b857600182013560b782036020035260005160388112157f010000000000000000000000000000000000000000000000000000000000000060018501350402155857808561044001528060b6838501038661046001378060b6830301830192506020810185019450506101ba565bfe5b5b5b5061006f565b601f841315155857602060208502016020810391505b6000821215156101fc578082604001510182826104400301526020820391506101d8565b808401610420528381018161044003f350505050505b6000f31b2d4f

3. This is the contract address: 0xCb969cAAad21A78a24083164ffa81604317Ab603

Low Level Built in Functions

Vyper contains a set of built in functions which execute opcodes such as SEND or SELFDESTRUCT.

send(to: address, value: uint256(wei)) → None

Sends ether from the contract to the specified Ethereum address.

  • to: The destination address to send ether to
  • value: The wei value to send to the address

주석

The amount to send is always specified in wei.

raw_call(to: address, data: bytes, outsize: int, gas: uint256, value: uint256(wei) = 0, is_delegate_call: bool = False) → bytes[outsize]

Calls to the specified Ethereum address.

  • to: Destination address to call to
  • data: Data to send to the destination address
  • outsize: Maximum length of the bytes array returned from the call
  • gas: Amount of gas to atttach to the call
  • value: The wei value to send to the address (Optional, default 0)
  • is_delegate_call: If True, the call will be sent as DELEGATECALL (Optional, default False)

Returns the data returned by the call as a bytes list, with outsize as the max length.

selfdestruct(to: address) → None

Triggers the SELFDESTRUCT opcode (0xFF), causing the contract to be destroyed.

  • to: Address to forward the contract's ether balance to

경고

This method will delete the contract from the Ethereum blockchain. All non-ether assets associated with this contract will be "burned" and the contract will be inaccessible.

raise(reason: str) → None

Raises an exception.

  • reason: The exception reason (must be <= 32 bytes)

This method triggers the REVERT opcode (0xFD) with the provided reason given as the error message. The code will stop operation, the contract's state will be reverted to the state before the transaction took place and the remaining gas will be returned to the transaction's sender.

주석

To give it a more Python-like syntax, the raise function can be called without parenthesis, the syntax would be raise "An exception". Even though both options will compile, it's recommended to use the Pythonic version without parentheses.

assert(cond: bool, reason: str = None) → None

Asserts the specified condition.

  • cond: The boolean condition to assert
  • reason: The exception reason (must be <= 32 bytes)

This method's behavior is equivalent to:

if not cond:
    raise reason

The only difference in behavior is that assert can be called without a reason string, while raise requires one.

If the reason string is set to UNREACHABLE, an INVALID opcode (0xFE) will be used instead of REVERT. In this case, calls that revert will not receive a gas refund.

You cannot directly assert the result of a non-constant function call. The proper pattern for doing so is to assign the result to a memory variable, and then call assert on that variable. Alternatively, use the assert_modifiable method.

주석

To give it a more Python-like syntax, the assert function can be called without parenthesis, the syntax would be assert your_bool_condition. Even though both options will compile, it's recommended to use the Pythonic version without parenthesis.

assert_modifiable(cond: bool) → None

Asserts a specified condition, without checking for constancy on a callable condition.

  • cond: The boolean condition to assert

Use assert_modifiable in place of assert when you wish to directly assert the result of a potentially state-changing call.

For example, a common use case is verifying the results of an ERC20 token transfer:

@public
def transferTokens(token: address, to: address, amount: uint256) -> bool:
    assert_modifiable(ERC20(token).transfer(to, amount))
    return True
raw_log(topics: bytes32[4], data: bytes) → None

Provides low level access to the LOG opcodes, emitting a log without having to specify an ABI type.

  • topics: List of bytes32 log topics
  • data: Unindexed event data to include in the log, bytes or bytes32

This method provides low-level access to the LOG opcodes (0xA0..``0xA4``). The length of topics determines which opcode will be used. topics is a list of bytes32 topics that will be indexed. The remaining unindexed parameters can be placed in the data parameter.

create_forwarder_to(target: address, value: uint256(wei) = 0) → address

Duplicates a contract's code and deploys it as a new instance, by means of a DELEGATECALL.

  • target: Address of the contract to duplicate
  • value: The wei value to send to the new contract address (Optional, default 0)

Returns the address of the duplicated contract.

blockhash(block_num: uint256) → bytes32

Returns the hash of the block at the specified height.

주석

The EVM only provides access to the most 256 blocks. This function will return 0 if the block number is greater than or equal to the current block number or more than 256 blocks behind the current block.

Types

Vyper is a statically typed language, which means that the type of each variable (state and local) needs to be specified or at least known at compile-time. Vyper provides several elementary types which can be combined to form complex types.

In addition, types can interact with each other in expressions containing operators.

Value Types

The following types are also called value types because variables of these types will always be passed by value, i.e. they are always copied when they are used as function arguments or in assignments.

Boolean

Keyword: bool

A boolean is a type to store a logical/truth value.

Values

The only possible values are the constants True and False.

Operators
Operator Description
x not y Logical negation
x and y Logical conjunction
x or y Logical disjunction
x == y Equality
x != y Inequality

The operators or and and do not apply short-circuiting rules, i.e. both x and y will always be evaluated.

Signed Integer (128 bit)

Keyword: int128

A signed integer (128 bit) is a type to store positive and negative integers.

Values

Signed integer values between -2127 and (2127 - 1), inclusive.

Operators
Comparisons

Comparisons return a boolean value.

Operator Description
x < y Less than
x <= y Less than or equal to
x == y Equals
x != y Does not equal
x >= y Greater than or equal to
x > y Greater than

x and y must be of the type int128.

Arithmetic Operators
Operator Description
x + y Addition
x - y Subtraction
-x Unary minus/Negation
x * y Multiplication
x / y Division
x**y Exponentiation
x % y Modulo
min(x, y) Minimum
max(x, y) Maximum

x and y must be of the type int128.

Unsigned Integer (256 bit)

Keyword: uint256

An unsigned integer (256 bit) is a type to store non-negative integers.

Values

Integer values between 0 and (2256-1).

주석

Integer literals are interpreted as int128 by default. In cases where uint256 is more appropriate, such as assignment, the literal might be interpreted as uint256. Example: _variable: uint256 = _literal. In order to explicitly cast a literal to a uint256 use convert(_literal, uint256).

Operators
Comparisons

Comparisons return a boolean value.

Operator Description
x < y Less than
x <= y Less than or equal to
x == y Equals
x != y Does not equal
x >= y Greater than or equal to
x > y Greater than

x and y must be of the type uint256.

Arithmetic Operators
Operator Description
x + y Addition
x - y Subtraction
uint256_addmod(x, y, z) Addition modulo z
x * y Multiplication
uint256_mulmod(x, y, z) Multiplication modulo z
x / y Division
x**y Exponentiation
x % y Modulo
min(x, y) Minimum
max(x, y) Maximum

x, y and z must be of the type uint256.

Bitwise Operators
Operator Description
bitwise_and(x, y) AND
bitwise_not(x, y) NOT
bitwise_or(x, y) OR
bitwise_xor(x, y) XOR
shift(x, _shift) Bitwise Shift

x and y must be of the type uint256. _shift must be of the type int128.

주석

Positive _shift equals a left shift; negative _shift equals a right shift. Values shifted above/below the most/least significant bit get discarded.

Decimals

Keyword: decimal

A decimal is a type to store a decimal fixed point value.

Values

A value with a precision of 10 decimal places between -2127 and (2127 - 1).

Operators
Comparisons

Comparisons return a boolean value.

Operator Description
x < y Less than
x <= y Less or equal
x == y Equals
x != y Does not equal
x >= y Greater or equal
x > y Greater than

x and y must be of the type decimal.

Arithmetic Operators
Operator Description
x + y Addition
x - y Subtraction
-x Unary minus/Negation
x * y Multiplication
x / y Division
x % y Modulo
min(x, y) Minimum
max(x, y) Maximum
floor(x) Largest integer <= x. Returns int128.
ceil(x) Smallest integer >= x. Returns int128.

x and y must be of the type decimal.

Address

Keyword: address

The address type holds an Ethereum address.

Values

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.

Members
Member Description
balance Query the balance of an address. Returns wei_value.
codesize Query the code size of an address. Returns int128.
is_contract Query whether it is a contract address. Returns bool.

Syntax as follows: _address.<member>, where _address is of the type address and <member> is one of the above keywords.

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 3 unit types built in, which are the following:

Time
Keyword Unit Base type Description
timestamp 1 sec uint256 This represents a point in time.
timedelta 1 sec uint256 This is a number of seconds.

주석

Two timedelta can be added together, as can a timedelta and a timestamp, but not two timestamps.

Wei
Keyword Unit Base type Description
wei_value 1 wei uint256 This is an amount of Ether in wei.
Custom Unit Types

Vyper allows you to add additional not-provided unit label to either uint256, int128 or decimal.

Custom units example:

# specify units used in the contract.
units: {
    cm: "centimeter",
    km: "kilometer"
}

Having defined the units they can be defined on variables as follows.

Custom units usage:

a: int128(cm)
b: uint256(km)
32-bit-wide Byte Array

Keyword: bytes32 This is a 32-bit-wide byte array that is otherwise similar to byte arrays.

Example:

# Declaration
hash: bytes32
# Assignment
self.hash = _hash
Operators
Keyword Description
keccak256(x) Return the keccak256 hash as bytes32.
concat(x, ...) Concatenate multiple inputs.
slice(x, start=_start, len=_len) Return a slice of _len starting at _start.

Where x is a byte array and _start as well as _len are integer values.

Fixed-size Byte Arrays

Keyword: bytes

A byte array with a fixed size. The syntax being bytes[maxLen], where maxLen is an integer which denotes the maximum number of bytes. On the ABI level the Fixed-size bytes array is annotated as bytes.

Example:

example_bytes: bytes[100] = b"\x01\x02\x03"
Fixed-size Strings

Keyword: string Fixed-size strings can hold strings with equal or fewer characters than the maximum length of the string. On the ABI level the Fixed-size bytes array is annotated as string.

Example:

example_str: string[100] = "Test String"
Operators
Keyword Description
len(x) Return the length as an integer.
keccak256(x) Return the keccak256 hash as bytes32.
concat(x, ...) Concatenate multiple inputs.
slice(x, start=_start, len=_len) Return a slice of _len starting at _start.

Where x is a byte array or string while _start and _len are integers. The len, keccak256, concat, slice operators can be used with string and bytes types.

Reference Types

Reference types do not fit into 32 bytes. Because of this, copying their value is not as feasible as with value types. Therefore only the location, i.e. the reference, of the data is passed.

Fixed-size Lists

Fixed-size lists hold a finite number of elements which belong to a specified type.

Syntax

Lists can be declared with _name: _ValueType[_Integer]. Multidimensional lists are also possible.

Example:

#Defining a list
exampleList: int128[3]
#Setting values
exampleList = [10, 11, 12]
exampleList[2] = 42
#Returning a value
return exampleList[0]
Structs

Structs are custom defined types that can group several variables.

Syntax

Structs can be accessed via struct.argname. Example:

#Defining a struct
struct MyStruct:
    value1: int128
    value2: decimal
exampleStruct: MyStruct
#Constructing a struct
exampleStruct = MyStruct({value1: 1, value2: 2})
#Accessing a value
exampleStruct.value1 = 1
Mappings

Mappings in Vyper can be seen as hash tables which are virtually initialized such that every possible key exists and is mapped to a value whose byte-representation is all zeros: a type's default value. The similarity ends here, though: The key data is not actually stored in a mapping, only its keccak256 hash used to look up the value. Because of this, mappings do not have a length or a concept of a key or value being "set".

It is possible to mark mappings public and have Vyper create a getter. The _KeyType will become a required parameter for the getter and it will return _ValueType.

주석

Mappings are only allowed as state variables.

Syntax

Mapping types are declared as map(_KeyType, _ValueType). Here _KeyType can be any base or bytes type. Mappings, contract or structs are not support as key types. _ValueType can actually be any type, including mappings.

Example:

#Defining a mapping
exampleMapping: map(int128, decimal)
#Accessing a value
exampleMapping[0] = 10.1

주석

Mappings can only be accessed, not iterated over.

Initial Values

In Vyper, there is no null option like most programming languages have. Thus, every variable type has a default value. In order to check if a variable is empty, you will need to compare it to its type's default value. If you would like to reset a variable to its type's default value, use the built-in clear() function.

주석

Memory variables must be assigned a value at the time they are declared. 내장된 상수들 may be used to initialize memory variables with their default values.

Here you can find a list of all types and default values:

Default Variable Values
Type Default Value
bool False
int128 0
uint256 0
decimal 0.0
address 0x0000000000000000000000000000000000000000
bytes32 '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'

주석

In bytes the array starts with the bytes all set to '\x00'

주석

In reference types all the type's members are set to their initial values.

Type Conversions

All type conversions in Vyper must be made explicitly using the built-in convert(a, b) function. Currently, the following type conversions are supported:

Basic Type Conversions
Destination Type (b) Input Type (a.type) Allowed Inputs Values (a) Additional Notes
bool bool Do not allow converting to/from the same type
bool decimal MINNUM...MAXNUM Has the effective conversion logic of: return (a != 0.0)
bool int128 MINNUM...MAXNUM Has the effective conversion logic of: return (a != 0)
bool uint256 0...MAX_UINT256 Has the effective conversion logic of: return (a != 0)
bool bytes32 (0x00 * 32)...(0xFF * 32) Has the effective conversion logic of: return (a != 0x00)
bool bytes (0x00 * 1)...(0xFF * 32) Has the effective conversion logic of: return (a != 0x00)
       
decimal bool True / False Result will be 0.0 or 1.0
decimal decimal Do not allow converting to/from the same type
decimal int128 MINNUM...MAXNUM  
decimal uint256 0...MAXDECIMAL  
decimal bytes32 (0x00 * 32)...(0xFF * 32)  
decimal bytes (0x00 * 1)...(0xFF * 32)  
       
int128 bool True / False Result will be 0 or 1
int128 decimal MINNUM...MAXNUM Only allow input within int128 supported range, truncates the decimal value
int128 int128 Do not allow converting to/from the same type
int128 uint256 0...MAXNUM  
int128 bytes32 (0x00 * 32)...(0xFF * 32)  
int128 bytes (0x00 * 1)...(0xFF * 32)  
       
uint256 bool True / False Result will be 0 or 1
uint256 decimal 0...MAXDECIMAL Truncates the decimal value
uint256 int128 0...MAXNUM  
uint256 uint256 Do not allow converting to/from the same type
uint256 bytes32 (0x00 * 32)...(0xFF * 32)  
uint256 bytes (0x00 * 1)...(0xFF * 32)  
       
bytes32 bool True / False Result will be either (0x00 * 32) or (0x00 * 31 + 0x01)
bytes32 decimal MINDECIMAL...MAXDECIMAL Has the effective behavior of multiplying the decimal value by the decimal divisor 10000000000 and then converting that signed integer value to a bytes32 byte array
bytes32 int128 MINNUM...MAXNUM  
bytes32 uint256 0...MAX_UINT256  
bytes32 bytes32 Do not allow converting to/from the same type
bytes32 bytes (0x00 * 1)...(0xFF * 32) Left-pad input bytes to size of 32

상수와 환경변수들

내장된 상수들

Vpyer는 편의를 위한 내장된 상수들이 있습니다.

Name Type Value
ZERO_ADDRESS address 0x0000000000000000000000000000000000000000
EMPTY_BYTES32 bytes32 0x0000000000000000000000000000000000000000000000000000000000000000
MAX_INT128 int128 2**127 - 1
MIN_INT128 int128 -2**127
MAX_DECIMAL decimal (2**127 - 1)
MIN_DECIMAL decimal (-2**127)
MAX_UINT256 uint256 2**256 - 1
ZERO_WEI uint256(wei) 0

커스텀한 상수

커스텀한 상수들은 Vyper에서 전역 수준으로 정의될 수 있습니다. constant 키워드를 이용하여 정의할 수 있습니다.

예시:

TOTAL_SUPPLY: constant(uint256) = 10000000
total_supply: public(uint256)

@public
def __init__():
    self.total_supply = TOTAL_SUPPLY

어려운 예시:

units: {
    share: "Share unit"
}

MAX_SHARES: constant(uint256(share)) = 1000
SHARE_PRICE: constant(uint256(wei/share)) = 5

@public
def market_cap() -> uint256(wei):
    return MAX_SHARES * SHARE_PRICE

환경 변수

환경 변수는 네임스페이스 속에 언제나 존재하며, 블록체인이나 현재 트랜젝션에 대한 정보를 제공할 때 사용되어집니다.

주석

msg.sendermsg.value 는 퍼블릭 함수만 접근 가능합니다. 프라이빗 함수에서 사용을 하려면 파라미터로 전송하셔야만 합니다.

Name Type Value
block.coinbase address 현 블록 채굴자의 주소
block.difficulty uint256 현 블록의 난이도
block.number uint256 현 블록의 번호
block.prevhash bytes32 blockhash(block.number - 1) 와 동일
block.timestamp uint256 현 블록의 epoch timestamp
msg.gas uint256 남은 가스
msg.sender address 메세지 전송자 (현 호출)
msg.value uint256(wei) 메세지로 전송된 Wei의 량
tx.origin address 트랜잭션의 전송자 (체인 내 전체)

Event Logging

Like Solidity and other EVM languages, Vyper can log events to be caught and displayed by user interfaces.

Example of Logging

This example is taken from the sample ERC20 contract and shows the basic flow of event logging.

# Events of the token.
Transfer: event({_from: indexed(address), _to: indexed(address), _value: uint256})
Approval: event({_owner: indexed(address), _spender: indexed(address), _value: uint256})

# Transfer some tokens from message sender to another address
def transfer(_to : address, _value : uint256) -> bool:

   ... Logic here to do the real work ...

   # All done, log the event for listeners
   log.Transfer(msg.sender, _to, _amount)

Let's look at what this is doing. First, we declare two event types to log. The two events are similar in that they contain two indexed address fields. Indexed fields do not make up part of the event data itself, but can be searched by clients that want to catch the event. Also, each event contains one single data field, in each case called _value. Events can contain several arguments with any names desired.

Next, in the transfer function, after we do whatever work is necessary, we log the event. We pass three arguments, corresponding with the three arguments of the Transfer event declaration.

Clients listening to the events will declare and handle the events they are interested in using a library such as web3.js:

var abi = /* abi as generated by the compiler */;
var MyToken = web3.eth.contract(abi);
var myToken = MyToken.at("0x1234...ab67" /* address */);

// watch for changes in the callback
var event = myToken.Transfer(function(error, result) {
    if (!error) {
        var args = result.args;
        console.log('value transferred = ', args._amount);
    }
});

In this example, the listening client declares the event to listen for. Any time the contract sends this log event, the callback will be invoked.

Declaring Events

Let's look at an event declaration in more detail.

Transfer: event({_from: indexed(address), _to: indexed(address), _value: uint256})

Event declarations look like state variable declarations but use the special keyword event. event takes its arguments that consists of all the arguments to be passed as part of the event. Typical events will contain two kinds of arguments:

  • Indexed arguments, which can be searched for by listeners. Each indexed argument is identified by the indexed keyword. Here, each indexed argument is an address. You can have any number of indexed arguments, but indexed arguments are not passed directly to listeners, although some of this information (such as the sender) may be available in the listener's results object.
  • Value arguments, which are passed through to listeners. You can have any number of value arguments and they can have arbitrary names, but each is limited by the EVM to be no more than 32 bytes.

Note that while the argument definition syntax looks like a Python dictionary, it's actually an order-sensitive definition. (Python dictionaries maintain order starting with 3.7.) Thus, the first element (_from) will be matched up with the first argument passed in the log.Transfer call.

Logging Events

Once an event is declared, you can log (send) events. You can send events as many times as you want to. Please note that events sent do not take state storage and thus do not cost gas: this makes events a good way to save some information. However, the drawback is that events are not available to contracts, only to clients.

Logging events is done using the magic keyword log:

log.Transfer(msg.sender, _to, _amount)

The order and types of arguments sent needs to match up with the order of declarations in the dictionary.

Listening for Events

In the example listener above, the result arg actually passes a large amount of information. Here we're most interested in result.args. This is an object with properties that match the properties declared in the event. Note that this object does not contain the indexed properties, which can only be searched in the original myToken.Transfer that created the callback.

Compiling a Contract

Command-Line Tools

Vyper includes the following command-line scripts for compiling contracts:

  • vyper: Compiles vyper contract files into LLL or bytecode
  • vyper-json: Provides a JSON interface to the compiler

주석

The --help flag gives verbose explanations of how to use each of these scripts.

vyper

vyper provides command-line access to the compiler. It can generate various outputs including simple binaries, ASTs, interfaces and source mappings.

To compile a contract:

$ vyper yourFileName.vy

Include the -f flag to specify which output formats to return. Use vyper --help for a full list of output options.

$ vyper -f abi,bytecode,bytecode_runtime,ir,asm,source_map,method_identifiers yourFileName.vy

The -p flag allows you to set a root path that is used when searching for interface files to import. If none is given, it will default to the current working directory. See Searching For Interface Files for more information.

$ vyper -p yourProject yourProject/yourFileName.vy
vyper-json

vyper-json provides a JSON interface for the compiler. It expects a JSON formatted input and returns the compilation result in a JSON formatted output.

Where possible, the JSON formats used by this script follow those of Solidity.

To compile from JSON supplied via stdin:

$ vyper-json

To compile from a JSON file:

$ vyper-json yourProject.json

By default, the output is sent to stdout. To redirect to a file, use the -o flag:

$ vyper-json -o compiled.json
Input JSON Description

The following example describes the expected input format of vyper-json. Comments are of course not permitted and used here only for explanatory purposes.

{
    // Required: Source code language. Must be set to "Vyper".
    "language": "Vyper",
    // Required
    // Source codes given here will be compiled.
    "sources": {
        "contracts/foo.vy": {
            // Optional: keccak256 hash of the source file
            "keccak256": "0x234...",
            // Required: literal contents of the source file
            "content": "@public\ndef foo() -> bool:\n    return True"
        }
    },
    // Optional
    // Interfaces given here are made available for import by the sources
    // that are compiled. If the suffix is ".vy", the compiler will expect
    // a contract-as-interface using proper Vyper syntax. If the suffix is
    // "abi" the compiler will expect an ABI object.
    "interfaces": {
        "contracts/bar.vy": {
            "content": ""
        },
        "contracts/baz.json": {
            "abi": []
        }
    },
    // Optional
    "settings": {
        "evmVersion": "byzantium"  // EVM version to compile for. Can be byzantium, constantinople or petersburg.
    },
    // The following is used to select desired outputs based on file names.
    // File names are given as keys, a star as a file name matches all files.
    // Outputs can also follow the Solidity format where second level keys
    // denoting contract names - all 2nd level outputs are applied to the file.
    //
    // To select all possible compiler outputs: "outputSelection: { '*': ["*"] }"
    // Note that this might slow down the compilation process needlessly.
    //
    // The available output types are as follows:
    //
    //    abi - The contract ABI
    //    ast - Abstract syntax tree
    //    interface - Derived interface of the contract, in proper Vyper syntax
    //    ir - LLL intermediate representation of the code
    //    evm.bytecode.object - Bytecode object
    //    evm.bytecode.opcodes - Opcodes list
    //    evm.deployedBytecode.object - Deployed bytecode object
    //    evm.deployedBytecode.opcodes - Deployed opcodes list
    //    evm.deployedBytecode.sourceMap - Deployed source mapping (useful for debugging)
    //    evm.methodIdentifiers - The list of function hashes
    //
    // Using `evm`, `evm.bytecode`, etc. will select every target part of that output.
    // Additionally, `*` can be used as a wildcard to request everything.
    //
    "outputSelection": {
        "*": ["evm.bytecode", "abi"],  // Enable the abi and bytecode outputs for every single contract
        "contracts/foo.vy": ["ast"]  // Enable the ast output for contracts/foo.vy
    }
}
Output JSON Description

The following example describes the output format of vyper-json. Comments are of course not permitted and used here only for explanatory purposes.

{
    // The compiler version used to generate the JSON
    "compiler": "vyper-0.1.0b12",
    // Optional: not present if no errors/warnings were encountered
    "errors": [
        {
        // Optional: Location within the source file.
        "sourceLocation": {
            "file": "source_file.vy",
            "lineno": 5,
            "col_offset": 11
        },
        // Mandatory: Exception type, such as "JSONError", "KeyError", "StructureException", etc.
        "type": "TypeMismatchException",
        // Mandatory: Component where the error originated, such as "json", "compiler", "vyper", etc.
        "component": "compiler",
        // Mandatory ("error" or "warning")
        "severity": "error",
        // Mandatory
        "message": "Unsupported type conversion: int128 to bool"
        // Optional: the message formatted with source location
        "formattedMessage": "line 5:11 Unsupported type conversion: int128 to bool"
        }
    ],
    // This contains the file-level outputs. Can be limited/filtered by the outputSelection settings.
    "sources": {
        "source_file.vy": {
            // Identifier of the source (used in source maps)
            "id": 0,
            // The AST object
            "ast": {},
        }
    },
    // This contains the contract-level outputs. Can be limited/filtered by the outputSelection settings.
    "contracts": {
        "source_file.vy": {
            // The contract name will always be the file name without a suffix
            "source_file": {
                // The Ethereum Contract ABI.
                // See https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI
                "abi": [],
                // Intermediate representation (string)
                "ir": "",
                // EVM-related outputs
                "evm": {
                    "bytecode": {
                        // The bytecode as a hex string.
                        "object": "00fe",
                        // Opcodes list (string)
                        "opcodes": ""
                    },
                    "deployedBytecode": {
                        // The deployed bytecode as a hex string.
                        "object": "00fe",
                        // Deployed opcodes list (string)
                        "opcodes": "",
                        // The deployed source mapping as a string.
                        "sourceMap": ""
                    },
                    // The list of function hashes
                    "methodIdentifiers": {
                        "delegate(address)": "5c19a95c"
                    }
                }
            }
        }
    }
}
Importing Interfaces

vyper-json searches for imported interfaces in the following sequence:

  1. Interfaces defined in the interfaces field of the input JSON
  2. Derived interfaces generated from contracts in the sources field of the input JSON
  3. (Optional) The local filesystem, if a root path was explicitely declared via the -p flag.

See Searching For Interface Files for more information on Vyper's import system.

Errors

Each error includes a component field, indicating the stage at which it occurred:

  • json: Errors that occur while parsing the input JSON. Usually a result of invalid JSON or a required value that is missing.
  • parser: Errors that occur while parsing the contracts. Usually a result of invalid Vyper syntax.
  • compiler: Errors that occur while compiling the contracts.
  • vyper: Unexpected errors that occur within Vyper. If you receive an error of this type, please open an issue.

You can also use the --traceback flag to receive a standard Python traceback when an error is encountered.

Online Compilers

Vyper Online Compiler

Vyper Online Compiler is an online compiler which lets you experiment with the language without having to install Vyper. It allows you to compile to bytecode as well as LLL.

주석

While the vyper version of the online compiler is updated on a regular basis it might be a bit behind the latest version found in the master branch of the repository.

Remix IDE

Remix IDE is a compiler and Javascript VM for developing and testing contracts in Vyper as well as Solidity.

주석

While the vyper version of the Remix IDE compiler is updated on a regular basis it might be a bit behind the latest version found in the master branch of the repository. Make sure the byte code matches the output from your local compiler.

컨트랙트 배포

메인넷이나 테스트넷에 컨트랙트를 배포할 준비가 되었다면, 다음과 같은 선택지들이 존재합니다.

  • vyper 컴파일러를 통해 생성된 바이트 코드를 갖고 geth나 mist를 통해 수동으로 배포하기
vyper yourFileName.vy
# returns bytecode
  • 바이트 코드와 ABI를 갖고 마이이더월렛 의 컨트랙트 메뉴를 통해서 웹 브라우저를 통해 배포하기
vyper -f abi yourFileName.vy
# returns ABI
  • Use the remote compiler provided by the Remix IDE to compile and deploy your contract on your net of choice. Remix also provides a JavaScript VM to test deploy your contract.
  • Remix IDE 에서 제공되는 리모트 컴파일러를 이용하여 컴파일하고 선택한 네트워크로 컨트랙트를 배포하기. Remix는 자바스크립트 VM을 제공하여 당신의 컨트랙트를 배포 전 테스트를 해 볼 수 있습니다.

주석

Remix IDE의 Vyper 버전은 이 레포지토리의 마스터 브랜치의 최신 버전보다 약간 옛날 버전을 사용할 수 있습니다. 당신의 로컬 컴파일러에서 나온 결과와 바이트 코드가 같은지 확실히 하십시오.

Testing a Contract

This documentation recommends the use of the pytest framework with the ethereum-tester package. Prior to testing, the vyper specific contract conversion and the blockchain related fixtures need to be set up. These fixtures will be used in every test file and should therefore be defined in conftest.py.

주석

Since the testing is done in the pytest framework, you can make use of pytest.ini, tox.ini and setup.cfg and you can use most IDEs' pytest plugins.

Vyper Contract and Basic Fixtures

This is the base requirement to load a vyper contract and start testing. The last two fixtures are optional and will be discussed later. The rest of this chapter assumes, that you have this code set up in your conftest.py file. Alternatively, you can import the fixtures to conftest.py or use pytest plugins.

Load Contract and Basic Tests

Assume the following simple contract storage.vy. It has a single integer variable and a function to set that value.

We create a test file test_storage.py where we write our tests in pytest style.

First we create a fixture for the contract which will compile our contract and set up a Web3 contract object. We then use this fixture for our test functions to interact with the contract.

주석

To run the tests, call pytest or python -m pytest from your project directory.

Events and Failed Transactions

To test events and failed transactions we expand our simple storage contract to include an event and two conditions for a failed transaction: advanced_storage.vy

Next, we take a look at the two fixtures that will allow us to read the event logs and to check for failed transactions.

The fixture to assert failed transactions defaults to check for a TransactionFailed exception, but can be used to check for different exceptions too, as shown below. Also note that the chain gets reverted to the state before the failed transaction.

This fixture will return a tuple with all the logs for a certain event and transaction. The length of the tuple equals the number of events (of the specified type) logged and should be checked first.

Finally, we create a new file test_advanced_storage.py where we use the new fixtures to test failed transactions and events.

자주 묻는 질문들

일반적인 질문

Vyper는 무엇인가요?

Vpyer는 스마트 컨트랙트 개발용 언어입니다. Vpyer는 감사 가능하고, 안전하고, 인간 친화적인 것을 목표로 하고 있습니다. 읽기 쉬운 것은 쓰기 쉬운 것보다 더 중요시 됩니다.

Vyper 또는 Solidity?

대다수의 유즈케이스에서, 개인 취향 차이입니다. 안전하고, 감사 가능하고, 인간 친화적인 것을 지원하기 위해서는 Solidity에서 포함되는 다수의 프로그램밍 구성개념들이 Vyper에서는 지원되지 않는다는 것을 의미합니다.

Vyper에서 지원되지 않는 것은 무엇인가요?

다음긔 구성개념들이 포함되어있지 않습니다. 코드를 이해하기 어렵게 하거나, 오독 할 수 있기 때문입니다.

  • 수식어 (Modifiers)
  • 클래스 상속
  • 인라인 어셈블리
  • 함수 오버로딩
  • 연산자 오버로딩
  • 이진 고정 소수점

Recursive calling and infinite-length loops are not included because they cannot set an upper bound on gas limits. An upper bound is required to prevent gas limit attacks and ensure the security of smart contracts built in Vyper. 가스 제한의 상한치를 예측 할 수 없기에, 재귀 호출이나 무한한 길이의 루프 또한 포한되지 않습니다. 상한치는 가스 제한 공격(Gas limit attacks)를 막기 위해서 필요하며, Vyper로 만들어진 스마트 컨트랙트의 안정성을 확보합니다.

그러면 루프는 어떻게 작동되나요?

파이썬의 루프처럼 작동되나 한 가지는 분명하게 다릅니다. Vyper는 변수 길이 만큼의 순회를 허가하지 않습니다. 변수를 이용한 순회는 무한한 길이의 루프를 만들어내어 공격이 가능하게 합니다.

구조체는 어떻게 작동하나요?

구조체는 변수를 묶고 struct.argname 형태로 접근 가능합니다. 파이썬 클래스와 비슷합니다.

# define the struct struct MyStruct:

arg1: int128 arg2: decimal

struct: MyStruct

#access arg1 in struct struct.arg1 = 1

기여하기

도움은 언제나 환영입니다.

시작하기 위해, installing Vyper 를 진행함으로써 Vyper의 컴포넌트와 빌드 과정에 익숙해지실 수 있습니다. 또한 Vyper로 스마트컨트랙트를 작성하는데 정통해질 수도 있을 것입니다.

기여의 종류

부분적으로 우리는 다음과 같은 부분에서 도움이 필요합니다.

어떻게 개선을 제안할 수 있는가?

개선 사항을 제안하기 위해서 Vyer 개선사항 제안서(짧게 VIP라고 합니다)를 만들어주세요. 다음의 VIP 템플릿 을 사용하세요.

이슈를 어떻게 보고하는가?

이슈를 보고 하기 위해서는, `GitHub 이슈 트래커 <https://github.com/vyperlang/vyper/issues>`_를 사용하세요. 이슈를 리포팅 할 때에는 다음의 세부사항이 필요합니다.

  • 어떤 버전의 Vyper를 사용하는지
  • 소스 코드가 어떤지 (응용 가능하다면)
  • 어떤 플랫폼에서 실행했는지
  • OS의 이름과 버전
  • 이슈를 재현하기 위한 자세한 방법
  • 이슈의 결과값이 어떤지
  • 원래 나와야하는 결과 값은 어떠해야하는지

소스 코드의 양을 줄여 이슈의 크기를 최대한 줄이는 것은 언제나 도움이 되고, 때때로 잘못 이해하는 것을 막기도 합니다.

버그 픽스

이슈 페이지 에서 버그를 찾거나 보고 하십시오. "bug"라고 태그 되어있으면 개선을 원하는 누구나 작업할 수 있습니다.

스타일 가이드

Vpyer의 코드 베이스는 Snake Charmer's Style Guide 를 따릅니다. 일부는 f-strings (명료성을 위해) 사용하고, 코드 베이스의 구조적 디자인 을 고수함으로써, 코드 베이스를 유지보수하는 데 쓰이는 스타일 가이드에 부합하지 않을 수 있습니다.

풀 리퀘스트를 위한 워크플로우

컨트리뷰션을 하기 위해서는 master 브랜치를 포크하시고 그곳에서 작업하십시오. 당신의 커밋 메세지들은 그 수정을 했는지와 추가적으로 무엇을 했는지에 대해서 자세히 설명해야합니다. (작은 수정 사항이 아니라면요)

포크를 한 이후에 (머지 컨플릭트를 해결 하기 위해서라던지의 이유로) 만약 master 에서 어떠한 변경 사항을 풀 할 필요가 있다면, git merge 를 사용하지 마시고 git rebase 를 당신의 브랜치에 사용하십시오.

기능 적용

만약 새로운 기능을 작성하고 있다면, 적절한 Boost 테스트 케이스를 작성하고 이를 test/ 디렉토리 안에 넣는 것을 확실히 하서야합니다.

만약 거대한 변경점을 만든다면, Gitter 채널에서 먼저 상담을 받아보시길 바랍니다.

저희는 CI 테스팅을 하지만, 지원되는 Python 버전에서 테스트를 통과하도록 하시고 로컬에서 빌드가 되는 상태에서 풀 리퀘스트를 보내시기를 바랍니다.

도움에 감사합니다!

Release Notes

v0.1.0-beta.14

Date released: 13-11-2019

Some of the bug and stability fixes:

  • Mucho Documentation and Example cleanup!
  • Python 3.8 support (#1678)
  • Disallow scientific notation in literals, which previously parsed incorrectly (#1681)
  • Add implicit rewrite rule for bytes[32] -> bytes32 (#1718)
  • Support bytes32 in raw_log (#1719)
  • Fixed EOF parsing bug (#1720)
  • Cleaned up arithmetic expressions (#1661)
  • Fixed off-by-one in check for homogeneous list element types (#1673)
  • Fixed stack valency issues in if and for statements (#1665)
  • Prevent overflow when using sqrt on certain datatypes (#1679)
  • Prevent shadowing of internal variables (#1601)
  • Reject unary substraction on unsigned types (#1638)
  • Disallow orelse syntax in for loops (#1633)
  • Increased clarity and efficiency of zero-padding (#1605)

v0.1.0-beta.13

Date released: 27-09-2019

The following VIPs were implemented for Beta 13:

  • Add vyper-json compilation mode (VIP #1520)
  • Environment variables and constants can now be used as default parameters (VIP #1525)
  • Require unitialized memory be set on creation (VIP #1493)

Some of the bug and stability fixes:

  • Type check for default params and arrays (#1596)
  • Fixed bug when using assertions inside for loops (#1619)
  • Fixed zero padding error for ABI encoder (#1611)
  • Check calldatasize before calldataload for function selector (#1606)

v0.1.0-beta.12

Date released: 27-08-2019

The following VIPs were implemented for Beta 12:

  • Support for relative imports (VIP #1367)
  • Restricted use of environment variables in private functions (VIP #1199)

Some of the bug and stability fixes:

  • @nonreentrant/@constant logical inconsistency (#1544)
  • Struct passthrough issue (#1551)
  • Private underflow issue (#1470)
  • Constancy check issue (#1480)
  • Prevent use of conflicting method IDs (#1530)
  • Missing arg check for private functions (#1579)
  • Zero padding issue (#1563)
  • vyper.cli rearchitecture of scripts (#1574)
  • AST end offsets and Solidity-compatible compressed sourcemap (#1580)

Special thanks to (@iamdefinitelyahuman) for lots of updates this release!

v0.1.0-beta.11

Date released: 23-07-2019

Beta 11 brings some performance and stability fixes.

  • Using calldata instead of memory parameters. (#1499)
  • Reducing of contract size, for large parameter functions. (#1486)
  • Improvements for Windows users (#1486) (#1488)
  • Array copy optimisation (#1487)
  • Fixing @nonreentrant decorator for return statements (#1532)
  • sha3 builtin function removed (#1328)
  • Disallow conflicting method IDs (#1530)
  • Additional convert() supported types (#1524) (#1500)
  • Equality operator for strings and bytes (#1507)
  • Change in compile_codes interface function (#1504)

Thanks to all the contributors!

v0.1.0-beta.10

Date released: 24-05-2019

  • Lots of linting and refactoring!
  • Bugfix with regards to using arrays as parameters to private functions (#1418). Please check your contracts, and upgrade to latest version, if you do use this.
  • Slight shrinking in init produced bytecode. (#1399)
  • Additional constancy protection in the for .. range expression. (#1397)
  • Improved bug report (#1394)
  • Fix returning of External Contract from functions (#1376)
  • Interface unit fix (#1303)
  • Not Equal (!=) optimisation (#1303) 1386
  • New assert <condition>, UNREACHABLE statement. (#711)

Special thanks to (Charles Cooper), for some excellent contributions this release.

v0.1.0-beta.9

Date released: 12-03-2019

  • Add support for list constants (#1211)
  • Add sha256 function (#1327)
  • Renamed create_with_code_of to create_forwarder_to (#1177)
  • @nonreentrant Decorator (#1204)
  • Add opcodes and opcodes_runtime flags to compiler (#1255)
  • Improved External contract call interfaces (#885)

Prior to v0.1.0-beta.9

Prior to this release, we managed our change log in a different fashion. Here is the old changelog:

  • 2019.04.05: Add stricter checking of unbalanced return statements. (#590)
  • 2019.03.04: create_with_code_of has been renamed to create_forwarder_to. (#1177)
  • 2019.02.14: Assigning a persistent contract address can only be done using the bar_contact = ERC20(<address>) syntax.
  • 2019.02.12: ERC20 interface has to be imported using from vyper.interfaces import ERC20 to use.
  • 2019.01.30: Byte array literals need to be annoted using b"", strings are represented as "".
  • 2018.12.12: Disallow use of None, disallow use of del, implemented clear() built-in function.
  • 2018.11.19: Change mapping syntax to use map(). (VIP564)
  • 2018.10.02: Change the convert style to use types instead of string. (VIP1026)
  • 2018.09.24: Add support for custom constants.
  • 2018.08.09: Add support for default parameters.
  • 2018.06.08: Tagged first beta.
  • 2018.05.23: Changed wei_value to be uint256.
  • 2018.04.03: Changed bytes declaration from bytes <= n to bytes[n].
  • 2018.03.27: Renaming signed256 to int256.
  • 2018.03.22: Add modifiable and static keywords for external contract calls.
  • 2018.03.20: Renaming __log__ to event.
  • 2018.02.22: Renaming num to int128, and num256 to uint256.
  • 2018.02.13: Ban functions with payable and constant decorators.
  • 2018.02.12: Division by num returns decimal type.
  • 2018.02.09: Standardize type conversions.
  • 2018.02.01: Functions cannot have the same name as globals.
  • 2018.01.27: Change getter from get_var to var.
  • 2018.01.11: Change version from 0.0.2 to 0.0.3
  • 2018.01.04: Types need to be specified on assignment (VIP545).
  • 2017.01.02 Change as_wei_value to use quotes for units.
  • 2017.12.25: Change name from Viper to Vyper.
  • 2017.12.22: Add continue for loops
  • 2017.11.29: @internal renamed to @private.
  • 2017.11.15: Functions require either @internal or @public decorators.
  • 2017.07.25: The def foo() -> num(const): ... syntax no longer works; you now need to do def foo() -> num: ... with a @constant decorator on the previous line.
  • 2017.07.25: Functions without a @payable decorator now fail when called with nonzero wei.
  • 2017.07.25: A function can only call functions that are declared above it (that is, A can call B only if B appears earlier in the code than A does). This was introduced