Blog

ERC20 Token with Vyper and Brownie

Web3

When implementing smart contracts, I've used Solidity and OpenZeppelin.
I knew about Vyper, so I wanted to use it someday.
And now is the time.

As I read through the official documentation, it seems that it's easier to develop and test using a framework called Brownie so I use it.

0. Requirements

1. Install Brownie

Before installing Brownie, we need ganache-cli or we should face an error when running test.

npm i -g ganache-cli

Then, install it with pipx.

python3 -m pip install pipx

pipx install eth-brownie

2. Create a new project with Brownie

mkdir sample
cd sample

brownie init

After that, you should see that some directories such as contracts/, tests/ are created in the sample/ directory.

Next, we need to prepare a Python virtual environment in sample directory to install Vyper.

# in sample/
python3 -m venv venv

source venv/bin/activate

3. Install Vyper

Make sure you are in a virtual environment, then do the following:

pip install vyper

4. Create a ERC20 smart contract using Vyper

Create a new file named SampleToken.vy in contracts/ directory.
Then implement a smart contract while referring to the Vyper ERC20 example.

sample/contracts/SampleToken.vy

# @version ^0.3.0

from vyper.interfaces import ERC20
from vyper.interfaces import ERC20Detailed

implements: ERC20
implements: ERC20Detailed

event Transfer:
sender: indexed(address)
receiver: indexed(address)
value: uint256

event Approval:
owner: indexed(address)
spender: indexed(address)
value: uint256

name: public(String[64])
symbol: public(String[32])
decimals: public(uint8)

balanceOf: public(HashMap[address, uint256])
allowance: public(HashMap[address, HashMap[address, uint256]])
totalSupply: public(uint256)
minter: address


@external
def __init__(_name: String[64], _symbol: String[32], _decimals: uint8, _supply: uint256):
init_supply: uint256 = _supply * 10 ** convert(_decimals, uint256)
self.name = _name
self.symbol = _symbol
self.decimals = _decimals
self.balanceOf[msg.sender] = init_supply
self.totalSupply = init_supply
self.minter = msg.sender
log Transfer(ZERO_ADDRESS, msg.sender, init_supply)



@external
def transfer(_to : address, _value : uint256) -> bool:
self.balanceOf[msg.sender] -= _value
self.balanceOf[_to] += _value
log Transfer(msg.sender, _to, _value)
return True


@external
def transferFrom(_from : address, _to : address, _value : uint256) -> bool:
self.balanceOf[_from] -= _value
self.balanceOf[_to] += _value
self.allowance[_from][msg.sender] -= _value
log Transfer(_from, _to, _value)
return True


@external
def approve(_spender : address, _value : uint256) -> bool:
self.allowance[msg.sender][_spender] = _value
log Approval(msg.sender, _spender, _value)
return True


@external
def mint(_to: address, _value: uint256):
assert msg.sender == self.minter
assert _to != ZERO_ADDRESS
self.totalSupply += _value
self.balanceOf[_to] += _value
log Transfer(ZERO_ADDRESS, _to, _value)


@internal
def _burn(_address: address, _value: uint256):
assert _address != ZERO_ADDRESS
self.totalSupply -= _value
self.balanceOf[_address] -= _value
log Transfer(_address, ZERO_ADDRESS, _value)


@external
def burn(_value: uint256):
self._burn(msg.sender, _value)


@external
def burnFrom(_address: address, _value: uint256):
self.allowance[_address][msg.sender] -= _value
self._burn(_address, _value)

5. Create Unit Tests with Brownie

Create a new file named test_sampletoken.py in tests/ directory.
The filename prefix/postfix must be "test_*.py" or "*_test.py".

In addition, please note that this is a .py file, not .vy.

sample/tests/test_sampletoken.py

import brownie
import pytest


INIT_NAME = "SampleToken"
INIT_SYMBOL = "ST"
INIT_DECIMALS = 18
INIT_SUPPLY = 1000


@pytest.fixture
def sampletoken_contract(SampleToken, accounts):
yield SampleToken.deploy(INIT_NAME, INIT_SYMBOL, INIT_DECIMALS, INIT_SUPPLY, {'from': accounts[0]})


def test_initial_state(sampletoken_contract):
assert sampletoken_contract.name() == INIT_NAME
assert sampletoken_contract.symbol() == INIT_SYMBOL
assert sampletoken_contract.decimals() == INIT_DECIMALS
assert sampletoken_contract.totalSupply() == INIT_SUPPLY * 10 ** INIT_DECIMALS


def test_transfer(sampletoken_contract, accounts):
values = 1000
sampletoken_contract.transfer(accounts[1], values, {'from': accounts[0]})

assert sampletoken_contract.balanceOf(accounts[1]) == values


def test_transferFrom(sampletoken_contract, accounts):
values1 = 1000
sampletoken_contract.transfer(accounts[1], values1, {'from': accounts[0]})

values2 = 500
sampletoken_contract.approve(accounts[0], values2, {'from': accounts[1]})
sampletoken_contract.transferFrom(accounts[1], accounts[2], values2, {'from': accounts[0]})

assert sampletoken_contract.balanceOf(accounts[2]) == values2


def test_mint(sampletoken_contract, accounts):
with brownie.reverts():
sampletoken_contract.mint(accounts[2], 1000, {'from': accounts[1]})

sampletoken_contract.mint(accounts[1], 1000, {'from': accounts[0]})
assert sampletoken_contract.balanceOf(accounts[1]) == 1000


def test_burn(sampletoken_contract, accounts):
burned_value = 1000
sampletoken_contract.burn(burned_value, {'from': accounts[0]})

assert sampletoken_contract.totalSupply() < INIT_SUPPLY * 10 ** INIT_DECIMALS


def test_burnFrom(sampletoken_contract, accounts):
sampletoken_contract.transfer(accounts[1], 1000, {'from': accounts[0]})

burned_value = 500
sampletoken_contract.approve(accounts[0], burned_value, {'from': accounts[1]})

sampletoken_contract.burnFrom(accounts[1], burned_value, {'from': accounts[0]})

6. Test

Finally, you can test it.

brownie test

If there are no errors, this ERC20 token is fine. Maybe.