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