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
- NodeJS
- Python and Brownie ETH, or any smart contract framework
- VSCode, or another code editor
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)
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
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
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
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
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)
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
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)
@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)
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
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
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
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)
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)
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)
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)
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
$ brownie networks add Avalanche avax-mainnet host=https://api.avax.network/ext/bc/C/rpc chainid=43114 explorer=https://snowtrace.io/ name=Mainnet
After we run these two commands brownie will add Avalanche to Brownie.
Avalanche
ββMainnet: avax-mainnet
ββFuji: avax-testnet
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"
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"
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})
To run this script write the following command on your terminal, this will deploy the contract to your hardhat local node.
$ brownie run deploy
To deploy it to a testnet write the following one.
$ brownie run deploy --network avax-testnet
And to deploy them to the Avalanche mainnet run the following command:
$ brownie run deploy --network avax-mainnet
And that would be it on the smart contract side, we created, tested, and deployed our wrapped token smart contract.
Top comments (0)