In the previous scaffold-eth challenge, we have created a Staker dApp. In this challenge, we are going to create a Token Vendor contract.
The goal of the dApp
The goal of this challenge is to create your own ERC20 Token and a Token Vendor Contract that will handle the sell/buy process of your token exchanging it with ETH sent by the user.
What are you going to learn?
- What is an ERC20 Token
- How to mint an ERC20 Token
- OpenZeppelin ERC20 implementation
- Ownership of a Contract
- How to create a Token Vendor contract to sell/buy your token
In addition to the content above we are going to learn a lot of new Solidity and web3 concepts and how to write well-made tests for your Solidity code. I’m going to skip some basic parts so if you feel lost just go back to the first challenge blog post and read all the explanations.
Some always useful links that you should always have in mind:
- Solidity by Example
- Solidity Documentation
- Hardhat Documentation
- Ethers-js Documentation
- OpenZeppelin Documentation
- OpenZeppelin Ethernaut tutorial
- CryptoZombies Tutorial
What is an ERC20 Token?
Before we start I will just give you an overview of what an ERC20 Token is quoting directly the Ethereum Documentation.
Tokens can represent virtually anything in Ethereum:
- reputation points in an online platform
- skills of a character in a game
- lottery tickets
- financial assets like a share in a company
- a fiat currency like USD
- an ounce of gold
- and more…
Such a powerful feature of Ethereum must be handled by a robust standard, right? That’s exactly where the ERC-20 plays its role! This standard allows developers to build token applications that are interoperable with other products and services.
The ERC-20 introduces a standard for Fungible Tokens, in other words, they have a property that makes each Token be exactly the same (in type and value) of another Token. For example, an ERC-20 Token acts just like the ETH, meaning that 1 Token is and will always be equal to all the other Tokens.
If you want to know more about the ERC-20 token you can look at these links:
Setup the project
First of all, we need to set up it. Clone the scaffold-eth repository, switch to the challenge 1 branch and install all the needed dependencies.
git clone https://github.com/austintgriffith/scaffold-eth.git challenge-2-token-vendor
cd challenge-2-token-vendor
git checkout challenge-2-token-vendor
yarn install
To locally test your application
yarn chain
to start your local hardhat chainyarn start
to start your local React appyarn deploy
to deploy/redeploy your contract and update the React app
OpenZeppelin and ERC20 Implementation
OpenZeppelin provides security products to build, automate, and operate decentralized applications.
We are going to use the OpenZeppelin Contract framework to build our own ERC20 Token.
The framework is a library for secure smart contract development. Build on a solid foundation of community-vetted code.
- Implementations of standards like ERC20 and ERC721.
- Flexible role-based permissions scheme.
- Reusable Solidity components to build custom contracts and complex decentralized systems.
If you want to learn more about the OpenZeppelin implementation you can follow these links:
Exercise Part 1: Create your own ERC20 Token and deploy it!
In the first part of the exercise, you need to create a Token Contract inhering from OpenZepllein’s ERC20 Contract.
In the constructor, you have to mint 1000 token
(remember that in Solidity an ERC20 token has 18 decimals) and send them to the msg.sender
(the one that deployed the contract).
Remember to update the deploy.js
file to send those tokens to the correct address. You can find your current address on the top right of your web application, just hit the copy icon!
To transfer tokens to your account, add this line to your deploy.js
:
const result = await yourToken.transfer("**YOUR FRONTEND ADDRESS**", utils.parseEther("1000"));
Don’t be scared, I’ll explain later after reviewing the code.
- Can you see on the frontend that the
balanceOf
your Wallet has those 1000 tokens? - Can you
transfer()
some of those tokens to another wallet address? Simply open a new incognito window on Chrome, type your localhost address and you should have a brand new burner account to send those tokens to!
Important Concepts to master
- OpenZeppelin ERC20 Contract
- Ethereum ERC-20 Standard
- Inheritance — Contracts can inherit from other contracts by using the
is
keyword. - Shadowing Inherited State Variables — As explained by SolidityByCode unlike functions, state variables cannot be overridden by re-declaring them in the child contract
YourToken.sol
As you can see we are importing the ERC20.sol Contract from the OpenZeppelin library. That Contract is the OpenZeppelin implementation of the ERC20 Standard and they made an amazing job on both security and optimization!
When in your code you is ERC20
that code make your YourContract
contract inherits all the function/state variables implemented in the ERC20 Contract from OpenZeppelin.
The amazing thing is that everything is open source. Try to CMD+click
on the ERC20 keyword or on the _mint
function.
As you can see when the constructor
of our contract is called, we are also calling the ERC20 constructor passing two arguments. The first one is the name
of our Token and the second one is the symbol
.
The second important part is the _mint
function, let’s take a look at it.
The first require
you see is just checking that the minter (the one that will receive all the token minted) is not the null address.
_beforeTokenTransfer
and _afterTokenTransfer
are function hooks that are called after any transfer of tokens. This includes minting and burning.
In the rest of the code, we are updating the _totalSupply
of the token (in our case it would be 1000 tokens with 18 decimals), updating the minter balance
with the amount and we are emitting a Transfer
event.
How cool is that? And in our TokenContract
we have only called one function.
Remember that I said to updated the deploy.js file to transfer all those tokens to our wallet in the web app? The code was this:
await yourToken.transfer(‘0xafDD110869ee36b7F2Af508ff4cEB2663f068c6A’, utils.parseEther(‘1000’));
transfer
is another function offered by the ERC20 Contract implementation.
I will not go much into detail but after checking that both the sender
and recipient
are not the null address
the function will check if the sender has enough balance to transfer the requested amount, will transfer it and will also emit a Transfer
event.
Exercise Part 2: Create a Vendor Contract
In this part of the exercise, we are going to create our Vendor Contract.
The Vendor will be responsible to allow users to exchange ETH for our Token. In order to do that we need to
- Set a price for our token (1 ETH = 100 Token)
- Implement a payable
buyToken()
function. To transfer tokens look at thetransfer()
function exposed by the OpenZeppelin ERC20 implementation. - Emit a
BuyTokens
event that will log who’s the buyer, the amount of ETH sent and the amount of Token bought - Transfer all the Tokens to the Vendor contract at deployment time
- Transfer the
ownership
of the Vendor contract (at deploy time) to our frontend address (you can see it on the top right of your web app) to withdraw the ETH in the balance
Important Concepts to master
- Events
- Payable functions
- Open Zeppelin Ownable & ownership — OpenZeppelin module used through inheritance. It will make available the modifier
onlyOwner
, which can be applied to your functions to restrict their use to the owner. - OpenZeppelin Address utility (not required but useful to known) — Collection of functions related to the address type. You could use it to safely transfer ETH funds from the Vendor to the owner
- Transfer function from OpenZeppelin ERC20 contract —
transfer(address recipient, uint256 amount)
movesamount
tokens from the caller’s account torecipient
and returns a boolean value indicating whether the operation succeeded. - Sending ether — As we saw in the previous challenge always use the
call
function to do that!
Vendor.sol
Let’s review the important part of the code.
In the buyTokens()
we are checking that the user has sent us at least some ETH otherwise we will revert the transaction (don’t be cheap!). Remember that in order to receive ETH our function must have the keyword payable
.
After that, we calculate, based on the token price how many tokens he will receive with the amount of ETH sent.
We are also checking that the Vendor contract has enough balance of Tokens to fill the user buy request, otherwise we revert the transaction.
If every check goes well we trigger the transfer
function of our Token Contract implemented inside the ERC20 contract that is inherited by the Token Contract (see the image above to view the code). That function is returning a boolean
that will notify us if the operation was successful.
The last thing to do is to emit the BuyTokens
event to notify to the blockchain that we made the deal!
The withdraw()
function is pretty simple. As you can see it rely on the onlyOwner
function modifier
that we inherited by the Owner
contract. That modifier is checking that the msg.sender
is the owner of the contract. We don’t want another user to withdraw the ETH we collected. Inside the function, we are transferring the ETH to the owner and checking if the operation was successful. Another way to do that, as I said previously is to use the sendValue
of the Address utility of OpenZeppelin.
Exercise Part 3: Allow the Vendor to buy back!
This is the last part of the exercise and it’s the most difficult one, not from a technology point of view but more from a concept and UX.
We want to allow the user to sell their token to our Vendor contract. As you know, Contract can accept ETH when their function is declared as payable
, but they are only allowed to receive ETH.
So what we need to implement is to allow our Vendor to take Tokens directly from our Token’s balance and trust him to give us back the equal value amount of ETH back. This is called the “Approve approach”.
This is the flow that will happen:
- The user requests to “approve” the Vendor contract to transfer tokens from the user’s balance to Vendor’s wallet (this will happen on the Token’s contract). When you invoke the
approve
function you will specify the number of tokens that you want to decide to let the other contract be able to transfer at max. - The user will invoke a
sellTokens
function on Vendor’s contract that will transfer user’s balance to Vendor’s balance - The vendor’s contract will transfer to the user’s wallet an equal amount of ETH
Important Concepts to master
- approve ERC20 function — Sets
amount
as the allowance ofspender
over the caller’s tokens. Returns a boolean value indicating whether the operation succeeded. Emits anApproval
event. - transferFrom ERC20 function — Moves
amount
tokens fromsender
torecipient
using the allowance mechanism.amount
is then deducted from the caller’s allowance. Returns a boolean value indicating whether the operation succeeded. Emits aTransfer
event.
An important note that I would like to explain: UX over security
This approve mechanism is not something new. If you ever used a DEX like Uniswap you already have done this.
The approve function allows other wallet/contract to transfer at max the number of tokens you specify within the function arguments. What does it mean? What if I want to trade 200 tokens I should approve the Vendor contract to only transfer to itself 200 tokens. If I want to sell another 100, I should approve it again. Is it a good UX? Maybe not but it’s the most secure one.
DEX uses another approach. To avoid to ask every time to the user to approve each time you want to swap TokenA for TokenB they simply ask to approve the MAX possible number of tokens directly. What does it mean? That every DEX contract could potentially steal all your tokens without you knowing it. You always should be aware of what’s happening behind the scene!
Vendor.sol
Let’s review sellTokens
.
First of all, we check that the tokenAmountToSell
is greater than 0
otherwise, we revert the transaction. You need to sell at least one of your tokens!
Then we check that the user’s token balance is at least greater than the amount of token he’s trying to sell. You cannot oversell what you don’t own!
After that, we calculate the amountOfETHToTransfer
to the user after the sell operation. We need to be sure that the Vendor can pay that amount so we’re checking that Vendor’s balance (in ETH) is greater than the amount to transfer to the user.
If everything is OK we proceed with the (bool sent) = yourToken.transferFrom(msg.sender, address(this), tokenAmountToSell);
operation. We are telling the YourToken contract to transfer tokenAmountToSell
from the user’s balance msg.sender
to the Vendor’s balance address(this)
. This operation can succeed only if the user has already approved at least that specific amount with the approve
function we already reviewed.
The last thing we do is to transfer the ETH amount for the sell operation back to the user’s address. And we’re done!
Update your App.jsx
In order to test this in your React app, you can update your App.jsx adding two Card
to Approve
and Sell
tokens (see the GitHub code repo at the end of the post) or you can just do everything from the Debug Contract tab that offers all the needed features.
Exercise Part 4: Create a test suite
You know already from the previous post that Tests are a great foundation for the security and optimization of your app. You should never skip them and they are a way to understand the flow of the operations that are involved in the logic of the overall application.
Tests on Solidity environment leverage on four libraries:
Let’s review one test and then I’ll dump the whole code
Testing the sellTokens() function
This is the test that will verify that our sellTokens
functions work as expected.
Let’s review the logic:
- First of all
addr1
buys some tokens from the Vendor contract - Before selling as we said before we need to approve the Vendor contract to be able to transfer to itself the amount of token that we want to sell.
- After the approval, we double-check that Vendor’s token allowance from addr1 is at least the amount of the token addr1 needs to sell (and transfer to the Vendor). This check could be skipped because we know that OpenZeppeling has already battle-tested their code but I just wanted to add it for learning purposes.
- We are ready to sell the amount of token we just bought using the
sellTokens
function of Vendor contract
At this point we need to check three things:
- The user’s token balance is 0 (we sold all our tokens)
- User’s wallet has increased by 1 ETH with that transaction
- The vendor’s token balance is 1000 (we bought 100 tokens)
Waffle offers some cool utilities to check changes in ether balance and changes in token balances but unfortunately, it seems that there’s an issue on the latter one (check out the GitHub issue I just created).
Test coverage complete code
Final step: deploy your Contract to the moon (testnet)
Ok, now it’s time. We have implemented our Smart Contract, we have tested the frontend UI, we have covered every edge case with our tests. We are ready to deploy it on the testnet.
Following the scaffold-eth documentation, these are the steps we need to follow:
- Change the
defaultNetwork
inpackages/hardhat/hardhat.config.js
to the testnet you would like to use (in my case rinkeby) - Updated the
infuriaProjectId
with one created on Infura - Generate a deployer account
with yarn generate
. This command should generate two.txt
file. One that will represent the account address and one with the seed phrase of the generated account. - Run
yarn account
to see details of the account like eth balances across different networks. - Make sure that the mnemonic.txt and relative account files are not pushed with your git repository, otherwise, anyone could get ownership of your Contract!
- Fund your deployer account with some funds. You can use an instant wallet to send funds to the QR code you just saw on your console.
- Deploy your contract with
yarn deploy
!
If everything goes well you should see something like this on your console
Deployment metadata is stored in the
/deployments
folder, and automatically copied to/packages/react-app/src/contracts/hardhat_contracts.json
via the--export-all
flag in theyarn deploy
command (see/packages/hardhat/packagen.json
).
If you want to check the deployed contract you can search for them on the Etherscan Rinkeby site:
Update your frontend app and deploy it on Surge!
We are going to use the Surge method but you could also deploy your app on AWS S3 or on IPFS, that’s up to you!
The scaffold-eth documentations always come in hand but I will summarize what you should do:
- If you are deploying on mainnet you should verify your contract on Etherscan. This procedure will add credibility and trust to your application. If you are interested in doing so just follow this guide for scaffold-eth.
- Turn off Debug Mode (it prints an awful lot of console.log, something that you don’t want to see in Chrome Developer Console, trust me!). Open
App.jsx
, findconst DEBUG = true;
and turn it tofalse
. - Take a look at
App.jsx
and remove all unused code, just be sure to ship only what you really need! - Make sure that your React app is pointing to the correct network (the one you just used to deploy your Contract). Look for
const targetNetwork = NETWORKS[“localhost”];
and replacelocalhost
with the network of your contract. In our case, it will berinkeby
- Make sure you are using your own nodes and not the ones in Scaffold-eth as they are public and there’s no guarantee they will be taken down or rate limited. Review lines 58 and 59 of
App.jsx
- Update
constants.js
and swap Infura, Etherscan, and Blocknative API Keys if you want to use their services.
Are we ready? Let’s go!
Now build your React App withyarn build
and when the build script has finished deploy it to Surge with yarn surge
.
If everything goes well you should see something like this. Your dApp is now live on Surge!
You can check out our deployed dApp here: https://woozy-cable.surge.sh/
Recap and Conclusions
That’s what we have learned and done so far
- Clone scaffold-eth challenge repo
- Learned a lot of web3/solidity concepts (deep dive into the ERC20 contract, approve pattern, and so on)
- Create an ERC20 Token contract
- Create a Vendor contract to allow users to buy and sell them
- Tested our Contract locally on hardhat network
- Deployed our contract on Rinkeby
- Deployed our dApp on Surge
If everything works as expected, you are ready to make the big jump and deploy everything on Ethereum main net!
GitHub Repo for this project: scaffold-eth-challenge-2-token-vendor
Did you like this content? Follow me for more!
- GitHub: https://github.com/StErMi
- Twitter: https://twitter.com/StErMi
- Medium: https://medium.com/@stermi
- Dev.to: https://dev.to/stermi