Multi-Layer Smart Staking Protocol Solidity, Web3, JavaScript
👤 Sharing: AI
Okay, here's a basic, illustrative example of a multi-layer smart staking protocol using Solidity, Web3.js, and JavaScript. It focuses on the core logic and assumes a simplified scenario for demonstration. Remember that this is a conceptual example and would need substantial auditing and security hardening for a real-world deployment.
**1. Solidity Smart Contract (MultiLayerStaking.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 "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract MultiLayerStaking is Ownable, ReentrancyGuard {
IERC20 public stakingToken; // The token users stake
uint256 public totalStaked; // Total amount staked in the contract
uint256 public totalRewardsDistributed;
// Reward Token: Could be the same as stakingToken or a different token.
IERC20 public rewardToken;
uint256 public rewardRatePerTokenPerSecond; // Rewards distributed per token staked, per second
uint256 public lastRewardUpdate; // Last time rewards were updated. Used for compounding.
struct Staker {
uint256 amount; // Amount staked by the user
uint256 rewardDebt; // Reward debt of the user (accumulated rewards that haven't been claimed).
uint256 lastStakeTime; // Timestamp of the last stake/unstake action
}
mapping(address => Staker) public stakers;
event Staked(address indexed user, uint256 amount);
event Unstaked(address indexed user, uint256 amount);
event RewardPaid(address indexed user, uint256 amount);
event RewardRateUpdated(uint256 newRate);
constructor(address _stakingToken, address _rewardToken) Ownable() {
stakingToken = IERC20(_stakingToken);
rewardToken = IERC20(_rewardToken);
}
// Modifier to check if the contract has enough rewards to distribute
modifier sufficientRewards(uint256 _amount) {
require(rewardToken.balanceOf(address(this)) >= _amount, "Insufficient rewards in contract");
_;
}
// Function to set the reward rate (only owner)
function setRewardRate(uint256 _rewardRatePerTokenPerSecond) external onlyOwner {
updateRewardPool(); // Update the reward pool before changing rate
rewardRatePerTokenPerSecond = _rewardRatePerTokenPerSecond;
lastRewardUpdate = block.timestamp;
emit RewardRateUpdated(_rewardRatePerTokenPerSecond);
}
// Function to stake tokens
function stake(uint256 _amount) external nonReentrant {
require(_amount > 0, "Amount must be greater than 0");
updateRewardPool(); // Update reward pool before staking
Staker storage staker = stakers[msg.sender];
// Claim pending rewards before staking
if (staker.amount > 0) {
_claimRewards(msg.sender, staker);
}
stakingToken.transferFrom(msg.sender, address(this), _amount);
staker.amount += _amount;
staker.rewardDebt = _calculateRewardDebt(msg.sender, staker);
staker.lastStakeTime = block.timestamp;
totalStaked += _amount;
emit Staked(msg.sender, _amount);
}
// Function to unstake tokens
function unstake(uint256 _amount) external nonReentrant {
require(_amount > 0, "Amount must be greater than 0");
require(stakers[msg.sender].amount >= _amount, "Insufficient staked balance");
updateRewardPool(); // Update reward pool before unstaking
Staker storage staker = stakers[msg.sender];
_claimRewards(msg.sender, staker);
staker.amount -= _amount;
staker.rewardDebt = _calculateRewardDebt(msg.sender, staker); //Recalculate for accuracy
totalStaked -= _amount;
stakingToken.transfer(msg.sender, _amount); // Send back staked tokens
emit Unstaked(msg.sender, _amount);
}
// Function to claim rewards
function claimRewards() external nonReentrant {
updateRewardPool(); // Update reward pool before claiming
Staker storage staker = stakers[msg.sender];
_claimRewards(msg.sender, staker);
}
// Internal function to claim rewards
function _claimRewards(address _user, Staker storage staker) internal {
uint256 reward = _calculateRewards(_user, staker);
if (reward > 0) {
staker.rewardDebt = _calculateRewardDebt(_user, staker); //Set rewardDebt *before* transferring
rewardToken.transfer(_user, reward); // Transfer rewards
totalRewardsDistributed += reward;
emit RewardPaid(_user, reward);
}
}
// Internal function to calculate rewards for a user
function _calculateRewards(address _user, Staker storage staker) internal view returns (uint256) {
uint256 rewardPerTokenPaid = _rewardPerTokenStored(); // Get up-to-date reward per token.
uint256 reward = (staker.amount * rewardPerTokenPaid) - staker.rewardDebt; // Calculate rewards
return reward;
}
//Internal function to calculate user's reward debt
function _calculateRewardDebt(address _user, Staker storage staker) internal view returns (uint256) {
uint256 rewardPerTokenPaid = _rewardPerTokenStored(); // Get up-to-date reward per token.
return (staker.amount * rewardPerTokenPaid);
}
// Internal function to get the accumulated rewards per token staked
function _rewardPerTokenStored() internal view returns (uint256) {
if (totalStaked == 0) {
return 0; // Prevent division by zero.
}
uint256 timeElapsed = block.timestamp - lastRewardUpdate;
uint256 rewardIncrease = timeElapsed * rewardRatePerTokenPerSecond;
return rewardIncrease;
}
//Function to update reward pool (should be called before any state change)
function updateRewardPool() public {
if (block.timestamp > lastRewardUpdate) {
lastRewardUpdate = block.timestamp;
}
}
// Function to allow the owner to add rewards to the contract
function addRewards(uint256 _amount) external onlyOwner sufficientRewards(_amount) {
rewardToken.transferFrom(msg.sender, address(this), _amount);
}
// Emergency withdraw tokens
function emergencyWithdrawal(address _tokenAddress, address _to, uint256 _amount) external onlyOwner {
IERC20(_tokenAddress).transfer(_to, _amount);
}
// Emergency withdraw ETH
function emergencyWithdrawalETH(address payable _to, uint256 _amount) external onlyOwner {
(bool success, ) = _to.call{value: _amount}("");
require(success, "ETH transfer failed");
}
}
```
**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 ERC20 interface from OpenZeppelin for interacting with ERC20 tokens. You'll need to install OpenZeppelin contracts (e.g., `npm install @openzeppelin/contracts`).
* **`import "@openzeppelin/contracts/access/Ownable.sol";`**: Imports the Ownable contract from OpenZeppelin to manage contract ownership and restrict certain functions to the owner.
* **`import "@openzeppelin/contracts/security/ReentrancyGuard.sol";`**: Imports the ReentrancyGuard contract from OpenZeppelin to prevent reentrancy attacks.
* **`contract MultiLayerStaking is Ownable, ReentrancyGuard`**: Defines the smart contract named `MultiLayerStaking` and inherits `Ownable` and `ReentrancyGuard`.
* **`stakingToken`**: An `IERC20` interface representing the token that users will stake.
* **`rewardToken`**: An `IERC20` interface representing the token that users will receive as rewards.
* **`totalStaked`**: The total amount of `stakingToken` currently staked in the contract.
* **`totalRewardsDistributed`**: The total amount of `rewardToken` already distributed.
* **`rewardRatePerTokenPerSecond`**: The amount of `rewardToken` distributed per unit of `stakingToken` staked per second.
* **`lastRewardUpdate`**: The timestamp of the last time rewards were updated. This is essential for accurately calculating accrued rewards.
* **`Staker` struct**:
* **`amount`**: The amount of `stakingToken` staked by a particular user.
* **`rewardDebt`**: A value that represents the accumulated but unclaimed rewards that the user is owed. This is a key mechanism to prevent rounding errors and ensure accurate reward distribution. It's calculated as `user.amount * rewardPerTokenStored()` at the time of staking/unstaking/claiming.
* **`lastStakeTime`**: The timestamp of the last stake or unstake action performed by the staker.
* **`stakers` mapping**: A mapping from user address to `Staker` struct, storing the staking information for each user.
* **Events**: `Staked`, `Unstaked`, `RewardPaid`, and `RewardRateUpdated` are emitted to provide a record of staking and reward events on the blockchain.
* **`constructor(address _stakingToken, address _rewardToken) Ownable()`**: The constructor that initializes the `stakingToken` and `rewardToken` addresses. The `Ownable()` constructor also sets the contract deployer as the owner.
* **`modifier sufficientRewards(uint256 _amount)`**: A modifier to ensure that the contract has enough reward tokens before distributing them.
* **`setRewardRate(uint256 _rewardRatePerTokenPerSecond)`**: A function to set the `rewardRatePerTokenPerSecond`. Only the owner can call this. It updates the reward pool first to ensure accurate calculation.
* **`stake(uint256 _amount)`**: Allows a user to stake `_amount` of `stakingToken`. It transfers the tokens from the user to the contract, updates the `stakers` mapping, and emits a `Staked` event. Critically, it *first* checks if the user has existing stake and claims any outstanding rewards. It also updates the reward pool.
* **`unstake(uint256 _amount)`**: Allows a user to unstake `_amount` of `stakingToken`. It transfers the tokens from the contract to the user, updates the `stakers` mapping, and emits an `Unstaked` event. It also *first* claims any outstanding rewards. It also updates the reward pool.
* **`claimRewards()`**: Allows a user to claim any accumulated rewards.
* **`_claimRewards(address _user, Staker storage staker)` (internal)**: This internal function performs the actual reward claiming logic. It calculates the rewards owed to the user, transfers the `rewardToken` to the user, and resets the user's `rewardDebt`.
* **`_calculateRewards(address _user, Staker storage staker)` (internal, view)**: Calculates the rewards owed to a user based on their staked amount, the current `rewardPerTokenStored`, and their `rewardDebt`.
* **`_calculateRewardDebt(address _user, Staker storage staker)` (internal, view)**: Calculates the reward debt for a user.
* **`_rewardPerTokenStored()` (internal, view)**: Calculates the cumulative rewards per token staked since the last update. This is a central calculation for accurate reward distribution.
* **`updateRewardPool()`**: Updates the `lastRewardUpdate` timestamp to the current block timestamp. This is called before any state-changing operation (stake, unstake, claim rewards, set reward rate) to ensure that the reward pool is up-to-date.
* **`addRewards(uint256 _amount)`**: Allows the owner to add `rewardToken` to the contract.
* **`emergencyWithdrawal(address _tokenAddress, address _to, uint256 _amount)`**: Allows the owner to withdraw any ERC20 token from the contract in case of emergency.
* **`emergencyWithdrawalETH(address payable _to, uint256 _amount)`**: Allows the owner to withdraw ETH from the contract in case of emergency.
**2. Web3.js and JavaScript (index.html and script.js):**
**index.html:**
```html
<!DOCTYPE html>
<html>
<head>
<title>Multi-Layer Staking Protocol</title>
<script src="https://cdn.jsdelivr.net/npm/web3@1.6.0/dist/web3.min.js"></script>
</head>
<body>
<h1>Multi-Layer Staking Protocol</h1>
<p>Connected Account: <span id="account">Not connected</span></p>
<label for="stakeAmount">Stake Amount:</label>
<input type="number" id="stakeAmount" min="0">
<button onclick="stakeTokens()">Stake</button>
<label for="unstakeAmount">Unstake Amount:</label>
<input type="number" id="unstakeAmount" min="0">
<button onclick="unstakeTokens()">Unstake</button>
<button onclick="claimRewards()">Claim Rewards</button>
<p>Your Staked Balance: <span id="stakedBalance">0</span></p>
<p>Your Claimable Rewards: <span id="claimableRewards">0</span></p>
<p>Contract Staked Balance: <span id="contractStakedBalance">0</span></p>
<script src="script.js"></script>
</body>
</html>
```
**script.js:**
```javascript
// Configuration (Replace with your actual contract address and token addresses)
const stakingContractAddress = 'YOUR_STAKING_CONTRACT_ADDRESS'; // Replace with the deployed contract address
const stakingTokenAddress = 'YOUR_STAKING_TOKEN_ADDRESS'; // Replace with the staking token address
const rewardTokenAddress = 'YOUR_REWARD_TOKEN_ADDRESS'; // Replace with the reward token address
// ABI (Replace with the actual ABI of your deployed contract)
const stakingContractABI = [
// Paste the ABI here. You can get it from the Solidity compiler output (e.g., Remix).
{
"inputs": [
{
"internalType": "address",
"name": "_stakingToken",
"type": "address"
},
{
"internalType": "address",
"name": "_rewardToken",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"inputs": [],
"name": "claimRewards",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_amount",
"type": "uint256"
}
],
"name": "stake",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_amount",
"type": "uint256"
}
],
"name": "unstake",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "stakers",
"outputs": [
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "rewardDebt",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "lastStakeTime",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "rewardToken",
"outputs": [
{
"internalType": "contract IERC20",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "stakingToken",
"outputs": [
{
"internalType": "contract IERC20",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
}
];
let web3;
let stakingContract;
let account;
async function connectWallet() {
if (window.ethereum) {
web3 = new Web3(window.ethereum);
try {
// Request account access if needed
await window.ethereum.enable(); // Deprecated, but kept for wider compatibility. Use requestAccounts() for modern usage.
const accounts = await web3.eth.getAccounts();
account = accounts[0];
document.getElementById('account').innerText = account;
stakingContract = new web3.eth.Contract(stakingContractABI, stakingContractAddress);
// Fetch initial data
await updateUI();
} catch (error) {
console.error("User denied account access or error connecting:", error);
}
} else {
console.log('Please install MetaMask!');
alert('Please install MetaMask!');
}
}
async function updateUI() {
try {
const stakedBalance = await getStakedBalance(account);
const claimableRewards = await getClaimableRewards(account);
const contractStakedBalance = await getContractStakedBalance();
document.getElementById('stakedBalance').innerText = stakedBalance;
document.getElementById('claimableRewards').innerText = claimableRewards;
document.getElementById('contractStakedBalance').innerText = contractStakedBalance;
} catch (error) {
console.error("Error updating UI:", error);
}
}
async function getStakedBalance(userAddress) {
try {
const stakerInfo = await stakingContract.methods.stakers(userAddress).call();
return web3.utils.fromWei(stakerInfo.amount, 'ether'); // Assuming tokens are in ether units
} catch (error) {
console.error("Error fetching staked balance:", error);
return 0;
}
}
async function getClaimableRewards(userAddress) {
try {
//This calls a read-only function to calculate the rewards
const stakerInfo = await stakingContract.methods.stakers(userAddress).call();
const rewards = await stakingContract.methods._calculateRewards(userAddress, stakerInfo).call();
return web3.utils.fromWei(rewards, 'ether');
} catch (error) {
console.error("Error fetching claimable rewards:", error);
return 0;
}
}
async function getContractStakedBalance() {
try {
const totalStaked = await stakingContract.methods.totalStaked().call();
return web3.utils.fromWei(totalStaked, 'ether');
} catch (error) {
console.error("Error fetching contract staked balance:", error);
return 0;
}
}
async function stakeTokens() {
const amount = document.getElementById('stakeAmount').value;
if (!amount) {
alert("Please enter an amount to stake.");
return;
}
const amountWei = web3.utils.toWei(amount, 'ether'); // Convert to Wei
try {
// Assuming you have a function to approve the staking contract to spend tokens.
// You'd need the ERC20 token contract ABI and address here to call approve().
// const tokenContract = new web3.eth.Contract(tokenABI, stakingTokenAddress);
// await tokenContract.methods.approve(stakingContractAddress, amountWei).send({ from: account });
await stakingContract.methods.stake(amountWei).send({ from: account });
alert("Stake successful!");
await updateUI(); // Update UI after staking
} catch (error) {
console.error("Error staking tokens:", error);
alert("Error staking tokens. See console for details.");
}
}
async function unstakeTokens() {
const amount = document.getElementById('unstakeAmount').value;
if (!amount) {
alert("Please enter an amount to unstake.");
return;
}
const amountWei = web3.utils.toWei(amount, 'ether'); // Convert to Wei
try {
await stakingContract.methods.unstake(amountWei).send({ from: account });
alert("Unstake successful!");
await updateUI(); // Update UI after unstaking
} catch (error) {
console.error("Error unstaking tokens:", error);
alert("Error unstaking tokens. See console for details.");
}
}
async function claimRewards() {
try {
await stakingContract.methods.claimRewards().send({ from: account });
alert("Rewards claimed successfully!");
await updateUI(); // Update UI after claiming rewards
} catch (error) {
console.error("Error claiming rewards:", error);
alert("Error claiming rewards. See console for details.");
}
}
// Connect wallet on page load
window.onload = connectWallet;
```
**Explanation of the JavaScript/Web3.js Code:**
* **Configuration:**
* `stakingContractAddress`: **Replace with the actual address** of your deployed `MultiLayerStaking` contract.
* `stakingTokenAddress`: **Replace with the actual address** of the ERC20 token you're using for staking.
* `rewardTokenAddress`: **Replace with the actual address** of the ERC20 token you're using for rewards.
* `stakingContractABI`: **Replace with the actual ABI** (Application Binary Interface) of your compiled Solidity contract. The ABI describes the contract's functions, inputs, and outputs. You can get this from Remix or your Solidity compiler output. This is very important!
* **`web3`**: The Web3.js instance used to interact with the Ethereum blockchain.
* **`stakingContract`**: A Web3.js contract object representing your deployed `MultiLayerStaking` contract.
* **`account`**: The user's Ethereum account address.
* **`connectWallet()`**:
* Checks if MetaMask (or another Web3 provider) is installed.
* Creates a Web3 instance using the provider.
* Requests access to the user's accounts (using `window.ethereum.enable()`, which is deprecated but kept for compatibility). For modern MetaMask usage, use `window.ethereum.request({ method: 'eth_requestAccounts' })` instead.
* Gets the user's account address.
* Creates a `stakingContract` instance using the contract ABI and address.
* Calls `updateUI()` to display initial data.
* **`updateUI()`**:
* Fetches the user's staked balance, claimable rewards, and the contract's total staked balance using the contract methods.
* Updates the corresponding HTML elements with the retrieved data.
* **`getStakedBalance(userAddress)`**: Fetches the staked balance of a given user by calling the `stakers` mapping.
* **`getClaimableRewards(userAddress)`**: Calls the `_calculateRewards` function on the smart contract to retrieve the claimable rewards for a user.
* **`getContractStakedBalance()`**: Fetches the total staked balance held by the contract.
* **`stakeTokens()`**:
* Gets the stake amount from the input field.
* Converts the amount to Wei (the smallest unit of Ether). It is very important to do this conversion!
* **Crucially, it includes a comment about needing to approve the staking contract to spend the user's tokens.** **This is a critical step that's often missed.** You'll need to interact with the *staking token contract* to call its `approve()` method, granting the `stakingContractAddress` permission to transfer tokens on the user's behalf. This is a standard ERC20 requirement.
* Calls the `stake()` function on the smart contract.
* Updates the UI after staking.
* **`unstakeTokens()`**: Similar to `stakeTokens()`, but calls the `unstake()` function.
* **`claimRewards()`**: Calls the `claimRewards()` function on the smart contract.
**How to Run This Example:**
1. **Install MetaMask (or another Web3 provider):** This is a browser extension that allows you to interact with Ethereum dApps.
2. **Deploy the Solidity Contract:** Use Remix (online IDE), Hardhat, or Truffle to deploy the `MultiLayerStaking.sol` contract to a local development blockchain (like Ganache) or a test network (like Ropsten, Goerli, Sepolia, or Holesky). **Make sure you have test ETH in your MetaMask wallet.**
3. **Deploy ERC20 Tokens:** You'll also need to deploy two ERC20 token contracts (one for staking, one for rewards) or use existing test tokens. OpenZeppelin provides example ERC20 contracts that can be used for this purpose.
4. **Update Addresses:** **Carefully replace the placeholder addresses** in `script.js` with the actual addresses of your deployed contracts and tokens. This is *essential*.
5. **Update ABI:** Paste the actual ABI generated during compilation into `script.js`.
6. **Serve the HTML:** You can use a simple web server (like `python -m http.server` in the directory) or any other web server to serve the `index.html` file.
7. **Open in Browser:** Open `index.html` in your browser, connect your MetaMask wallet, and try staking, unstaking, and claiming rewards.
8. **Mint and Transfer Tokens:** Make sure to mint (create) some staking tokens and reward tokens and transfer them to your MetaMask account and to the staking contract (for rewards) before testing. For example, if using OpenZeppelin's ERC20, you can call the `mint()` function on the token contract (as the owner) to create tokens.
**Important Considerations and Improvements:**
* **Error Handling:** The JavaScript code has basic error handling, but you should add more robust error handling and user feedback. Display meaningful error messages to the user.
* **Gas Limit:** You might need to adjust the gas limit when sending transactions. MetaMask usually estimates this, but it's good to be aware of it.
* **Approvals (Very Important):** The JavaScript code *mentions* the need to approve the staking contract to spend tokens, but it doesn't actually implement the approval logic. You *must* implement this! You'll need to get the ABI of your ERC20 token contract and use Web3.js to call the `approve()` method on the token contract, allowing the staking contract to transfer tokens on the user's behalf. This is a standard ERC20 requirement.
* **Security:** This is a simplified example and has not been audited for security vulnerabilities. **Never deploy code like this to a production environment without a thorough security audit.** Consider things like:
* Reentrancy attacks (the `ReentrancyGuard` helps, but careful code review is still needed).
* Integer overflow/underflow.
* Denial-of-service (DoS) attacks.
* Front-running.
* **UI/UX:** The UI is very basic. Improve the user experience with better styling, loading indicators, and more informative messages.
* **Events:** Listen for the `Staked`, `Unstaked`, and `RewardPaid` events emitted by the contract and update the UI in real-time.
* **Token Decimals:** The code assumes that both the staking token and the reward token have 18 decimals. If they have a different number of decimals, you'll need to adjust the `toWei` and `fromWei` conversions accordingly.
* **Gas Optimization:** Solidity code can be optimized for gas efficiency.
* **Testing:** Write comprehensive unit tests for the Solidity contract using a testing framework like Hardhat or Truffle.
* **Centralization:** Note that this version relies on the owner to set the reward rate. Decentralized governance mechanisms could be introduced to manage this aspect in a more community-driven way.
* **Price Oracles:** In a real-world staking protocol, you might use price oracles to determine the value of the staking and reward tokens. This helps to ensure that the rewards are fair and sustainable.
* **Layered Staking:** This basic example does *not* implement multiple staking layers. To create multiple layers, you could add:
* Different reward rates for different tiers of stakers (based on the amount staked or the duration of staking).
* Locking periods (stakers can only unstake after a certain period).
* Tiered reward multipliers (higher tiers get higher rewards).
This enhanced version is more secure and robust. Remember to thoroughly test and audit your code before deploying it to a production environment. Remember to install the necessary npm packages (e.g., `@openzeppelin/contracts`).
👁️ Viewed: 8
Comments