This challenge will provide a tutorial to help you build/understand a simple decentralized exchange, with one token-pair (ERC20 BALLOONS ($BAL) and ETH). This readme is an upated version of the original tutorial. Please read the intro for a background on what we are building! There is also a Youtube video that may help you understand the concepts covered within this challenge too: https://www.youtube.com/watch?v=eP5w6Ger1EQ&t=364s&ab_channel=SimplyExplained.
This branch was heavily based off of this branch: https://github.com/scaffold-eth/scaffold-eth-challenges/tree/challenge-5-dex.
TODO: Update this with appropriate links
git clone https://github.com/scaffold-eth/scaffold-eth-challenges.git challenge-5-dex
cd challenge-5-dex
git checkout challenge-5-dex
yarn install
You'll have three terminals up for:
yarn start
(react app frontend)
yarn chain
(hardhat backend)
yarn deploy
(to compile, deploy, and publish your contracts to the frontend)
Navigate to the Debug Contracts tab and you should see two smart contracts displayed called DEX
and Balloons
.
๐ฉโ๐ป Rerun
yarn deploy
whenever you want to deploy new contracts to the frontend (runyarn deploy --reset
for a completely fresh deploy if you have made no contract changes).
Balloons.sol
is just an example ERC20 contract that mints 1000 to whatever address deploys it.
DEX.sol
is what we will build in this tutorial and you can see it starts with a SafeMath library to help us prevent overflows and underflows and also tracks a token (ERC20 interface) that we set in the constructor (on deploy).
Feature Details: We want to create an automatic market where our contract will hold reserves of both ETH and ๐ Balloons. These reserves will provide liquidity that allows anyone to swap between the assets.
Letโs add a couple new variables to DEX.sol
:
uint256 public totalLiquidity;
mapping (address => uint256) public liquidity;
These variables track the total liquidity, but also by individual addresses too.
Then, letโs create an init() function in DEX.sol
that is payable and then we can define an amount of tokens that it will transfer to itself:
function init(uint256 tokens) public payable returns (uint256) {
require(totalLiquidity==0,"DEX:init - already has liquidity");
totalLiquidity = address(this).balance;
liquidity[msg.sender] = totalLiquidity;
require(token.transferFrom(msg.sender, address(this), tokens));
return totalLiquidity;
}
Calling init() will load our contract up with both ETH and ๐ Balloons.
We can see that the DEX starts empty. We want to be able to call init() to start it off with liquidity, but we donโt have any funds or tokens yet. Add some ETH to your local account using the faucet and then find the 00_deploy_your_contract.js
file. Uncomment the line below and add your address:
// // paste in your address here to get 10 balloons on deploy:
// await balloons.transfer("YOUR_ADDRESS","" + (10 * 10 ** 18));
Run yarn deploy
. The front end should show you that you have balloon tokens. We canโt just call init() yet because the DEX contract isnโt allowed to transfer tokens from our account. We need to approve() the DEX contract with the Balloons UI. Copy and paste the DEX address and then set the amount to 5000000000000000000 (5 10ยนโธ). You can confirm this worked using the allowance() function. Now we are ready to call init() on the DEX. We will tell it to take (5 10ยนโธ) of our tokens and we will also send 0.01 ETH with the transaction. You can see the DEX contract's value update and you can check the DEX token balance using the balanceOf function on the Balloons UI.
This works pretty well, but it will be a lot easier if we just call the init() function as we deploy the contract. In the 00_deploy_your_contract.js
script try uncommenting the init section so our DEX will start with 3 ETH and 3 Balloons of liquidity:
// // uncomment to init DEX on deploy:
// console.log("Approving DEX ("+dex.address+") to take Balloons from main account...")
// // If you are going to the testnet make sure your deployer account has enough ETH
// await balloons.approve(dex.address, ethers.utils.parseEther('100'));
// console.log("INIT exchange...")
// await dex.init("" + (3 * 10 ** 18), {value:ethers.utils.parseEther('3'), gasLimit:200000})
Now when we yarn deploy --reset
then our contract should be initialized as soon as it deploys and we should have equal reserves of ETH and tokens.
Feature Details: Follow along with the original tutorial Price section for an understanding of the DEX's pricing model and for a price function to add to your contract. You may need to update the Solidity syntax (e.g. use + instead of .add, * instead of .mul, etc). Deploy when you are done.
Hints: See this link (https://hackernoon.com/formulas-of-uniswap-a-deep-dive), solve for the change in the Output Reserve. Also, don't forget to think about how to implement the trading fee. Solidity doesn't allow for decimals, so one way that contracts are written to implement percentage is using whole uints (997 and 1000) as numerator and denominator factors, respectively.
Feature Details: Letโs edit the DEX.sol smart contract and add two new functions for swapping from each asset to the other.
Each of these functions calculate the resulting amount of output asset using our price function that looks at the ratio of the reserves vs the input asset. We can call tokenToEth and it will take our tokens and send us ETH or we can call ethToToken with some ETH in the transaction and it will send us tokens. Letโs deploy our contract then move over to the frontend. Exchange some ETH for tokens and some tokens for ETH!
Feature Details: So far, only the init() function controls liquidity. To make this more decentralized, it would be better if anyone could add to the liquidity pool by sending the DEX both ETH and tokens at the correct ratio. Letโs create two new functions that let us deposit and withdraw liquidity. How would you write this function out? Try before taking a peak!
TODO: Context for people newer to ReactJS
For those that are really new to anything front-end development, there are many resources out there to possibly use. This one was particularly helpful from minutes 15 to 20 describing the typical folder structure within a ReactJS project.
https://www.youtube.com/watch?v=w7ejDZ8SWv8&ab_channel=TraversyMedia
TODO: *From a fresh master branch off of scaffold-eth repo, we found the following was needed to get things hooked up with the front-end:
export { default as Dex } from "./DEX";
export { default as Curve } from "./Curve";
Other files you'll need from OG repo: DEX.jsx, Curve.jsx TODO: INSERT LINKS TO OG REPO
You will likely run into errors from your front-end assuming you've ran yarn start
already. Let's fix those!
Find useEventListener, useContractLoader, useContractReader, useBlockNumber, useBlanace, useTokenBalance within DEX.jsx file. You will see them calling for .hooks --> delete those and replace with the following:
import { useEventListener } from "eth-hooks/events/useEventListener";
import { useContractLoader } from "eth-hooks";
import { useContractReader } from "eth-hooks";
import { useBlockNumber } from "eth-hooks";
import { useBalance } from "eth-hooks";
import { useTokenBalance } from "eth-hooks/erc/erc-20/useTokenBalance";
These replacements are needed because the pointers within the DEX.jsx
and Curve.jsx
files from the OG repo are not accurate with the master branch
off of scaffold-eth
repo.
In 00_deploy_your_contracts.js, you'll have to write in the necessary code to get your contracts, using Ethers.js.
As well, make sure that the tags are updated to your contract names, it should be something like module.exports.tags = ["YourContract"];
and you'll want to change it to:
module.exports.tags = ["Balloons", "DEX"];
Further Check-Ups between DEX in Ch-3 and Ch-5
TODO: CLEAN THIS DOC UP
What's in Ch-5 but not Ch-3 Imports Blockies Missing Button and List DownloadOutlined and UploadOutlined
import { Card, Col, Divider, Input, Row } from "antd"; ๐
import { useBalance, useContractReader, useBlockNumber } from "eth-hooks"; ๐
.
.
.
importing Address, TokenBalance are coming from their respectively named subdirectories: "./Address;," and "./TokenBalance;" ๐
.
.
.
export default function DEX(props) {etc.} <-- this line may have the wrong name, be careful because you are likely exporting Dex, not DEX. --> challnege 3 has Dex, ch5 has DEX
.
.
.
const ethBalance = useBalance(contractAddress, props.localProvider);
๐ญ breaks challenge 3 code!
.
const tx = Transactor(props.injectedProvider...) is different than challenge 3 set up for const tx... in ch3 it is set up so we do not use injectedProvider ๐
.
.
.
const contractAddress = ternary operators in challenge5, whereas in challenge3 it is just direct, no ternary.
.
.
.
const tokenBalance = useTokenBalance --> this is different but we think it isn't breaking changes.
.
.
.
nonce is in challenge 5 and not challenge 3.
.
.
let swapTx differs just cause of nonce showing up.
.
.
consolelogging extras
.
.
let addingEth = 0 is in challenge 5.
.
.
Balloons button is in Challenge 3 DEX not Challenge 5 DEX
So the debug tab was taken care, or should be working now if all pointers have been corrected and variables instantiated, respectively.
The front-end is brought in through several steps.
State the aspects within the actual front-end display. First, find the comment in your App.jsx
:
{/* pass in any web3 props to this Home component. For example, yourLocalBalance */}
There you will see the debug code blob below it, it is here where you will outline details for your home-page. Follow the medium blog post and you will see two inputs to bring into your App.jsx
file:
TODO: NOTE TO SELF TO FIX THIS PART AS I'M NOT GETTING THE DEX TO LOAD ON THE FRONT END! ONLY BALLOONS :(
<DEX
address={address}
injectedProvider={injectedProvider}
localProvider={localProvider}
mainnetProvider={mainnetProvider}
readContracts={readContracts}
price={price}
/>
<Contract
title={"๐ Balloons"}
name={"Balloons"}
show={["balanceOf","approve"]}
provider={localProvider}
address={address}
/>
Your front-end should now load accordingly!
In App.jsx, look at line 460 onwards at the Dex component, and then the Contracts for DEX and Balloons. This is where they are loaded onto the main UI. With these, the user can enter the amount of ETH or tokens they want to swap, and the chart will display how the price is calculated. You can also visualize how larger swaps result in more slippage and less output asset. You can also deposit and withdraw from the liquidity pool, earning fees.
๐ก Edit the defaultNetwork
in packages/hardhat/hardhat.config.js
, as well as targetNetwork
in packages/react-app/src/App.jsx
, to your choice of public EVM networks
๐ฉโ๐ You will want to run yarn account
to see if you have a deployer address
๐ If you don't have one, run yarn generate
to create a mnemonic and save it locally for deploying.
๐ฐ Use an instantwallet.io to fund your deployer address (run yarn account
again to view balances)
๐ Run
yarn deploy
to deploy to your public network of choice (๐ wherever you can get โฝ๏ธ gas)
๐ฌ Inspect the block explorer for the network you deployed to... make sure your contract is there.
๐ฎ Your token contract source needs to be verified... (source code publicly available on the block explorer)
Update the api-key in packages/hardhat/package.json file. You can get your key here.
Now you are ready to run the
yarn verify --network your_network
command to verify your contracts on etherscan ๐ฐ
This will be the URL you submit to SpeedRun.
๐ฆ Run yarn build
to package up your frontend.
๐ฝ Upload your app to surge with yarn surge
(you could also yarn s3
or maybe even yarn ipfs
?)
๐ Traffic to your url might break the Infura rate limit, edit your key: constants.js
in packages/ract-app/src
.
๐ฌ Problems, questions, comments on the stack? Post them to the ๐ scaffold-eth developers chat