DEV Community

Cover image for Casually Deploying a Staking dApp on CrossFi Chain with Hardhat: A Technical Walkthrough
Aliyu Anate
Aliyu Anate

Posted on

Casually Deploying a Staking dApp on CrossFi Chain with Hardhat: A Technical Walkthrough

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);
    }
}

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode
  • 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;
Enter fullscreen mode Exit fullscreen mode
  • 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);
}

Enter fullscreen mode Exit fullscreen mode

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;
}

Enter fullscreen mode Exit fullscreen mode

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;
}

Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

⚠️ 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);
  });


Enter fullscreen mode Exit fullscreen mode

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

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more