Multi-Tier Staking Reward Calculator JavaScript, Solidity

👤 Sharing: AI
Okay, let's craft a multi-tier staking reward calculator using JavaScript and Solidity.  This example will illustrate a simplified version of how such a system could work.

**Solidity (Smart Contract - `StakingContract.sol`)**

```solidity
pragma solidity ^0.8.0;

contract StakingContract {

    // State Variables
    address public owner;
    uint256 public totalStaked;

    struct StakerInfo {
        uint256 stakeAmount;
        uint256 startTime;
        uint8 tier; // Staking tier
    }

    mapping(address => StakerInfo) public stakers;

    // Staking Tiers and APR (Annual Percentage Rate)
    uint8 public constant NUM_TIERS = 3;  // Define number of tiers
    uint256[NUM_TIERS] public aprs = [5, 10, 15]; // APRs for each tier (in percentage, i.e. 5 = 5%)
    uint256[NUM_TIERS] public stakingMinimums = [100, 500, 1000]; // Minimum stake required for each tier

    // Event
    event Staked(address staker, uint256 amount, uint8 tier);
    event Unstaked(address staker, uint256 amount, uint256 reward);

    // Modifier to restrict access to the contract owner
    modifier onlyOwner() {
        require(msg.sender == owner, "Only owner can call this function");
        _;
    }

    // Constructor
    constructor() {
        owner = msg.sender;
    }

    // Function to set APRs for each tier. Only owner can call this function
    function setAprs(uint256[NUM_TIERS] memory _aprs) public onlyOwner {
        aprs = _aprs;
    }

     // Function to set minimum staking amounts for each tier. Only owner can call this function
    function setStakingMinimums(uint256[NUM_TIERS] memory _stakingMinimums) public onlyOwner {
        stakingMinimums = _stakingMinimums;
    }


    // Function to stake tokens
    function stake(uint256 _amount) public {
        require(_amount > 0, "Amount must be greater than zero");
        require(stakers[msg.sender].stakeAmount == 0, "Already staking");

        // Determine the tier based on the staking amount
        uint8 tier = determineTier(_amount);

        // Update staker information
        stakers[msg.sender] = StakerInfo({
            stakeAmount: _amount,
            startTime: block.timestamp,
            tier: tier
        });

        totalStaked += _amount;

        // Emit event
        emit Staked(msg.sender, _amount, tier);
    }

    // Internal function to determine the tier based on the staking amount
    function determineTier(uint256 _amount) internal view returns (uint8) {
        for (uint8 i = NUM_TIERS - 1; i > 0; i--) {
            if (_amount >= stakingMinimums[i]) {
                return i;
            }
        }
        return 0;  // Default to tier 0 if amount is less than all minimums
    }

    // Function to calculate reward
    function calculateReward(address _staker) public view returns (uint256) {
        StakerInfo storage staker = stakers[_staker];
        require(staker.stakeAmount > 0, "Not staking");

        uint256 timeStaked = block.timestamp - staker.startTime;
        uint256 apr = aprs[staker.tier];

        // Calculate reward based on APR and time staked.  Simple calculation for example.
        // (stakeAmount * apr * timeStaked) / (100 * 365 days in seconds)
        return (staker.stakeAmount * apr * timeStaked) / (100 * 365 * 1 days); //Adjust for day calculation.
    }


    // Function to unstake tokens
    function unstake() public {
        StakerInfo storage staker = stakers[msg.sender];
        require(staker.stakeAmount > 0, "Not staking");

        uint256 reward = calculateReward(msg.sender);
        uint256 amount = staker.stakeAmount;

        // Reset staker info
        delete stakers[msg.sender]; //Resets the struct to default values.
        totalStaked -= amount;

        // Emit event
        emit Unstaked(msg.sender, amount, reward);

        // Normally you would transfer the amount and reward to the staker here.
        // For this example, we'll just leave a comment.
        // transferToken(msg.sender, amount + reward);
    }


    // In a real-world scenario, you would have a function to transfer tokens
    // from the contract to the user.  This is a placeholder.
    // function transferToken(address _to, uint256 _amount) internal {
    //     // Actual transfer logic would go here
    // }


    //Optional functions to read contract state.
    function getStakerInfo(address _staker) public view returns (uint256 stakeAmount, uint256 startTime, uint8 tier) {
        StakerInfo memory staker = stakers[_staker];
        return (staker.stakeAmount, staker.startTime, staker.tier);
    }

     function getAprForTier(uint8 _tier) public view returns (uint256) {
        require(_tier < NUM_TIERS, "Invalid tier");
        return aprs[_tier];
    }

    function getStakingMinimumForTier(uint8 _tier) public view returns (uint256) {
        require(_tier < NUM_TIERS, "Invalid tier");
        return stakingMinimums[_tier];
    }

}
```

**Explanation (Solidity):**

1.  **`pragma solidity ^0.8.0;`**: Specifies the Solidity compiler version.
2.  **`contract StakingContract { ... }`**: Defines the smart contract.
3.  **State Variables:**
    *   `owner`: Address of the contract owner (who can set APRs).
    *   `totalStaked`: The total amount staked in the contract.
    *   `StakerInfo` struct: Stores information about each staker (stake amount, start time, tier).
    *   `stakers` mapping: Maps staker addresses to their `StakerInfo`.
    *   `NUM_TIERS`: Constant that defines the number of tiers available
    *   `aprs`: Array that stores the Annual Percentage Rates (APRs) for each tier.
    *   `stakingMinimums`: Array that stores the minimum staking amounts for each tier.
4.  **Events:** `Staked` and `Unstaked` events are emitted when staking or unstaking occurs. These are essential for tracking activity on the blockchain.
5.  **`onlyOwner` Modifier:**  A modifier to restrict certain functions to only be callable by the contract owner.
6.  **`constructor()`**: Sets the `owner` upon contract deployment.
7. **`setAprs(uint256[NUM_TIERS] memory _aprs)`**: Sets the APR values for each staking tier.
8. **`setStakingMinimums(uint256[NUM_TIERS] memory _stakingMinimums)`**: Sets the minimum staking amount for each tier.
9.  **`stake(uint256 _amount)`**:
    *   Allows users to stake tokens.
    *   Determines the staking tier based on the amount staked using the `determineTier` function.
    *   Updates the `stakers` mapping with the stake amount, start time, and tier.
    *   Updates `totalStaked`.
    *   Emits the `Staked` event.
10. **`determineTier(uint256 _amount)`**:  An internal function that determines the staking tier based on the staked amount by comparing to `stakingMinimums`.
11. **`calculateReward(address _staker)`**: Calculates the reward based on the staked amount, tier, and the time the tokens have been staked. The reward calculation is simplified for this example.
12. **`unstake()`**:
    *   Allows users to unstake their tokens.
    *   Calculates the reward using `calculateReward()`.
    *   Resets the staker's information in the `stakers` mapping.
    *   Updates `totalStaked`.
    *   Emits the `Unstaked` event.
    *   *Important:*  In a real implementation, this function would also handle the transfer of the staked tokens and the accrued rewards back to the staker.  This example only includes a comment where the token transfer logic would go.
13. **`getStakerInfo(address _staker)`**: A public view function to retrieve information about a staker.
14. **`getAprForTier(uint8 _tier)`**: A public view function to retrieve the APR for a given tier.
15. **`getStakingMinimumForTier(uint8 _tier)`**: A public view function to retrieve the minimum staking amount for a given tier.

**JavaScript (Frontend Interaction - `app.js`)**

```javascript
//  Needs to be run in an environment that supports ethers.js (e.g., Node.js with ethers installed, or a browser with MetaMask)
const { ethers } = require("ethers");

// Replace with your contract address and ABI
const contractAddress = "YOUR_CONTRACT_ADDRESS"; // Replace with your actual contract address
const contractABI = [
    // Paste your contract ABI here (from Solidity compiler output)
    {
        "inputs": [],
        "stateMutability": "nonpayable",
        "type": "constructor"
    },
    {
        "anonymous": false,
        "inputs": [
            {
                "indexed": true,
                "internalType": "address",
                "name": "staker",
                "type": "address"
            },
            {
                "indexed": false,
                "internalType": "uint256",
                "name": "amount",
                "type": "uint256"
            },
            {
                "indexed": false,
                "internalType": "uint8",
                "name": "tier",
                "type": "uint8"
            }
        ],
        "name": "Staked",
        "type": "event"
    },
    {
        "anonymous": false,
        "inputs": [
            {
                "indexed": true,
                "internalType": "address",
                "name": "staker",
                "type": "address"
            },
            {
                "indexed": false,
                "internalType": "uint256",
                "name": "amount",
                "type": "uint256"
            },
            {
                "indexed": false,
                "internalType": "uint256",
                "name": "reward",
                "type": "uint256"
            }
        ],
        "name": "Unstaked",
        "type": "event"
    },
    {
        "inputs": [
            {
                "internalType": "address",
                "name": "_staker",
                "type": "address"
            }
        ],
        "name": "calculateReward",
        "outputs": [
            {
                "internalType": "uint256",
                "name": "",
                "type": "uint256"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [
            {
                "internalType": "uint8",
                "name": "_tier",
                "type": "uint8"
            }
        ],
        "name": "getAprForTier",
        "outputs": [
            {
                "internalType": "uint256",
                "name": "",
                "type": "uint256"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [
            {
                "internalType": "uint8",
                "name": "_tier",
                "type": "uint8"
            }
        ],
        "name": "getStakingMinimumForTier",
        "outputs": [
            {
                "internalType": "uint256",
                "name": "",
                "type": "uint256"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [
            {
                "internalType": "address",
                "name": "_staker",
                "type": "address"
            }
        ],
        "name": "getStakerInfo",
        "outputs": [
            {
                "internalType": "uint256",
                "name": "stakeAmount",
                "type": "uint256"
            },
            {
                "internalType": "uint256",
                "name": "startTime",
                "type": "uint256"
            },
            {
                "internalType": "uint8",
                "name": "tier",
                "type": "uint8"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [
            {
                "internalType": "uint256",
                "name": "_amount",
                "type": "uint256"
            }
        ],
        "name": "stake",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function"
    },
    {
        "inputs": [],
        "name": "unstake",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function"
    }
];

async function main() {
    // Connect to a provider (e.g., Infura, Alchemy, or a local Ganache instance)
    const provider = new ethers.providers.JsonRpcProvider("YOUR_PROVIDER_URL");  // Replace with your provider URL

    // Get a signer (e.g., from MetaMask) - you'll need to unlock your wallet
    const signer = new ethers.Wallet("YOUR_PRIVATE_KEY", provider); // Replace with your private key, for testing purposes only. NEVER hardcode private keys in production.

    // Create a contract instance
    const stakingContract = new ethers.Contract(contractAddress, contractABI, signer);

    // Example usage:

    // 1. Stake tokens
    const stakeAmount = ethers.utils.parseEther("600"); // Stake 600 tokens (adjust amount as needed)
    console.log(`Staking ${ethers.utils.formatEther(stakeAmount)} tokens...`);
    const stakeTx = await stakingContract.stake(stakeAmount);
    await stakeTx.wait();  // Wait for the transaction to be mined
    console.log("Staking transaction confirmed!");

    // 2. Get Staker Info
    const stakerInfo = await stakingContract.getStakerInfo(signer.address);
    console.log("Staker Info:", {
        stakeAmount: ethers.utils.formatEther(stakerInfo.stakeAmount),
        startTime: stakerInfo.startTime.toString(),
        tier: stakerInfo.tier.toString(),
    });

    // 3. Calculate Reward
    const reward = await stakingContract.calculateReward(signer.address);
    console.log("Estimated Reward:", ethers.utils.formatEther(reward), "tokens");

    // 4. Unstake tokens
    console.log("Unstaking tokens...");
    const unstakeTx = await stakingContract.unstake();
    await unstakeTx.wait();
    console.log("Unstaking transaction confirmed!");

}

main().catch((error) => {
    console.error(error);
});
```

**Explanation (JavaScript):**

1.  **Dependencies:** Requires the `ethers` library to interact with the Ethereum blockchain.  You'll need to install it: `npm install ethers`.
2.  **Configuration:**
    *   `contractAddress`:  Replace `"YOUR_CONTRACT_ADDRESS"` with the actual address of your deployed `StakingContract` on the blockchain.
    *   `contractABI`: Replace the placeholder with the ABI (Application Binary Interface) of your `StakingContract`.  You'll get this when you compile your Solidity code.  It describes the contract's functions and data structures.
    *   `provider`:  Replace `"YOUR_PROVIDER_URL"` with the URL of an Ethereum provider.  This could be:
        *   Infura: A popular hosted Ethereum API service (you'll need an account and API key).
        *   Alchemy: Another hosted Ethereum API service.
        *   A local Ganache instance: If you're developing locally.
    *   `signer`:  Replace `"YOUR_PRIVATE_KEY"` with the private key of an Ethereum account that you want to use to interact with the contract.  **Important:** *Never* hardcode private keys in production code. Use a secure way to manage private keys (e.g., a hardware wallet or a secure vault). For testing, you can use a private key generated by Ganache.
3.  **`main()` function:**
    *   Connects to the Ethereum provider using `ethers.providers.JsonRpcProvider()`.
    *   Creates a `signer` using `ethers.Wallet()`.  The `signer` is the account that will be used to send transactions to the contract.
    *   Creates a `stakingContract` instance using `new ethers.Contract()`. This creates a JavaScript object that represents your smart contract.
    *   **Example Usage:**  The code then demonstrates how to call various functions of the contract:
        *   `stake()`: Stakes a specified amount of tokens. `ethers.utils.parseEther()` is used to convert the amount from a human-readable format (e.g., "1.0") to the smallest unit of Ether (Wei).
        *   `getStakerInfo()`: Retrieves information about a staker (stake amount, start time, tier).
        *   `calculateReward()`: Calculates the estimated reward for a staker.
        *   `unstake()`: Unstakes the tokens and claims the reward.
        *   `ethers.utils.formatEther()` is used to convert the amounts from Wei back to a human-readable Ether format.
4.  **Error Handling:**  The `main()` function is wrapped in a `try...catch` block to handle any errors that might occur.

**To Run This Example:**

1.  **Set up your environment:**
    *   Install Node.js and npm.
    *   Install `ethers`: `npm install ethers`.
    *   Install hardhat: `npm install --save-dev hardhat`
2.  **Compile and Deploy the Solidity Contract:**
    *   You'll need to use a tool like Hardhat, Remix, or Truffle to compile and deploy the `StakingContract.sol` smart contract to a blockchain (e.g., a local Ganache instance, a testnet like Goerli, or the mainnet).
    *   **Get the Contract Address and ABI:** After deployment, you'll get the address of the deployed contract and its ABI.  You'll need these values for the JavaScript code.
3.  **Configure `app.js`:**
    *   Update `contractAddress`, `contractABI`, `provider`, and `signer` in `app.js` with the correct values.
4.  **Run the JavaScript Code:**
    *   Execute the JavaScript code using Node.js: `node app.js`.

**Important Considerations:**

*   **Security:**  This is a simplified example and should *not* be used in a production environment without thorough security audits and testing.  Smart contracts are vulnerable to attacks, so security is paramount.
*   **Token Transfers:**  The `transferToken()` function in the Solidity code is a placeholder.  In a real implementation, you would need to integrate with an ERC-20 token contract to transfer tokens into the staking contract and back to the stakers.
*   **Gas Costs:**  Smart contract operations cost gas.  Be mindful of gas costs when designing your staking system.  Optimize your code to minimize gas usage.
*   **Time Calculations:**  The time calculations in this example are simplified.  For more accurate reward calculations, you might need to consider block timestamps and potential time zone differences.
*   **Reentrancy Attacks:** Staking contracts are particularly vulnerable to reentrancy attacks. Implement reentrancy guards to protect against such attacks.
*   **Decimals:** Remember to account for the number of decimals used by the token you're staking.  Many ERC-20 tokens have 18 decimals.

This provides a foundational example. Remember to thoroughly research and adapt this code to your specific needs, considering security and best practices for smart contract development.  Good luck!
👁️ Viewed: 9

Comments