Smart Staking System Solidity, JavaScript
👤 Sharing: AI
Okay, here's a simplified example of a "Smart Staking System" implemented with Solidity and JavaScript. This is a basic illustration and would need further development for real-world deployment, including proper security audits and more sophisticated features.
**Solidity Contract (Smart Contract - `StakingContract.sol`)**
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
// import "hardhat/console.sol";
contract StakingContract is Ownable {
IERC20 public stakingToken;
IERC20 public rewardToken;
uint256 public totalStaked;
uint256 public rewardRate;
uint256 public lastRewardTime;
uint256 public stakingStartTime;
mapping(address => uint256) public stakedBalances;
mapping(address => uint256) public earnedRewards;
mapping(address => uint256) public lastClaimTime;
event Staked(address indexed user, uint256 amount);
event Unstaked(address indexed user, uint256 amount);
event RewardPaid(address indexed user, uint256 amount);
constructor(address _stakingToken, address _rewardToken, uint256 _rewardRate, uint256 _stakingStartTime) Ownable() {
stakingToken = IERC20(_stakingToken);
rewardToken = IERC20(_rewardToken);
rewardRate = _rewardRate; // Rewards per second. Adjust accordingly.
lastRewardTime = block.timestamp;
stakingStartTime = _stakingStartTime;
totalStaked = 0;
}
// Modifier to ensure staking period is active
modifier stakingActive() {
require(block.timestamp >= stakingStartTime, "Staking not yet started");
_;
}
// Update reward rate
function setRewardRate(uint256 _rewardRate) external onlyOwner {
rewardRate = _rewardRate;
lastRewardTime = block.timestamp; // Reset last reward time when rate changes
}
// Stake tokens
function stake(uint256 _amount) external stakingActive {
require(_amount > 0, "Amount must be greater than zero");
stakingToken.transferFrom(msg.sender, address(this), _amount); // Ensure user has approved this contract to spend tokens
stakedBalances[msg.sender] += _amount;
totalStaked += _amount;
lastRewardTime = block.timestamp; // Update reward time
emit Staked(msg.sender, _amount);
}
// Withdraw tokens
function withdraw(uint256 _amount) external {
require(_amount > 0, "Amount must be greater than zero");
require(_amount <= stakedBalances[msg.sender], "Insufficient staked balance");
stakedBalances[msg.sender] -= _amount;
totalStaked -= _amount;
stakingToken.transfer(msg.sender, _amount);
lastRewardTime = block.timestamp; // Update reward time
emit Unstaked(msg.sender, _amount);
}
// Calculate rewards
function calculateRewards(address _user) public view returns (uint256) {
if (block.timestamp <= lastClaimTime[_user]) {
return earnedRewards[_user];
}
uint256 timeElapsed = block.timestamp - lastClaimTime[_user];
uint256 reward = (stakedBalances[_user] * rewardRate * timeElapsed) / 100;
return earnedRewards[_user] + reward;
}
// Claim rewards
function claimRewards() external {
uint256 reward = calculateRewards(msg.sender);
require(reward > 0, "No rewards to claim");
earnedRewards[msg.sender] = 0;
lastClaimTime[msg.sender] = block.timestamp;
rewardToken.transfer(msg.sender, reward);
emit RewardPaid(msg.sender, reward);
}
// Function to allow the owner to add reward tokens to the contract
function addRewardTokens(uint256 _amount) external onlyOwner {
rewardToken.transferFrom(msg.sender, address(this), _amount);
}
}
```
**Explanation of the Solidity Contract:**
* **`SPDX-License-Identifier: MIT`**: Specifies the license.
* **`pragma solidity ^0.8.0;`**: Specifies the Solidity compiler version.
* **`import "@openzeppelin/contracts/token/ERC20/IERC20.sol";`**: Imports the IERC20 interface from OpenZeppelin. This allows the contract to interact with ERC20 tokens (the staking token and reward token).
* **`import "@openzeppelin/contracts/access/Ownable.sol";`**: Imports the `Ownable` contract from OpenZeppelin. This provides basic access control, allowing only the owner to perform certain actions.
* **`contract StakingContract is Ownable { ... }`**: Defines the smart contract. It inherits from `Ownable`.
* **`IERC20 public stakingToken;`**: Stores the address of the ERC20 token that users will stake.
* **`IERC20 public rewardToken;`**: Stores the address of the ERC20 token that users will receive as rewards.
* **`uint256 public totalStaked;`**: Keeps track of the total amount of tokens staked in the contract.
* **`uint256 public rewardRate;`**: Defines the reward rate (e.g., reward tokens per second per staked token). Important: Choose a suitable scale to avoid division by zero or loss of precision.
* **`uint256 public lastRewardTime;`**: Stores the last time the reward was paid.
* **`uint256 public stakingStartTime;`**: Stores the time at which staking starts.
* **`mapping(address => uint256) public stakedBalances;`**: A mapping that stores the amount of tokens each user has staked.
* **`mapping(address => uint256) public earnedRewards;`**: A mapping that stores the earned rewards of each user.
* **`mapping(address => uint256) public lastClaimTime;`**: A mapping to record the last time each user claimed their rewards.
* **`event Staked(address indexed user, uint256 amount);`**: An event emitted when a user stakes tokens.
* **`event Unstaked(address indexed user, uint256 amount);`**: An event emitted when a user unstakes tokens.
* **`event RewardPaid(address indexed user, uint256 amount);`**: An event emitted when a user claims rewards.
* **`constructor(address _stakingToken, address _rewardToken, uint256 _rewardRate, uint256 _stakingStartTime) Ownable() { ... }`**: The constructor. It's called when the contract is deployed. It initializes the `stakingToken`, `rewardToken`, `rewardRate` and `stakingStartTime`. The `Ownable()` part initializes the owner.
* **`modifier stakingActive() { ... }`**: A modifier to check if the staking period has started.
* **`function setRewardRate(uint256 _rewardRate) external onlyOwner { ... }`**: Allows the owner to change the reward rate. It resets the `lastRewardTime` to the current time.
* **`function stake(uint256 _amount) external stakingActive { ... }`**: The `stake` function. It allows users to stake tokens. It transfers the staking tokens from the user to the contract. It updates the user's staked balance and the total staked balance. It update the `lastRewardTime`.
* **`function withdraw(uint256 _amount) external { ... }`**: The `withdraw` function. It allows users to withdraw tokens. It transfers the staking tokens from the contract to the user. It updates the user's staked balance and the total staked balance. It update the `lastRewardTime`.
* **`function calculateRewards(address _user) public view returns (uint256) { ... }`**: Calculates the rewards a user has earned. It calculates the time elapsed since the last claim or staking and multiplies it by the reward rate and the user's staked balance.
* **`function claimRewards() external { ... }`**: The `claimRewards` function. It allows users to claim their earned rewards. It transfers the reward tokens from the contract to the user. It resets the user's `earnedRewards`.
* **`function addRewardTokens(uint256 _amount) external onlyOwner { ... }`**: Allows the owner to add reward tokens to the contract. This is necessary for the contract to be able to pay out rewards.
**Important Considerations for the Solidity Contract:**
* **Security**: This is a simplified example and is vulnerable to various attacks. A real-world staking contract would require thorough security auditing and formal verification. Consider reentrancy attacks, integer overflows/underflows, and front-running.
* **Approval**: Users need to approve the contract to spend their staking tokens using `IERC20.approve()`.
* **Reward Rate Scale**: The `rewardRate` needs careful consideration. If it's too small, rewards will be negligible. If it's too large, the contract could run out of reward tokens quickly. You might need to use a scaling factor (e.g., rewards per 10<sup>18</sup> seconds) and adjust calculations accordingly.
* **Gas Costs**: Complex reward calculations can be expensive in terms of gas. Consider optimizing calculations and using techniques like checkpoints or snapshots to reduce gas costs.
* **Decimals**: ERC20 tokens have different decimal precisions. The contract needs to handle these differences correctly to avoid precision errors.
* **Upgradability**: Consider using an upgradeable contract pattern (e.g., proxy contracts) if you need to update the contract's logic in the future.
* **Testing**: Write comprehensive unit and integration tests to ensure the contract functions as expected.
**JavaScript (Frontend - `index.js`)**
```javascript
// index.js
const ethers = require('ethers');
// Replace with your contract address and ABI
const contractAddress = 'YOUR_CONTRACT_ADDRESS';
const stakingTokenAddress = 'YOUR_STAKING_TOKEN_ADDRESS'; // Optional, for fetching token details
const rewardTokenAddress = 'YOUR_REWARD_TOKEN_ADDRESS'; // Optional, for fetching token details
const contractABI = [
// Paste your contract ABI here (from the Solidity compiler output)
// Example (replace with your actual ABI):
{
"inputs": [
{
"internalType": "address",
"name": "_stakingToken",
"type": "address"
},
{
"internalType": "address",
"name": "_rewardToken",
"type": "address"
},
{
"internalType": "uint256",
"name": "_rewardRate",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "_stakingStartTime",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "claimRewards",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_user",
"type": "address"
}
],
"name": "calculateRewards",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_amount",
"type": "uint256"
}
],
"name": "stake",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "stakingStartTime",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "stakedBalances",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "stakingToken",
"outputs": [
{
"internalType": "contract IERC20",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "withdraw",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
]; // Replace with your ABI
// Connect to a provider (e.g., MetaMask, Infura)
const provider = new ethers.providers.Web3Provider(window.ethereum); // For MetaMask
// Get the signer (the user's account)
async function getSigner() {
await provider.send("eth_requestAccounts", []); // Request access to accounts
return provider.getSigner();
}
// Create a contract instance
async function getContract() {
const signer = await getSigner();
return new ethers.Contract(contractAddress, contractABI, signer);
}
// Example function to stake tokens
async function stakeTokens(amount) {
try {
const contract = await getContract();
const amountWei = ethers.utils.parseEther(amount); // Convert to Wei (smallest unit)
const tx = await contract.stake(amountWei);
console.log('Stake Transaction:', tx.hash);
await tx.wait(); // Wait for the transaction to be mined
console.log('Tokens staked successfully!');
} catch (error) {
console.error('Error staking tokens:', error);
}
}
// Example function to withdraw tokens
async function withdrawTokens(amount) {
try {
const contract = await getContract();
const amountWei = ethers.utils.parseEther(amount); // Convert to Wei (smallest unit)
const tx = await contract.withdraw(amountWei);
console.log('Withdraw Transaction:', tx.hash);
await tx.wait(); // Wait for the transaction to be mined
console.log('Tokens withdrawn successfully!');
} catch (error) {
console.error('Error withdrawing tokens:', error);
}
}
// Example function to claim rewards
async function claimRewards() {
try {
const contract = await getContract();
const tx = await contract.claimRewards();
console.log('Claim Rewards Transaction:', tx.hash);
await tx.wait(); // Wait for the transaction to be mined
console.log('Rewards claimed successfully!');
} catch (error) {
console.error('Error claiming rewards:', error);
}
}
// Example function to calculate rewards
async function calculateUserRewards(userAddress) {
try {
const contract = await getContract();
const rewards = await contract.calculateRewards(userAddress);
const readableRewards = ethers.utils.formatEther(rewards);
console.log('User Rewards:', readableRewards);
return readableRewards;
} catch (error) {
console.error('Error getting rewards:', error);
return null;
}
}
// Example function to get staked balance
async function getStakedBalance(userAddress) {
try {
const contract = await getContract();
const balance = await contract.stakedBalances(userAddress);
const readableBalance = ethers.utils.formatEther(balance);
console.log('Staked Balance:', readableBalance);
return readableBalance;
} catch (error) {
console.error('Error getting staked balance:', error);
return null;
}
}
// Example usage (call these functions from your HTML buttons/UI)
async function main() {
//Connect to MetaMask on load
await provider.send("eth_requestAccounts", []);
//Example calling
// await stakeTokens("10"); // Stake 10 tokens
// await withdrawTokens("5"); // Withdraw 5 tokens
// await claimRewards();
// await calculateUserRewards("0xaC946C2917E842a257D91d86f292411b8485f12a");
}
main();
// Expose functions to window for browser usage
window.stakeTokens = stakeTokens;
window.withdrawTokens = withdrawTokens;
window.claimRewards = claimRewards;
window.calculateUserRewards = calculateUserRewards;
window.getStakedBalance = getStakedBalance;
```
**Explanation of the JavaScript Code:**
* **`const ethers = require('ethers');`**: Imports the `ethers.js` library, which is used for interacting with Ethereum.
* **`const contractAddress = 'YOUR_CONTRACT_ADDRESS';`**: Replace this with the address of your deployed Solidity contract.
* **`const contractABI = [...]`**: Replace this with the ABI (Application Binary Interface) of your Solidity contract. The ABI is a JSON array that describes the functions and events in your contract. You can get this from the Solidity compiler output.
* **`const provider = new ethers.providers.Web3Provider(window.ethereum);`**: Creates a provider to connect to the Ethereum network. This example assumes you're using MetaMask (or another web3-enabled browser). `window.ethereum` is injected by MetaMask. You might need to use a different provider (e.g., Infura) if you're not using a browser extension.
* **`async function getSigner() { ... }`**: Gets the signer (the user's Ethereum account) from the provider. It prompts the user to connect their MetaMask account.
* **`async function getContract() { ... }`**: Creates an instance of the smart contract using the contract address, ABI, and signer.
* **`async function stakeTokens(amount) { ... }`**: A function that calls the `stake` function in the smart contract. It converts the `amount` to Wei (the smallest unit of Ether) using `ethers.utils.parseEther()`.
* **`async function withdrawTokens(amount) { ... }`**: A function that calls the `withdraw` function in the smart contract. It converts the `amount` to Wei (the smallest unit of Ether) using `ethers.utils.parseEther()`.
* **`async function claimRewards() { ... }`**: A function that calls the `claimRewards` function in the smart contract.
* **`async function calculateUserRewards(userAddress) { ... }`**: A function that calls the `calculateRewards` function in the smart contract.
* **`async function getStakedBalance(userAddress) { ... }`**: A function that calls the `stakedBalances` function in the smart contract.
* **`async function main() { ... }`**: An example `main` function that you can use to call the other functions. It's just for demonstration purposes.
* **`window.stakeTokens = stakeTokens;`**: Expose the functions for browser use.
**To Run This Example:**
1. **Install Dependencies:**
```bash
npm install ethers @openzeppelin/contracts
```
2. **Deploy the Solidity Contract:**
* You'll need to use a tool like Remix, Hardhat, or Truffle to compile and deploy the Solidity contract to a test network (e.g., Ganache, Goerli, Sepolia) or the main Ethereum network.
* **Important:** Fund the contract with reward tokens *after* deployment by calling the `addRewardTokens` function or sending tokens directly to the contract address.
* Get the deployed contract address and the ABI.
* Also, deploy a mock ERC20 token for both the staking token and reward token if you don't already have one. You can use OpenZeppelin's `ERC20PresetMinterPauser` contract for this.
3. **Create an HTML File (e.g., `index.html`):**
```html
<!DOCTYPE html>
<html>
<head>
<title>Staking DApp</title>
</head>
<body>
<h1>Staking DApp</h1>
<label for="stakeAmount">Stake Amount:</label>
<input type="number" id="stakeAmount" placeholder="Enter amount to stake">
<button onclick="stakeTokens(document.getElementById('stakeAmount').value)">Stake</button>
<label for="withdrawAmount">Withdraw Amount:</label>
<input type="number" id="withdrawAmount" placeholder="Enter amount to withdraw">
<button onclick="withdrawTokens(document.getElementById('withdrawAmount').value)">Withdraw</button>
<button onclick="claimRewards()">Claim Rewards</button>
<label for="userAddress">User Address:</label>
<input type="text" id="userAddress" placeholder="Enter address to calculate rewards">
<button onclick="calculateUserRewards(document.getElementById('userAddress').value)">Calculate Rewards</button>
<label for="address">User Address:</label>
<input type="text" id="address" placeholder="Enter address to see staked balance">
<button onclick="getStakedBalance(document.getElementById('address').value)">Get Staked Balance</button>
<script src="index.js"></script>
</body>
</html>
```
4. **Configure JavaScript:**
* Replace `YOUR_CONTRACT_ADDRESS` in `index.js` with the actual address of your deployed contract.
* Replace the `contractABI` in `index.js` with the ABI of your contract.
5. **Serve the HTML File:**
* You can use a simple web server (e.g., `npx serve`) to serve the `index.html` file.
6. **Open in Browser:**
* Open the `index.html` file in a browser that has MetaMask installed and connected to the correct Ethereum network.
**How to Use:**
1. **Connect MetaMask:** The DApp will prompt you to connect your MetaMask account.
2. **Approve Tokens:** Before staking, you need to approve the staking contract to spend your staking tokens. You'll need to call the `approve` function on the staking token contract. You can add this to the JavaScript code:
```javascript
const stakingTokenContractAddress = 'YOUR_STAKING_TOKEN_CONTRACT_ADDRESS'; // Replace with staking token contract address
async function approveStaking(amount) {
try {
const signer = await getSigner();
const tokenContract = new ethers.Contract(stakingTokenContractAddress, [
"function approve(address spender, uint256 amount) external returns (bool)"
], signer);
const amountWei = ethers.utils.parseEther(amount);
const tx = await tokenContract.approve(contractAddress, amountWei);
console.log('Approve Transaction:', tx.hash);
await tx.wait();
console.log('Approval successful!');
} catch (error) {
console.error('Error approving tokens:', error);
}
}
```
Add a button to your HTML to call this function and approve a sufficient amount.
3. **Stake Tokens:** Enter the amount of tokens you want to stake and click the "Stake" button.
4. **Withdraw Tokens:** Enter the amount of tokens you want to withdraw and click the "Withdraw" button.
5. **Claim Rewards:** Click the "Claim Rewards" button to claim your earned rewards.
6. **Calculate Rewards:** Enter the user address you want to see rewards.
7. **Get Staked Balance:** Enter the user address you want to see stake balance.
**Important Notes:**
* **Error Handling:** The JavaScript code includes basic error handling with `try...catch` blocks, but you should add more robust error handling and user feedback to your DApp.
* **UI/UX:** This is a very basic example. You'll need to create a more user-friendly interface with better styling, loading indicators, and notifications.
* **Asynchronous Operations:** Remember that interacting with the blockchain is asynchronous. Use `async/await` properly to handle promises.
* **Network Fees (Gas):** Users will need to pay gas fees to execute transactions on the Ethereum network.
This example provides a starting point for building a more complete and secure smart staking system. Remember to prioritize security and thorough testing before deploying any smart contract to a production environment. Good luck!
👁️ Viewed: 9
Comments