Atharva Deosthale Web Developer and Designer | JavaScript = ❤ | MERN Stack Developer

Creating a full-stack DeFi app with Polygon

10 min read 2902

DeFi is now a major topic of discussion in the cryptocurrency space. DeFi stands for “Decentralized finance,” which means that there’s no central authority keeping an eye on and controlling the transfer of funds. This also means that transactions in DeFi are P2P (peer to peer), which means that no central authority is responsible for transferral, and funds are sent directly from one entity to another.

In this article we will learn how to get started with DeFi by making a full-stack DeFi app on the Polygon chain using Next.js as the frontend. This app will sell and purchase OKToken (a fictional token) from the user. However, every purchase transaction reduces one token from the amount of tokens you can get per MATIC (selling increases this number by one). This is not an ideal demonstration, but this way you can understand how to use your own logic in Solidity smart contracts and learn to create your own full-stack DeFi app using Polygon.

Contents

Requirements

To get started with this tutorial, make sure you have the following:

Now that you have checked the requirements, let’s proceed with creating our Hardhat project to work with our Solidity smart contracts.

Creating a Hardhat project

Navigate to a safe directory and run the following command in the terminal to initialize your Hardhat project:

npx hardhat

Once you run the command, you should see the following Hardhat initialization wizard in your terminal.

hardhat-initialization

From the list, choose Create an advanced sample project. Then you will be asked where you want to initialize the Hardhat project; don’t change the field, just press Enter so that the project gets initialized in the current directory.

Then you will be asked whether or not you want to install dependencies required for your Hardhat project to run. Press y because we will be needing these dependencies, and installing them right now is the best idea.

Installation of dependencies will start, and might take a few seconds or minutes depending upon the machine you’re running. Now, run the following command in the terminal to install another dependency we will need to ease our Solidity contract development:

We made a custom demo for .
No really. Click here to check it out.

npm install @openzeppelin/contracts

OpenZeppelin provides smart contract standards that we can use in our own smart contracts to easily create an Ownable, ERC-20 and ERC-721 contracts, and more.

Once the dependencies are successfully installed, open the directory in a code editor. I’ll be using VS Code for this tutorial.

We will be creating two smart contracts: the first one will be our ERC-20 token itself and the second will be a vendor contract, which will facilitate buying and selling of these tokens.

Creating our smart contracts

Now, go to the contracts folder and create a new Solidity file named OKToken.sol, which will contain our ERC-20 token contract.

Use the following code for this file:

// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.4;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract OKToken is ERC20 {
    constructor() ERC20("OKT", "OKToken"){
        _mint(msg.sender, 10000 * 10 ** 18);
    }
}

In the above code, we are importing the ERC20.sol file from @openzeppelin/contracts which will help us get started with an ERC-20 token easily. Then, in the constructor, we are providing the symbol "OKT" and name "OKToken" for our token.

That’s all for the token contract! Now, let’s work on the vendor contract. Under the contracts folder, create a new file named OKVendor.sol with the following code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "./OKToken.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract OKVendor is Ownable {
  OKToken yourToken;
  uint256 public tokensPerNativeCurrency = 100;
  event BuyTokens(address buyer, uint256 amountOfNativeCurrency, uint256 amountOfTokens);
  constructor(address tokenAddress) {
    yourToken = OKToken(tokenAddress);
  }

  function buyTokens() public payable returns (uint256 tokenAmount) {
    require(msg.value > 0, "You need to send some NativeCurrency to proceed");
    uint256 amountToBuy = msg.value * tokensPerNativeCurrency;

    uint256 vendorBalance = yourToken.balanceOf(address(this));
    require(vendorBalance >= amountToBuy, "Vendor contract has not enough tokens to perform transaction");

    (bool sent) = yourToken.transfer(msg.sender, amountToBuy);
    require(sent, "Failed to transfer token to user");
    tokensPerNativeCurrency = tokensPerNativeCurrency - 1;

    emit BuyTokens(msg.sender, msg.value, amountToBuy);
    return amountToBuy;
  }
  function sellTokens(uint256 tokenAmountToSell) public {

    require(tokenAmountToSell > 0, "Specify an amount of token greater than zero");

    uint256 userBalance = yourToken.balanceOf(msg.sender);
    require(userBalance >= tokenAmountToSell, "You have insufficient tokens");

    uint256 amountOfNativeCurrencyToTransfer = tokenAmountToSell / tokensPerNativeCurrency;
    uint256 ownerNativeCurrencyBalance = address(this).balance;
    require(ownerNativeCurrencyBalance >= amountOfNativeCurrencyToTransfer, "Vendor has insufficient funds");
    (bool sent) = yourToken.transferFrom(msg.sender, address(this), tokenAmountToSell);
    require(sent, "Failed to transfer tokens from user to vendor");

    (sent,) = msg.sender.call{value: amountOfNativeCurrencyToTransfer}("");
    tokensPerNativeCurrency = tokensPerNativeCurrency + 1;
    require(sent, "Failed to send NativeCurrency to the user");
  }
  function getNumberOfTokensInNativeCurrency() public view returns(uint256) {
    return tokensPerNativeCurrency;
  }

  function withdraw() public onlyOwner {
    uint256 ownerBalance = address(this).balance;
    require(ownerBalance > 0, "No NativeCurrency present in Vendor");
    (bool sent,) = msg.sender.call{value: address(this).balance}("");
    require(sent, "Failed to withdraw");
  }
}

This will help us facilitate the buying and selling of tokens.

In the above contract, first we are importing our token contract, which we need in order to interact with our token contract using the vendor contract and call functions.

We are also importing Ownable.sol from @openzeppelin/contracts. This means that the owner of the smart contract can transfer its ownership and have access to owners-only functions.

After initializing the smart contract, we define the variable tokensPerNativeCurrency which states the number of tokens which can be purchased using 1 MATIC. We will be altering this number based on the transactions made.

We then have a constructor which will take OKToken’s contract address so that we can communicate with the deployed contract and perform functions on them.

In the buyTokens() function, we are performing checks to ensure the proper amount of MATIC is sent to the smart contract, and that the vendor contract has the required amount of tokens. Then we call the function transfer() from the OKToken instance we previously created to transfer the tokens to the request sender.

In the sellTokens() function, we are performing checks to ensure that the request sender has enough tokens and if the vendor contract has enough MATIC to send back to the request sender. Then, we use the transferFrom() function from the OKToken instance we previously created to transfer the tokens from the request sender’s wallet to the smart contract. However, the sender needs to approve this transaction; we perform this approval on the client side before making the request. We will cover this part when we make the front end of this application.

Finally, we have the withdraw() function, which is only accessible by the owner of the contracts. It allows them to withdraw all the MATIC present on the contract.

Now that we have the smart contracts ready, let’s deploy them to Polygon Mumbai testnet!

Deploying our smart contracts

We will be creating a script to deploy our contract to Polygon Mumbai. Once the contracts are deployed, we will programmatically send all the tokens stored on the deployer’s wallet to the vendor contract.

First go to hardhat.config.js and under module.exports, add the following object so that Hardhat knows which network to connect to:

networks: {
  mumbai: {
    url: "https://matic-mumbai.chainstacklabs.com",
    accounts: ["PRIVATE KEY HERE"],
  }
}

We are providing the network a name (mumbai in this case) and providing an RPC URL. The mentioned RPC URL is for Polygon Mumbai. If you want to use Polygon Mainnet you can choose your RPC URL. Remember to enter your own wallet private key with some test MATIC to pay for gas fees involved in the smart contract deployment process.

Now, under the scripts folder, create a new file called deploy.js. Paste in the following:

const { BigNumber, utils } = require("ethers");
const hardhat = require("hardhat");
async function main() {
  const OKToken = await hardhat.ethers.getContractFactory("OKToken");
  const oktoken = await OKToken.deploy();
  await oktoken.deployed();
  console.log("[📥] OKToken deployed to address: " + oktoken.address);
  const OKVendor = await hardhat.ethers.getContractFactory("OKVendor");
  const okvendor = await OKVendor.deploy(oktoken.address);
  console.log("[📥] OKVendor deployed to address: " + okvendor.address);
  await oktoken.deployed();
  // Transfer oktokens to vendor
  await oktoken.functions.transfer(okvendor.address, utils.parseEther("10000"));
  console.log("[🚀] Tokens transferred to OKVendor");
}
main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
});

In the above file, we are instructing Hardhat how to deploy our contract. The main() function is the entry point here. First, we get the OKToken contract and deploy it. Then, we get the OKVendor contract, provide OKToken contract address in the constructor, and deploy the contract. Then, we transfer all the funds from OKToken contract to OKVendor contract.

Run the following command in the terminal to run the script and deploy our contracts to the Polygon Mumbai network:

npx hardhat run --network mumbai scripts/deploy.js --show-stack-traces

Note that the network name must match the one mentioned in hardhat.config.js. After running the script, the contracts should be deployed and you should see the following in your terminal:

deployed OKToken contracts

If you see an output similar to this, your smart contracts have been deployed and configured successfully. Now, let’s proceed with creating our Next.js application.

Creating a Next.js DeFi app

Under the same directory, run the following command in the terminal to create your Next.js app:

npx create-next-app frontend

The above command will create a new app and automatically install necessary dependencies.

Navigate to the frontend folder and use the following command in the terminal to install additional dependencies, which will help us interact with our smart contracts:

yarn add @thirdweb-dev/react @thirdweb-dev/sdk ethers web3

We are installing @thirdweb-dev/react and @thirdweb-dev/sdk so that we can easily authenticate the user and connect their wallets to our app using MetaMask. ethers is a required dependency for thirdweb so we need to install that as well. Finally, we are installing web3 so that we can interact with our smart contract.

Adding the thirdweb provider

To get started, we need to wrap our app within a thirdwebProvider so that thirdweb can function properly.

Go to your _app.js file under pages folder and add in the following:

import { thirdwebProvider, ChainId } from "@thirdweb-dev/react";
import "../styles/globals.css";
function MyApp({ Component, pageProps }) {
  return (
    <thirdwebProvider desiredChainId={ChainId.Mumbai}>
      <Component {...pageProps} />
    </thirdwebProvider>
  );
}
export default MyApp;

In the above code, we are importing thirdwebProvider and enclosing our app within it. We are also providing a desiredChainId of the chain ID of Polygon Mumbai. You can also use the chain ID for Polygon Mainnet if you wish to do so.

Create a new file in your Next.js app root called contracts.js and add the following content:

export const oktoken = {
  contractAddress: "0xE83DD81890C76BB8c4b8Bc6365Ad95E5e71495E5",
  abi: [
    {
      inputs: [],
      stateMutability: "nonpayable",
      type: "constructor",
    },
    {
      anonymous: false,
      inputs: [
        {
          indexed: true,
          internalType: "address",
          name: "owner",
          type: "address",
        },
        {
          indexed: true,
          internalType: "address",
          name: "spender",
          type: "address",
        },
        {
          indexed: false,
          internalType: "uint256",
          name: "value",
          type: "uint256",
        },
      ],
      name: "Approval",
      type: "event",
    },
    {
      anonymous: false,
      inputs: [
        {
          indexed: true,
          internalType: "address",
          name: "from",
          type: "address",
        },
        {
          indexed: true,
          internalType: "address",
          name: "to",
          type: "address",
        },
        {
          indexed: false,
          internalType: "uint256",
          name: "value",
          type: "uint256",
        },
      ],
      name: "Transfer",
      type: "event",
    },
    {
      inputs: [
        {
          internalType: "address",
          name: "owner",
          type: "address",
        },
        {
          internalType: "address",
          name: "spender",
          type: "address",
        },
      ],
      name: "allowance",
      outputs: [
        {
          internalType: "uint256",
          name: "",
          type: "uint256",
        },
      ],
      stateMutability: "view",
      type: "function",
    },
    {
      inputs: [
        {
          internalType: "address",
          name: "spender",
          type: "address",
        },
        {
          internalType: "uint256",
          name: "amount",
          type: "uint256",
        },
      ],
      name: "approve",
      outputs: [
        {
          internalType: "bool",
          name: "",
          type: "bool",
        },
      ],
      stateMutability: "nonpayable",
      type: "function",
    },
    {
      inputs: [
        {
          internalType: "address",
          name: "account",
          type: "address",
        },
      ],
      name: "balanceOf",
      outputs: [
        {
          internalType: "uint256",
          name: "",
          type: "uint256",
        },
      ],
      stateMutability: "view",
      type: "function",
    },
    {
      inputs: [],
      name: "decimals",
      outputs: [
        {
          internalType: "uint8",
          name: "",
          type: "uint8",
        },
      ],
      stateMutability: "view",
      type: "function",
    },
    {
      inputs: [
        {
          internalType: "address",
          name: "spender",
          type: "address",
        },
        {
          internalType: "uint256",
          name: "subtractedValue",
          type: "uint256",
        },
      ],
      name: "decreaseAllowance",
      outputs: [
        {
          internalType: "bool",
          name: "",
          type: "bool",
        },
      ],
      stateMutability: "nonpayable",
      type: "function",
    },
    {
      inputs: [
        {
          internalType: "address",
          name: "spender",
          type: "address",
        },
        {
          internalType: "uint256",
          name: "addedValue",
          type: "uint256",
        },
      ],
      name: "increaseAllowance",
      outputs: [
        {
          internalType: "bool",
          name: "",
          type: "bool",
        },
      ],
      stateMutability: "nonpayable",
      type: "function",
    },
    {
      inputs: [],
      name: "name",
      outputs: [
        {
          internalType: "string",
          name: "",
          type: "string",
        },
      ],
      stateMutability: "view",
      type: "function",
    },
    {
      inputs: [],
      name: "symbol",
      outputs: [
        {
          internalType: "string",
          name: "",
          type: "string",
        },
      ],
      stateMutability: "view",
      type: "function",
    },
    {
      inputs: [],
      name: "totalSupply",
      outputs: [
        {
          internalType: "uint256",
          name: "",
          type: "uint256",
        },
      ],
      stateMutability: "view",
      type: "function",
    },
    {
      inputs: [
        {
          internalType: "address",
          name: "to",
          type: "address",
        },
        {
          internalType: "uint256",
          name: "amount",
          type: "uint256",
        },
      ],
      name: "transfer",
      outputs: [
        {
          internalType: "bool",
          name: "",
          type: "bool",
        },
      ],
      stateMutability: "nonpayable",
      type: "function",
    },
    {
      inputs: [
        {
          internalType: "address",
          name: "from",
          type: "address",
        },
        {
          internalType: "address",
          name: "to",
          type: "address",
        },
        {
          internalType: "uint256",
          name: "amount",
          type: "uint256",
        },
      ],
      name: "transferFrom",
      outputs: [
        {
          internalType: "bool",
          name: "",
          type: "bool",
        },
      ],
      stateMutability: "nonpayable",
      type: "function",
    },
  ],
};
export const okvendor = {
  contractAddress: "0xAa3b8cbB24aF3EF68a0B1760704C969E57c53D7A",
  abi: [
    {
      inputs: [
        {
          internalType: "address",
          name: "tokenAddress",
          type: "address",
        },
      ],
      stateMutability: "nonpayable",
      type: "constructor",
    },
    {
      anonymous: false,
      inputs: [
        {
          indexed: false,
          internalType: "address",
          name: "buyer",
          type: "address",
        },
        {
          indexed: false,
          internalType: "uint256",
          name: "amountOfNativeCurrency",
          type: "uint256",
        },
        {
          indexed: false,
          internalType: "uint256",
          name: "amountOfTokens",
          type: "uint256",
        },
      ],
      name: "BuyTokens",
      type: "event",
    },
    {
      anonymous: false,
      inputs: [
        {
          indexed: true,
          internalType: "address",
          name: "previousOwner",
          type: "address",
        },
        {
          indexed: true,
          internalType: "address",
          name: "newOwner",
          type: "address",
        },
      ],
      name: "OwnershipTransferred",
      type: "event",
    },
    {
      inputs: [],
      name: "buyTokens",
      outputs: [
        {
          internalType: "uint256",
          name: "tokenAmount",
          type: "uint256",
        },
      ],
      stateMutability: "payable",
      type: "function",
    },
    {
      inputs: [],
      name: "getNumberOfTokensInNativeCurrency",
      outputs: [
        {
          internalType: "uint256",
          name: "",
          type: "uint256",
        },
      ],
      stateMutability: "view",
      type: "function",
    },
    {
      inputs: [],
      name: "owner",
      outputs: [
        {
          internalType: "address",
          name: "",
          type: "address",
        },
      ],
      stateMutability: "view",
      type: "function",
    },
    {
      inputs: [],
      name: "renounceOwnership",
      outputs: [],
      stateMutability: "nonpayable",
      type: "function",
    },
    {
      inputs: [
        {
          internalType: "uint256",
          name: "tokenAmountToSell",
          type: "uint256",
        },
      ],
      name: "sellTokens",
      outputs: [],
      stateMutability: "nonpayable",
      type: "function",
    },
    {
      inputs: [],
      name: "tokensPerNativeCurrency",
      outputs: [
        {
          internalType: "uint256",
          name: "",
          type: "uint256",
        },
      ],
      stateMutability: "view",
      type: "function",
    },
    {
      inputs: [
        {
          internalType: "address",
          name: "newOwner",
          type: "address",
        },
      ],
      name: "transferOwnership",
      outputs: [],
      stateMutability: "nonpayable",
      type: "function",
    },
    {
      inputs: [],
      name: "withdraw",
      outputs: [],
      stateMutability: "nonpayable",
      type: "function",
    },
  ],
};

Remember to replace the contract addresses with your own so that the Next.js app tries to connect to the correct smart contract.

Now let’s start coding up our app. Open index.js file under pages folder and add the following:

import { useAddress, useContract, useMetamask } from "@thirdweb-dev/react";
import Head from "next/head";
import Image from "next/image";
import { oktoken, okvendor } from "../contracts";
import styles from "../styles/Home.module.css";
import { useEffect, useState } from "react";
import Web3 from "web3";
const web3 = new Web3(Web3.givenProvider);
export default function Home() {
  const [tokensPerCurrency, setTokensPerCurrency] = useState(0);
  const [tokens, setTokens] = useState(0);
  const address = useAddress();
  const connectUsingMetamask = useMetamask();
  const account = web3.defaultAccount;
  const purchase = async () => {
    const contract = new web3.eth.Contract(
      okvendor.abi,
      okvendor.contractAddress
    );
    const ethToSend = tokens / tokensPerCurrency;
    const purchase = await contract.methods.buyTokens().send({
      from: address,
      value: web3.utils.toWei(ethToSend.toString(), "ether"),
    });
    console.log(purchase);
    await fetchPrice();
  };
  const sell = async () => {
    const vendorContract = new web3.eth.Contract(
      okvendor.abi,
      okvendor.contractAddress
    );
    const tokenContract = new web3.eth.Contract(
      oktoken.abi,
      oktoken.contractAddress
    );
    const approve = await tokenContract.methods
      .approve(
        okvendor.contractAddress,
        web3.utils.toWei(tokens.toString(), "ether")
      )
      .send({
        from: address,
      });
    const sellTokens = await vendorContract.methods.sellTokens(tokens).send({
      from: address,
    });
    await fetchPrice();
  };
  const fetchPrice = async () => {
    const contract = new web3.eth.Contract(
      okvendor.abi,
      okvendor.contractAddress
    );
    const priceFromContract = await contract.methods
      .getNumberOfTokensInNativeCurrency()
      .call();
    setTokensPerCurrency(priceFromContract);
  };
  useEffect(() => {
    fetchPrice();
  }, []);
  return (
    <div>
      <Head>
        <title>Exchange OKTokens</title>
      </Head>
      {address ? (
        <div>
          <p>Tokens per currency: {tokensPerCurrency}</p>
          <div>
            <input
              type="number"
              value={tokens}
              onChange={(e) => setTokens(e.target.value)}
            />
          </div>
          <button onClick={purchase}>Purchase</button>
          <button onClick={sell}>Sell</button>
        </div>
      ) : (
        <div>
          <button onClick={connectUsingMetamask}>Connect using MetaMask</button>
        </div>
      )}
    </div>
  );
}

This is a long code block, so let’s see what the code is doing step by step:

  • Initializing the web3 package using a provider set up by thirdweb
  • Using thirdweb hooks useMetamask() to authenticate and useAddress() to check authentication state, then rendering the login button if the user doesn’t have wallet connected using MetaMask
  • Setting various states to map text boxes in our app
  • Creating a fetchPrice() function to interact with our smart contract and check how many tokens one MATIC can get, while also creating an useEffect to check this price whenever the page loads
  • Creating a purchase() function, which initializes our vendor contract and calls the buyTokens() function from the contract, then sending some MATIC along with this transaction. Then, we call fetchPrice() so that the latest price is shown

Finally, we are creating a sell() function, which initializes both token and vendor contract. First we interact with token contract’s approve() function and allow the vendor contract to transfer funds on our behalf. We then are calling sellTokens() function from the vendor contract to finally sell the tokens and receive MATIC. We are also calling fetchPrice() to get the latest price after transaction.

Our simple DeFi app is complete! You can view this app in your browser by running the following command:

yarn dev

Now once you visit http://localhost:3000, you should see the following screen, and you should be able to make transactions.

transaction functionality

Conclusion

This was a simple tutorial on how to create your own full-stack DeFi app based on Polygon. You can implement your own logic on the smart contracts to make it even better depending on your organization. I suggest tinkering around with the code so that you can learn in the best way possible.

WazirX, Bitso, and Coinsquare use LogRocket to proactively monitor their Web3 apps

Client-side issues that impact users’ ability to activate and transact in your apps can drastically affect your bottom line. If you’re interested in monitoring UX issues, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.https://logrocket.com/signup/

LogRocket is like a DVR for web and mobile apps, recording everything that happens in your web app or site. Instead of guessing why problems happen, you can aggregate and report on key frontend performance metrics, replay user sessions along with application state, log network requests, and automatically surface all errors.

Modernize how you debug web and mobile apps — .

Atharva Deosthale Web Developer and Designer | JavaScript = ❤ | MERN Stack Developer

Leave a Reply