├── README.md ├── hardhat-tutorial ├── .gitignore ├── README.md ├── constants │ └── index.js ├── contracts │ ├── Exchange.sol │ └── Greeter.sol ├── hardhat.config.js ├── package-lock.json ├── package.json ├── scripts │ ├── deploy.js │ └── sample-script.js └── test │ └── sample-test.js └── my-app ├── .eslintrc.json ├── .gitignore ├── README.md ├── constants └── index.js ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.js ├── api │ └── hello.js └── index.js ├── public ├── cryptodev.svg ├── favicon.ico └── vercel.svg ├── styles ├── Home.module.css └── globals.css └── utils ├── addLiquidity.js ├── getAmounts.js ├── removeLiquidity.js └── swap.js /README.md: -------------------------------------------------------------------------------- 1 | # Build your own Decentralized Exchange like Uniswap 2 | 3 | Now its time for you to launch a DeFi Exchange for your `Crypto Dev` tokens 4 | 5 |  6 | 7 |  8 | 9 | --- 10 | 11 | ## Requirements 12 | 13 | - Build an exhange with only one asset pair (Eth / Crypto Dev) 14 | - Your Decentralized Exchange should take a fees of `1%` on swaps 15 | - When user adds liquidity, they should be given `Crypto Dev LP` tokens (Liquidity Provider tokens) 16 | - CD LP tokens should be given propotional to the `Ether` user is willing to add to the liquidity 17 | 18 | Lets start building 🚀 19 | 20 | --- 21 | 22 | ## Prerequisites 23 | 24 | - You have completed the [ICO tutorial](https://github.com/LearnWeb3DAO/ICO) 25 | - You have completed the [Defi Exchange Theory Tutorial](https://github.com/LearnWeb3DAO/Defi-Exchange-Theory) 26 | - You have completed the [Mixed Topics Tutorial](https://github.com/LearnWeb3DAO/Sophomore-Mixed-Topics) 27 | 28 | --- 29 | 30 | ### Smart Contract 31 | 32 | To build the smart contract we would be using [Hardhat](https://hardhat.org/). 33 | Hardhat is an Ethereum development environment and framework designed for full stack development in Solidity. In simple words you can write your smart contract, deploy them, run tests, and debug your code. 34 | 35 | - To setup a Hardhat project, Open up a terminal and execute these commands 36 | 37 | ```bash 38 | mkdir hardhat-tutorial 39 | cd hardhat-tutorial 40 | npm init --yes 41 | npm install --save-dev hardhat 42 | ``` 43 | 44 | - In the same directory where you installed Hardhat run: 45 | 46 | ```bash 47 | npx hardhat 48 | ``` 49 | 50 | - Select `Create a Javascript project` 51 | - Press enter for the already specified `Hardhat Project root` 52 | - Press enter for the question on if you want to add a `.gitignore` 53 | - Press enter for `Do you want to install this sample project's dependencies with npm (@nomicfoundation/hardhat-toolbox)?` 54 | 55 | - Now you have a hardhat project ready to go! 56 | 57 | - If you are on Windows, please do this extra step and install these libraries as well :) 58 | 59 | ```bash 60 | npm install --save-dev @nomicfoundation/hardhat-toolbox 61 | ``` 62 | 63 | - In the same terminal now install `@openzeppelin/contracts` as we would be importing [Openzeppelin's ERC20 Contract](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol) in our `Exchange` contract 64 | 65 | ```bash 66 | npm install @openzeppelin/contracts 67 | ``` 68 | 69 | - Create a new file inside the `contracts` directory called `Exchange.sol`. In this tutorial we would cover each part of the contract seperately 70 | 71 | - First lets start by importing `ERC20.sol` 72 | 73 | - We imported `ERC20.sol` because our Exchange needs to mint and create `Crypto Dev LP` tokens thats why it needs to inherit ERC20.sol 74 | 75 | ```go 76 | // SPDX-License-Identifier: MIT 77 | pragma solidity ^0.8.4; 78 | 79 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 80 | 81 | contract Exchange is ERC20 { 82 | } 83 | ``` 84 | 85 | - Now lets create a `constructor` for our contract 86 | 87 | - It takes the address of the `_CryptoDevToken` that you deployed in the `ICO` tutorial as an input param 88 | - It then checks if the address is a null address 89 | - After all the checks, it assigns the value to the input param to the `cryptoDevTokenAddress` variable 90 | - Constructor also sets the `name` and `symbol` for our `Crypto Dev LP` token 91 | 92 | ```go 93 | // SPDX-License-Identifier: MIT 94 | pragma solidity ^0.8.4; 95 | 96 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 97 | 98 | contract Exchange is ERC20 { 99 | 100 | address public cryptoDevTokenAddress; 101 | 102 | // Exchange is inheriting ERC20, because our exchange would keep track of Crypto Dev LP tokens 103 | constructor(address _CryptoDevtoken) ERC20("CryptoDev LP Token", "CDLP") { 104 | require(_CryptoDevtoken != address(0), "Token address passed is a null address"); 105 | cryptoDevTokenAddress = _CryptoDevtoken; 106 | } 107 | } 108 | ``` 109 | 110 | - Time to create a function to get reserves of the `Eth` and `Crypto Dev` tokens held 111 | by the contract. 112 | 113 | - Eth reserve as we all know would be equal to the balance of the contract and can be found using `address(this).balance` so lets just create a function only for getting reserves of the `Crypto Dev` tokens 114 | - We know that the `Crypto Dev Token` contract that we deployed is an ERC20 115 | - So we can just call the `balanceOf` to check the balance of `CryptoDev Tokens` 116 | that the contract `address` holds 117 | 118 | ```go 119 | /** 120 | * @dev Returns the amount of `Crypto Dev Tokens` held by the contract 121 | */ 122 | function getReserve() public view returns (uint) { 123 | return ERC20(cryptoDevTokenAddress).balanceOf(address(this)); 124 | } 125 | ``` 126 | 127 | - Time to create an `addLiquidity` function which would add `liquidity` in the form of `Ether` and `Crypto Dev tokens` to our contract 128 | 129 | - If `cryptoDevTokenReserve` is zero it means that it is the first time someone is adding `Crypto Dev` tokens and `Ether` to the contract 130 | - When the user is adding initial liquidity we dont have to maintain any ratio because 131 | we dont have any liquidity. So we accept any amount of tokens that user has sent with the initial call 132 | - If `cryptoDevTokenReserve` is not zero, then we have to make sure that when someone adds the liquidity it doesnt impact the price which the market currently has 133 | - To ensure this, we maintain a ratio which has to remain constant 134 | - Ratio is `(cryptoDevTokenAmount user can add/cryptoDevTokenReserve in the contract) = (Eth Sent by the user/Eth Reserve in the contract)` 135 | - This ratio decides how much `Crypto Dev` tokens user can supply given a certain amount of Eth 136 | - When user adds liquidity, we need to provide him with some `LP` tokens because we need to keep track of the amount of liquiidty he has supplied to the contract 137 | - The amount of `LP` tokens that get minted to the user are propotional to the `Eth` supplied by the user 138 | - In the initial liquidity case, when there is no liquidity: The amount of `LP` tokens that would be minted to the user is equal to the `Eth` balance of the contract (because balance is equal to the `Eth` sent by the user in the `addLiquidity` call) 139 | - When there is already liquidity in the contract, the amount of `LP` tokens that get minted is based on a ratio. 140 | - The ratio is `(LP tokens to be sent to the user (liquidity) / totalSupply of LP tokens in contract) = (Eth sent by the user) / (Eth reserve in the contract)` 141 | 142 | ```go 143 | /** 144 | * @dev Adds liquidity to the exchange. 145 | */ 146 | function addLiquidity(uint _amount) public payable returns (uint) { 147 | uint liquidity; 148 | uint ethBalance = address(this).balance; 149 | uint cryptoDevTokenReserve = getReserve(); 150 | ERC20 cryptoDevToken = ERC20(cryptoDevTokenAddress); 151 | /* 152 | If the reserve is empty, intake any user supplied value for 153 | `Ether` and `Crypto Dev` tokens because there is no ratio currently 154 | */ 155 | if(cryptoDevTokenReserve == 0) { 156 | // Transfer the `cryptoDevToken` from the user's account to the contract 157 | cryptoDevToken.transferFrom(msg.sender, address(this), _amount); 158 | // Take the current ethBalance and mint `ethBalance` amount of LP tokens to the user. 159 | // `liquidity` provided is equal to `ethBalance` because this is the first time user 160 | // is adding `Eth` to the contract, so whatever `Eth` contract has is equal to the one supplied 161 | // by the user in the current `addLiquidity` call 162 | // `liquidity` tokens that need to be minted to the user on `addLiquidity` call should always be proportional 163 | // to the Eth specified by the user 164 | liquidity = ethBalance; 165 | _mint(msg.sender, liquidity); 166 | // _mint is ERC20.sol smart contract function to mint ERC20 tokens 167 | } else { 168 | /* 169 | If the reserve is not empty, intake any user supplied value for 170 | `Ether` and determine according to the ratio how many `Crypto Dev` tokens 171 | need to be supplied to prevent any large price impacts because of the additional 172 | liquidity 173 | */ 174 | // EthReserve should be the current ethBalance subtracted by the value of ether sent by the user 175 | // in the current `addLiquidity` call 176 | uint ethReserve = ethBalance - msg.value; 177 | // Ratio should always be maintained so that there are no major price impacts when adding liquidity 178 | // Ratio here is -> (cryptoDevTokenAmount user can add/cryptoDevTokenReserve in the contract) = (Eth Sent by the user/Eth Reserve in the contract); 179 | // So doing some maths, (cryptoDevTokenAmount user can add) = (Eth Sent by the user * cryptoDevTokenReserve /Eth Reserve); 180 | uint cryptoDevTokenAmount = (msg.value * cryptoDevTokenReserve)/(ethReserve); 181 | require(_amount >= cryptoDevTokenAmount, "Amount of tokens sent is less than the minimum tokens required"); 182 | // transfer only (cryptoDevTokenAmount user can add) amount of `Crypto Dev tokens` from users account 183 | // to the contract 184 | cryptoDevToken.transferFrom(msg.sender, address(this), cryptoDevTokenAmount); 185 | // The amount of LP tokens that would be sent to the user should be propotional to the liquidity of 186 | // ether added by the user 187 | // Ratio here to be maintained is -> 188 | // (LP tokens to be sent to the user (liquidity)/ totalSupply of LP tokens in contract) = (Eth sent by the user)/(Eth reserve in the contract) 189 | // by some maths -> liquidity = (totalSupply of LP tokens in contract * (Eth sent by the user))/(Eth reserve in the contract) 190 | liquidity = (totalSupply() * msg.value)/ ethReserve; 191 | _mint(msg.sender, liquidity); 192 | } 193 | return liquidity; 194 | } 195 | ``` 196 | 197 | - Now lets create a function for `removing liquidity` from the contract. 198 | 199 | - The amount of ether that would be sent back to the user would be based on a ratio 200 | - Ratio is `(Eth sent back to the user) / (current Eth reserve) = (amount of LP tokens that user wants to withdraw) / (total supply of LP tokens)` 201 | - The amount of Crypto Dev tokens that would be sent back to the user would also be based on a ratio 202 | - Ratio is `(Crypto Dev sent back to the user) / (current Crypto Dev token reserve) = (amount of LP tokens that user wants to withdraw) / (total supply of LP tokens)` 203 | - The amount of `LP` tokens that user would use to remove liquidity would be burnt 204 | 205 | ```go 206 | /** 207 | * @dev Returns the amount Eth/Crypto Dev tokens that would be returned to the user 208 | * in the swap 209 | */ 210 | function removeLiquidity(uint _amount) public returns (uint , uint) { 211 | require(_amount > 0, "_amount should be greater than zero"); 212 | uint ethReserve = address(this).balance; 213 | uint _totalSupply = totalSupply(); 214 | // The amount of Eth that would be sent back to the user is based 215 | // on a ratio 216 | // Ratio is -> (Eth sent back to the user) / (current Eth reserve) 217 | // = (amount of LP tokens that user wants to withdraw) / (total supply of LP tokens) 218 | // Then by some maths -> (Eth sent back to the user) 219 | // = (current Eth reserve * amount of LP tokens that user wants to withdraw) / (total supply of LP tokens) 220 | uint ethAmount = (ethReserve * _amount)/ _totalSupply; 221 | // The amount of Crypto Dev token that would be sent back to the user is based 222 | // on a ratio 223 | // Ratio is -> (Crypto Dev sent back to the user) / (current Crypto Dev token reserve) 224 | // = (amount of LP tokens that user wants to withdraw) / (total supply of LP tokens) 225 | // Then by some maths -> (Crypto Dev sent back to the user) 226 | // = (current Crypto Dev token reserve * amount of LP tokens that user wants to withdraw) / (total supply of LP tokens) 227 | uint cryptoDevTokenAmount = (getReserve() * _amount)/ _totalSupply; 228 | // Burn the sent LP tokens from the user's wallet because they are already sent to 229 | // remove liquidity 230 | _burn(msg.sender, _amount); 231 | // Transfer `ethAmount` of Eth from user's wallet to the contract 232 | payable(msg.sender).transfer(ethAmount); 233 | // Transfer `cryptoDevTokenAmount` of Crypto Dev tokens from the user's wallet to the contract 234 | ERC20(cryptoDevTokenAddress).transfer(msg.sender, cryptoDevTokenAmount); 235 | return (ethAmount, cryptoDevTokenAmount); 236 | } 237 | ``` 238 | 239 | - Next lets implement the swap functionality 240 | - Swap would go two ways. One way would be `Eth` to `Crypto Dev` tokens and other would be `Crypto Dev` to `Eth` 241 | - Its important for us to determine: Given `x` amount of `Eth`/`Crypto Dev` token sent by the user, how many `Eth`/`Crypto Dev` tokens would he receive from the swap? 242 | - So let's create a function which calculates this: 243 | 244 | - We will charge `1%`. This means the amount of input tokens with fees would equal `Input amount with fees = (input amount - (1*(input amount)/100)) = ((input amount)*99)/100` 245 | - We need to follow the concept of `XY = K` curve 246 | - We need to make sure `(x + Δx) * (y - Δy) = x * y`, so the final formula is `Δy = (y * Δx) / (x + Δx)`; 247 | - `Δy` in our case is `tokens to be received`, `Δx = ((input amount)*99)/100`, `x` = Input Reserve, `y` = Output Reserve 248 | - Input Reserve and Ouput Reserve would depend on which swap we are implementing. `Eth` to `Crypto Dev` token or vice versa 249 | 250 | ```go 251 | /** 252 | * @dev Returns the amount Eth/Crypto Dev tokens that would be returned to the user 253 | * in the swap 254 | */ 255 | function getAmountOfTokens( 256 | uint256 inputAmount, 257 | uint256 inputReserve, 258 | uint256 outputReserve 259 | ) public pure returns (uint256) { 260 | require(inputReserve > 0 && outputReserve > 0, "invalid reserves"); 261 | // We are charging a fee of `1%` 262 | // Input amount with fee = (input amount - (1*(input amount)/100)) = ((input amount)*99)/100 263 | uint256 inputAmountWithFee = inputAmount * 99; 264 | // Because we need to follow the concept of `XY = K` curve 265 | // We need to make sure (x + Δx) * (y - Δy) = x * y 266 | // So the final formula is Δy = (y * Δx) / (x + Δx) 267 | // Δy in our case is `tokens to be received` 268 | // Δx = ((input amount)*99)/100, x = inputReserve, y = outputReserve 269 | // So by putting the values in the formulae you can get the numerator and denominator 270 | uint256 numerator = inputAmountWithFee * outputReserve; 271 | uint256 denominator = (inputReserve * 100) + inputAmountWithFee; 272 | return numerator / denominator; 273 | } 274 | ``` 275 | 276 | - Now lets implement a function to swap `Ether` for `Crypto Dev` tokens 277 | 278 | ```go 279 | /** 280 | * @dev Swaps Eth for CryptoDev Tokens 281 | */ 282 | function ethToCryptoDevToken(uint _minTokens) public payable { 283 | uint256 tokenReserve = getReserve(); 284 | // call the `getAmountOfTokens` to get the amount of Crypto Dev tokens 285 | // that would be returned to the user after the swap 286 | // Notice that the `inputReserve` we are sending is equal to 287 | // `address(this).balance - msg.value` instead of just `address(this).balance` 288 | // because `address(this).balance` already contains the `msg.value` user has sent in the given call 289 | // so we need to subtract it to get the actual input reserve 290 | uint256 tokensBought = getAmountOfTokens( 291 | msg.value, 292 | address(this).balance - msg.value, 293 | tokenReserve 294 | ); 295 | 296 | require(tokensBought >= _minTokens, "insufficient output amount"); 297 | // Transfer the `Crypto Dev` tokens to the user 298 | ERC20(cryptoDevTokenAddress).transfer(msg.sender, tokensBought); 299 | } 300 | ``` 301 | 302 | - Now lets implement a function to swap `Crypto Dev` tokens to `Ether` 303 | 304 | ```go 305 | /** 306 | * @dev Swaps CryptoDev Tokens for Eth 307 | */ 308 | function cryptoDevTokenToEth(uint _tokensSold, uint _minEth) public { 309 | uint256 tokenReserve = getReserve(); 310 | // call the `getAmountOfTokens` to get the amount of Eth 311 | // that would be returned to the user after the swap 312 | uint256 ethBought = getAmountOfTokens( 313 | _tokensSold, 314 | tokenReserve, 315 | address(this).balance 316 | ); 317 | require(ethBought >= _minEth, "insufficient output amount"); 318 | // Transfer `Crypto Dev` tokens from the user's address to the contract 319 | ERC20(cryptoDevTokenAddress).transferFrom( 320 | msg.sender, 321 | address(this), 322 | _tokensSold 323 | ); 324 | // send the `ethBought` to the user from the contract 325 | payable(msg.sender).transfer(ethBought); 326 | } 327 | ``` 328 | 329 | - Your final contract should look like this: 330 | 331 | ```go 332 | // SPDX-License-Identifier: MIT 333 | pragma solidity ^0.8.4; 334 | 335 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 336 | 337 | contract Exchange is ERC20 { 338 | 339 | address public cryptoDevTokenAddress; 340 | 341 | // Exchange is inheriting ERC20, because our exchange would keep track of Crypto Dev LP tokens 342 | constructor(address _CryptoDevtoken) ERC20("CryptoDev LP Token", "CDLP") { 343 | require(_CryptoDevtoken != address(0), "Token address passed is a null address"); 344 | cryptoDevTokenAddress = _CryptoDevtoken; 345 | } 346 | 347 | /** 348 | * @dev Returns the amount of `Crypto Dev Tokens` held by the contract 349 | */ 350 | function getReserve() public view returns (uint) { 351 | return ERC20(cryptoDevTokenAddress).balanceOf(address(this)); 352 | } 353 | 354 | /** 355 | * @dev Adds liquidity to the exchange. 356 | */ 357 | function addLiquidity(uint _amount) public payable returns (uint) { 358 | uint liquidity; 359 | uint ethBalance = address(this).balance; 360 | uint cryptoDevTokenReserve = getReserve(); 361 | ERC20 cryptoDevToken = ERC20(cryptoDevTokenAddress); 362 | /* 363 | If the reserve is empty, intake any user supplied value for 364 | `Ether` and `Crypto Dev` tokens because there is no ratio currently 365 | */ 366 | if(cryptoDevTokenReserve == 0) { 367 | // Transfer the `cryptoDevToken` address from the user's account to the contract 368 | cryptoDevToken.transferFrom(msg.sender, address(this), _amount); 369 | // Take the current ethBalance and mint `ethBalance` amount of LP tokens to the user. 370 | // `liquidity` provided is equal to `ethBalance` because this is the first time user 371 | // is adding `Eth` to the contract, so whatever `Eth` contract has is equal to the one supplied 372 | // by the user in the current `addLiquidity` call 373 | // `liquidity` tokens that need to be minted to the user on `addLiquidity` call should always be proportional 374 | // to the Eth specified by the user 375 | liquidity = ethBalance; 376 | _mint(msg.sender, liquidity); 377 | } else { 378 | /* 379 | If the reserve is not empty, intake any user supplied value for 380 | `Ether` and determine according to the ratio how many `Crypto Dev` tokens 381 | need to be supplied to prevent any large price impacts because of the additional 382 | liquidity 383 | */ 384 | // EthReserve should be the current ethBalance subtracted by the value of ether sent by the user 385 | // in the current `addLiquidity` call 386 | uint ethReserve = ethBalance - msg.value; 387 | // Ratio should always be maintained so that there are no major price impacts when adding liquidity 388 | // Ration here is -> (cryptoDevTokenAmount user can add/cryptoDevTokenReserve in the contract) = (Eth Sent by the user/Eth Reserve in the contract); 389 | // So doing some maths, (cryptoDevTokenAmount user can add) = (Eth Sent by the user * cryptoDevTokenReserve /Eth Reserve); 390 | uint cryptoDevTokenAmount = (msg.value * cryptoDevTokenReserve)/(ethReserve); 391 | require(_amount >= cryptoDevTokenAmount, "Amount of tokens sent is less than the minimum tokens required"); 392 | // transfer only (cryptoDevTokenAmount user can add) amount of `Crypto Dev tokens` from users account 393 | // to the contract 394 | cryptoDevToken.transferFrom(msg.sender, address(this), cryptoDevTokenAmount); 395 | // The amount of LP tokens that would be sent to the user should be propotional to the liquidity of 396 | // ether added by the user 397 | // Ratio here to be maintained is -> 398 | // (lp tokens to be sent to the user (liquidity)/ totalSupply of LP tokens in contract) = (Eth sent by the user)/(Eth reserve in the contract) 399 | // by some maths -> liquidity = (totalSupply of LP tokens in contract * (Eth sent by the user))/(Eth reserve in the contract) 400 | liquidity = (totalSupply() * msg.value)/ ethReserve; 401 | _mint(msg.sender, liquidity); 402 | } 403 | return liquidity; 404 | } 405 | 406 | /** 407 | * @dev Returns the amount Eth/Crypto Dev tokens that would be returned to the user 408 | * in the swap 409 | */ 410 | function removeLiquidity(uint _amount) public returns (uint , uint) { 411 | require(_amount > 0, "_amount should be greater than zero"); 412 | uint ethReserve = address(this).balance; 413 | uint _totalSupply = totalSupply(); 414 | // The amount of Eth that would be sent back to the user is based 415 | // on a ratio 416 | // Ratio is -> (Eth sent back to the user/ Current Eth reserve) 417 | // = (amount of LP tokens that user wants to withdraw) / (total supply of LP tokens) 418 | // Then by some maths -> (Eth sent back to the user) 419 | // = (current Eth reserve * amount of LP tokens that user wants to withdraw) / (total supply of LP tokens) 420 | uint ethAmount = (ethReserve * _amount)/ _totalSupply; 421 | // The amount of Crypto Dev token that would be sent back to the user is based 422 | // on a ratio 423 | // Ratio is -> (Crypto Dev sent back to the user) / (current Crypto Dev token reserve) 424 | // = (amount of LP tokens that user wants to withdraw) / (total supply of LP tokens) 425 | // Then by some maths -> (Crypto Dev sent back to the user) 426 | // = (current Crypto Dev token reserve * amount of LP tokens that user wants to withdraw) / (total supply of LP tokens) 427 | uint cryptoDevTokenAmount = (getReserve() * _amount)/ _totalSupply; 428 | // Burn the sent `LP` tokens from the user's wallet because they are already sent to 429 | // remove liquidity 430 | _burn(msg.sender, _amount); 431 | // Transfer `ethAmount` of Eth from user's wallet to the contract 432 | payable(msg.sender).transfer(ethAmount); 433 | // Transfer `cryptoDevTokenAmount` of `Crypto Dev` tokens from the user's wallet to the contract 434 | ERC20(cryptoDevTokenAddress).transfer(msg.sender, cryptoDevTokenAmount); 435 | return (ethAmount, cryptoDevTokenAmount); 436 | } 437 | 438 | /** 439 | * @dev Returns the amount Eth/Crypto Dev tokens that would be returned to the user 440 | * in the swap 441 | */ 442 | function getAmountOfTokens( 443 | uint256 inputAmount, 444 | uint256 inputReserve, 445 | uint256 outputReserve 446 | ) public pure returns (uint256) { 447 | require(inputReserve > 0 && outputReserve > 0, "invalid reserves"); 448 | // We are charging a fee of `1%` 449 | // Input amount with fee = (input amount - (1*(input amount)/100)) = ((input amount)*99)/100 450 | uint256 inputAmountWithFee = inputAmount * 99; 451 | // Because we need to follow the concept of `XY = K` curve 452 | // We need to make sure (x + Δx) * (y - Δy) = x * y 453 | // So the final formula is Δy = (y * Δx) / (x + Δx) 454 | // Δy in our case is `tokens to be received` 455 | // Δx = ((input amount)*99)/100, x = inputReserve, y = outputReserve 456 | // So by putting the values in the formulae you can get the numerator and denominator 457 | uint256 numerator = inputAmountWithFee * outputReserve; 458 | uint256 denominator = (inputReserve * 100) + inputAmountWithFee; 459 | return numerator / denominator; 460 | } 461 | 462 | /** 463 | * @dev Swaps Eth for CryptoDev Tokens 464 | */ 465 | function ethToCryptoDevToken(uint _minTokens) public payable { 466 | uint256 tokenReserve = getReserve(); 467 | // call the `getAmountOfTokens` to get the amount of Crypto Dev tokens 468 | // that would be returned to the user after the swap 469 | // Notice that the `inputReserve` we are sending is equal to 470 | // `address(this).balance - msg.value` instead of just `address(this).balance` 471 | // because `address(this).balance` already contains the `msg.value` user has sent in the given call 472 | // so we need to subtract it to get the actual input reserve 473 | uint256 tokensBought = getAmountOfTokens( 474 | msg.value, 475 | address(this).balance - msg.value, 476 | tokenReserve 477 | ); 478 | 479 | require(tokensBought >= _minTokens, "insufficient output amount"); 480 | // Transfer the `Crypto Dev` tokens to the user 481 | ERC20(cryptoDevTokenAddress).transfer(msg.sender, tokensBought); 482 | } 483 | 484 | 485 | /** 486 | * @dev Swaps CryptoDev Tokens for Eth 487 | */ 488 | function cryptoDevTokenToEth(uint _tokensSold, uint _minEth) public { 489 | uint256 tokenReserve = getReserve(); 490 | // call the `getAmountOfTokens` to get the amount of Eth 491 | // that would be returned to the user after the swap 492 | uint256 ethBought = getAmountOfTokens( 493 | _tokensSold, 494 | tokenReserve, 495 | address(this).balance 496 | ); 497 | require(ethBought >= _minEth, "insufficient output amount"); 498 | // Transfer `Crypto Dev` tokens from the user's address to the contract 499 | ERC20(cryptoDevTokenAddress).transferFrom( 500 | msg.sender, 501 | address(this), 502 | _tokensSold 503 | ); 504 | // send the `ethBought` to the user from the contract 505 | payable(msg.sender).transfer(ethBought); 506 | } 507 | } 508 | ``` 509 | 510 | - Now we would install `dotenv` package to be able to import the env file and use it in our config. Open up a terminal pointing at `hardhat-tutorial` directory and execute this command 511 | 512 | ```bash 513 | npm install dotenv 514 | ``` 515 | 516 | - Now create a `.env` file in the `hardhat-tutorial` folder and add the following lines, use the instructions in the comments to get your Alchemy API Key URL and Rinkeby Private Key. Make sure that the account from which you get your Rinkeby private key is funded with Rinkeby Ether. 517 | 518 | ```bash 519 | // Go to https://www.alchemyapi.io, sign up, create 520 | // a new App in its dashboard and select the network as Rinkeby, and replace "add-the-alchemy-key-url-here" with its key url 521 | ALCHEMY_API_KEY_URL="add-the-alchemy-key-url-here" 522 | 523 | // Replace this private key with your RINKEBY account private key 524 | // To export your private key from Metamask, open Metamask and 525 | // go to Account Details > Export Private Key 526 | // Be aware of NEVER putting real Ether into testing accounts 527 | RINKEBY_PRIVATE_KEY="add-the-rinkeby-private-key-here" 528 | ``` 529 | 530 | - Lets also create a constants folder to keep track of any constants we have. Under the `hardhat-tutorial` folder create a new folder named `constants` and under the `constants` folder create a new file `index.js` 531 | 532 | - Inside the `index.js` file add the following lines of code. Remember to replace `ADDRESS-OF-CRYPTO-DEV-TOKEN` with the contract address of the `Crypto Dev` token contract that you deployed in the `ICO` tutorial 533 | 534 | ```js 535 | const CRYPTO_DEV_TOKEN_CONTRACT_ADDRESS = "ADDRESS-OF-CRYPTO-DEV-TOKEN"; 536 | 537 | module.exports = { CRYPTO_DEV_TOKEN_CONTRACT_ADDRESS }; 538 | ``` 539 | 540 | - Lets deploy the contract to `rinkeby` network. Create a new file, or replace the existing default one, named `deploy.js` under the `scripts` folder 541 | 542 | - Now we would write some code to deploy the contract in `deploy.js` file. 543 | 544 | ```js 545 | const { ethers } = require("hardhat"); 546 | require("dotenv").config({ path: ".env" }); 547 | const { CRYPTO_DEV_TOKEN_CONTRACT_ADDRESS } = require("../constants"); 548 | 549 | async function main() { 550 | const cryptoDevTokenAddress = CRYPTO_DEV_TOKEN_CONTRACT_ADDRESS; 551 | /* 552 | A ContractFactory in ethers.js is an abstraction used to deploy new smart contracts, 553 | so exchangeContract here is a factory for instances of our Exchange contract. 554 | */ 555 | const exchangeContract = await ethers.getContractFactory("Exchange"); 556 | 557 | // here we deploy the contract 558 | const deployedExchangeContract = await exchangeContract.deploy( 559 | cryptoDevTokenAddress 560 | ); 561 | await deployedExchangeContract.deployed(); 562 | 563 | // print the address of the deployed contract 564 | console.log("Exchange Contract Address:", deployedExchangeContract.address); 565 | } 566 | 567 | // Call the main function and catch if there is any error 568 | main() 569 | .then(() => process.exit(0)) 570 | .catch((error) => { 571 | console.error(error); 572 | process.exit(1); 573 | }); 574 | ``` 575 | 576 | - Now open the `hardhat.config.js` file, we would add the `rinkeby` network here so that we can deploy our contract to rinkeby. Replace all the lines in the `hardhat.config.js` file with the given below lines 577 | 578 | ```js 579 | require("@nomicfoundation/hardhat-toolbox"); 580 | require("dotenv").config({ path: ".env" }); 581 | 582 | const ALCHEMY_API_KEY_URL = process.env.ALCHEMY_API_KEY_URL; 583 | 584 | const RINKEBY_PRIVATE_KEY = process.env.RINKEBY_PRIVATE_KEY; 585 | 586 | module.exports = { 587 | solidity: "0.8.4", 588 | networks: { 589 | rinkeby: { 590 | url: ALCHEMY_API_KEY_URL, 591 | accounts: [RINKEBY_PRIVATE_KEY], 592 | }, 593 | }, 594 | }; 595 | ``` 596 | 597 | - Compile the contract, open up a terminal pointing at `hardhat-tutorial` directory and execute this command 598 | 599 | ```bash 600 | npx hardhat compile 601 | ``` 602 | 603 | - To deploy, open up a terminal pointing at`hardhat-tutorial` directory and execute this command 604 | ```bash 605 | npx hardhat run scripts/deploy.js --network rinkeby 606 | ``` 607 | - Save the Exchange Contract Address that was printed on your terminal in your notepad, you would need it further down in the tutorial. 608 | 609 | ### Website 610 | 611 | - To develop the website we would be using [React](https://reactjs.org/) and [Next Js](https://nextjs.org/). React is a javascript framework which is used to make websites and Next Js is built on top of React. 612 | - First, You would need to create a new `next` app. Your folder structure should look something like 613 | 614 | ``` 615 | - DeFi-Exchange 616 | - hardhat-tutorial 617 | - my-app 618 | ``` 619 | 620 | - To create this `my-app`, in the terminal point to DeFi-Exchange folder and type 621 | 622 | ```bash 623 | npx create-next-app@latest 624 | ``` 625 | 626 | and press `enter` for all the questions 627 | 628 | - Now to run the app, execute these commands in the terminal 629 | 630 | ``` 631 | cd my-app 632 | npm run dev 633 | ``` 634 | 635 | - Now go to `http://localhost:3000`, your app should be running 🤘 636 | 637 | - Now lets install Web3Modal library(https://github.com/Web3Modal/web3modal). Web3Modal is an easy-to-use library to help developers add support for multiple providers in their apps with a simple customizable configuration. By default Web3Modal Library supports injected providers like (Metamask, Dapper, Gnosis Safe, Frame, Web3 Browsers, etc), You can also easily configure the library to support Portis, Fortmatic, Squarelink, Torus, Authereum, D'CENT Wallet and Arkane. 638 | Open up a terminal pointing at`my-app` directory and execute this command 639 | 640 | ```bash 641 | npm install web3modal 642 | ``` 643 | 644 | - In the same terminal also install `ethers.js` 645 | 646 | ```bash 647 | npm i ethers 648 | ``` 649 | 650 | - In your public folder, download this [image](https://github.com/LearnWeb3DAO/Defi-Exchange/blob/main/my-app/public/cryptodev.svg) and rename it to `cryptodev.svg`. 651 | 652 | - Now go to styles folder and replace all the contents of `Home.modules.css` file with the following code, this would add some styling to your dapp: 653 | 654 | ```css 655 | .main { 656 | min-height: 90vh; 657 | display: flex; 658 | flex-direction: row; 659 | justify-content: center; 660 | align-items: center; 661 | font-family: "Courier New", Courier, monospace; 662 | } 663 | 664 | .footer { 665 | display: flex; 666 | padding: 2rem 0; 667 | border-top: 1px solid #eaeaea; 668 | justify-content: center; 669 | align-items: center; 670 | } 671 | 672 | .image { 673 | width: 70%; 674 | height: 50%; 675 | margin-left: 20%; 676 | } 677 | 678 | .input { 679 | width: 200px; 680 | height: 100%; 681 | padding: 1%; 682 | margin: 2%; 683 | box-shadow: 0 0 15px 4px rgba(0, 0, 0, 0.06); 684 | border-radius: 10px; 685 | } 686 | 687 | .title { 688 | font-size: 2rem; 689 | margin: 2rem 0; 690 | } 691 | 692 | .description { 693 | line-height: 1; 694 | margin: 2%; 695 | font-size: 1.2rem; 696 | } 697 | 698 | .button { 699 | border-radius: 4px; 700 | background-color: purple; 701 | border: none; 702 | color: #ffffff; 703 | font-size: 15px; 704 | padding: 5px; 705 | width: 100px; 706 | cursor: pointer; 707 | margin: 2%; 708 | } 709 | 710 | .inputDiv { 711 | width: 200px; 712 | height: 100%; 713 | padding: 1%; 714 | margin: 2%; 715 | border: lightslategray; 716 | box-shadow: 0 0 15px 4px rgba(0, 0, 0, 0.06); 717 | border-radius: 10px; 718 | } 719 | .select { 720 | border-radius: 4px; 721 | font-size: 15px; 722 | padding: 5px; 723 | width: 175px; 724 | cursor: pointer; 725 | margin: 2%; 726 | } 727 | 728 | .button1 { 729 | border-radius: 4px; 730 | background-color: blue; 731 | border: none; 732 | color: #ffffff; 733 | font-size: 15px; 734 | padding: 5px; 735 | width: 100px; 736 | cursor: pointer; 737 | margin: 2%; 738 | } 739 | @media (max-width: 1000px) { 740 | .main { 741 | width: 100%; 742 | flex-direction: column; 743 | justify-content: center; 744 | align-items: center; 745 | } 746 | } 747 | ``` 748 | 749 | - Now lets create a constants folder to keep track of any constants we might have. Create a `constants` folder under `my-app` folder and inside the `constants` folder create a file names index.js 750 | - Paste the following code. 751 | 752 | - Replace `ABI-CRYPTO-DEV-TOKEN-CONTRACT` with the abi of the `Crypto Dev` token contract that you deployed in the ICO tutorial. 753 | - Replace `ADDRESS-OF-CRYPTO-DEV-TOKEN-CONTRACT` with the address of the `Crypto Dev` token contract that you deployed in the ICO tutorial 754 | - Replace `ABI-EXCHANGE-CONTRACT` with the abi of the Exchange Contract. To get the abi for your contract, go to your `hardhat-tutorial/artifacts/contracts/Exchange.sol` folder and from your `Exchange.json` file get the array marked under the `"abi"` key. 755 | - Replace `ADDRESS-EXCHANGE-CONTRACT` with the address of the exchange contract that you deployed above and saved to your notepad 756 | 757 | ```javascript 758 | export const TOKEN_CONTRACT_ABI = "ABI-CRYPTO-DEV-TOKEN-CONTRACT"; 759 | export const TOKEN_CONTRACT_ADDRESS = 760 | "ADDRESS-OF-CRYPTO-DEV-TOKEN-CONTRACT"; 761 | export const EXCHANGE_CONTRACT_ABI = "ABI-EXCHANGE-CONTRACT"; 762 | export const EXCHANGE_CONTRACT_ADDRESS = "ADDRESS-EXCHANGE-CONTRACT"; 763 | ``` 764 | 765 | - Now we would create some utility files which would help us to better interact with the contract. Create a `utils` folder inside the `my-app` folder and inside the folder create 4 files: `addLiquidity.js`, `removeLiquidity.js`, `getAmounts.js`, and `swap.js` 766 | 767 | - Lets start by writing some code in `getAmounts.js`. This file is used to retrieve balances and reserves for assets 768 | 769 | ```javascript 770 | import { Contract } from "ethers"; 771 | import { 772 | EXCHANGE_CONTRACT_ABI, 773 | EXCHANGE_CONTRACT_ADDRESS, 774 | TOKEN_CONTRACT_ABI, 775 | TOKEN_CONTRACT_ADDRESS, 776 | } from "../constants"; 777 | 778 | /** 779 | * getEtherBalance: Retrieves the ether balance of the user or the contract 780 | */ 781 | export const getEtherBalance = async ( 782 | provider, 783 | address, 784 | contract = false 785 | ) => { 786 | try { 787 | // If the caller has set the `contract` boolean to true, retrieve the balance of 788 | // ether in the `exchange contract`, if it is set to false, retrieve the balance 789 | // of the user's address 790 | if (contract) { 791 | const balance = await provider.getBalance(EXCHANGE_CONTRACT_ADDRESS); 792 | return balance; 793 | } else { 794 | const balance = await provider.getBalance(address); 795 | return balance; 796 | } 797 | } catch (err) { 798 | console.error(err); 799 | return 0; 800 | } 801 | }; 802 | 803 | /** 804 | * getCDTokensBalance: Retrieves the Crypto Dev tokens in the account 805 | * of the provided `address` 806 | */ 807 | export const getCDTokensBalance = async (provider, address) => { 808 | try { 809 | const tokenContract = new Contract( 810 | TOKEN_CONTRACT_ADDRESS, 811 | TOKEN_CONTRACT_ABI, 812 | provider 813 | ); 814 | const balanceOfCryptoDevTokens = await tokenContract.balanceOf(address); 815 | return balanceOfCryptoDevTokens; 816 | } catch (err) { 817 | console.error(err); 818 | } 819 | }; 820 | 821 | /** 822 | * getLPTokensBalance: Retrieves the amount of LP tokens in the account 823 | * of the provided `address` 824 | */ 825 | export const getLPTokensBalance = async (provider, address) => { 826 | try { 827 | const exchangeContract = new Contract( 828 | EXCHANGE_CONTRACT_ADDRESS, 829 | EXCHANGE_CONTRACT_ABI, 830 | provider 831 | ); 832 | const balanceOfLPTokens = await exchangeContract.balanceOf(address); 833 | return balanceOfLPTokens; 834 | } catch (err) { 835 | console.error(err); 836 | } 837 | }; 838 | 839 | /** 840 | * getReserveOfCDTokens: Retrieves the amount of CD tokens in the 841 | * exchange contract address 842 | */ 843 | export const getReserveOfCDTokens = async (provider) => { 844 | try { 845 | const exchangeContract = new Contract( 846 | EXCHANGE_CONTRACT_ADDRESS, 847 | EXCHANGE_CONTRACT_ABI, 848 | provider 849 | ); 850 | const reserve = await exchangeContract.getReserve(); 851 | return reserve; 852 | } catch (err) { 853 | console.error(err); 854 | } 855 | }; 856 | ``` 857 | 858 | - Lets now write some code for `addLiquidity.js`. 859 | 860 | - `addLiquidity.js` has two functions `addLiquidity` and `calculateCD` 861 | 862 | - `addLiquidity` is used to call the `addLiquidity` function in the contract to add liquidity 863 | - It also gets the `Crypto Dev` tokens approved for the contract by the user. The reason why `Crypto Dev` tokens need approval is because they are an ERC20 token. For the contract to withdraw an ERC20 from a user's account, it needs the approval from the user's account 864 | - `calculateCD` tells you for a given amount of `Eth`, how many `Crypto Dev` tokens can be added to the `liquidity` 865 | - We calculate this by maintaining a ratio. The ratio we follow is `(amount of Crypto Dev tokens to be added) / (Crypto Dev tokens balance) = (Eth that would be added) / (Eth reserve in the contract)` 866 | - So by maths we get `(amount of Crypto Dev tokens to be added) = (Eth that would be added * Crypto Dev tokens balance) / (Eth reserve in the contract)` 867 | - The ratio is needed so that adding liquidity doesn't largely impact the price 868 | 869 | - Note `tx.wait()` means we are waiting for the transaction to get mined 870 | 871 | ```javascript 872 | import { Contract, utils } from "ethers"; 873 | import { 874 | EXCHANGE_CONTRACT_ABI, 875 | EXCHANGE_CONTRACT_ADDRESS, 876 | TOKEN_CONTRACT_ABI, 877 | TOKEN_CONTRACT_ADDRESS, 878 | } from "../constants"; 879 | 880 | /** 881 | * addLiquidity helps add liquidity to the exchange, 882 | * If the user is adding initial liquidity, user decides the ether and CD tokens he wants to add 883 | * to the exchange. If he is adding the liquidity after the initial liquidity has already been added 884 | * then we calculate the Crypto Dev tokens he can add, given the Eth he wants to add by keeping the ratios 885 | * constant 886 | */ 887 | export const addLiquidity = async ( 888 | signer, 889 | addCDAmountWei, 890 | addEtherAmountWei 891 | ) => { 892 | try { 893 | // create a new instance of the token contract 894 | const tokenContract = new Contract( 895 | TOKEN_CONTRACT_ADDRESS, 896 | TOKEN_CONTRACT_ABI, 897 | signer 898 | ); 899 | // create a new instance of the exchange contract 900 | const exchangeContract = new Contract( 901 | EXCHANGE_CONTRACT_ADDRESS, 902 | EXCHANGE_CONTRACT_ABI, 903 | signer 904 | ); 905 | // Because CD tokens are an ERC20, user would need to give the contract allowance 906 | // to take the required number CD tokens out of his contract 907 | let tx = await tokenContract.approve( 908 | EXCHANGE_CONTRACT_ADDRESS, 909 | addCDAmountWei.toString() 910 | ); 911 | await tx.wait(); 912 | // After the contract has the approval, add the ether and cd tokens in the liquidity 913 | tx = await exchangeContract.addLiquidity(addCDAmountWei, { 914 | value: addEtherAmountWei, 915 | }); 916 | await tx.wait(); 917 | } catch (err) { 918 | console.error(err); 919 | } 920 | }; 921 | 922 | /** 923 | * calculateCD calculates the CD tokens that need to be added to the liquidity 924 | * given `_addEtherAmountWei` amount of ether 925 | */ 926 | export const calculateCD = async ( 927 | _addEther = "0", 928 | etherBalanceContract, 929 | cdTokenReserve 930 | ) => { 931 | // `_addEther` is a string, we need to convert it to a Bignumber before we can do our calculations 932 | // We do that using the `parseEther` function from `ethers.js` 933 | const _addEtherAmountWei = utils.parseEther(_addEther); 934 | 935 | // Ratio needs to be maintained when we add liquidty. 936 | // We need to let the user know for a specific amount of ether how many `CD` tokens 937 | // He can add so that the price impact is not large 938 | // The ratio we follow is (amount of Crypto Dev tokens to be added) / (Crypto Dev tokens balance) = (Eth that would be added) / (Eth reserve in the contract) 939 | // So by maths we get (amount of Crypto Dev tokens to be added) = (Eth that would be added * Crypto Dev tokens balance) / (Eth reserve in the contract) 940 | 941 | const cryptoDevTokenAmount = _addEtherAmountWei 942 | .mul(cdTokenReserve) 943 | .div(etherBalanceContract); 944 | return cryptoDevTokenAmount; 945 | }; 946 | ``` 947 | 948 | - Now add some code to `removeLiquidity.js` 949 | 950 | - We have two functions here: One is `removeLiquidity` and the other is `getTokensAfterRemove` 951 | - `removeLiquidity` calls the `removeLiquidity` function from the contract, 952 | to remove the amount of `LP` tokens specified by the user 953 | - `getTokensAfterRemove` calulates the amount of `Ether` and `CD` tokens 954 | that would be sent back to the user after he removes a certain amount of `LP` 955 | tokens from the pool 956 | - The amount of `Eth` that would be sent back to the user after he withdraws the `LP` token is calculated based on a ratio, 957 | - Ratio is -> `(amount of Eth that would be sent back to the user / Eth reserve) = (LP tokens withdrawn) / (total supply of LP tokens)` 958 | - By some maths we get -> `(amount of Eth that would be sent back to the user) = (Eth Reserve * LP tokens withdrawn) / (total supply of LP tokens)` 959 | - Similarly we also maintain a ratio for the `CD` tokens, so here in our case 960 | - Ratio is -> `(amount of CD tokens sent back to the user / CD Token reserve) = (LP tokens withdrawn) / (total supply of LP tokens)` 961 | - Then `(amount of CD tokens sent back to the user) = (CD token reserve * LP tokens withdrawn) / (total supply of LP tokens)` 962 | 963 | ```javascript 964 | import { Contract, providers, utils, BigNumber } from "ethers"; 965 | import { EXCHANGE_CONTRACT_ABI, EXCHANGE_CONTRACT_ADDRESS } from "../constants"; 966 | 967 | /** 968 | * removeLiquidity: Removes the `removeLPTokensWei` amount of LP tokens from 969 | * liquidity and also the calculated amount of `ether` and `CD` tokens 970 | */ 971 | export const removeLiquidity = async (signer, removeLPTokensWei) => { 972 | // Create a new instance of the exchange contract 973 | const exchangeContract = new Contract( 974 | EXCHANGE_CONTRACT_ADDRESS, 975 | EXCHANGE_CONTRACT_ABI, 976 | signer 977 | ); 978 | const tx = await exchangeContract.removeLiquidity(removeLPTokensWei); 979 | await tx.wait(); 980 | }; 981 | 982 | /** 983 | * getTokensAfterRemove: Calculates the amount of `Eth` and `CD` tokens 984 | * that would be returned back to user after he removes `removeLPTokenWei` amount 985 | * of LP tokens from the contract 986 | */ 987 | export const getTokensAfterRemove = async ( 988 | provider, 989 | removeLPTokenWei, 990 | _ethBalance, 991 | cryptoDevTokenReserve 992 | ) => { 993 | try { 994 | // Create a new instance of the exchange contract 995 | const exchangeContract = new Contract( 996 | EXCHANGE_CONTRACT_ADDRESS, 997 | EXCHANGE_CONTRACT_ABI, 998 | provider 999 | ); 1000 | // Get the total supply of `Crypto Dev` LP tokens 1001 | const _totalSupply = await exchangeContract.totalSupply(); 1002 | // Here we are using the BigNumber methods of multiplication and division 1003 | // The amount of Eth that would be sent back to the user after he withdraws the LP token 1004 | // is calculated based on a ratio, 1005 | // Ratio is -> (amount of Eth that would be sent back to the user / Eth reserve) = (LP tokens withdrawn) / (total supply of LP tokens) 1006 | // By some maths we get -> (amount of Eth that would be sent back to the user) = (Eth Reserve * LP tokens withdrawn) / (total supply of LP tokens) 1007 | // Similarly we also maintain a ratio for the `CD` tokens, so here in our case 1008 | // Ratio is -> (amount of CD tokens sent back to the user / CD Token reserve) = (LP tokens withdrawn) / (total supply of LP tokens) 1009 | // Then (amount of CD tokens sent back to the user) = (CD token reserve * LP tokens withdrawn) / (total supply of LP tokens) 1010 | const _removeEther = _ethBalance.mul(removeLPTokenWei).div(_totalSupply); 1011 | const _removeCD = cryptoDevTokenReserve 1012 | .mul(removeLPTokenWei) 1013 | .div(_totalSupply); 1014 | return { 1015 | _removeEther, 1016 | _removeCD, 1017 | }; 1018 | } catch (err) { 1019 | console.error(err); 1020 | } 1021 | }; 1022 | ``` 1023 | 1024 | - Now it's time to write code for `swap.js` our last `utils` file 1025 | 1026 | - It has two functions `getAmountOfTokenReceivedFromSwap` and `swapTokens` 1027 | - `swapTokens` swaps certain amount of `Eth/Crypto Dev` tokens with `Crypto Dev/Eth` tokens 1028 | - If `Eth` has been selected by the user from the UI, it means that the user has `Eth` and he wants to swap it for a certain amount of `Crypto Dev` tokens 1029 | - In this case we call the `ethToCryptoDevToken` function. Note that `Eth` is sent as a value in the function because the user is paying this `Eth` to the contract. `Eth` sent is not an input param value in this case 1030 | - On the other hand, if `Eth` is not selected this means that the user wants to swap `Crypto Dev` tokens for `Eth` 1031 | - Here we call the `cryptoDevTokenToEth` 1032 | - `getAmountOfTokensReceivedFromSwap` is a function which calculates, given a certain amount of `Eth/Crypto Dev` tokens, how many `Eth/Crypto Dev` tokens would be sent back to the user 1033 | - If `Eth` is selected it calls the `getAmountOfTokens` from the contract which takes in an `input` reserve and an `output` reserve. Here, input reserve would be the `Eth` balance of the contract and output reserve would be the `Crypto Dev` token reserve. Opposite would be true, if `Eth` is not selected 1034 | 1035 | ```javascript 1036 | import { Contract } from "ethers"; 1037 | import { 1038 | EXCHANGE_CONTRACT_ABI, 1039 | EXCHANGE_CONTRACT_ADDRESS, 1040 | TOKEN_CONTRACT_ABI, 1041 | TOKEN_CONTRACT_ADDRESS, 1042 | } from "../constants"; 1043 | 1044 | /* 1045 | getAmountOfTokensReceivedFromSwap: Returns the number of Eth/Crypto Dev tokens that can be received 1046 | when the user swaps `_swapAmountWei` amount of Eth/Crypto Dev tokens. 1047 | */ 1048 | export const getAmountOfTokensReceivedFromSwap = async ( 1049 | _swapAmountWei, 1050 | provider, 1051 | ethSelected, 1052 | ethBalance, 1053 | reservedCD 1054 | ) => { 1055 | // Create a new instance of the exchange contract 1056 | const exchangeContract = new Contract( 1057 | EXCHANGE_CONTRACT_ADDRESS, 1058 | EXCHANGE_CONTRACT_ABI, 1059 | provider 1060 | ); 1061 | let amountOfTokens; 1062 | // If `Eth` is selected this means our input value is `Eth` which means our input amount would be 1063 | // `_swapAmountWei`, the input reserve would be the `ethBalance` of the contract and output reserve 1064 | // would be the `Crypto Dev` token reserve 1065 | if (ethSelected) { 1066 | amountOfTokens = await exchangeContract.getAmountOfTokens( 1067 | _swapAmountWei, 1068 | ethBalance, 1069 | reservedCD 1070 | ); 1071 | } else { 1072 | // If `Eth` is not selected this means our input value is `Crypto Dev` tokens which means our input amount would be 1073 | // `_swapAmountWei`, the input reserve would be the `Crypto Dev` token reserve of the contract and output reserve 1074 | // would be the `ethBalance` 1075 | amountOfTokens = await exchangeContract.getAmountOfTokens( 1076 | _swapAmountWei, 1077 | reservedCD, 1078 | ethBalance 1079 | ); 1080 | } 1081 | 1082 | return amountOfTokens; 1083 | }; 1084 | 1085 | /* 1086 | swapTokens: Swaps `swapAmountWei` of Eth/Crypto Dev tokens with `tokenToBeReceivedAfterSwap` amount of Eth/Crypto Dev tokens. 1087 | */ 1088 | export const swapTokens = async ( 1089 | signer, 1090 | swapAmountWei, 1091 | tokenToBeReceivedAfterSwap, 1092 | ethSelected 1093 | ) => { 1094 | // Create a new instance of the exchange contract 1095 | const exchangeContract = new Contract( 1096 | EXCHANGE_CONTRACT_ADDRESS, 1097 | EXCHANGE_CONTRACT_ABI, 1098 | signer 1099 | ); 1100 | const tokenContract = new Contract( 1101 | TOKEN_CONTRACT_ADDRESS, 1102 | TOKEN_CONTRACT_ABI, 1103 | signer 1104 | ); 1105 | let tx; 1106 | // If Eth is selected call the `ethToCryptoDevToken` function else 1107 | // call the `cryptoDevTokenToEth` function from the contract 1108 | // As you can see you need to pass the `swapAmount` as a value to the function because 1109 | // it is the ether we are paying to the contract, instead of a value we are passing to the function 1110 | if (ethSelected) { 1111 | tx = await exchangeContract.ethToCryptoDevToken( 1112 | tokenToBeReceivedAfterSwap, 1113 | { 1114 | value: swapAmountWei, 1115 | } 1116 | ); 1117 | } else { 1118 | // User has to approve `swapAmountWei` for the contract because `Crypto Dev` token 1119 | // is an ERC20 1120 | tx = await tokenContract.approve( 1121 | EXCHANGE_CONTRACT_ADDRESS, 1122 | swapAmountWei.toString() 1123 | ); 1124 | await tx.wait(); 1125 | // call cryptoDevTokenToEth function which would take in `swapAmountWei` of `Crypto Dev` tokens and would 1126 | // send back `tokenToBeReceivedAfterSwap` amount of `Eth` to the user 1127 | tx = await exchangeContract.cryptoDevTokenToEth( 1128 | swapAmountWei, 1129 | tokenToBeReceivedAfterSwap 1130 | ); 1131 | } 1132 | await tx.wait(); 1133 | }; 1134 | ``` 1135 | 1136 | - Now its time for the final stages of our app, lets add some code to the `pages/index.js` file which next already gives you. Replace all the contents of the file with the following content 1137 | 1138 | ```js 1139 | import { BigNumber, providers, utils } from "ethers"; 1140 | import Head from "next/head"; 1141 | import React, { useEffect, useRef, useState } from "react"; 1142 | import Web3Modal from "web3modal"; 1143 | import styles from "../styles/Home.module.css"; 1144 | import { addLiquidity, calculateCD } from "../utils/addLiquidity"; 1145 | import { 1146 | getCDTokensBalance, 1147 | getEtherBalance, 1148 | getLPTokensBalance, 1149 | getReserveOfCDTokens, 1150 | } from "../utils/getAmounts"; 1151 | import { 1152 | getTokensAfterRemove, 1153 | removeLiquidity, 1154 | } from "../utils/removeLiquidity"; 1155 | import { swapTokens, getAmountOfTokensReceivedFromSwap } from "../utils/swap"; 1156 | 1157 | export default function Home() { 1158 | /** General state variables */ 1159 | // loading is set to true when the transaction is mining and set to false when 1160 | // the transaction has mined 1161 | const [loading, setLoading] = useState(false); 1162 | // We have two tabs in this dapp, Liquidity Tab and Swap Tab. This variable 1163 | // keeps track of which Tab the user is on. If it is set to true this means 1164 | // that the user is on `liquidity` tab else he is on `swap` tab 1165 | const [liquidityTab, setLiquidityTab] = useState(true); 1166 | // This variable is the `0` number in form of a BigNumber 1167 | const zero = BigNumber.from(0); 1168 | /** Variables to keep track of amount */ 1169 | // `ethBalance` keeps track of the amount of Eth held by the user's account 1170 | const [ethBalance, setEtherBalance] = useState(zero); 1171 | // `reservedCD` keeps track of the Crypto Dev tokens Reserve balance in the Exchange contract 1172 | const [reservedCD, setReservedCD] = useState(zero); 1173 | // Keeps track of the ether balance in the contract 1174 | const [etherBalanceContract, setEtherBalanceContract] = useState(zero); 1175 | // cdBalance is the amount of `CD` tokens help by the users account 1176 | const [cdBalance, setCDBalance] = useState(zero); 1177 | // `lpBalance` is the amount of LP tokens held by the users account 1178 | const [lpBalance, setLPBalance] = useState(zero); 1179 | /** Variables to keep track of liquidity to be added or removed */ 1180 | // addEther is the amount of Ether that the user wants to add to the liquidity 1181 | const [addEther, setAddEther] = useState(zero); 1182 | // addCDTokens keeps track of the amount of CD tokens that the user wants to add to the liquidity 1183 | // in case when there is no initial liquidity and after liquidity gets added it keeps track of the 1184 | // CD tokens that the user can add given a certain amount of ether 1185 | const [addCDTokens, setAddCDTokens] = useState(zero); 1186 | // removeEther is the amount of `Ether` that would be sent back to the user based on a certain number of `LP` tokens 1187 | const [removeEther, setRemoveEther] = useState(zero); 1188 | // removeCD is the amount of `Crypto Dev` tokens that would be sent back to the user based on a certain number of `LP` tokens 1189 | // that he wants to withdraw 1190 | const [removeCD, setRemoveCD] = useState(zero); 1191 | // amount of LP tokens that the user wants to remove from liquidity 1192 | const [removeLPTokens, setRemoveLPTokens] = useState("0"); 1193 | /** Variables to keep track of swap functionality */ 1194 | // Amount that the user wants to swap 1195 | const [swapAmount, setSwapAmount] = useState(""); 1196 | // This keeps track of the number of tokens that the user would receive after a swap completes 1197 | const [tokenToBeReceivedAfterSwap, settokenToBeReceivedAfterSwap] = useState( 1198 | zero 1199 | ); 1200 | // Keeps track of whether `Eth` or `Crypto Dev` token is selected. If `Eth` is selected it means that the user 1201 | // wants to swap some `Eth` for some `Crypto Dev` tokens and vice versa if `Eth` is not selected 1202 | const [ethSelected, setEthSelected] = useState(true); 1203 | /** Wallet connection */ 1204 | // Create a reference to the Web3 Modal (used for connecting to Metamask) which persists as long as the page is open 1205 | const web3ModalRef = useRef(); 1206 | // walletConnected keep track of whether the user's wallet is connected or not 1207 | const [walletConnected, setWalletConnected] = useState(false); 1208 | 1209 | /** 1210 | * getAmounts call various functions to retrive amounts for ethbalance, 1211 | * LP tokens etc 1212 | */ 1213 | const getAmounts = async () => { 1214 | try { 1215 | const provider = await getProviderOrSigner(false); 1216 | const signer = await getProviderOrSigner(true); 1217 | const address = await signer.getAddress(); 1218 | // get the amount of eth in the user's account 1219 | const _ethBalance = await getEtherBalance(provider, address); 1220 | // get the amount of `Crypto Dev` tokens held by the user 1221 | const _cdBalance = await getCDTokensBalance(provider, address); 1222 | // get the amount of `Crypto Dev` LP tokens held by the user 1223 | const _lpBalance = await getLPTokensBalance(provider, address); 1224 | // gets the amount of `CD` tokens that are present in the reserve of the `Exchange contract` 1225 | const _reservedCD = await getReserveOfCDTokens(provider); 1226 | // Get the ether reserves in the contract 1227 | const _ethBalanceContract = await getEtherBalance(provider, null, true); 1228 | setEtherBalance(_ethBalance); 1229 | setCDBalance(_cdBalance); 1230 | setLPBalance(_lpBalance); 1231 | setReservedCD(_reservedCD); 1232 | setReservedCD(_reservedCD); 1233 | setEtherBalanceContract(_ethBalanceContract); 1234 | } catch (err) { 1235 | console.error(err); 1236 | } 1237 | }; 1238 | 1239 | /**** SWAP FUNCTIONS ****/ 1240 | 1241 | /** 1242 | * swapTokens: Swaps `swapAmountWei` of Eth/Crypto Dev tokens with `tokenToBeReceivedAfterSwap` amount of Eth/Crypto Dev tokens. 1243 | */ 1244 | const _swapTokens = async () => { 1245 | try { 1246 | // Convert the amount entered by the user to a BigNumber using the `parseEther` library from `ethers.js` 1247 | const swapAmountWei = utils.parseEther(swapAmount); 1248 | // Check if the user entered zero 1249 | // We are here using the `eq` method from BigNumber class in `ethers.js` 1250 | if (!swapAmountWei.eq(zero)) { 1251 | const signer = await getProviderOrSigner(true); 1252 | setLoading(true); 1253 | // Call the swapTokens function from the `utils` folder 1254 | await swapTokens( 1255 | signer, 1256 | swapAmountWei, 1257 | tokenToBeReceivedAfterSwap, 1258 | ethSelected 1259 | ); 1260 | setLoading(false); 1261 | // Get all the updated amounts after the swap 1262 | await getAmounts(); 1263 | setSwapAmount(""); 1264 | } 1265 | } catch (err) { 1266 | console.error(err); 1267 | setLoading(false); 1268 | setSwapAmount(""); 1269 | } 1270 | }; 1271 | 1272 | /** 1273 | * _getAmountOfTokensReceivedFromSwap: Returns the number of Eth/Crypto Dev tokens that can be received 1274 | * when the user swaps `_swapAmountWEI` amount of Eth/Crypto Dev tokens. 1275 | */ 1276 | const _getAmountOfTokensReceivedFromSwap = async (_swapAmount) => { 1277 | try { 1278 | // Convert the amount entered by the user to a BigNumber using the `parseEther` library from `ethers.js` 1279 | const _swapAmountWEI = utils.parseEther(_swapAmount.toString()); 1280 | // Check if the user entered zero 1281 | // We are here using the `eq` method from BigNumber class in `ethers.js` 1282 | if (!_swapAmountWEI.eq(zero)) { 1283 | const provider = await getProviderOrSigner(); 1284 | // Get the amount of ether in the contract 1285 | const _ethBalance = await getEtherBalance(provider, null, true); 1286 | // Call the `getAmountOfTokensReceivedFromSwap` from the utils folder 1287 | const amountOfTokens = await getAmountOfTokensReceivedFromSwap( 1288 | _swapAmountWEI, 1289 | provider, 1290 | ethSelected, 1291 | _ethBalance, 1292 | reservedCD 1293 | ); 1294 | settokenToBeReceivedAfterSwap(amountOfTokens); 1295 | } else { 1296 | settokenToBeReceivedAfterSwap(zero); 1297 | } 1298 | } catch (err) { 1299 | console.error(err); 1300 | } 1301 | }; 1302 | 1303 | /*** END ***/ 1304 | 1305 | /**** ADD LIQUIDITY FUNCTIONS ****/ 1306 | 1307 | /** 1308 | * _addLiquidity helps add liquidity to the exchange, 1309 | * If the user is adding initial liquidity, user decides the ether and CD tokens he wants to add 1310 | * to the exchange. If he is adding the liquidity after the initial liquidity has already been added 1311 | * then we calculate the crypto dev tokens he can add, given the Eth he wants to add by keeping the ratios 1312 | * constant 1313 | */ 1314 | const _addLiquidity = async () => { 1315 | try { 1316 | // Convert the ether amount entered by the user to Bignumber 1317 | const addEtherWei = utils.parseEther(addEther.toString()); 1318 | // Check if the values are zero 1319 | if (!addCDTokens.eq(zero) && !addEtherWei.eq(zero)) { 1320 | const signer = await getProviderOrSigner(true); 1321 | setLoading(true); 1322 | // call the addLiquidity function from the utils folder 1323 | await addLiquidity(signer, addCDTokens, addEtherWei); 1324 | setLoading(false); 1325 | // Reinitialize the CD tokens 1326 | setAddCDTokens(zero); 1327 | // Get amounts for all values after the liquidity has been added 1328 | await getAmounts(); 1329 | } else { 1330 | setAddCDTokens(zero); 1331 | } 1332 | } catch (err) { 1333 | console.error(err); 1334 | setLoading(false); 1335 | setAddCDTokens(zero); 1336 | } 1337 | }; 1338 | 1339 | /**** END ****/ 1340 | 1341 | /**** REMOVE LIQUIDITY FUNCTIONS ****/ 1342 | 1343 | /** 1344 | * _removeLiquidity: Removes the `removeLPTokensWei` amount of LP tokens from 1345 | * liquidity and also the calculated amount of `ether` and `CD` tokens 1346 | */ 1347 | const _removeLiquidity = async () => { 1348 | try { 1349 | const signer = await getProviderOrSigner(true); 1350 | // Convert the LP tokens entered by the user to a BigNumber 1351 | const removeLPTokensWei = utils.parseEther(removeLPTokens); 1352 | setLoading(true); 1353 | // Call the removeLiquidity function from the `utils` folder 1354 | await removeLiquidity(signer, removeLPTokensWei); 1355 | setLoading(false); 1356 | await getAmounts(); 1357 | setRemoveCD(zero); 1358 | setRemoveEther(zero); 1359 | } catch (err) { 1360 | console.error(err); 1361 | setLoading(false); 1362 | setRemoveCD(zero); 1363 | setRemoveEther(zero); 1364 | } 1365 | }; 1366 | 1367 | /** 1368 | * _getTokensAfterRemove: Calculates the amount of `Ether` and `CD` tokens 1369 | * that would be returned back to user after he removes `removeLPTokenWei` amount 1370 | * of LP tokens from the contract 1371 | */ 1372 | const _getTokensAfterRemove = async (_removeLPTokens) => { 1373 | try { 1374 | const provider = await getProviderOrSigner(); 1375 | // Convert the LP tokens entered by the user to a BigNumber 1376 | const removeLPTokenWei = utils.parseEther(_removeLPTokens); 1377 | // Get the Eth reserves within the exchange contract 1378 | const _ethBalance = await getEtherBalance(provider, null, true); 1379 | // get the crypto dev token reserves from the contract 1380 | const cryptoDevTokenReserve = await getReserveOfCDTokens(provider); 1381 | // call the getTokensAfterRemove from the utils folder 1382 | const { _removeEther, _removeCD } = await getTokensAfterRemove( 1383 | provider, 1384 | removeLPTokenWei, 1385 | _ethBalance, 1386 | cryptoDevTokenReserve 1387 | ); 1388 | setRemoveEther(_removeEther); 1389 | setRemoveCD(_removeCD); 1390 | } catch (err) { 1391 | console.error(err); 1392 | } 1393 | }; 1394 | 1395 | /**** END ****/ 1396 | 1397 | /** 1398 | * connectWallet: Connects the MetaMask wallet 1399 | */ 1400 | const connectWallet = async () => { 1401 | try { 1402 | // Get the provider from web3Modal, which in our case is MetaMask 1403 | // When used for the first time, it prompts the user to connect their wallet 1404 | await getProviderOrSigner(); 1405 | setWalletConnected(true); 1406 | } catch (err) { 1407 | console.error(err); 1408 | } 1409 | }; 1410 | 1411 | /** 1412 | * Returns a Provider or Signer object representing the Ethereum RPC with or 1413 | * without the signing capabilities of Metamask attached 1414 | * 1415 | * A `Provider` is needed to interact with the blockchain - reading 1416 | * transactions, reading balances, reading state, etc. 1417 | * 1418 | * A `Signer` is a special type of Provider used in case a `write` transaction 1419 | * needs to be made to the blockchain, which involves the connected account 1420 | * needing to make a digital signature to authorize the transaction being 1421 | * sent. Metamask exposes a Signer API to allow your website to request 1422 | * signatures from the user using Signer functions. 1423 | * 1424 | * @param {*} needSigner - True if you need the signer, default false 1425 | * otherwise 1426 | */ 1427 | const getProviderOrSigner = async (needSigner = false) => { 1428 | // Connect to Metamask 1429 | // Since we store `web3Modal` as a reference, we need to access the `current` value to get access to the underlying object 1430 | const provider = await web3ModalRef.current.connect(); 1431 | const web3Provider = new providers.Web3Provider(provider); 1432 | 1433 | // If user is not connected to the Rinkeby network, let them know and throw an error 1434 | const { chainId } = await web3Provider.getNetwork(); 1435 | if (chainId !== 4) { 1436 | window.alert("Change the network to Rinkeby"); 1437 | throw new Error("Change network to Rinkeby"); 1438 | } 1439 | 1440 | if (needSigner) { 1441 | const signer = web3Provider.getSigner(); 1442 | return signer; 1443 | } 1444 | return web3Provider; 1445 | }; 1446 | 1447 | // useEffects are used to react to changes in state of the website 1448 | // The array at the end of function call represents what state changes will trigger this effect 1449 | // In this case, whenever the value of `walletConnected` changes - this effect will be called 1450 | useEffect(() => { 1451 | // if wallet is not connected, create a new instance of Web3Modal and connect the MetaMask wallet 1452 | if (!walletConnected) { 1453 | // Assign the Web3Modal class to the reference object by setting it's `current` value 1454 | // The `current` value is persisted throughout as long as this page is open 1455 | web3ModalRef.current = new Web3Modal({ 1456 | network: "rinkeby", 1457 | providerOptions: {}, 1458 | disableInjectedProvider: false, 1459 | }); 1460 | connectWallet(); 1461 | getAmounts(); 1462 | } 1463 | }, [walletConnected]); 1464 | 1465 | /* 1466 | renderButton: Returns a button based on the state of the dapp 1467 | */ 1468 | const renderButton = () => { 1469 | // If wallet is not connected, return a button which allows them to connect their wllet 1470 | if (!walletConnected) { 1471 | return ( 1472 | 1475 | ); 1476 | } 1477 | 1478 | // If we are currently waiting for something, return a loading button 1479 | if (loading) { 1480 | return ; 1481 | } 1482 | 1483 | if (liquidityTab) { 1484 | return ( 1485 |