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:
Ready to start building? Let's jump in!
Prerequisites
Before getting started, make sure you have the following:
- A thirdweb account
- Node.js installed on your machine
- Web3 wallet like Metamask
- Some testnet funds on the testnet of your choice
Deploying the Smart Contracts
Our staking app will make use of three key smart contracts:
- ERC-20 Staking Token: the token that users stake to earn rewards
- ERC-20 Reward Token: the token that users receive as staking rewards
- 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:
- Deploy 2 ERC-20 Token contracts, one for the staking token and the other for the reward token.
- 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.
- 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.
- Open the Reward Token contract you deployed earlier
- Go to the "Explorer" page and use the "approve" function
- Set the spender as the Staking Contract address
- Set the amount to the max rewards you want to distribute (this value should be in wei)
- Back on the Staking Contract "Explorer" page, call the "depositRewardTokens" function
- 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:
- Open your terminal and create a new Next.js project:
npx create-next-app <your_project_name>
- Choose a project name, select Next.js and TypeScript when prompted
- Navigate to the project directory:
cd <your_project_name>
- Install the thirdweb SDK: (supports
npm
,pnpm
,yarn
, andbun
)
npm i thirdweb
- Open the project in your code editor of choice
Configure the thirdweb Connect SDK
Back in our project, configure the thirdweb SDK:
- 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,
});
- 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>" );
- If using Next.js app router, export
ThirdwebProvider
withuse-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.
- Create a
utils
folder, then create acontracts
folder. - 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
});
- 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
- In
page.tsx
file, you can remove the templated code and replace it with the connect component of your choice.
'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
- Create a new file
components/Stake.tsx
- 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 thebalanceOf
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,
}
}
);
- Display the balances in the app
{loadingStakeTokenBalance && (
<p>Staking Token: {toEther(stakingTokenBalance!)}</p>
)}
{loadingRewardTokenBalance && (
<p>Reward Token: {toEther(rewardTokenBalance!)}</p>
)}
- 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();
};
- 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>
- 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);
- 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>
- 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);
- 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!