Deploying your first Smart Contract on Moonbeam using Hardhat
Staking has gone mainstream in recent years with some of the biggest staking pools like Staking Rewards and Staking Assistance making millions by simply offering a route for users to stake their assets in a pool quickly.
In this tutorial, we're going to be demonstrating how you can deploy your first Staking Smart Contract on Moonbeam.
What is Staking?
If you're new to Staking, here's a quick breakdown.
In Decentralized Finance (DeFi), Staking is a process in blockchain networks where participants lock up their cryptocurrency tokens to support network operations and, in return, earn rewards.
In contrast to traditional banking practices, staking can be best compared to investment banking, where you lock up your funds in an investment product for some time, during which the principal amount will earn some returns.
Recently, Staking has become an alternative to the energy-intensive proof-of-work (PoW) consensus mechanism used by networks like Bitcoin. In a proof-of-stake (PoS) system, the ability to validate transactions and create new blocks is typically proportional to the amount of cryptocurrency a user has staked.
What is Moonbeam
Moonbeam is a smart contract platform that provides Ethereum compatibility within the Polkadot ecosystem. It allows developers to deploy existing Solidity smart contracts and dApp frontends to Moonbeam with minimal changes, effectively creating a bridge between Ethereum and Polkadot.
Prerequisites
This is a beginner-friendly tutorial, but you'll need to understand two essential topics to be able to follow along:
Next JS: A react js framework built with TypeScript
Solidity: The primary programming language for developing smart contracts on Ethereum-compatible platforms, including Moonbeam.
Hardhat: A development environment for Ethereum, providing tools for compiling, testing, and deploying smart contracts. It includes a local Ethereum network for development.
Ethers.js: A compact library for interacting with the Ethereum blockchain, offering wallet management, contract interactions, and blockchain utilities.
Writing our Smart Contract
Let's create a simple staking contract that allows users to stake tokens, earn rewards, and unstake their tokens after a specified period. We'll use Solidity to write our smart contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract MoonbeamStaking is ReentrancyGuard {
IERC20 public stakingToken;
uint256 public constant STAKING_PERIOD = 30 days;
uint256 public constant REWARD_RATE = 5; // 5% reward
struct Stake {
uint256 amount;
uint256 timestamp;
}
mapping(address => Stake) public stakes;
event Staked(address indexed user, uint256 amount);
event Unstaked(address indexed user, uint256 amount, uint256 reward);
constructor(address _stakingToken) {
stakingToken = IERC20(_stakingToken);
}
function stake(uint256 _amount) external nonReentrant {
require(_amount > 0, "Must stake a positive amount");
require(stakingToken.transferFrom(msg.sender, address(this), _amount), "Transfer failed");
if (stakes[msg.sender].amount > 0) {
uint256 reward = calculateReward(msg.sender);
stakes[msg.sender].amount += reward;
}
stakes[msg.sender].amount += _amount;
stakes[msg.sender].timestamp = block.timestamp;
emit Staked(msg.sender, _amount);
}
function unstake() external nonReentrant {
Stake storage userStake = stakes[msg.sender];
require(userStake.amount > 0, "No staked balance");
require(block.timestamp >= userStake.timestamp + STAKING_PERIOD, "Staking period not completed");
uint256 reward = calculateReward(msg.sender);
uint256 totalAmount = userStake.amount + reward;
userStake.amount = 0;
userStake.timestamp = 0;
require(stakingToken.transfer(msg.sender, totalAmount), "Transfer failed");
emit Unstaked(msg.sender, userStake.amount, reward);
}
function calculateReward(address _user) public view returns (uint256) {
Stake storage userStake = stakes[_user];
if (userStake.amount == 0) return 0;
uint256 stakingDuration = block.timestamp - userStake.timestamp;
uint256 rewardPeriods = stakingDuration / STAKING_PERIOD;
return (userStake.amount * REWARD_RATE * rewardPeriods) / 100;
}
function getStakeInfo(address _user) external view returns (uint256 amount, uint256 timestamp, uint256 reward) {
Stake storage userStake = stakes[_user];
return (userStake.amount, userStake.timestamp, calculateReward(_user));
}
}
Let's break down our staking smart contract:
Our smart contract imports two key components from OpenZeppelin, a library of secure, community-vetted smart contracts:
IERC20: An interface for interacting with ERC20 tokens.
ReentrancyGuard: A security module to prevent reentrancy attacks.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract MoonbeamStaking is ReentrancyGuard {
}
A Stake struct is defined to store information about each user's stake, including the amount and timestamp. The stakes mapping associates each user's address with their Stake.
Later, we will get into the importance of this struct
:
struct Stake {
uint256 amount;
uint256 timestamp;
}
mapping(address => Stake) public stakes;
Two events, Staked and Unstaked, are declared to emit important information when users stake or unstake tokens.
event Staked(address indexed user, uint256 amount);
event Unstaked(address indexed user, uint256 amount, uint256 reward);
Our contract declares several important state variables:
stakingToken
: The ERC20 token that users will stake.STAKING_PERIOD
: A constant set to 30 days, defining how long tokens must be staked.REWARD_RATE
: A constant 5% reward rate.
Our contract's The constructor takes the address of the ERC20
token that will be used for staking. It initializes the stakingToken variable with this address, casting it to the IERC20 interface.
constructor(address _stakingToken) {
stakingToken = IERC20(_stakingToken);
}
Our stake
function allows users to stake tokens. It's marked as nonReentrant
to prevent reentrancy attacks.
The function:
Check that the staking amount is positive.
Transfers tokens from the user to the contract.
If the user already has a stake, it calculates and adds any accrued rewards.
Updates the user's stake amount and timestamp.
Emits a Staked event:
function stake(uint256 _amount) external nonReentrant {
require(_amount > 0, "Must stake a positive amount");
require(stakingToken.transferFrom(msg.sender, address(this), _amount), "Transfer failed");
if (stakes[msg.sender].amount > 0) {
uint256 reward = calculateReward(msg.sender);
stakes[msg.sender].amount += reward;
}
stakes[msg.sender].amount += _amount;
stakes[msg.sender].timestamp = block.timestamp;
emit Staked(msg.sender, _amount);
}
The unstake
function allows users to withdraw their staked tokens and rewards. It:
Checks that the user has a stake and the staking period has elapsed.
Calculates the reward.
Determines the total amount to return (staked amount + reward).
Resets the user's stake.
Transfers the total amount to the user.
Emits an Unstaked event.
function unstake() external nonReentrant {
Stake storage userStake = stakes[msg.sender];
require(userStake.amount > 0, "No staked balance");
require(block.timestamp >= userStake.timestamp + STAKING_PERIOD, "Staking period not completed");
uint256 reward = calculateReward(msg.sender);
uint256 totalAmount = userStake.amount + reward;
userStake.amount = 0;
userStake.timestamp = 0;
require(stakingToken.transfer(msg.sender, totalAmount), "Transfer failed");
emit Unstaked(msg.sender, userStake.amount, reward);
}
The calculateReward
function calculates the reward for a given user based on their staking duration and amount. It:
Retrieves the user's stake.
If the staked amount is zero, it returns zero reward.
Calculates the staking duration and number of completed staking periods.
Computes the reward based on the staked amount, reward rate, and number of periods.
The getStakeInfo
function provides a convenient way to retrieve a user's staking information, including their staked amount, staking timestamp, and current reward.
function getStakeInfo(address _user) external view returns (uint256 amount, uint256 timestamp, uint256 reward) {
Stake storage userStake = stakes[_user];
return (userStake.amount, userStake.timestamp, calculateReward(_user));
}
Deploying our Smart Contract
Next, we'll go through the process of deploying our smart contract to the Moonbase Alpha TestNet using hardhat.
First, let's install all important dependencies:
mkdir contract-deployer && cd contract-deployer
npm init -y
npm install --save-dev hardhat @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers
Next, we're going to create a hardhat project. Run the following command and choose "Create an empty hardhat.config.js":
npx hardhat
Next, we're going to configure our hardhat for Moonbase Alpha. Replace the contents of hardhat.config.js
with the following:
require("@nomiclabs/hardhat-waffle");
const PRIVATE_KEY = "your-private-key-here";
module.exports = {
solidity: "0.8.0",
networks: {
moonbase: {
url: "https://rpc.api.moonbase.moonbeam.network",
chainId: 1287, // Moonbase Alpha TestNet
accounts: [`0x${PRIVATE_KEY}`]
},
},
};
Replace your-private-key-here
with your actual private key. Make sure your account has some DEV tokens for gas fees.
Next, create a new file scripts/deploy.js
with the following content:
const hre = require("hardhat");
async function main() {
const [deployer] = await hre.ethers.getSigners();
console.log("Deploying contracts with the account:", deployer.address);
const MoonbeamStaking = await hre.ethers.getContractFactory("MoonbeamStaking");
const stakingToken = "0x1234567890123456789012345678901234567890"; // Replace with actual token address
const moonbeamStaking = await MoonbeamStaking.deploy(stakingToken);
await moonbeamStaking.deployed();
console.log("MoonbeamStaking deployed to:", moonbeamStaking.address);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
For our stakingToken
, you can replace it with any token of your choice on the Moonbeam Alpha Testnet.
Next, let's compile our contract. Run the following command to compile your contract:
npx hardhat compile
Now, let's finally deploy our contract to Moonbase Alpha:
npx hardhat run scripts/deploy.js --network moonbase
If our deployment is successful, you'll see an output similar to:
Deploying contracts with the account: 0x...
MoonbeamStaking deployed to: 0x...
Building our Dapp
Now that we have our smart contract, let's create a user interface using Next.js. We'll create a simple dApp that allows users to connect their wallet, stake tokens, view their staking information, and unstake tokens.
First, set up a new Next.js project:
npx create-next-app moonbeam-staking-dapp
cd moonbeam-staking-dapp
npm install ethers @web3-react/core @web3-react/injected-connector
Replace the contents of src/app/page.tsx
with the following code:
import { useState, useEffect } from 'react'
import { ethers } from 'ethers'
import { useWeb3React } from '@web3-react/core'
import { InjectedConnector } from '@web3-react/injected-connector'
const injected = new InjectedConnector({ supportedChainIds: [1287] }) // Moonbase Alpha TestNet
const contractAddress = 'YOUR_CONTRACT_ADDRESS'
const contractABI = [
// Add your contract ABI here
]
export default function Home() {
const { active, account, library, activate, deactivate } = useWeb3React()
const [stakeAmount, setStakeAmount] = useState('')
const [stakeInfo, setStakeInfo] = useState(null)
useEffect(() => {
if (active) {
fetchStakeInfo()
}
}, [active, account])
async function connect() {
try {
await activate(injected)
} catch (ex) {
console.log(ex)
}
}
async function disconnect() {
try {
deactivate()
} catch (ex) {
console.log(ex)
}
}
async function fetchStakeInfo() {
if (library) {
const contract = new ethers.Contract(contractAddress, contractABI, library.getSigner())
const info = await contract.getStakeInfo(account)
setStakeInfo({
amount: ethers.utils.formatEther(info.amount),
timestamp: new Date(info.timestamp.toNumber() * 1000).toLocaleString(),
reward: ethers.utils.formatEther(info.reward)
})
}
}
async function handleStake() {
if (library && stakeAmount) {
const contract = new ethers.Contract(contractAddress, contractABI, library.getSigner())
try {
const tx = await contract.stake(ethers.utils.parseEther(stakeAmount))
await tx.wait()
fetchStakeInfo()
} catch (error) {
console.error('Staking failed:', error)
}
}
}
async function handleUnstake() {
if (library) {
const contract = new ethers.Contract(contractAddress, contractABI, library.getSigner())
try {
const tx = await contract.unstake()
await tx.wait()
fetchStakeInfo()
} catch (error) {
console.error('Unstaking failed:', error)
}
}
}
return (
<div className="max-w-2xl mx-auto p-6 bg-gray-100 rounded-lg shadow-md">
<h1 className="text-3xl font-bold mb-6 text-center text-indigo-600">Moonbeam Staking dApp</h1>
{active ? (
<div className="space-y-6">
<div className="flex justify-between items-center bg-white p-4 rounded-md shadow">
<p className="text-gray-700">Connected with <span className="font-mono text-sm">{account}</span></p>
<button onClick={disconnect} className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600 transition duration-300">Disconnect</button>
</div>
<div className="bg-white p-6 rounded-md shadow">
<h2 className="text-2xl font-semibold mb-4 text-gray-800">Stake Tokens</h2>
<div className="flex space-x-4">
<input
type="text"
value={stakeAmount}
onChange={(e) => setStakeAmount(e.target.value)}
placeholder="Amount to stake"
className="flex-grow px-4 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"
/>
<button onClick={handleStake} className="px-6 py-2 bg-indigo-500 text-white rounded-md hover:bg-indigo-600 transition duration-300">Stake</button>
</div>
</div>
<div className="bg-white p-6 rounded-md shadow">
<h2 className="text-2xl font-semibold mb-4 text-gray-800">Your Stake</h2>
{stakeInfo && (
<div className="space-y-2 text-gray-700">
<p>Staked Amount: <span className="font-semibold">{stakeInfo.amount}</span></p>
<p>Stake Date: <span className="font-semibold">{stakeInfo.timestamp}</span></p>
<p>Reward: <span className="font-semibold">{stakeInfo.reward}</span></p>
</div>
)}
<button onClick={handleUnstake} className="mt-4 w-full py-2 bg-yellow-500 text-white rounded-md hover:bg-yellow-600 transition duration-300">Unstake</button>
</div>
</div>
) : (
<button onClick={connect} className="w-full py-3 bg-green-500 text-white rounded-md hover:bg-green-600 transition duration-300">Connect to MetaMask</button>
)}
</div>
)
}
We're starting off with our UI by initializing Web3React's InjectedConnector
to be able to access the user crypto wallet like MetaMask:
const injected = new InjectedConnector({ supportedChainIds: [1287] }) // Moonbase Alpha TestNet
Next, set the contractAddress
and contractABI
to the deployed contract address and ABI respectively, that was generated in the previous segment.
Web3React provides us useWeb3React()
a hook containing some helper functions to easily connect and work with a user's wallet.
const { active, account, library, activate, deactivate } = useWeb3React()
active
: This is a boolean that indicates whether the wallet is currently connected to the dApp. It's true if there's an active Web3 connection, and false otherwise.account
: This is a string that represents the currently connected Ethereum address. It's the user's public address that they're interacting with the dApp from.library
: This is an instance of the Web3 provider library. In most cases, it will be an instance of the Web3Provider from ethers.js. You can use this to create contract instances or perform other Web3-related operations.activate
: This is a function that you can call to activate (connect) a Web3 connector. It's typically used to initiate a connection with the user's wallet.deactivate
: This is a function that you can call to deactivate (disconnect) the current Web3 connection. It's used to disconnect the user's wallet from the dApp.
Two (2) very important functions in our dApp are the handleStake()
and handleUnstake()
functions. Let's take a closer look at how we're interacting with the Blockchain using ethers
:
First, we're fetching our contract using the contractAddress
and contractABI
:
const contract = new ethers.Contract(contractAddress, contractABI, library.getSigner())
Now that we've got access to our contract, we'll call the functions in the contracts using ethers. In this case, we're calling the stake()
function that exists on our smart contract:
const tx = await contract.stake(ethers.utils.parseEther(stakeAmount))
await tx.wait() // wait for block to be created
This same process is used with the other functions that interact with our smart contract. Putting it together:
async function handleStake() {
if (library && stakeAmount) { // check that the user has entered an amount and that we have an ethers instance
const contract = new ethers.Contract(contractAddress, contractABI, library.getSigner())
try {
const tx = await contract.stake(ethers.utils.parseEther(stakeAmount))
await tx.wait()
fetchStakeInfo()
} catch (error) {
console.error('Staking failed:', error)
}
}
}
Lastly, we're building our UI:
return (
<div className="max-w-2xl mx-auto p-6 bg-gray-100 rounded-lg shadow-md">
<h1 className="text-3xl font-bold mb-6 text-center text-indigo-600">Moonbeam Staking dApp</h1>
{active ? (
<div className="space-y-6">
<div className="flex justify-between items-center bg-white p-4 rounded-md shadow">
<p className="text-gray-700">Connected with <span className="font-mono text-sm">{account}</span></p>
<button onClick={disconnect} className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600 transition duration-300">Disconnect</button>
</div>
<div className="bg-white p-6 rounded-md shadow">
<h2 className="text-2xl font-semibold mb-4 text-gray-800">Stake Tokens</h2>
<div className="flex space-x-4">
<input
type="text"
value={stakeAmount}
onChange={(e) => setStakeAmount(e.target.value)}
placeholder="Amount to stake"
className="flex-grow px-4 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"
/>
<button onClick={handleStake} className="px-6 py-2 bg-indigo-500 text-white rounded-md hover:bg-indigo-600 transition duration-300">Stake</button>
</div>
</div>
<div className="bg-white p-6 rounded-md shadow">
<h2 className="text-2xl font-semibold mb-4 text-gray-800">Your Stake</h2>
{stakeInfo && (
<div className="space-y-2 text-gray-700">
<p>Staked Amount: <span className="font-semibold">{stakeInfo.amount}</span></p>
<p>Stake Date: <span className="font-semibold">{stakeInfo.timestamp}</span></p>
<p>Reward: <span className="font-semibold">{stakeInfo.reward}</span></p>
</div>
)}
<button onClick={handleUnstake} className="mt-4 w-full py-2 bg-yellow-500 text-white rounded-md hover:bg-yellow-600 transition duration-300">Unstake</button>
</div>
</div>
) : (
<button onClick={connect} className="w-full py-3 bg-green-500 text-white rounded-md hover:bg-green-600 transition duration-300">Connect to MetaMask</button>
)}
</div>
)
Let's run our app and test it out!
npm run dev
Now, our UI should look something like this:
Conclusion
Thanks for reading! We'd love to see what you're building and we hope that this guide will help you in your journey to become a Moonbeam dApp builder.
Subscribe to my newsletter
Read articles from Favor Onuoha directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Favor Onuoha
Favor Onuoha
Developer Advocate at Swing Finance | Software Developer | Content Creator | Technical Writer | I am on a journey to make Web3 accessible to everyone ๐