Part 2: Building a Decentralized Exchange with Solidity and React

Part 2: Building a Decentralized Exchange with Solidity and React

Write and test your first token swap function, fork the Binance Smart Chain with Hardhat and Moralis and integrate your local blockchain with Metamask

Welcome back frens!

In Part One we got to setting up our base project and our constructor function for the DEX. Today we'll be working on our first token swap function and exploring testing with hardhat.

Here's a live github repo of the project to keep-up with the article and video series.

To understand more about PancakeRouter integration for this project, watch Gabi's breakdown

First Swap Function - $BNB

In our very first function, we'll be allowing the user to send Binance native tokens $BNB to the smart contract for exchange, giving the Router permission to transact the token for the swap, performing the actual token swap and emitting an event which says "Hey! Just swapped my BNB for some select tokens!"

First, we need to declare an event which will be emitted when token swaps are successfully complete…

event SwapTransfer (address from, address to, address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOut);

but why?

Turns out, events are Solidity's way of logging transaction details in smart contracts. Essentially, we declare an event that accepts certain parameters, and then we emit it in a function after a certain transaction has occurred. The event is basically an EVM dispatch signal we can listen to on the client side, with information about whatever transaction triggers it.

Next, we declare our function body,

function swapExactBNBForTokens(uint amountOutMin, address tokenOut) external payable {
}

external? this indicates a function that can only by called by external parties, and not within the same smart contract.

payable? this modifier is used to indicate that a function can transact (receive and send) within a contract.

Inside this function, we'll declare a fixed-size array that holds two addresses. The first address being that of the token we are trading in, and the second address being that of the token we would be receiving in return.

function swapExactBNBForTokens(uint amountOutMin, address tokenOut) external payable {
address[] memory path = new address[](2);
path[0] = pancakeRouter.WETH();  // technically wrappedBNB
path(1) =  tokenOut;   // address of selected token on frontend
}

Next, we approve the Router to transact our token so it can perform the swap. For this, we'll need a dependency, the IERC20 approve() function which allows another contract to transact tokens for a user.

Here's what the IERC20 approval function looks like

approve(address spender, uint265 amount);

Navigate to your terminal and install this dependency

npm install @openzeppelin/contracts

Then import this line at the top of your contract

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

Back in our function, we'll implement this function using our Router

IERC20(pancakeRouter.WETH()).approve(address(pancakeRouter), msg.value);
// technically wrappedBNB

This line means, the "wrapped" token-$BNB is approving pancakeRouter to transact a user provided amount of tokens . msg.value represents the user inputted amount.

Note: WETH here means wrapped $ETH, but since we're on the Binance Smart Chain, it represents wrapped $BNB, the native token of BSC. Now we write the swap!

pancakeRouter.swapExactETHForTokens{value: msg.value}(amountOutMin, path, msg.sender, block.timestamp + 60 * 10);

// technically its swapExactBNBForTokens

We call the function in pancakeRouter to swap our exact amount of BNB tokens for any other token we want. Let's examine the interface of this function to understand the parameters it accepts.

function swapExactETHForTokens(
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external payable returns (uint[] memory amounts);

the amountOutMin represents the minimum amount of tokens sent back to the user. the path array holds the addresses of both tokens being swapped. the address to refers to the destination of the token after it's swapped. the deadline is a specified time limit in which a transaction fails, to avoid long pending transactions and inaccurate prices. But holdon! There's a certain piece of syntax lost on us - {value: msg.value}

This represents solidity's way of sending a blockchain's native token to a payable function. Without the payable keyword, this transaction will revert. So this is how we send the user's BNB to the contract.

Moving On…

The next thing we need to do is get the amountsOut from the router, we do so using an internal pancakeRouter function that accepts the user's input, the path array and returns two amounts - the number of tokens sent in and number of tokens sent back to the user.

uint256[] memory amounts = pancakeRouter.getAmountsOut(msg.value, path);

We then log our token swap using the emit keyword and passing arguments to the event we created earlier.

emit SwapTransfer(address(pancakeRouter), msg.sender, pancakeRouter.WETH(), tokenOut, msg.value, amounts[1]);

Marvelous work! We've effectively sent BNB to our contract, approved the contract to transact our tokens, swapped the tokens and logged that transaction from the blockchain!

Your function should look like this now.

func.png

Testing our first Function with Hardhat and Moralis

We're ready to test our function and see it in action. So head over to the folder named tests in your project directory and open sample.js. In there, you should find some sample test functions that come by default with a hardhat project.

Screenshot 2022-06-08 120708.png

We'll be changing the file name to TestDexter.js and updating the function descriptions inside the describe block.

...
describe("Dex Testing", () => {

  it("Should accept user's BNB and swap for Cake", async () => { }
}

Next, we'll setup our constants, including parameters for the swap function and then a beforeEach method to deploy the smart contract so we can run the swap function.

For our swap function, recall we need the contract address of both tokens.

Note - in production, these token addresses are automatically fetched by the DEX after user picks what token they wish to exchange for. But for the purpose of writing tests, we need to provide these addresses ourselves.

Head over to pancakeswap docs and copy the contract address for the PancakeRouter.

And retrieve the contract address of the $cake token over here.

Once you're done setting up constants, the next line you need is getting a signer with ethers.

const accounts = await ethers.getSigners();

owner = accounts[0]

A Signer in Ethers.js is an object that represents an Ethereum account. It's used to send transactions to contracts and other accounts. Here we're getting a list of the accounts in the node we're connected to, which in this case is Hardhat Network, and only keeping the first/owner account for our test purposes - source is Hardhat documentation on testing

Next, we create a contract factory for any instances of our exchange we need to deploy

const DexterExchangeContract = await ethers.getContractFactory("DexterExchange");

This line basically says, "from our smart contract, generate a factory and store in a variable" - this makes it easy to run tests on instances of the same smart contract.

read up on the factory pattern in OOP to gain more insight.

After creating the factory, we deploy the smart contract with ethers and log the address of the contract to the console.

...

dexterExchange = await DexterExchangeContract.deploy(pancakeRouterAddress); 
await dexterExchange.deployed();
console.log(`dexterExchange deployed at ${dexterExchange.address}`);

For the actual test block,

it("Should accept user's BNB and swap for Cake", async () => {
     const bnb_cake_swap_tx = await dexterExchange.connect(owner).swapExactBNBForTokens
(0, 
cakeTokenAddress,
{value: ethers.utils.parseEther("500")});  
});

This code snippet gets the connect method from our exchange to connect the owner to the contract, then tests the swap function we wrote.

For our function parameters, we will pass in zero as theamountOutMin parameter, pass in the cakeTokenAddress for the tokenOut parameter and pass in the amount of tokens we wish to send to the contract using the utils.parseEther method.

At this point, your entire test file should look like this.

test ready.png

Up Next: Setting up Hardhat Config

Head over to your hardhat.config.js file and replace the existing module.exports object with this

...
module.exports = {
  solidity: "0.8.4",
  networks: {
    hardhat: {
      chainId: 31337,
    },
    localhost: {
      url: "https://localhost:8545",
      chainId: 31337,
    },
  },
};

This sets up our hardhat local chain id and sets up localhost as the destination url for the forked version of the BSC mainnet.

Forking Binance Smartchain Mainnet From Moralis

Next, you want to head to moralis, create an account and head over to the admin dashboard. On the bottom-left side of the dashboard, click on "speedy nodes". This will give you a list of options.

moralis.png

On the Binance Smart Chain, click on the button that says endpoints and it should reveal a list of endpoints.

Copy the url to the "mainnet archive", head back to your project terminal and enter the following command

npx hardhat node --fork "paste mainnet archive url here"

This command will complete a fork of the BSC mainnet and you should see the following result in your terminal.

hardhatForked.png

Congrats! You now have a local running node of the Binance Smart Chain.

Setting up Local Blockchain in Metamask

Now, we get to add our mainnet fork to Metamask so we can view changes in token amounts after running our test swaps.

If you haven't already, head over to the download page and install metamask for your web browser of choice. Once you're done setting installing and setting up a wallet, head over to settings, click on networks and hit add network. This should open up the following page

metamask.png

Enter the following details next...

details.png

You might encounter an error here since we entered the values AFTER running the node. To fix this, close your current running node in the terminal and run the command again

npx hardhat node --fork "paste mainnet archive url here"

Then re-enter your chain id in the field and click "save".

Voila! Your own local binance chain is running and recognized by metamask. You should be able to see all your wallet information now with the currency $BNB.

binance complete.png

Import a Wallet

To import one of the available free accounts on the BSC localhost fork, right click on the account button in the metamask extension.

import.png

head to the terminal in your project and copy the private key from the first account and paste in the 'private key' field.

asaas.png

Running our Tests

Whew! Finally!

To run your test function, type this command into the terminal

npx hardhat test --network localhost

If all's well, this should be your result!

green test.png

Now head back to your metamask wallet and notice your native BNB balance is no longer 10,000 but now

BNBSwap.png

Wait! Where's the token I swapped for? Where's my CAKE?!!!!!

Well...see, we lied...

JK

Turns out you need to import a token for your wallet to recognize it.

So, on your wallet page, click "import token", head over to coinmarketcap and copy the contract address for the $CAKE token.

Paste it into your import form and it should automatically recognize the token.

Great work! You've officially performed the sexiest token swap in DeFi history, on your own local smart chain. Here's my result!

swap tokens.png

Tweet us about it!

Rounding up - Next Lesson Preview

Hey! I know this was a much longer lesson but you did amazing, if you have any questions or would simply like to keep up with the series, reach myself or Gabi.

In the next lesson, we'll be writing and testing more swap functions for various types of tokens and discussing advanced solidity patterns. Thanks for joining us!