ERC20 Token with Vyper and Brownie
Tags: ethereum, vyper
Published: 2021-12-29 09:07:16

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](https://vyper.readthedocs.io/en/stable/testing-contracts.html), it seems that it's easier to develop and test using a framework called **[Brownie](https://eth-brownie.readthedocs.io/en/stable/)** 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.

```sh
npm i -g ganache-cli
```

Then, install it with [*pipx*](https://github.com/pypa/pipx).  

```
python3 -m pip install pipx

pipx install eth-brownie
```

## 2. Create a new project with Brownie

```sh
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.  

```sh
# 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:  

```sh
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](https://github.com/vyperlang/vyper/blob/master/examples/tokens/ERC20.vy).  

*sample/contracts/SampleToken.vy*

```py
# @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*

```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.  

```sh
brownie test
```

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