Blockchain-Based Staking Yield Dashboard Solidity, JavaScript

👤 Sharing: AI
Okay, let's craft a simplified example of a blockchain-based staking yield dashboard using Solidity and JavaScript.  This example will focus on the core functionality: a Solidity contract for basic staking and a JavaScript-based dashboard to display staking information.

**Important Considerations and Simplifications:**

*   **Security:** This is a simplified example. In a real-world staking contract, security audits are crucial to prevent vulnerabilities (e.g., reentrancy attacks, integer overflows).
*   **Gas Optimization:** Real-world contracts require significant gas optimization.
*   **Complexity:** A full staking dashboard would involve more complex calculations, UI elements, and interaction with external data sources (e.g., token price feeds).
*   **Error Handling:**  The code includes basic error handling, but more robust error handling is generally recommended.
*   **Frontend Framework:**  We'll use vanilla JavaScript for simplicity. A real application would likely use a framework like React, Vue, or Angular.
*   **Blockchain:** This example is designed to work with a local development blockchain (e.g., Ganache).  You'll need to deploy the Solidity contract to your local blockchain to test it.

**1. Solidity Contract (StakingContract.sol):**

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract StakingContract {
    // Token to be staked
    IERC20 public token;

    // Reward token
    IERC20 public rewardToken;

    // Staking start and end time
    uint256 public startTime;
    uint256 public endTime;

    // Staking cap
    uint256 public stakingCap;

    // Staking reward pool
    uint256 public rewardPool;

    // User info
    struct UserInfo {
        uint256 amount;
        uint256 rewardDebt;
        uint256 rewardClaimed;
    }

    mapping(address => UserInfo) public userInfo;

    // Reward per token stored
    uint256 public rewardPerTokenStored;

    // Total amount of tokens staked
    uint256 public totalStaked;

    // Events
    event Staked(address indexed user, uint256 amount);
    event Unstaked(address indexed user, uint256 amount);
    event RewardPaid(address indexed user, uint256 reward);

    // Initialize constructor
    constructor(IERC20 _token, IERC20 _rewardToken, uint256 _startTime, uint256 _endTime, uint256 _stakingCap, uint256 _rewardPool) {
        token = _token;
        rewardToken = _rewardToken;
        startTime = _startTime;
        endTime = _endTime;
        stakingCap = _stakingCap;
        rewardPool = _rewardPool;
    }

    // Function to allow users to stake tokens
    function stake(uint256 _amount) public {
        require(block.timestamp >= startTime && block.timestamp <= endTime, "Staking period not active");
        require(_amount > 0, "Amount must be greater than 0");
        require(totalStaked + _amount <= stakingCap, "Staking cap exceeded");

        token.transferFrom(msg.sender, address(this), _amount);

        UserInfo storage user = userInfo[msg.sender];

        // Update reward
        updateReward(msg.sender);

        totalStaked += _amount;
        user.amount += _amount;

        user.rewardDebt = userReward(msg.sender);

        emit Staked(msg.sender, _amount);
    }

    // Function to allow users to unstake tokens
    function unstake(uint256 _amount) public {
        require(_amount > 0, "Amount must be greater than 0");
        require(userInfo[msg.sender].amount >= _amount, "Insufficient balance");

        UserInfo storage user = userInfo[msg.sender];

        // Update reward
        updateReward(msg.sender);

        totalStaked -= _amount;
        user.amount -= _amount;

        token.transfer(msg.sender, _amount);

        user.rewardDebt = userReward(msg.sender);

        emit Unstaked(msg.sender, _amount);
    }

    // Function to allow users to claim rewards
    function claimReward() public {
        UserInfo storage user = userInfo[msg.sender];
        uint256 reward = earned(msg.sender);

        require(reward > 0, "No reward to claim");

        user.rewardClaimed += reward;
        rewardToken.transfer(msg.sender, reward);
        user.rewardDebt = userReward(msg.sender);

        emit RewardPaid(msg.sender, reward);
    }

    // Function to calculate reward
    function earned(address _account) public view returns (uint256) {
        UserInfo storage user = userInfo[_account];
        uint256 currentReward = userReward(_account);
        return currentReward - user.rewardDebt;
    }

    // Function to update reward
    function updateReward(address _account) internal {
        rewardPerTokenStored = rewardPerToken();
        UserInfo storage user = userInfo[_account];
        user.rewardDebt = userReward(_account);
    }

    // Function to calculate user reward
    function userReward(address _account) public view returns (uint256) {
        UserInfo storage user = userInfo[_account];
        return user.amount * (rewardPerTokenStored - user.rewardClaimed);
    }

    // Function to calculate reward per token
    function rewardPerToken() public view returns (uint256) {
        if (totalStaked == 0) {
            return rewardPerTokenStored;
        }

        return rewardPerTokenStored + (rewardPool * (block.timestamp - startTime)) / totalStaked;
    }
}

// Interface of ERC20 Token
interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount) external returns (bool);
    function allowance(address owner, address spender) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
}
```

**Explanation of Solidity Contract:**

*   **`pragma solidity ^0.8.0;`**: Specifies the Solidity compiler version.
*   **`contract StakingContract { ... }`**:  Defines the smart contract.
*   **`IERC20 public token;`**: Address of the token contract to be staked.
*   **`IERC20 public rewardToken;`**: Address of the reward token contract.
*   **`uint256 public startTime;`**: Staking start time.
*   **`uint256 public endTime;`**: Staking end time.
*   **`uint256 public stakingCap;`**: Staking Cap.
*   **`uint256 public rewardPool;`**: Staking reward pool.
*   **`struct UserInfo { ... }`**: Defines the structure to store user-specific staking information (staked amount, reward debt, reward claimed).
*   **`mapping(address => UserInfo) public userInfo;`**:  Maps user addresses to their `UserInfo`.
*   **`uint256 public rewardPerTokenStored;`**: Reward per token stored.
*   **`uint256 public totalStaked;`**: Total amount of tokens staked.
*   **`event Staked(address indexed user, uint256 amount);`**: Event emitted when a user stakes tokens.
*   **`event Unstaked(address indexed user, uint256 amount);`**: Event emitted when a user unstakes tokens.
*   **`event RewardPaid(address indexed user, uint256 reward);`**: Event emitted when a user claims rewards.
*   **`constructor(IERC20 _token, IERC20 _rewardToken, uint256 _startTime, uint256 _endTime, uint256 _stakingCap, uint256 _rewardPool) { ... }`**:  The constructor initializes the contract with the token address.
*   **`stake(uint256 _amount) public { ... }`**: Allows users to stake tokens. Transfers tokens from the user to the contract and updates the user's staking info.
*   **`unstake(uint256 _amount) public { ... }`**: Allows users to unstake tokens.  Transfers tokens from the contract to the user.
*   **`claimReward() public { ... }`**: Allows users to claim accumulated rewards.  Transfers reward tokens to the user.
*   **`earned(address _account) public view returns (uint256) { ... }`**: Calculates the reward earned by a user.
*   **`updateReward(address _account) internal { ... }`**: Updates reward of a user.
*   **`userReward(address _account) public view returns (uint256) { ... }`**: Calculates the user reward.
*   **`rewardPerToken() public view returns (uint256) { ... }`**: Calculates reward per token.

**2. JavaScript (dashboard.js):**

```javascript
// dashboard.js
const contractAddress = "YOUR_CONTRACT_ADDRESS"; // Replace with your deployed contract address
const tokenAddress = "YOUR_TOKEN_ADDRESS"; // Replace with your token address
const rewardTokenAddress = "YOUR_REWARD_TOKEN_ADDRESS"; // Replace with your reward token address
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();

// Get ABI from JSON file
async function getAbi() {
  const response = await fetch("StakingContract.json");
  const data = await response.json();
  const abi = data.abi;
  return abi;
}

async function getTokenAbi() {
  const response = await fetch("IERC20.json");
  const data = await response.json();
  const abi = data.abi;
  return abi;
}

// Load contract
async function loadContract() {
    const abi = await getAbi();
    return new ethers.Contract(contractAddress, abi, signer);
}

// Load token contract
async function loadTokenContract(tokenAddress) {
    const abi = await getTokenAbi();
    return new ethers.Contract(tokenAddress, abi, signer);
}

async function updateDashboard() {
    try {
        const contract = await loadContract();
        const tokenContract = await loadTokenContract(tokenAddress);
        const rewardTokenContract = await loadTokenContract(rewardTokenAddress);

        // Get accounts
        const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
        const account = accounts[0];

        // Fetch data from contract
        const stakedBalance = await contract.userInfo(account);
        const rewardBalance = await contract.earned(account);
        const stakingCap = await contract.stakingCap();
        const rewardPool = await contract.rewardPool();
        const tokenName = await tokenContract.name();
        const rewardTokenName = await rewardTokenContract.name();
        const tokenBalance = await tokenContract.balanceOf(account);

        // Update HTML elements
        document.getElementById("stakedBalance").textContent = ethers.utils.formatEther(stakedBalance.amount);
        document.getElementById("rewardBalance").textContent = ethers.utils.formatEther(rewardBalance);
        document.getElementById("stakingCap").textContent = ethers.utils.formatEther(stakingCap);
        document.getElementById("rewardPool").textContent = ethers.utils.formatEther(rewardPool);
        document.getElementById("tokenName").textContent = tokenName;
        document.getElementById("rewardTokenName").textContent = rewardTokenName;
        document.getElementById("tokenBalance").textContent = ethers.utils.formatEther(tokenBalance);
    } catch (error) {
        console.error("Error fetching data:", error);
        alert(`Error fetching data: ${error.message}`);
    }
}

// Stake function
async function stakeTokens() {
    try {
        const contract = await loadContract();
        const tokenContract = await loadTokenContract(tokenAddress);

        const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
        const account = accounts[0];

        const stakeAmount = document.getElementById("stakeAmount").value;
        const amount = ethers.utils.parseEther(stakeAmount);

        // Approve the contract to spend tokens
        const approval = await tokenContract.approve(contractAddress, amount);
        await approval.wait();

        // Stake tokens
        const transaction = await contract.stake(amount);
        await transaction.wait();

        // Update the dashboard
        await updateDashboard();
        alert("Stake successful!");
    } catch (error) {
        console.error("Error staking:", error);
        alert(`Error staking: ${error.message}`);
    }
}

// Unstake function
async function unstakeTokens() {
    try {
        const contract = await loadContract();

        const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
        const account = accounts[0];

        const unstakeAmount = document.getElementById("unstakeAmount").value;
        const amount = ethers.utils.parseEther(unstakeAmount);

        // Unstake tokens
        const transaction = await contract.unstake(amount);
        await transaction.wait();

        // Update the dashboard
        await updateDashboard();
        alert("Unstake successful!");
    } catch (error) {
        console.error("Error unstaking:", error);
        alert(`Error unstaking: ${error.message}`);
    }
}

// Claim reward function
async function claimRewards() {
    try {
        const contract = await loadContract();

        const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
        const account = accounts[0];

        // Claim rewards
        const transaction = await contract.claimReward();
        await transaction.wait();

        // Update the dashboard
        await updateDashboard();
        alert("Claim successful!");
    } catch (error) {
        console.error("Error claiming:", error);
        alert(`Error claiming: ${error.message}`);
    }
}

// Event listeners for buttons
document.addEventListener("DOMContentLoaded", async () => {
    // Check if MetaMask is installed
    if (typeof window.ethereum !== 'undefined') {
        console.log('MetaMask is installed!');

        // Add event listener for stake button
        document.getElementById("stakeButton").addEventListener("click", stakeTokens);

        // Add event listener for unstake button
        document.getElementById("unstakeButton").addEventListener("click", unstakeTokens);

        // Add event listener for claim button
        document.getElementById("claimButton").addEventListener("click", claimRewards);

        // Initial dashboard update
        await updateDashboard();

        // Update dashboard every 10 seconds
        setInterval(updateDashboard, 10000);
    } else {
        console.log('MetaMask is not installed!');
        alert('MetaMask is not installed! Please install MetaMask to use this dApp.');
    }
});
```

**Explanation of JavaScript (dashboard.js):**

*   **Dependencies:** Requires `ethers.js` library for interacting with Ethereum. Include it in your HTML file: `<script src="https://cdn.ethers.io/lib/ethers-5.4.umd.min.js" type="application/javascript"></script>`
*   **`contractAddress`**:  Replace `"YOUR_CONTRACT_ADDRESS"` with the address of your deployed `StakingContract`.
*   **`tokenAddress`**:  Replace `"YOUR_TOKEN_ADDRESS"` with the address of your token contract.
*   **`rewardTokenAddress`**:  Replace `"YOUR_REWARD_TOKEN_ADDRESS"` with the address of your reward token contract.
*   **`provider = new ethers.providers.Web3Provider(window.ethereum);`**: Creates an Ethereum provider using MetaMask (or other injected provider).
*   **`signer = provider.getSigner();`**: Gets the signer (user's account) from the provider.
*   **`loadContract()`**: Loads the deployed smart contract using `ethers.Contract`. It needs the contract address, ABI (Application Binary Interface), and the signer.
*   **`loadTokenContract(tokenAddress)`**: Loads the token smart contract using `ethers.Contract`. It needs the token contract address, ABI (Application Binary Interface), and the signer.
*   **`updateDashboard()`**:  Fetches staking data from the contract (e.g., staked balance, reward balance, staking cap, reward pool) and updates the HTML elements on the dashboard. Uses `ethers.utils.formatEther()` to convert Wei to Ether for display.
*   **`stakeTokens()`**:  Handles the staking process:
    *   Gets the stake amount from the input field.
    *   Converts the amount to Wei using `ethers.utils.parseEther()`.
    *   Calls the `approve()` function on the token contract to allow the staking contract to spend tokens on behalf of the user.
    *   Calls the `stake()` function on the staking contract.
    *   Updates the dashboard.
*   **`unstakeTokens()`**: Handles the unstaking process:
    *   Gets the unstake amount from the input field.
    *   Converts the amount to Wei using `ethers.utils.parseEther()`.
    *   Calls the `unstake()` function on the staking contract.
    *   Updates the dashboard.
*   **`claimRewards()`**: Handles the reward claiming process:
    *   Calls the `claimReward()` function on the staking contract.
    *   Updates the dashboard.
*   **`document.addEventListener("DOMContentLoaded", async () => { ... });`**:  This code runs when the HTML document is fully loaded.  It initializes the dashboard, sets up event listeners for the buttons, and starts a timer to update the dashboard periodically.
*   **MetaMask Check:** Checks if MetaMask is installed in the browser.

**3. HTML (index.html):**

```html
<!DOCTYPE html>
<html>
<head>
    <title>Staking Yield Dashboard</title>
    <script src="https://cdn.ethers.io/lib/ethers-5.4.umd.min.js" type="application/javascript"></script>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <h1>Staking Yield Dashboard</h1>

    <div>
        <p>Token Name: <span id="tokenName">Loading...</span></p>
        <p>Token Balance: <span id="tokenBalance">Loading...</span></p>
        <p>Reward Token Name: <span id="rewardTokenName">Loading...</span></p>
        <p>Staked Balance: <span id="stakedBalance">Loading...</span></p>
        <p>Reward Balance: <span id="rewardBalance">Loading...</span></p>
        <p>Staking Cap: <span id="stakingCap">Loading...</span></p>
        <p>Reward Pool: <span id="rewardPool">Loading...</span></p>
    </div>

    <div>
        <h2>Stake Tokens</h2>
        <input type="number" id="stakeAmount" placeholder="Amount to stake">
        <button id="stakeButton">Stake</button>
    </div>

    <div>
        <h2>Unstake Tokens</h2>
        <input type="number" id="unstakeAmount" placeholder="Amount to unstake">
        <button id="unstakeButton">Unstake</button>
    </div>

    <div>
        <h2>Claim Rewards</h2>
        <button id="claimButton">Claim Rewards</button>
    </div>

    <script src="dashboard.js"></script>
</body>
</html>
```

**Explanation of HTML:**

*   Includes `ethers.js` from a CDN.
*   Contains placeholders (`<span>` elements with IDs) to display staking information.
*   Provides input fields and buttons for staking, unstaking, and claiming rewards.
*   Links the `dashboard.js` file.

**4. IERC20.json, StakingContract.json:**

Create two JSON files with the above names. Paste the contract's ABI. You can get this data from Remix after compiling the contract.

**Steps to Run this Example:**

1.  **Set up a Development Environment:**
    *   Install Node.js and npm (Node Package Manager).
    *   Install Ganache (a local blockchain emulator) or use Hardhat.
    *   Install MetaMask (a browser extension for interacting with Ethereum).

2.  **Install Dependencies:**
    *   Create a project directory.
    *   Navigate to the directory in your terminal.
    *   Run `npm init -y` to create a `package.json` file.
    *   Install `ethers.js`: `npm install ethers`

3.  **Deploy the Solidity Contract:**
    *   Compile the `StakingContract.sol` contract using Remix (an online Solidity IDE) or Hardhat.
    *   Deploy the compiled contract to your local Ganache blockchain.  Make sure Ganache is running.

4.  **Get the Contract Address:**
    *   After deployment, note the contract address provided by Remix or Hardhat.

5.  **Update `dashboard.js`:**
    *   Replace `"YOUR_CONTRACT_ADDRESS"` in `dashboard.js` with the actual contract address you obtained in step 4.
    *   Replace `"YOUR_TOKEN_ADDRESS"` with the address of your token address.
    *   Replace `"YOUR_REWARD_TOKEN_ADDRESS"` with the address of your reward token address.

6.  **Set up ERC20 Tokens**
    *   Create two ERC20 token contracts (one for staking, one for rewards) using Remix or Hardhat. You can copy a simple ERC20 implementation from OpenZeppelin's contracts.
    *   Deploy these token contracts to your local Ganache blockchain.
    *   Mint some tokens to your MetaMask account for testing.
    *   Approve the StakingContract to spend tokens on behalf of your account.

7.  **Run the Dashboard:**
    *   Open `index.html` in your browser.
    *   Make sure MetaMask is connected to your local Ganache network and is using the account you minted tokens to.

8.  **Interact with the Dashboard:**
    *   Enter amounts in the input fields and click the "Stake," "Unstake," and "Claim Rewards" buttons.
    *   Observe the updated balances on the dashboard.

**Important Notes:**

*   **Error Handling:**  The JavaScript code includes basic `try...catch` blocks, but you should add more comprehensive error handling in a real application.
*   **User Experience:** This is a basic example.  A real dashboard would have a much richer user interface with loading indicators, transaction confirmations, and better error messages.
*   **Security:** This example is *not* production-ready. You must conduct thorough security audits before deploying any staking contract to a live blockchain.
*   **Gas Costs:**  Be mindful of gas costs when interacting with the contract. Complex staking logic can be expensive.
*   **Frontend Frameworks:** Consider using a frontend framework like React, Vue, or Angular for building a more maintainable and scalable dashboard.

This comprehensive example gives you a solid foundation for building a more complex and feature-rich blockchain-based staking yield dashboard. Remember to prioritize security and user experience in a real-world application.
👁️ Viewed: 12

Comments