DEV Community


Posted on • Originally published at

ERC20 Token with Vyper and Brownie

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
Enter fullscreen mode Exit fullscreen mode

Then, install it with pipx.

python3 -m pip install pipx

pipx install eth-brownie
Enter fullscreen mode Exit fullscreen mode

2. Create a new project with Brownie

mkdir sample
cd sample

brownie init
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

3. Install Vyper

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

pip install vyper
Enter fullscreen mode Exit fullscreen mode

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.


# @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

def __init__(_name: String[64], _symbol: String[32], _decimals: uint8, _supply: uint256):
    init_supply: uint256 = _supply * 10 ** convert(_decimals, uint256) = _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)

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

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

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

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)

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

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

def burnFrom(_address: address, _value: uint256):
    self.allowance[_address][msg.sender] -= _value
    self._burn(_address, _value)
Enter fullscreen mode Exit fullscreen mode

5. Create Unit Tests with Brownie

Create a new file named in tests/ directory.

The filename prefix/postfix must be "test_*.py" or "*".

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


import brownie
import pytest

INIT_NAME = "SampleToken"

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 == 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():[2], 1000, {'from': accounts[1]})[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]})
Enter fullscreen mode Exit fullscreen mode

6. Test

Finally, you can test it.

brownie test
Enter fullscreen mode Exit fullscreen mode

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

Discussion (0)