ERC20 Token with Vyper and Brownie

By Hideki Ishiguro at

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

  • Python3.6 and higher

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.