How to Build an ERC-20 Staking App

How to Build an ERC-20 Staking App

In this guide, we'll walk through how to create an ERC-20 token staking app using thirdweb's Connect SDK and pre-built smart contracts.

By the end, you'll have an application that allows users to:

  • Stake an ERC-20 token and earn a reward token
  • Withdraw staked tokens
  • Claim accumulated rewards
  • View staking balances and earned rewards

The full video tutorial is available here:

Here is the repo of the ERC-20 staking app we will be building:

GitHub - thirdweb-example/youtube-erc20-staking
Contribute to thirdweb-example/youtube-erc20-staking development by creating an account on GitHub.

Ready to start building? Let's jump in!

Prerequisites

Before getting started, make sure you have the following:

  1. A thirdweb account
  2. Node.js installed on your machine
  3. Web3 wallet like Metamask
  4. Some testnet funds on the testnet of your choice

Deploying the Smart Contracts

Our staking app will make use of three key smart contracts:

  1. ERC-20 Staking Token: the token that users stake to earn rewards
  2. ERC-20 Reward Token: the token that users receive as staking rewards
  3. ERC-20 Staking Contract: manages staking, rewards, withdrawals

To deploy these contracts, head over to the thirdweb dashboard, connect your wallet, and go to the Contracts page:

  1. Deploy 2 ERC-20 Token contracts, one for the staking token and the other for the reward token.
Token - ERC20 | Published Smart Contract
Create cryptocurrency compliant with ERC20 standard. Deploy Token in one click with thirdweb.
  1. Deploy an ERC-20 Staking contract for the staking contract that will handle the staking functionality.

    When deploying the ERC-20 Staking contract you will have to use the contract addresses of the 2 ERC-20 tokens (staking and reward token), so be sure to deploy those smart contracts first before deploying the staking.
StakeERC20 | Published Smart Contract
Contract for staking ERC20 tokens, for another ERC20 token as rewards.. Deploy StakeERC20 in one click with thirdweb.
  1. Next, mint a supply of the staking token and reward token from their contracts (e.g. 1,000,000 tokens)

Depositing Reward Tokens in Staking Smart Contract

To allow the Staking contract to distribute rewards we need to allow it permission to distribute and deposit the tokens for it to distribute.

  1. Open the Reward Token contract you deployed earlier
  2. Go to the "Explorer" page and use the "approve" function
  3. Set the spender as the Staking Contract address
  4. Set the amount to the max rewards you want to distribute (this value should be in wei)
  5. Back on the Staking Contract "Explorer" page, call the "depositRewardTokens" function
  6. Input the rewards amount that was approved in the previous step (this value should be in wei)

Project Setup

We'll be using Next.js and the thirdweb Connect SDK to build our staking app. To get started:

  1. Open your terminal and create a new Next.js project:
npx create-next-app <your_project_name>
  1. Choose a project name, select Next.js and TypeScript when prompted
  2. Navigate to the project directory:
cd <your_project_name>
  1. Install the thirdweb SDK: (supports npm, pnpm, yarn, and bun)
npm i thirdweb
  1. Open the project in your code editor of choice

Configure the thirdweb Connect SDK

Back in our project, configure the thirdweb SDK:

  1. Create your thirdweb client with createThirdwebClient (Be sure to add your client ID into your environment variables)
import { createThirdwebClient } from 'thirdweb';

const CLIENT_ID = process.env.NEXT_PUBLIC_CLIENT_ID as string;

export const client = createThirdwebClient({
    clientId: CLIENT_ID,
});
  1. Define your chain you deployed your smart contracts to
import { defineChain } from "thirdweb";

// Replace <chain_id> with the chain id of your chain
export const chain = defineChain( "<chain_id>" );
  1. If using Next.js app router, export ThirdwebProvider with use-client
'use client';

export { ThirdwebProvider, ConnectEmbed } from 'thirdweb/react';

Setup and Configure Smart Contracts with Connect SDK

Now, lets wrap our contracts for easy use within the Connect SDK.

  1. Create a utils folder, then create a contracts folder.
  2. Using getContract we can create a wrapper for our ERC-20 and ERC-20 Staking smart contracts
import { chain } from "@/app/chain";
import { client } from "@/app/client";
import { getContract } from "thirdweb";
import { STAKING_CONTRACT_ABI } from "./stakingContractABI";

// Replace <contract_address> with the contract address of your contract
const stakeTokenContractAddress = "<contract_address>";
const rewardTokenContractAddress = "<contract_address>";
const stakingContractAddress = "<contract_address>";

export const STAKE_TOKEN_CONTRACT = getContract({
    client: client,
    chain: chain,
    address: stakeTokenContractAddress,
});

export const REWARD_TOKEN_CONTRACT = getContract({
    client: client,
    chain: chain,
    address: rewardTokenContractAddress,
});


export const STAKING_CONTRACT = getContract({
    client: client,
    chain: chain,
    address: stakingContractAddress,
    abi: STAKING_CONTRACT_ABI
});
  1. Create another file to store the staking contracts ABI, which can be set when using getContract.

Add Component to Connect a Wallet

Next, we will add a component to connect a user's web3 wallet to our app. You can use thirdweb's out of the box components like ConnectButton or ConnectEmbed

  1. In page.tsx file, you can remove the templated code and replace it with the connect component of your choice.
💡
If use Next app router, make sure to export connect component from file tagged with 'use-client'
import { client } from "./client";
import { chain } from "./chain";
import { ConnectEmbed } from "@/app/thirdweb";

export default function Home() {
  return (
    <div>
        <ConnectEmbed
          client={client}
          chain={chain}
        />
    </div>
  );
}

Building the Staking Interface

It's time to build out the UI for the core staking functionality. We'll create a component that displays:

  • Staking token balance
  • Reward token balance
  • Staked amount
  • Reward amount
  1. Create a new file components/Stake.tsx
  2. First, lets display the Stake token and Reward token balance the connected wallet holds. We can do this by using the useReadContract hook along with the balanceOf extension.
const { 
  data: stakingTokenBalance, 
  isLoading: loadingStakeTokenBalance, 
  refetch: refetchStakingTokenBalance 
} = useReadContract(
    balanceOf,
    {
        contract: STAKE_TOKEN_CONTRACT,
        address: account?.address || "",
        queryOptions: {
            enabled: !!account,
        }
    }
);

const { 
  data: rewardTokenBalance, 
  isLoading: loadingRewardTokenBalance, 
  refetch: refetchRewardTokenBalance 
} = useReadContract(
    balanceOf,
    {
        contract: REWARD_TOKEN_CONTRACT,
        address: account?.address || "",
        queryOptions: {
            enabled: !!account,
        }
    }
);
  1. Display the balances in the app
{loadingStakeTokenBalance && (
  <p>Staking Token: {toEther(stakingTokenBalance!)}</p>
)}

{loadingRewardTokenBalance && (
  <p>Reward Token: {toEther(rewardTokenBalance!)}</p>
)}
  1. Get staking info from the Staking smart contract to display token amount staked and claimable reward amount. You can also use useEffect to refetch the information to have an update reward amount.
const { 
  data: stakeInfo, 
  refetch: refetchStakeInfo 
} = useReadContract({
    contract: STAKING_CONTRACT,
    method: "getStakeInfo",
    params: [account?.address as string],
    queryOptions: {
        enabled: !!account,
    }
});

useEffect(() => {
    setInterval(() => {
        refetchData();
    }, 10000);
}, []);

const refetchData = () => {
    refetchStakeInfo();
};
  1. Display the staked information and add a TransactionButton to be able to claim the claimable rewards from the contract.
<p>Balance Staked: {toEther(stakeInfo[0]).toString())}</p>
<p>Reward Balance: {toEther(stakeInfo[1]).toString())}</p>

<TransactionButton
    transaction={() => (
        prepareContractCall({
            contract: STAKING_CONTRACT,
            method: "claimRewards",
        })
    )}
    onTransactionConfirmed={() => {
        refetchData();
        refetchStakingTokenBalance();
        refetchRewardTokenBalance();
    }}
>Claim Rewards</TransactionButton>
  1. To build the staking component we will need to have a user approve the stake tokens to the Staking contract before staking. Create state variables for the staking amount, if approved or not, and staking status.
const [stakeAmount, setStakeAmount] = useState(0);
const [stakingState, setStakingState] = useState("init" || "approved");
const [isStaking, setIsStaking] = useState(false);
  1. Now, we can build the staking component
<div>
  <div>
    <button
      onClick={() => {
          setIsStaking(false)
          setStakeAmount(0);
          setStakingState("init");
      }}
    >Close</button>
    <h3>Stake</h3>
    <p>Balance: {toEther(stakingTokenBalance!)}</p>
    {stakingState === "init" ? (
      <>
        <input 
            type="number" 
            placeholder="0.0"
            value={stakeAmount}
            onChange={(e) => setStakeAmount(parseFloat(e.target.value))}
        />
        <TransactionButton
          transaction={() => (
            approve({
              contract: STAKE_TOKEN_CONTRACT,
              spender: STAKING_CONTRACT.address,
              amount: stakeAmount,
            })
          )}
           onTransactionConfirmed={() => setStakingState("approved")}
        >Set Approval</TransactionButton>
      </>
    ) : (
      <>
        <h3>{stakeAmount}</h3>
        <TransactionButton
          transaction={() => (
            prepareContractCall({
              contract: STAKING_CONTRACT,
              method: "stake",
              params: [toWei(stakeAmount.toString())],
            })
          )}
          onTransactionConfirmed={() => {
            setStakeAmount(0);
            setStakingState("init")
            refetchData();
            refetchStakingTokenBalance();
            setIsStaking(false);
          }}
        >Stake</TransactionButton>
      </>
    )}
  </div>
</div>
  1. Now, lets build the ability to withdraw staked funds. We'll need a state variable for the amount to withdraw and the state of the withdraw
const [withdrawAmount, setWithdrawAmount] = useState(0);
const [isWithdrawing, setIsWithdrawing] = useState(false);
  1. Lets build the withdraw component of our staking app
<div>
  <div>
    <button
      onClick={() => {
        setIsWithdrawing(false)
      }}
    >Close</button>
    <h3>Withraw</h3>
    <input 
      type="number" 
      placeholder="0.0"
      value={withdrawAmount}
      onChange={(e) => setWithdrawAmount(parseFloat(e.target.value))}
    />
    <TransactionButton
      transaction={() => (
        prepareContractCall({
          contract: STAKING_CONTRACT,
          method: "withdraw",
          params: [toWei(withdrawAmount.toString())],
        })
      )}
      onTransactionConfirmed={() => {
        setWithdrawAmount(0);
        refetchData();
        refetchStakingTokenBalance();
        setIsWithdrawing(false);
      }}
    >Withdraw</TransactionButton>
  </div>
</div>

With this we can now see the token balance of the staking and reward token within the connected wallet, the amount of tokens staked, the amount of claimable rewards, and the ability to stake, withdraw, and claim rewards from the staking contract.

Conclusion

And there you have it! We've now built a complete ERC-20 staking app using the thirdweb Connect SDK.

Users can:

  • Stake ERC-20 tokens by inputting an amount and setting approval
  • Withdraw staked tokens
  • View their staked token balance and earned rewards
  • Claim earned reward tokens with the click of a button

The thirdweb Connect SDK made it simple to interact with our deployed contracts, handle transaction flows, and update our UI to reflect on-chain state.

I hope this guide has been helpful in demonstrating the power and ease-of-use of thirdweb for building web3 apps.

Feel free to extend this demo and add your own additional features. And if you have any other questions, don't hesitate to reach out.

Happy building!