Multi-Tier Staking Reward Calculator JavaScript, Solidity

👤 Sharing: AI
Okay, here's an example implementation of a multi-tier staking reward calculator, combining JavaScript for a simple user interface (simulated) and Solidity for the core logic of the staking contract.

**Important Considerations:**

*   **Security:** This is a simplified example.  Real-world staking contracts require rigorous security audits to prevent vulnerabilities like reentrancy attacks, integer overflows/underflows, and other exploits.  Always use established and audited libraries (e.g., OpenZeppelin) in production environments.
*   **Gas Optimization:** Solidity contracts can become expensive due to gas costs. Careful design and optimization are crucial.
*   **Precision:**  Solidity doesn't natively support floating-point numbers.  Calculations often involve fixed-point arithmetic or integer approximations. This example uses basic integer math.
*   **Scalability:**  Consider the impact of a large number of stakers on the contract's performance.

**Solidity Contract (MultiTierStaking.sol):**

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract MultiTierStaking {

    struct Staker {
        uint256 amount;
        uint256 startTime;
        uint8 tier;
    }

    mapping(address => Staker) public stakers;
    IERC20 public token; // Address of the staking token
    IERC20 public rewardToken; // Address of the reward token
    uint256 public totalStaked;

    //Tier Definitions
    struct Tier {
        uint8 tierId;
        uint256 apr; // annual percentage rate (as a percentage 1000 = 10%)
        uint256 minStake;
    }
    Tier[] public tiers;

    uint256 public constant SECONDS_IN_YEAR = 365 days;

    event Staked(address indexed user, uint256 amount, uint8 tier);
    event Unstaked(address indexed user, uint256 amount);
    event RewardPaid(address indexed user, uint256 amount);

    constructor(address _tokenAddress, address _rewardTokenAddress) {
        token = IERC20(_tokenAddress);
        rewardToken = IERC20(_rewardTokenAddress);
    }

    // Define a Tier
    function addTier(uint8 _tierId, uint256 _apr, uint256 _minStake) external {
        require(tiers.length < 5, "Max Tiers reached"); //Limit number of tiers.
        tiers.push(Tier(_tierId, _apr, _minStake));
    }

    function stake(uint256 _amount, uint8 _tierId) external {
        require(stakers[msg.sender].amount == 0, "Already staked");
        require(_amount > 0, "Amount must be greater than zero");

        Tier memory selectedTier = getTier(_tierId);
        require(_amount >= selectedTier.minStake, "Insufficient amount for selected tier");

        token.transferFrom(msg.sender, address(this), _amount);
        stakers[msg.sender] = Staker(_amount, block.timestamp, _tierId);
        totalStaked += _amount;

        emit Staked(msg.sender, _amount, _tierId);
    }

    function unstake() external {
        Staker storage staker = stakers[msg.sender];
        require(staker.amount > 0, "Not staked");

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

        delete stakers[msg.sender];
        totalStaked -= amount;

        token.transfer(msg.sender, amount);
        rewardToken.transfer(msg.sender, reward);

        emit Unstaked(msg.sender, amount);
        emit RewardPaid(msg.sender, reward);
    }

    function calculateReward(address _user) public view returns (uint256) {
        Staker storage staker = stakers[_user];
        require(staker.amount > 0, "Not staked");

        Tier memory selectedTier = getTier(staker.tier);

        uint256 timeElapsed = block.timestamp - staker.startTime;
        uint256 reward = (staker.amount * selectedTier.apr * timeElapsed) / (1000 * SECONDS_IN_YEAR); // APR is a percentage
        return reward;
    }

    function getTier(uint8 _tierId) internal view returns (Tier memory){
        for(uint i = 0; i < tiers.length; i++){
            if(tiers[i].tierId == _tierId){
                return tiers[i];
            }
        }
        revert("Tier does not exist.");
    }
}

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount) external returns (bool);
    function allowance(address owner, address spender) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
}
```

**Explanation of Solidity Contract:**

1.  **`pragma solidity ^0.8.0;`**: Specifies the Solidity compiler version.

2.  **`contract MultiTierStaking { ... }`**: Defines the staking contract.

3.  **`struct Staker { ... }`**:  A struct to hold information about each staker:
    *   `amount`: The amount of tokens staked.
    *   `startTime`: The block timestamp when the staking began.
    *   `tier`: The tier the user is staking in

4.  **`mapping(address => Staker) public stakers;`**: A mapping from Ethereum address to `Staker` structs, allowing you to look up a staker's info.

5.  **`IERC20 public token;`**: The address of the ERC20 token that will be staked.

6.  **`IERC20 public rewardToken;`**: The address of the ERC20 token used to reward stakers.

7.  **`uint256 public totalStaked;`**: Tracks the total amount of tokens staked in the contract.

8.  **`struct Tier { ... }`**: Defines tiers for staking:
    *   `tierId`: A unique ID for this tier.
    *   `apr`:  The annual percentage rate for this tier (expressed as a percentage e.g. 1000 = 10%).
    *   `minStake`: The minimum stake amount for this tier.

9.  **`Tier[] public tiers;`**: A dynamically sized array to hold the tiers.

10. **`uint256 public constant SECONDS_IN_YEAR = 365 days;`**: A constant to represent the number of seconds in a year.

11. **`event Staked( ... );`**: An event emitted when a user stakes tokens.

12. **`event Unstaked( ... );`**: An event emitted when a user unstakes tokens.

13. **`event RewardPaid( ... );`**: An event emitted when a user receives a reward.

14. **`constructor(address _tokenAddress, address _rewardTokenAddress) { ... }`**: The constructor, which sets the addresses of the staking token and reward token.

15. **`addTier(uint8 _tierId, uint256 _apr, uint256 _minStake) external { ... }`**: Adds a new staking tier. Only contract owner can call this function.

16. **`stake(uint256 _amount, uint8 _tierId) external { ... }`**:  Allows a user to stake tokens:
    *   Requires that the user isn't already staking.
    *   Requires that the amount staked is greater than zero and meets the minimum stake for the tier.
    *   Transfers tokens from the user to the contract using `token.transferFrom()`.  (The user needs to `approve` the contract to spend their tokens first).
    *   Updates the `stakers` mapping with the stake information.
    *   Updates `totalStaked`.
    *   Emits the `Staked` event.

17. **`unstake() external { ... }`**:  Allows a user to unstake their tokens and claim rewards:
    *   Requires that the user is currently staking.
    *   Calculates the reward using `calculateReward()`.
    *   Removes the staker's information from the `stakers` mapping.
    *   Transfers the staked tokens and rewards to the user using `token.transfer()` and `rewardToken.transfer()`.
    *   Emits `Unstaked` and `RewardPaid` events.

18. **`calculateReward(address _user) public view returns (uint256) { ... }`**: Calculates the reward for a given user:
    *   Requires that the user is currently staking.
    *   Calculates the time elapsed since the staking began.
    *   Calculates the reward based on the staked amount, APR, and time elapsed.
        *   `reward = (staker.amount * selectedTier.apr * timeElapsed) / (1000 * SECONDS_IN_YEAR);`
    *   Returns the calculated reward.

19.  **`getTier(uint8 _tierId) internal view returns (Tier memory){ ... }`**: Helper function to retrieve a tier by its ID.

20. **`interface IERC20 { ... }`**: An interface for interacting with ERC20 tokens.  It defines the essential functions (like `transfer`, `balanceOf`, `approve`, etc.) that your contract needs to call on the staking and reward tokens.

**JavaScript (Simulated UI - console-based):**

```javascript
// Simulated Token Contract (for demonstration purposes)
class MockERC20 {
  constructor(name, symbol, initialSupply, decimals = 18) {
    this.name = name;
    this.symbol = symbol;
    this.totalSupply = initialSupply;
    this.balances = {};
    this.allowances = {};
    this.decimals = decimals;
  }

  balanceOf(account) {
    return this.balances[account] || 0;
  }

  transfer(recipient, amount) {
    if (this.balances[msg.sender] >= amount) {
      this.balances[msg.sender] -= amount;
      this.balances[recipient] = (this.balances[recipient] || 0) + amount;
      return true;
    }
    return false;
  }

  transferFrom(sender, recipient, amount) {
    if (this.allowances[sender][msg.sender] >= amount && this.balances[sender] >= amount) {
      this.balances[sender] -= amount;
      this.balances[recipient] = (this.balances[recipient] || 0) + amount;
      this.allowances[sender][msg.sender] -= amount;
      return true;
    }
    return false;
  }

  approve(spender, amount) {
    this.allowances[msg.sender] = this.allowances[msg.sender] || {};
    this.allowances[msg.sender][spender] = amount;
    return true;
  }
}


// Mock Blockchain and Context
let block = {
    timestamp: Math.floor(Date.now() / 1000) // Current Unix timestamp
};

let msg = {
    sender: "0xf39Fd6e515471Ac92314Dc00b966FA861295180e" // Example address
};

// Simulated Staking Contract (Interacts with MockERC20)
class StakingContract {
    constructor(token, rewardToken) {
        this.token = token;
        this.rewardToken = rewardToken;
        this.stakers = {};
        this.totalStaked = 0;
        this.tiers = [];
        this.SECONDS_IN_YEAR = 365 * 24 * 60 * 60;
    }

    addTier(tierId, apr, minStake) {
        if (this.tiers.length < 5) {
            this.tiers.push({ tierId, apr, minStake });
        } else {
            console.log("Max tiers reached.");
        }
    }

    stake(amount, tierId) {
        if (this.stakers[msg.sender]) {
            console.log("Already staked.");
            return;
        }

        const selectedTier = this.getTier(tierId);
        if (!selectedTier) {
            console.log("Invalid tier.");
            return;
        }

        if (amount < selectedTier.minStake) {
            console.log("Insufficient amount for selected tier.");
            return;
        }

        if (this.token.transferFrom(msg.sender, this, amount)) {
            this.stakers[msg.sender] = {
                amount: amount,
                startTime: block.timestamp,
                tierId: tierId
            };
            this.totalStaked += amount;
            console.log(`${msg.sender} staked ${amount} tokens in tier ${tierId}.`);
        } else {
            console.log("Stake transfer failed.");
        }
    }

    unstake() {
        const staker = this.stakers[msg.sender];
        if (!staker) {
            console.log("Not staked.");
            return;
        }

        const reward = this.calculateReward(msg.sender);
        const amount = staker.amount;

        delete this.stakers[msg.sender];
        this.totalStaked -= amount;

        if (this.token.transfer(msg.sender, amount)) {
            if (this.rewardToken.transfer(msg.sender, reward)) {
                console.log(`${msg.sender} unstaked ${amount} tokens and received ${reward} reward tokens.`);
            } else {
                console.log("Reward transfer failed.");
                //Revert Logic (In a real system, you'd want proper state reversion)
            }
        } else {
            console.log("Unstake transfer failed.");
            //Revert Logic
        }
    }

    calculateReward(user) {
        const staker = this.stakers[user];
        if (!staker) {
            console.log("Not staked.");
            return 0;
        }

        const selectedTier = this.getTier(staker.tierId);
        if (!selectedTier) {
            console.log("Invalid tier.");
            return 0;
        }

        const timeElapsed = block.timestamp - staker.startTime;
        const reward = (staker.amount * selectedTier.apr * timeElapsed) / (1000 * this.SECONDS_IN_YEAR);
        return reward;
    }

    getTier(tierId) {
        return this.tiers.find(tier => tier.tierId === tierId);
    }
}

// Example Usage
const stakingToken = new MockERC20("StakingToken", "STK", 1000000);
const rewardToken = new MockERC20("RewardToken", "RWD", 100000);
const stakingContract = new StakingContract(stakingToken, rewardToken);

//Initial Token Distribution
stakingToken.balances[msg.sender] = 10000;

// Set up some tiers
stakingContract.addTier(1, 50, 100);  // Tier 1: 5% APR, min stake 100
stakingContract.addTier(2, 100, 1000); // Tier 2: 10% APR, min stake 1000

// Simulate staking
stakingToken.approve(stakingContract, 5000); //Approve contract to spend tokens
stakingContract.stake(500, 1); // Stake 500 in Tier 1
console.log("Staking Token Balance: ", stakingToken.balanceOf(msg.sender));

//Advance Time
block.timestamp += 365 * 24 * 60 * 60; //One year later

// Simulate unstaking
stakingContract.unstake();
console.log("Staking Token Balance: ", stakingToken.balanceOf(msg.sender));
console.log("Reward Token Balance: ", rewardToken.balanceOf(msg.sender));
```

**Explanation of JavaScript:**

1.  **`MockERC20` class**: Simulates an ERC20 token.  It's a simplified version of an ERC20 and includes `balanceOf`, `transfer`, `transferFrom`, and `approve` methods.  In a real application, you'd use a library like `ethers.js` or `web3.js` to interact with actual ERC20 contracts on the blockchain.

2.  **`block` and `msg` objects**: These are mock objects that simulate the blockchain environment.  `block.timestamp` represents the current block timestamp, and `msg.sender` represents the address of the user interacting with the contract.

3.  **`StakingContract` class**:  This simulates the Solidity staking contract.
    *   `constructor`: Initializes the contract with the staking token and reward token.
    *   `addTier`:  Adds a staking tier.
    *   `stake`:  Simulates staking tokens. It checks if the user is already staking, verifies the amount, and then transfers the tokens.
    *   `unstake`: Simulates unstaking tokens and claiming rewards. It calculates the reward, transfers tokens back to the user, and transfers the reward tokens.
    *   `calculateReward`:  Calculates the reward based on the staked amount, APR, and time elapsed.
    *   `getTier`: retrieves tier based on tierID.

4.  **Example Usage**:
    *   Creates instances of `MockERC20` for the staking token and reward token.
    *   Creates an instance of the `StakingContract`.
    *   Sets up some staking tiers using `addTier`.
    *   Simulates a user staking tokens using `stake`.
    *   Advances the `block.timestamp` to simulate time passing.
    *   Simulates a user unstaking tokens using `unstake`.
    *   Prints the balances of the user to show the results.

**How to Run:**

1.  **Solidity (Deployment):**
    *   You'll need a Solidity development environment like Remix, Truffle, or Hardhat.
    *   Compile the `MultiTierStaking.sol` contract.
    *   Deploy the staking token, reward token, and the staking contract to a test network (e.g., Ganache, Hardhat Network, Goerli).
    *   Note the addresses of the deployed contracts.

2.  **JavaScript (Simulation):**
    *   Copy the JavaScript code into a file (e.g., `staking.js`).
    *   Open your browser's developer console (usually by pressing F12).
    *   Copy and paste the JavaScript code into the console and press Enter.
    *   The script will execute, simulating the staking process and printing results to the console.

**Key Improvements and Considerations for Real-World Deployment:**

*   **Use a Proper Blockchain Interaction Library:** Replace the `MockERC20` class with a library like `ethers.js` or `web3.js` to interact with real ERC20 contracts and the Ethereum blockchain.
*   **Error Handling:** Add more robust error handling to the JavaScript code.
*   **User Interface:** Create a proper HTML/CSS-based user interface for a better user experience.
*   **Security Audits:** Before deploying to a live network, have your Solidity contract audited by security professionals.
*   **Gas Optimization:**  Optimize your Solidity code to minimize gas costs.  Consider using techniques like:
    *   Using `calldata` instead of `memory` for function arguments when appropriate.
    *   Packing variables to reduce storage costs.
    *   Using assembly for critical calculations.
*   **Upgradability:**  Consider using a proxy pattern to make your contract upgradable in the future (using OpenZeppelin's libraries for secure upgradability).
*   **Events:** Use events extensively to provide off-chain applications with information about the contract's state.
*   **Access Control:**  Implement proper access control using `Ownable` from OpenZeppelin to restrict certain functions (like adding tiers) to the contract owner.
*   **Time Manipulation:** Be careful when manipulating time in tests or simulations.  Use functions like `block.timestamp` carefully and avoid relying on wall-clock time in production code.

This example provides a basic framework.  Building a secure and robust staking contract requires a deep understanding of Solidity, blockchain security principles, and best practices.  Good luck!
👁️ Viewed: 9

Comments