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 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 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.
- Solidity by Example
- Solidity Documentation
- Hardhat Documentation
- Ethers-js Documentation
- OpenZeppelin Documentation
- OpenZeppelin Ethernaut tutorial
- CryptoZombies Tutorial
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:
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](https://github.com/austintgriffith/scaffold-eth.git) challenge-2-token-vendor
git checkout challenge-2-token-vendor
To locally test your application
yarn chainto start your local hardhat chain
yarn startto start your local React app
yarn deployto deploy/redeploy your contract and update the React app
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:
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
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
balanceOfyour 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!
- OpenZeppelin ERC20 Contract
- Ethereum ERC-20 Standard
- Inheritance — Contracts can inherit from other contracts by using the
- Shadowing Inherited State Variables — As explained by SolidityByCode unlike functions, state variables cannot be overridden by re-declaring them in the child contract
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
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
The second important part is the
_mint function, let’s take a look at it.
require you see is just checking that the minter (the one that will receive all the token minted) is not the null address.
_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
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
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
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 the
transfer()function exposed by the OpenZeppelin ERC20 implementation.
- Emit a
BuyTokensevent 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
ownershipof 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
- 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)moves
amounttokens from the caller’s account to
recipientand returns a boolean value indicating whether the operation succeeded.
- Sending ether — As we saw in the previous challenge always use the
callfunction to do that!
Let’s review the important part of the code.
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
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!
withdraw() function is pretty simple. As you can see it rely on the
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.
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
approvefunction 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
sellTokensfunction 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
- approve ERC20 function — Sets
amountas the allowance of
spenderover the caller’s tokens. Returns a boolean value indicating whether the operation succeeded. Emits an
- transferFrom ERC20 function — Moves
recipientusing the allowance mechanism.
amountis then deducted from the caller’s allowance. Returns a boolean value indicating whether the operation succeeded. Emits a
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!
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!
In order to test this in your React app, you can update your App.jsx adding two
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.
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
This is the test that will verify that our
sellTokens functions work as expected.
Let’s review the logic:
- First of all
addr1buys 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
sellTokensfunction 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).
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
packages/hardhat/hardhat.config.jsto the testnet you would like to use (in my case rinkeby)
- Updated the
infuriaProjectIdwith one created on Infura
- Generate a deployer account
with yarn generate. This command should generate two
.txtfile. One that will represent the account address and one with the seed phrase of the generated account.
yarn accountto 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
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
_--export-all_flag in the
_yarn deploy_command (see
If you want to check the deployed contract you can search for them on the Etherscan Rinkeby site:
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
const DEBUG = true;and turn it to
- Take a look at
App.jsxand 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 replace
localhostwith the network of your contract. In our case, it will be
- 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
constants.jsand 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 with
yarn build and when the build script has finished deploy it to Surge with
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/
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