Blockchain-Based Staking Yield Dashboard Solidity, JavaScript
👤 Sharing: AI
Okay, let's craft a basic example of a blockchain-based staking yield dashboard. I'll use Solidity for a simplified staking contract, and then JavaScript with Ethers.js to interact with the contract and display yield data on a hypothetical dashboard. This example is geared towards demonstrating the core concepts and won't be production-ready (e.g., it lacks robust security measures, error handling, and advanced features).
**Solidity (Staking Contract)**
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract Staking is Ownable {
IERC20 public immutable stakingToken; // The ERC20 token to stake
uint256 public totalStaked;
uint256 public constant APR = 10; // 10% APR (annual percentage rate)
struct Stake {
uint256 amount;
uint256 startTime;
}
mapping(address => Stake) public stakes;
mapping(address => bool) public hasStaked;
event Staked(address indexed user, uint256 amount);
event Unstaked(address indexed user, uint256 amount);
constructor(address _stakingTokenAddress) {
stakingToken = IERC20(_stakingTokenAddress);
}
function stake(uint256 _amount) public {
require(_amount > 0, "Amount must be greater than zero.");
require(stakingToken.allowance(msg.sender, address(this)) >= _amount, "Allowance too low.");
stakingToken.transferFrom(msg.sender, address(this), _amount);
totalStaked += _amount;
if (!hasStaked[msg.sender]) {
stakes[msg.sender] = Stake({
amount: _amount,
startTime: block.timestamp
});
hasStaked[msg.sender] = true;
} else {
stakes[msg.sender].amount += _amount;
}
emit Staked(msg.sender, _amount);
}
function unstake() public {
require(hasStaked[msg.sender], "You have not staked anything.");
uint256 amount = stakes[msg.sender].amount;
stakes[msg.sender].amount = 0;
hasStaked[msg.sender] = false; // Reset staking status
stakingToken.transfer(msg.sender, amount);
totalStaked -= amount;
emit Unstaked(msg.sender, amount);
}
function calculateYield(address _user) public view returns (uint256) {
if (!hasStaked[_user]) {
return 0;
}
uint256 stakeAmount = stakes[_user].amount;
uint256 stakeDuration = block.timestamp - stakes[_user].startTime; // Duration in seconds
uint256 secondsInYear = 365 * 24 * 60 * 60;
// Calculate yield based on APR
uint256 yield = (stakeAmount * APR * stakeDuration) / (100 * secondsInYear); // Simplification for example
return yield;
}
// Emergency function to recover any stuck tokens, only callable by the owner.
function recoverERC20(address _tokenAddress, address _recipient, uint256 _amount) public onlyOwner {
IERC20 token = IERC20(_tokenAddress);
token.transfer(_recipient, _amount);
}
}
```
**Explanation of Solidity Code:**
1. **`pragma solidity ^0.8.0;`**: Specifies the Solidity compiler version.
2. **`import "@openzeppelin/contracts/token/ERC20/IERC20.sol";`**: Imports the IERC20 interface from OpenZeppelin for interacting with ERC20 tokens. We use this interface to define that our staking contract will interact with a specific ERC20 token.
3. **`import "@openzeppelin/contracts/access/Ownable.sol";`**: Imports the Ownable contract from OpenZeppelin, which provides basic access control to ensure only the owner can call certain functions (e.g., `recoverERC20`).
4. **`contract Staking is Ownable`**: Defines the `Staking` contract, inheriting from the OpenZeppelin `Ownable` contract for ownership management.
5. **`IERC20 public immutable stakingToken;`**: Declares a public, immutable variable `stakingToken` of type `IERC20`. This will store the address of the ERC20 token that users can stake. `immutable` means it can only be set in the constructor and cannot be changed afterward.
6. **`uint256 public totalStaked;`**: Tracks the total amount of tokens staked in the contract.
7. **`uint256 public constant APR = 10;`**: Defines a constant `APR` (Annual Percentage Rate) for calculating the yield. In this example, it's set to 10%.
8. **`struct Stake`**: Defines a structure to hold the staking information for each user. It contains the `amount` staked and the `startTime` of the staking period.
9. **`mapping(address => Stake) public stakes;`**: A mapping from user address to their `Stake` information.
10. **`mapping(address => bool) public hasStaked;`**: A mapping from user address to a boolean value, indicating whether a user has already staked tokens. This helps differentiate new stakers from existing ones when calling the stake function.
11. **`event Staked(address indexed user, uint256 amount);`**: Emits an event when a user stakes tokens. The `indexed` keyword allows for easier filtering of events by the `user` address.
12. **`event Unstaked(address indexed user, uint256 amount);`**: Emits an event when a user unstakes tokens.
13. **`constructor(address _stakingTokenAddress)`**: The constructor of the contract. It takes the address of the ERC20 token to be used for staking as input and sets the `stakingToken` variable.
14. **`stake(uint256 _amount)`**: Allows users to stake tokens. It requires the user to have approved the contract to spend their tokens (using `approve` on the ERC20 token contract). It transfers the specified amount of tokens from the user to the contract and updates the user's stake information.
15. **`unstake()`**: Allows users to unstake their tokens. It transfers the staked amount back to the user and resets their stake information.
16. **`calculateYield(address _user)`**: Calculates the yield earned by a user based on their staked amount, staking duration, and the APR.
17. **`recoverERC20(address _tokenAddress, address _recipient, uint256 _amount)`**: An emergency function (only callable by the contract owner) to recover any ERC20 tokens accidentally sent to the contract.
**JavaScript (Dashboard Interaction - Ethers.js)**
```javascript
<!DOCTYPE html>
<html>
<head>
<title>Staking Yield Dashboard</title>
<script src="https://cdn.ethers.io/lib/ethers-5.6.umd.min.js" type="application/javascript"></script>
<style>
body { font-family: sans-serif; }
.container { width: 80%; margin: 0 auto; }
.data-item { margin-bottom: 10px; }
button { padding: 10px 15px; cursor: pointer; }
</style>
</head>
<body>
<div class="container">
<h1>Staking Yield Dashboard</h1>
<div class="data-item">
<label for="contractAddress">Contract Address:</label>
<input type="text" id="contractAddress" value="YOUR_CONTRACT_ADDRESS_HERE" size="50">
</div>
<div class="data-item">
<label for="tokenAddress">Token Address:</label>
<input type="text" id="tokenAddress" value="YOUR_TOKEN_ADDRESS_HERE" size="50">
</div>
<div class="data-item">
<label for="accountAddress">Account Address:</label>
<input type="text" id="accountAddress" value="YOUR_ACCOUNT_ADDRESS_HERE" size="50">
</div>
<div class="data-item">
<label for="stakeAmount">Stake Amount:</label>
<input type="number" id="stakeAmount" value="100">
</div>
<button id="connectWalletButton">Connect Wallet</button>
<p id="walletStatus"></p>
<button id="stakeButton" disabled>Stake</button>
<button id="unstakeButton" disabled>Unstake</button>
<div class="data-item">
<strong>Total Staked:</strong> <span id="totalStaked">Loading...</span>
</div>
<div class="data-item">
<strong>Your Staked Amount:</strong> <span id="yourStakedAmount">Loading...</span>
</div>
<div class="data-item">
<strong>Estimated Yield:</strong> <span id="estimatedYield">Loading...</span>
</div>
</div>
<script>
const contractAddressInput = document.getElementById('contractAddress');
const tokenAddressInput = document.getElementById('tokenAddress');
const accountAddressInput = document.getElementById('accountAddress');
const stakeAmountInput = document.getElementById('stakeAmount');
const connectWalletButton = document.getElementById('connectWalletButton');
const walletStatus = document.getElementById('walletStatus');
const stakeButton = document.getElementById('stakeButton');
const unstakeButton = document.getElementById('unstakeButton');
const totalStakedSpan = document.getElementById('totalStaked');
const yourStakedAmountSpan = document.getElementById('yourStakedAmount');
const estimatedYieldSpan = document.getElementById('estimatedYield');
let signer = null;
let stakingContract = null;
let tokenContract = null;
// Function to update UI elements.
async function updateUI() {
try {
const contractAddress = contractAddressInput.value;
const tokenAddress = tokenAddressInput.value;
const accountAddress = accountAddressInput.value;
if (!ethers.utils.isAddress(contractAddress) || !ethers.utils.isAddress(tokenAddress) || !ethers.utils.isAddress(accountAddress)) {
console.error("Invalid contract, token, or account address");
return;
}
const stakingContract = new ethers.Contract(contractAddress, stakingAbi, signer);
const tokenContract = new ethers.Contract(tokenAddress, erc20Abi, signer);
const totalStaked = await stakingContract.totalStaked();
totalStakedSpan.textContent = ethers.utils.formatEther(totalStaked);
const stakeInfo = await stakingContract.stakes(accountAddress);
yourStakedAmountSpan.textContent = ethers.utils.formatEther(stakeInfo.amount);
const estimatedYield = await stakingContract.calculateYield(accountAddress);
estimatedYieldSpan.textContent = ethers.utils.formatEther(estimatedYield);
} catch (error) {
console.error("Error fetching data:", error);
}
}
connectWalletButton.addEventListener('click', async () => {
if (window.ethereum) {
try {
await window.ethereum.request({ method: "eth_requestAccounts" });
const provider = new ethers.providers.Web3Provider(window.ethereum);
signer = provider.getSigner();
const contractAddress = contractAddressInput.value;
const tokenAddress = tokenAddressInput.value;
if (!ethers.utils.isAddress(contractAddress) || !ethers.utils.isAddress(tokenAddress)) {
alert("Invalid contract or token address");
return;
}
stakingContract = new ethers.Contract(contractAddress, stakingAbi, signer);
tokenContract = new ethers.Contract(tokenAddress, erc20Abi, signer);
walletStatus.textContent = "Wallet connected";
stakeButton.disabled = false;
unstakeButton.disabled = false;
updateUI();
} catch (error) {
walletStatus.textContent = "Wallet connection failed: " + error.message;
console.error(error);
}
} else {
walletStatus.textContent = "Please install MetaMask!";
}
});
stakeButton.addEventListener('click', async () => {
try {
const amount = ethers.utils.parseEther(stakeAmountInput.value);
//Approve
const approveTx = await tokenContract.approve(contractAddressInput.value, amount);
await approveTx.wait();
const tx = await stakingContract.stake(amount);
await tx.wait();
alert("Stake successful!");
updateUI();
} catch (error) {
console.error("Stake failed:", error);
alert("Stake failed: " + error.message);
}
});
unstakeButton.addEventListener('click', async () => {
try {
const tx = await stakingContract.unstake();
await tx.wait();
alert("Unstake successful!");
updateUI();
} catch (error) {
console.error("Unstake failed:", error);
alert("Unstake failed: " + error.message);
}
});
//ABIs (replace with your actual ABI)
const stakingAbi = [
"constructor(address _stakingTokenAddress)",
"function stake(uint256 _amount)",
"function unstake()",
"function calculateYield(address _user) public view returns (uint256)",
"function totalStaked() public view returns (uint256)",
"function stakes(address) view returns (uint256 amount, uint256 startTime)",
];
const erc20Abi = [
"function approve(address spender, uint256 amount) external returns (bool)",
"function allowance(address owner, address spender) external view returns (uint256)",
"function balanceOf(address account) external view returns (uint256)",
"function transfer(address recipient, uint256 amount) external returns (bool)"
];
</script>
</body>
</html>
```
**Explanation of JavaScript Code:**
1. **HTML Structure:** Creates a basic HTML structure with input fields for the contract address, account address, and stake amount. It also includes elements to display the total staked amount, the user's staked amount, and the estimated yield.
2. **Ethers.js Integration:** Includes the Ethers.js library from a CDN.
3. **`const ethers = require('ethers');`**: Imports the Ethers.js library. *Note:* In a browser environment, you'd typically include Ethers.js via a `<script>` tag from a CDN, as I've done in the HTML above. The `require` statement is for Node.js environments.
4. **Variable Declarations:** Declares variables to store references to HTML elements and contract instances.
5. **`connectWalletButton.addEventListener('click', ...)`**: Attaches an event listener to the "Connect Wallet" button. When clicked, it:
* Checks if MetaMask (or another Ethereum provider) is installed.
* Requests access to the user's accounts using `window.ethereum.request({ method: "eth_requestAccounts" })`.
* Creates a `Web3Provider` instance using `window.ethereum`.
* Gets a `signer` (an account that can sign transactions) from the provider.
* Creates instances of the `Staking` and ERC20 token contracts using their addresses and ABIs.
* Updates the UI to indicate that the wallet is connected and enables the stake/unstake buttons.
6. **`stakeButton.addEventListener('click', ...)`**: Attaches an event listener to the Stake button. When clicked, it:
* Parses the stake amount from the input field.
* Calls the `approve` function on the ERC20 token contract to allow the staking contract to spend the user's tokens.
* Calls the `stake` function on the staking contract.
* Waits for the transaction to be confirmed.
* Updates the UI.
7. **`unstakeButton.addEventListener('click', ...)`**: Attaches an event listener to the Unstake button. When clicked, it:
* Calls the `unstake` function on the staking contract.
* Waits for the transaction to be confirmed.
* Updates the UI.
8. **`updateUI()`**: Fetches data from the smart contract and updates the UI elements with the retrieved values. It retrieves the total staked amount, the user's staked amount, and the estimated yield. Error handling is included to catch potential issues when fetching data.
9. **ABIs** Store a simplified version of the contract's ABIs for easier interaction with the contract.
**How to Run This Example (Simplified):**
1. **Prerequisites:**
* Node.js and npm installed.
* MetaMask or another Ethereum wallet browser extension installed and configured.
* A local Ethereum development environment (e.g., Hardhat, Ganache) or a testnet (e.g., Goerli).
2. **Deploy the Solidity Contract:**
* Use a tool like Remix, Hardhat, or Truffle to deploy the `Staking` contract to your local environment or testnet.
* Deploy a mock ERC20 token as well. You can find a simple ERC20 example on OpenZeppelin.
* *Important:* Note the contract addresses of both the `Staking` contract and the ERC20 token.
3. **Set Up the JavaScript Dashboard:**
* Create an HTML file (e.g., `index.html`) and copy the JavaScript code into it.
* Replace `"YOUR_CONTRACT_ADDRESS_HERE"` with the actual address of your deployed `Staking` contract.
* Replace `"YOUR_TOKEN_ADDRESS_HERE"` with the address of your deployed ERC20 token contract.
* Replace `"YOUR_ACCOUNT_ADDRESS_HERE"` with the address of an account you control in MetaMask.
4. **Serve the HTML File:**
* You can use a simple web server (e.g., `npx serve`) to serve the HTML file. Navigate to the URL in your browser (usually `http://localhost:3000` or similar).
5. **Connect MetaMask:**
* Open MetaMask and connect to the same network where you deployed the contracts (e.g., your local Hardhat network).
6. **Approve and Stake:**
* In the dashboard, enter the amount of tokens you want to stake.
* Click the "Connect Wallet" button.
* Click the "Stake" button. MetaMask will prompt you to approve the transaction.
7. **View Results:**
* After the transaction is confirmed, the dashboard will update to show the total staked amount, your staked amount, and the estimated yield.
**Important Notes and Improvements:**
* **ABIs:** The `stakingAbi` and `erc20Abi` in the JavaScript code are simplified. In a real application, you would use the complete ABIs generated by your Solidity compiler. You can get the full ABI from the compilation output of Remix, Hardhat, or Truffle.
* **Error Handling:** The error handling in the JavaScript code is basic. You should add more robust error handling to catch potential issues and provide informative messages to the user.
* **Security:** This example is not secure for production use. Consider security best practices, such as using OpenZeppelin contracts, auditing your code, and implementing appropriate access controls.
* **Gas Optimization:** The Solidity code can be optimized for gas efficiency.
* **User Experience:** The user experience can be improved by adding loading indicators, more detailed transaction confirmations, and better error messages.
* **Real-World APR:** In a real-world staking system, the APR would likely be more dynamic and based on factors such as the total staked amount and the governance of the protocol.
* **Events:** Use events to efficiently update the UI when staking and unstaking actions occur. Listen for `Staked` and `Unstaked` events.
* **Displaying Token Decimals:** When formatting token amounts (using `ethers.utils.formatEther`), ensure you account for the token's decimal precision. ERC20 tokens can have different numbers of decimal places. You'll need to retrieve the token's `decimals` value from the token contract.
This example provides a foundation for building a more sophisticated staking yield dashboard. Remember to prioritize security, user experience, and robust error handling in a production application.
👁️ Viewed: 8
Comments