Creating Your Own ERC20 Token with Vyper and Web3.py (Part 1)

With Mamba, you can create your own ERC20 token. In this first part of tutorial, we focus on the test.

ERC20 token is one of the most famous projects that you can build on top of blockchain. Let's create your own token.

Create a Mamba project directory and setup Mamba project directory.


(.venv) $ mkdir erc20
(.venv) $ cd erc20
(.venv) $ mamba init

There is already an ERC20 token application source code in Vyper programming language. You can download it from Vyper GitHub page: https://github.com/ethereum/vyper/blob/master/examples/tokens/ERC20.vy. Put it in contracts directory inside Mamba project directory. For reference, this is the content of ERC20.vy.


# @dev Implementation of ERC-20 token standard.
# @author Takayuki Jimba (@yudetamago)
# https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md

from vyper.interfaces import ERC20

implements: ERC20

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

name: public(string[64])
symbol: public(string[32])
decimals: public(uint256)

# NOTE: By declaring `balanceOf` as public, vyper automatically generates a 'balanceOf()' getter
#       method to allow access to account balances.
#       The _KeyType will become a required parameter for the getter and it will return _ValueType.
#       See: https://vyper.readthedocs.io/en/v0.1.0-beta.8/types.html?highlight=getter#mappings
balanceOf: public(map(address, uint256))
allowances: map(address, map(address, uint256))
total_supply: uint256
minter: address


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


@public
@constant
def totalSupply() -> uint256:
    """
    @dev Total number of tokens in existence.
    """
    return self.total_supply


@public
@constant
def allowance(_owner : address, _spender : address) -> uint256:
    """
    @dev Function to check the amount of tokens that an owner allowed to a spender.
    @param _owner The address which owns the funds.
    @param _spender The address which will spend the funds.
    @return An uint256 specifying the amount of tokens still available for the spender.
    """
    return self.allowances[_owner][_spender]


@public
def transfer(_to : address, _value : uint256) -> bool:
    """
    @dev Transfer token for a specified address
    @param _to The address to transfer to.
    @param _value The amount to be transferred.
    """
    # NOTE: vyper does not allow underflows
    #       so the following subtraction would revert on insufficient balance
    self.balanceOf[msg.sender] -= _value
    self.balanceOf[_to] += _value
    log.Transfer(msg.sender, _to, _value)
    return True


@public
def transferFrom(_from : address, _to : address, _value : uint256) -> bool:
    """
     @dev Transfer tokens from one address to another.
          Note that while this function emits a Transfer event, this is not required as per the specification,
          and other compliant implementations may not emit the event.
     @param _from address The address which you want to send tokens from
     @param _to address The address which you want to transfer to
     @param _value uint256 the amount of tokens to be transferred
    """
    # NOTE: vyper does not allow underflows
    #       so the following subtraction would revert on insufficient balance
    self.balanceOf[_from] -= _value
    self.balanceOf[_to] += _value
    # NOTE: vyper does not allow underflows
    #      so the following subtraction would revert on insufficient allowance
    self.allowances[_from][msg.sender] -= _value
    log.Transfer(_from, _to, _value)
    return True


@public
def approve(_spender : address, _value : uint256) -> bool:
    """
    @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender.
         Beware that changing an allowance with this method brings the risk that someone may use both the old
         and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this
         race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards:
         https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
    @param _spender The address which will spend the funds.
    @param _value The amount of tokens to be spent.
    """
    self.allowances[msg.sender][_spender] = _value
    log.Approval(msg.sender, _spender, _value)
    return True


@public
def mint(_to: address, _value: uint256):
    """
    @dev Mint an amount of the token and assigns it to an account. 
         This encapsulates the modification of balances such that the
         proper events are emitted.
    @param _to The account that will receive the created tokens.
    @param _value The amount that will be created.
    """
    assert msg.sender == self.minter
    assert _to != ZERO_ADDRESS
    self.total_supply += _value
    self.balanceOf[_to] += _value
    log.Transfer(ZERO_ADDRESS, _to, _value)


@private
def _burn(_to: address, _value: uint256):
    """
    @dev Internal function that burns an amount of the token of a given
         account.
    @param _to The account whose tokens will be burned.
    @param _value The amount that will be burned.
    """
    assert _to != ZERO_ADDRESS
    self.total_supply -= _value
    self.balanceOf[_to] -= _value
    log.Transfer(_to, ZERO_ADDRESS, _value)


@public
def burn(_value: uint256):
    """
    @dev Burn an amount of the token of msg.sender.
    @param _value The amount that will be burned.
    """
    self._burn(msg.sender, _value)


@public
def burnFrom(_to: address, _value: uint256):
    """
    @dev Burn an amount of the token from a given account.
    @param _to The account whose tokens will be burned.
    @param _value The amount that will be burned.
    """
    self.allowances[_to][msg.sender] -= _value
    self._burn(_to, _value)

Compile it.

(.venv) $ mamba compile

Now, let's create a test for this voting application. Create test/test_erc20.py.


from black_mamba.testlib import contract, eth_tester
import pytest
from eth_tester.exceptions import TransactionFailed

ZERO_ADDRESS = '0x' + '0' * 40

@pytest.fixture()
def erc20_contract():
    erc20_contract = contract("ERC20", ["Haha Coin", "HAH", 3, 1000])
    yield erc20_contract

@pytest.fixture()
def accounts(eth_tester):
    yield eth_tester.get_accounts()

def allow_account(erc20_contract, manager, middle, allowance_money):
    assert erc20_contract.functions.allowance(manager, middle).call() == 0
    erc20_contract.functions.approve(middle, allowance_money).transact({ "from": manager })
    assert erc20_contract.functions.allowance(manager, middle).call() == allowance_money
    approval_log = erc20_contract.events.Approval.getLogs()[0].args
    assert approval_log._owner == manager
    assert approval_log._spender == middle
    assert approval_log._value == allowance_money

def test_initial_erc20(accounts, erc20_contract):
    assert erc20_contract.functions.name().call() == "Haha Coin"
    assert erc20_contract.functions.symbol().call() == "HAH"
    assert erc20_contract.functions.decimals().call() == 3
    assert erc20_contract.functions.totalSupply().call() == 1000000
    assert erc20_contract.functions.balanceOf(accounts[0]).call() == 1000000

def test_transfer(accounts, erc20_contract):
    manager = accounts[0]
    destination = accounts[1]
    total_supply = 1000000
    transfer_money = 900
    assert erc20_contract.functions.balanceOf(manager).call() == total_supply
    assert erc20_contract.functions.balanceOf(destination).call() == 0
    erc20_contract.functions.transfer(destination, transfer_money).transact({ "from": manager })
    assert erc20_contract.functions.balanceOf(manager).call() == total_supply - transfer_money
    assert erc20_contract.functions.balanceOf(destination).call() == transfer_money
    transfer_log = erc20_contract.events.Transfer.getLogs()[0].args
    assert transfer_log._to == destination
    assert transfer_log._from == manager
    assert transfer_log._value == transfer_money

def test_mint(accounts, erc20_contract):
    manager = accounts[0]
    destination = accounts[1]
    total_supply = 1000000
    mint_amount = 1000
    assert erc20_contract.functions.totalSupply().call() == total_supply
    assert erc20_contract.functions.balanceOf(destination).call() == 0
    erc20_contract.functions.mint(destination, mint_amount).transact({ "from": manager })
    assert erc20_contract.functions.totalSupply().call() == total_supply + mint_amount
    assert erc20_contract.functions.balanceOf(destination).call() == mint_amount
    transfer_log = erc20_contract.events.Transfer.getLogs()[0].args
    assert transfer_log._to == destination
    assert transfer_log._from == ZERO_ADDRESS
    assert transfer_log._value == mint_amount

def test_transferFrom(accounts, erc20_contract):
    manager = accounts[0]
    middle = accounts[1]
    destination = accounts[1]
    total_supply = 1000000
    allowance_money = 900
    transfer_money = 700
    too_big_money = 1000

    allow_account(erc20_contract, manager, middle, allowance_money)

    assert erc20_contract.functions.balanceOf(destination).call() == 0
    with pytest.raises(TransactionFailed):
        erc20_contract.functions.transferFrom(manager, destination, too_big_money).transact({ "from": middle })
    erc20_contract.functions.transferFrom(manager, destination, transfer_money).transact({ "from": middle })
    assert erc20_contract.functions.balanceOf(destination).call() == transfer_money
    assert erc20_contract.functions.balanceOf(manager).call() == total_supply - transfer_money
    assert erc20_contract.functions.allowance(manager, middle).call() == allowance_money - transfer_money
    transfer_log = erc20_contract.events.Transfer.getLogs()[0].args
    assert transfer_log._to == destination
    assert transfer_log._from == manager
    assert transfer_log._value == transfer_money

def test_burn(accounts, erc20_contract):
    manager = accounts[0]
    total_supply = 1000000
    burn_amount = 1000
    assert erc20_contract.functions.totalSupply().call() == total_supply
    erc20_contract.functions.burn(burn_amount).transact({ "from": manager })
    assert erc20_contract.functions.totalSupply().call() == total_supply - burn_amount
    assert erc20_contract.functions.balanceOf(manager).call() == total_supply - burn_amount
    transfer_log = erc20_contract.events.Transfer.getLogs()[0].args
    assert transfer_log._to == ZERO_ADDRESS
    assert transfer_log._from == manager
    assert transfer_log._value == burn_amount

def test_burnFrom(accounts, erc20_contract):
    manager = accounts[0]
    middle = accounts[1]
    destination = accounts[1]
    total_supply = 1000000
    allowance_money = 900
    burn_money = 700
    too_big_money = 1000

    allow_account(erc20_contract, manager, middle, allowance_money)

    with pytest.raises(TransactionFailed):
        erc20_contract.functions.burnFrom(manager, too_big_money).transact({ "from": middle })
    erc20_contract.functions.burnFrom(manager, burn_money).transact({ "from": middle })
    assert erc20_contract.functions.balanceOf(manager).call() == total_supply - burn_money
    assert erc20_contract.functions.allowance(manager, middle).call() == allowance_money - burn_money
    transfer_log = erc20_contract.events.Transfer.getLogs()[0].args
    assert transfer_log._to == ZERO_ADDRESS
    assert transfer_log._from == manager
    assert transfer_log._value == burn_money

Let's discuss the test file. First, you need fixtures to make your tests simpler.


@pytest.fixture()
def erc20_contract():
    erc20_contract = contract("ERC20", ["Haha Coin", "HAH", 3, 1000])
    yield erc20_contract

@pytest.fixture()
def accounts(eth_tester):
    yield eth_tester.get_accounts()

Here, you create two Pytest fixtures for creating a smart contract object and getting testing accounts. Then later, you must pass these fixtures to each test method.

Let's create a helper method to approve the allowance.


def allow_account(erc20_contract, manager, middle, allowance_money):
    assert erc20_contract.functions.allowance(manager, middle).call() == 0
    erc20_contract.functions.approve(middle, allowance_money).transact({ "from": manager })
    assert erc20_contract.functions.allowance(manager, middle).call() == allowance_money
    approval_log = erc20_contract.events.Approval.getLogs()[0].args
    assert approval_log._owner == manager
    assert approval_log._spender == middle
    assert approval_log._value == allowance_money

You check the logs of Approval event and make sure the parameters are correct.

Let's create the first test method of this ERC20 token.


def test_initial_erc20(accounts, erc20_contract):
    assert erc20_contract.functions.name().call() == "Haha Coin"
    assert erc20_contract.functions.symbol().call() == "HAH"
    assert erc20_contract.functions.decimals().call() == 3
    assert erc20_contract.functions.totalSupply().call() == 1000000
    assert erc20_contract.functions.balanceOf(accounts[0]).call() == 1000000

Here, you check the name, symbol, decimals, total supply and the balance of the manager's account. Notice that your test method has the fixtures in its arguments (accounts, erc20_contract).

Let's create the second test method to test the transfer method.


def test_transfer(accounts, erc20_contract):
    manager = accounts[0]
    destination = accounts[1]
    total_supply = 1000000
    transfer_money = 900
    assert erc20_contract.functions.balanceOf(manager).call() == total_supply
    assert erc20_contract.functions.balanceOf(destination).call() == 0
    erc20_contract.functions.transfer(destination, transfer_money).transact({ "from": manager })
    assert erc20_contract.functions.balanceOf(manager).call() == total_supply - transfer_money
    assert erc20_contract.functions.balanceOf(destination).call() == transfer_money
    transfer_log = erc20_contract.events.Transfer.getLogs()[0].args
    assert transfer_log._to == destination
    assert transfer_log._from == manager
    assert transfer_log._value == transfer_money

Basically you check the balance of the accounts before and after the transfer happening. On top of that, don't forget to check the Transfer event logs.

Let's create a test method for minting.


def test_mint(accounts, erc20_contract):
    manager = accounts[0]
    destination = accounts[1]
    total_supply = 1000000
    mint_amount = 1000
    assert erc20_contract.functions.totalSupply().call() == total_supply
    assert erc20_contract.functions.balanceOf(destination).call() == 0
    erc20_contract.functions.mint(destination, mint_amount).transact({ "from": manager })
    assert erc20_contract.functions.totalSupply().call() == total_supply + mint_amount
    assert erc20_contract.functions.balanceOf(destination).call() == mint_amount
    transfer_log = erc20_contract.events.Transfer.getLogs()[0].args
    assert transfer_log._to == destination
    assert transfer_log._from == ZERO_ADDRESS
    assert transfer_log._value == mint_amount

Basically you test the total supply before and after the minting method is being executed. You also check the balance of the account which receive the additional coins. Don't forget to check the Transfer event logs.

Let's create a test for transferFrom method.


def test_transferFrom(accounts, erc20_contract):
    manager = accounts[0]
    middle = accounts[1]
    destination = accounts[1]
    total_supply = 1000000
    allowance_money = 900
    transfer_money = 700
    too_big_money = 1000

    allow_account(erc20_contract, manager, middle, allowance_money)

    assert erc20_contract.functions.balanceOf(destination).call() == 0
    with pytest.raises(TransactionFailed):
        erc20_contract.functions.transferFrom(manager, destination, too_big_money).transact({ "from": middle })
    erc20_contract.functions.transferFrom(manager, destination, transfer_money).transact({ "from": middle })
    assert erc20_contract.functions.balanceOf(destination).call() == transfer_money
    assert erc20_contract.functions.balanceOf(manager).call() == total_supply - transfer_money
    assert erc20_contract.functions.allowance(manager, middle).call() == allowance_money - transfer_money
    transfer_log = erc20_contract.events.Transfer.getLogs()[0].args
    assert transfer_log._to == destination
    assert transfer_log._from == manager
    assert transfer_log._value == transfer_money

Basically you test the allowance account of related accounts before and after transferFrom method is being executed. On top of that, you also check the balances of sender and destination account. Remember, you don't check the balance of the middleman's account. Finally, you check the Transfer event logs.

Let's create a test for burning coins.


def test_burn(accounts, erc20_contract):
    manager = accounts[0]
    total_supply = 1000000
    burn_amount = 1000
    assert erc20_contract.functions.totalSupply().call() == total_supply
    erc20_contract.functions.burn(burn_amount).transact({ "from": manager })
    assert erc20_contract.functions.totalSupply().call() == total_supply - burn_amount
    assert erc20_contract.functions.balanceOf(manager).call() == total_supply - burn_amount
    transfer_log = erc20_contract.events.Transfer.getLogs()[0].args
    assert transfer_log._to == ZERO_ADDRESS
    assert transfer_log._from == manager
    assert transfer_log._value == burn_amount

Basically you test the total supply of token before and after burning token happening. You also check the balance of the account which bear the burden of burning. Don't forget to check the Transfer event logs.

Let's create a test for burningFrom method.


def test_burnFrom(accounts, erc20_contract):
    manager = accounts[0]
    middle = accounts[1]
    destination = accounts[1]
    total_supply = 1000000
    allowance_money = 900
    burn_money = 700
    too_big_money = 1000

    allow_account(erc20_contract, manager, middle, allowance_money)

    with pytest.raises(TransactionFailed):
        erc20_contract.functions.burnFrom(manager, too_big_money).transact({ "from": middle })
    erc20_contract.functions.burnFrom(manager, burn_money).transact({ "from": middle })
    assert erc20_contract.functions.balanceOf(manager).call() == total_supply - burn_money
    assert erc20_contract.functions.allowance(manager, middle).call() == allowance_money - burn_money
    transfer_log = erc20_contract.events.Transfer.getLogs()[0].args
    assert transfer_log._to == ZERO_ADDRESS
    assert transfer_log._from == manager
    assert transfer_log._value == burn_money

It's similar to transferFrom method. You must execute approve method first. Then check the total supply before and after burningFrom method is being executed. You also check the balance of the account which bear the burden of burning. Then you check the allowance of related accounts. Don't forget to check the Transfer event logs.

Now you can run the test.

(.venv) $ py.test test/test_ballot.py

(To be continued...)