DEV Community

Cover image for Create a Wrapped ERC20 token on Avalanche with Vyper! ๐Ÿ๐Ÿช™
Rafael Abuawad
Rafael Abuawad

Posted on

Create a Wrapped ERC20 token on Avalanche with Vyper! ๐Ÿ๐Ÿช™

Introduction

In this article, we will describe the use cases of an ERC20 token and explain how to create one from scratch using Vyper, and we will deploy it to the Avalanche Fuji test network.

Prerequisites

  • Basic programming and console/terminal use experience
  • A simple understanding of Solidity or Vyper

Requirements

What is an ERC20?

An ERC20 smart contract keeps track of fungible tokens, and the contract itself allows us to transfer, burn, and do much interesting stuff with that particular token.

The interface itself is really simple and doesn't include a lot of information about the token itself.

# Functions
totalSupply()
balanceOf(account)
transfer(to, amount)
allowance(owner, spender)
approve(spender, amount)
transferFrom(from, to, amount)
Enter fullscreen mode Exit fullscreen mode

These six functions are the original function defined on the EIP20, we can extend a lot of functionality by adding more functions (more features) to our smart contract.

In this article we are going to create an ERC20 smart contract, that takes in AVAX and returns WAVAX, our wrapped implementation of the cryptocurrency.

We are going to allow the users to mint tokens by depositing AVAX and burn tokens and get AVAX in return.

Why they are important?

Tokens are a fundamental part of the web3 space, they can be used in a lot of creative and unique ways, like shares in a community, transactional value, currency in a protocol, and much more. In this tutorial, we are going to create a token that wraps a cryptocurrency (AVAX in this case) and that token can be used in defi protocols, web3 protocols, and more.

Setup

We are going to use Brownie in this tutorial, after you installed Brownie, create a new folder called avalanche-swap and inside it run the following command:

$ brownie init
Enter fullscreen mode Exit fullscreen mode

Also, we are going to be using the Hardhat node to test our smart contracts, so inside your project folder, run the following command:

$ npm install --save-dev hardhat
Enter fullscreen mode Exit fullscreen mode

Smart contract

Let's start with the boilerplate code. We create a file named contracts/WAVAX.vy, define the vyper version as >= 0.3.7, and import the ERC20 and ERC20 Detailed interfaces from vyper.

# @version >=0.3.7

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

implements: ERC20
implements: ERC20Detailed
Enter fullscreen mode Exit fullscreen mode

Events

We need to define our events, these are messages that the smart contract emits when called and the ones that we're defining at what the EIP20 proposed.

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

event Approval:
    owner: indexed(address)
    spender: indexed(address)
    value: uint256
Enter fullscreen mode Exit fullscreen mode

State

Our smart contract needs state, or variables that will persist across the entire life of the smart contract. These variables contain information that is going to be useful to our users and to the methods that we will define below, like balanceOf, totalSupply, and allowance

# @dev name of the token ("Wrapped AVAX")
name: public(String[32])

# @dev symbol or ticker of the token ("WAVAX")
symbol: public(String[32])

# @dev the amount of decimals the token contains
decimals: public(uint8)

# @dev the balance of a particular address
balanceOf: public(HashMap[address, uint256])

# @dev addresses can allow other's addresses to spend their tokens
allowance: public(HashMap[address, HashMap[address, uint256]])

# @dev the number of tokens in circulation
totalSupply: public(uint256)
Enter fullscreen mode Exit fullscreen mode

As you can see we define a variable to store how many decimals our token will have, this is because Solidity, the main programming langue used on EVM blockchains, has no support for using decimals, and we define decimals like integers, for example, we want to use 2 decimals, to express the number 100.99, we would put it like this 10099, being the last to digits the actual decimal part.

Constructor

After that is complete we now define the constructor. Here we specify the name of the token, the symbol, and the number of decimals this token will have.

@external
def __init__():
    self.name = "Wrapped AVAX"
    self.symbol = "WAVAX"
    self.decimals = 18
Enter fullscreen mode Exit fullscreen mode

Internal methods

The internal methods are really simple we need a method to mint our Wrapped AVAX tokens if the user sends the amount that they want and on to burn them to retrieve their tokens.

@internal
def _mint(_to: address, _value: uint256):
    assert _to != empty(address)
    self.totalSupply += _value
    self.balanceOf[_to] += _value
    log Transfer(empty(address), _to, _value)
Enter fullscreen mode Exit fullscreen mode
@internal
def _burn(_to: address, _value: uint256):
    assert _to != empty(address)
    self.totalSupply -= _value
    self.balanceOf[_to] -= _value
    send(_to, _value)
    log Transfer(_to, empty(address), _value)
Enter fullscreen mode Exit fullscreen mode

These two methods are standard on ERC20 implementations, since we need a way to mint the tokens, and (if we choose so) burn them as well.

External methods

We now need to define the main methods of an ERC20 token, transfer, transferFrom, and approve. These three methods allow our token holders to use the tokens however they want.

The transfer method is self-explanatory and allows the token holder to transfer tokens to another user or a smart contract.

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

The token standard needs a approve method, to enable the transferFrom method that we define next. The approve method gives the token holder a way to allow another account to manage a defined amount of tokens for them.

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

And the transferFrom method gives the user the ability to another account to transfer tokens on behalf of the token holder or to manage a set amount of tokens for them.

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

Since this is a wrapped token, we need a way to receive the cryptocurrency that we are trying to wrap and mint an equivalent amount of tokens, and also a way to do this in reverse, burn several tokens and receive the equivalent cryptocurrency back.

For the mint method, the user sends some cryptocurrency with the method call and gets the same amount back in tokens.

@external
@payable
def mint():
    self._mint(msg.sender, msg.value)
Enter fullscreen mode Exit fullscreen mode

And for the burn method, the user passes the number of tokens they want to burn, to receive the equivalent in cryptocurrency.

@external
def burn(_value: uint256):
    self._burn(msg.sender, _value)
Enter fullscreen mode Exit fullscreen mode

We will also define a default method, this method will be triggered if a user sends some AVAX to the contract directly, without calling the mint function. And in turn, the contract will mint that user the equivalent in tokens.

@external
@payable
def __default__():
    self._mint(msg.sender, msg.value)
Enter fullscreen mode Exit fullscreen mode

And that would be it. Now we have a token smart contract, that mints wrapped tokens. Yours should look something like this:

# @version >=0.3.7

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

# @dev name of the token ("Wrapped AVAX")
name: public(String[32])

# @dev symbol or ticker of the token ("WAVAX")
symbol: public(String[32])

# @dev the amount of decimals the token contains
decimals: public(uint8)

# @dev the balance of a particular address
balanceOf: public(HashMap[address, uint256])

# @dev addresses can allow other's addresses to spend their tokens
allowance: public(HashMap[address, HashMap[address, uint256]])

# @dev the number of tokens in circulation
totalSupply: public(uint256)


@external
def __init__():
    self.name = "Wrapped AVAX"
    self.symbol = "WAVAX"
    self.decimals = 18


@internal
def _mint(_to: address, _value: uint256):
    assert _to != empty(address)
    self.totalSupply += _value
    self.balanceOf[_to] += _value
    log Transfer(empty(address), _to, _value)


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


@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 approve(_spender : address, _value : uint256) -> bool:
    self.allowance[msg.sender][_spender] = _value
    log Approval(msg.sender, _spender, _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
@payable
def mint():
    self._mint(msg.sender, msg.value)


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


@external
@payable
def __default__():
    self._mint(msg.sender, msg.value)
Enter fullscreen mode Exit fullscreen mode

Deploy using Brownie

In this tutorial, we are going to use Brownie to deploy our smart contracts, you can use any other smart contract framework if you are familiar with it Hardhat or ApeWorx both have support for developing and deploying vyper smart contracts.

Since we are deploying to Avalanche, we need to add the Avalanche mainnet and the Avalanche Fuji testnet to our supported networks. In our terminal type the following:

$ brownie networks add Avalanche avax-testnet host=https://api.avax-test.network/ext/bc/C/rpc chainid=43113 explorer=https://testnet.snowtrace.io/ name=Fuji
Enter fullscreen mode Exit fullscreen mode
$ brownie networks add Avalanche avax-mainnet host=https://api.avax.network/ext/bc/C/rpc chainid=43114 explorer=https://snowtrace.io/ name=Mainnet
Enter fullscreen mode Exit fullscreen mode

After we run these two commands brownie will add Avalanche to Brownie.

Avalanche
  โ”œโ”€Mainnet: avax-mainnet
  โ””โ”€Fuji: avax-testnet
Enter fullscreen mode Exit fullscreen mode

After that is complete, you can write our deploy script.

Our script is really simple we are going to take our smart contract, and deploy it.

Our script will first check if we are on any of the Avalanche networks, and if so will deploy the smart contracts using a real wallet (with some AVAX in it), if not we will assume that we are on our local network and deploy them there.

To deploy to a live blockchain (Fuji or Mainnet) we need to have a wallet with some AVAX on it, you can get some testnet AVAX tokens on this faucet. Also, we need to define our brownie-config.yaml file to let Brownie know where to find our private key, and some important configurations as well.

Create a brownie-config.yaml on the root of your project. It should look something like this.

dotenv: .env

networks:
  default: hardhat

wallets:
  from_key: ${PRIVATE_KEY}

compiler:
  vyper:
    version: "0.3.7"
Enter fullscreen mode Exit fullscreen mode

The configuration file is expecting a .env file to work properly, create one on the root of your project and populate it with this.

PRIVATE_KEY="YOUR_PRIVATE_KEY_HERE"
Enter fullscreen mode Exit fullscreen mode

After we have our configuration setup, we need to write our deploy script. On the scripts folder create a new file called deploy.py this python program will be responsible for deploying our smart contracts.

The deploy.py file should look something like this:

from brownie import accounts, network, config, WAVAX


def main():
    supported_networks = ["avax-mainnet", "avax-testnet"]
    active_network = network.show_active()

    deployer = accounts[0]
    if active_network in supported_networks:
        deployer = accounts.add(config["wallets"]["from_key"])

    WAVAX.deploy({"from": deployer}) 
Enter fullscreen mode Exit fullscreen mode

To run this script write the following command on your terminal, this will deploy the contract to your hardhat local node.

$ brownie run deploy
Enter fullscreen mode Exit fullscreen mode

To deploy it to a testnet write the following one.

$ brownie run deploy --network avax-testnet
Enter fullscreen mode Exit fullscreen mode

And to deploy them to the Avalanche mainnet run the following command:

$ brownie run deploy --network avax-mainnet
Enter fullscreen mode Exit fullscreen mode

And that would be it on the smart contract side, we created, tested, and deployed our wrapped token smart contract.

Testnet smart contract (Fuji)

WAVAX

Top comments (0)