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