In this article, we’ll explore how to build and deploy a staking dApp on the CrossFi chain using Hardhat. The dApp lets users stake XFI tokens and, after a chosen duration, withdraw their stake along with MPX rewards. We’ll explain the token contracts, the staking smart contract, and the process of deploying them with Hardhat.
1. Overview of the Contracts
The project consists of three main Solidity contracts:
- XFIToken Contract: An ERC20 token that users stake.
- MPXToken Contract: An ERC20 token used for rewards.
- StakeXFI Contract: The staking contract that connects the two tokens and handles staking, reward calculation, and withdrawals.
All contracts are built using OpenZeppelin’s ERC20 libraries and follow the MIT license.
2. Token Contracts: XFIToken and MPXToken
Both token contracts inherit from OpenZeppelin’s ERC20, ERC20Burnable, and Ownable contracts. They are very similar; here’s a simplified version of the XFIToken contract:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.27;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract XFIToken is ERC20, ERC20Burnable, Ownable {
constructor(address initialOwner) ERC20("XFIToken", "XFI") Ownable(initialOwner) {}
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
}
The MPXToken is nearly identical, differing only in the token’s name and symbol. Both contracts allow the owner to mint new tokens, which can later be used in staking and rewards.
3. The StakeXFI Contract
The core of the dApp is the StakeXFI contract. It connects the XFI and MPX tokens and manages staking. Let’s break down its key components.
a. Contract State Variables
IERC20 public XFIContract;
IERC20 public MPXContract;
address internal owner;
address internal newOwner;
bool internal locked;
uint256 immutable MINIMUM_STAKE_AMOUNT = 1000 * (10**18);
uint256 immutable MAXIMUM_STAKE_AMOUNT = 100000 * (10**18);
uint32 internal constant REWARD_PER_SECOND = 1000000; // Reward rate per second
- Token Interfaces: The contract holds references to the XFI and MPX token contracts.
- Ownership & Security: Variables for owner management and a locked flag serve as a reentrancy guard.
- Staking Limits: Minimum and maximum staking amounts are set.
- Reward Rate: A constant that defines how many reward tokens are generated per second.
b. Struct and Mapping for Staking
struct Staking {
uint256 amount;
uint256 startTime;
uint256 duration;
bool hasWithdrawn;
}
mapping (address => Staking[]) stakers;
- Staking Structure: Each staking entry records the staked amount, start time, the time when the stake matures (duration), and a flag to check if it has been withdrawn.
- Mapping: Each staker (an address) can have multiple stakes.
c. The stake Function
function stake(uint256 _amount, uint256 _duration) external reentrancyGuard {
require(msg.sender != address(0), "Zero address not allowed");
require(_amount >= MINIMUM_STAKE_AMOUNT && _amount <= MAXIMUM_STAKE_AMOUNT, "Amount is out of range");
require(_duration > 0, "Duration is too short");
require(XFIContract.balanceOf(msg.sender) >= _amount, "You don't have enough");
require(XFIContract.allowance(msg.sender, address(this)) >= _amount, "Amount allowed is not enough");
XFIContract.transferFrom(msg.sender, address(this), _amount);
Staking memory staking;
staking.amount = _amount;
staking.duration = block.timestamp + _duration;
staking.startTime = block.timestamp;
stakers[msg.sender].push(staking);
emit DepositSuccessful(msg.sender, _amount, block.timestamp);
}
Explanation:
- Validation: Checks if the sender address is valid, the staking amount is within range, the duration is positive, and that the user has enough balance and allowance.
- Token Transfer: Transfers the staked XFI tokens from the user to the contract.
- Recording the Stake: A new Staking entry is created and stored.
- Event Emission: An event is emitted upon successful deposit.
d. The withdrawStake Function
Users call this function to withdraw their stake after the staking period.
function withdrawStake(uint8 _index) external reentrancyGuard returns (bool) {
require(msg.sender != address(0), "Zero address not allowed");
require(_index < stakers[msg.sender].length, "Out of range");
Staking storage staking = stakers[msg.sender][_index];
require(block.timestamp > staking.duration, "Not yet time");
require(!staking.hasWithdrawn, "Stake already withdrawn");
uint256 amountStaked_ = staking.amount;
uint256 rewardAmount_ = calculateReward(staking.startTime, staking.duration);
staking.hasWithdrawn = true;
staking.amount = 0;
staking.startTime = 0;
staking.duration = 0;
XFIContract.transfer(msg.sender, amountStaked_);
MPXContract.transfer(msg.sender, rewardAmount_);
emit WithdrawalSuccessful(msg.sender, amountStaked_, rewardAmount_);
return true;
}
Explanation:
- Validation: Ensures that the index is valid, the staking period has ended, and that the stake hasn’t been withdrawn already.
- Reward Calculation: Uses the calculateReward function to determine the reward based on the staking period.
- Resetting State: Marks the stake as withdrawn and resets its fields.
- Token Transfers: Returns the staked XFI tokens and sends the calculated MPX rewards to the user.
- Event Emission: An event confirms the successful withdrawal.
e. Reward Calculation
function calculateReward(uint256 _startTime, uint256 _endTime) private pure returns (uint256) {
uint256 stakeDuration = _endTime - _startTime;
return stakeDuration * REWARD_PER_SECOND;
}
The reward is simply the product of the staking duration (in seconds) and the reward rate. For example, if a user stakes for 100 seconds and the reward rate is 1,000,000 per second, the reward would be 100 × 1,000,000.
f. Ownership and Utility Functions
Additional functions manage contract ownership and provide utility:
- Ownership Transfer: transferOwnership and claimOwnership allow safe transition of control.
- Information Getters: Functions such as getStakerInfo, getContractMPXBalance, and getContractXFIBalance provide insights into the staking state and contract balances.
- Reentrancy Guard: A modifier ensures functions like stakeand withdrawStake are protected from reentrancy attacks.
4. Deploying the Contracts with Hardhat
Hardhat is a development environment that allows you to compile, deploy, test, and debug Ethereum (and EVM-compatible) contracts. Here’s how you can deploy these contracts on CrossFi chain using Hardhat.
a. Setting Up the Hardhat Environment
Initialize a Hardhat project:
using your terminal
npx hardhat init
1. Install Dependencies:
npm install --save-dev @nomiclabs/hardhat-ethers ethers @openzeppelin/contracts
Configure hardhat.config.js:
Make sure to add the CrossFi chain network settings. For example:
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
require("dotenv").config();
const config: HardhatUserConfig = {
solidity: "0.8.26",
networks: {
crossFi: {
url: process.env.CROSSFI_RPC_URL,
accounts: [process.env.PRIVATE_KEY as string],
gasPrice: 1000000000,
},
}
};
export default config;
Storing RPC URL and Private Key Securely
Instead of hardcoding sensitive details like your RPC URL and private key, store them in a .env file.
1. Create a .env file
In your project root, create a file named .env and add:
CROSSFI_RPC_URL=https: https://rpc.testnet.ms
PRIVATE_KEY=your-private-key
⚠️ Keep this private! Never share your private key.
https://rpc.testnet.ms is the rpc url for the crossFi testnet
2. Install dotenv
If not installed, run:
npm install dotenv
add the .env file to the gitIgnore
b. Writing a Deployment Script
Create a deployment script (e.g., scripts/deploy.js) that deploys the XFIToken, MPXToken, and StakeXFI contracts sequentially:
const hre = require("hardhat");
const {ethers} = hre
async function main() {
const [deployer] = await ethers.getSigners();
console.log("Deploying contracts with account:", deployer.address);
const XFIToken = await ethers.getContractFactory("XFIToken");
const xfiToken = await XFIToken.deploy(deployer.address);
await xfiToken.deployed();
console.log("XFIToken deployed to:", xfiToken.address);
const MPXToken = await ethers.getContractFactory("MPXToken");
const mpxToken = await MPXToken.deploy(deployer.address);
await mpxToken.deployed();
console.log("MPXToken deployed to:", mpxToken.address);
const StakeXFI = await ethers.getContractFactory("StakeXFI");
const stakeXFI = await StakeXFI.deploy(xfiToken.address, mpxToken.address);
await stakeXFI.deployed();
console.log("StakeXFI deployed to:", stakeXFI.address);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
c. Deploying on the CrossFi Chain
Run the deployment script using Hardhat’s command-line interface:
npx hardhat run scripts/deploy.js --network crossFi
This command deploys the token contracts and then the staking contract on the CrossFi network. Be sure that the network RPC endpoint and private key are correctly configured in your Hardhat config.
5. Summary
In this article, we:
- Explored the Contracts: We broke down the XFIToken, MPXToken, and StakeXFI contracts—examining state variables, functions, and calculations (such as reward computation).
- Explained Key Functions: We looked in detail at staking, withdrawal, reward calculations, and ownership management.
- Walked Through Deployment: We provided a Hardhat configuration and a deployment script that sequentially deploys the token and staking contracts on the CrossFi chain.
This walkthrough provides a foundation for developing, testing, and deploying a staking dApp using familiar tools like Hardhat while taking full advantage of CrossFi Chain’s scalability and interoperability.
Here is the link to the gitHub repo
CrossFi-Stake
Top comments (0)