Multi-Layer Smart Staking Protocol Solidity, Web3, JavaScript

👤 Sharing: AI
```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";

/**
 * @title Multi-Layer Smart Staking Protocol
 * @notice This contract allows users to stake ERC20 tokens and earn rewards based on different staking tiers and lock-up periods.
 * @dev Implements a simplified multi-layer staking system with basic functionality.  Further development could include:
 *      - Dynamic reward rates based on total staked amount.
 *      - Governance for modifying reward parameters.
 *      - More complex unlocking schedules.
 *      - Penalty for early unstaking.
 */
contract MultiLayerStaking is Ownable, ReentrancyGuard {

    IERC20 public stakingToken; // ERC20 token used for staking
    uint256 public totalStaked; // Total amount of tokens staked across all users

    // Struct to represent staking tiers
    struct StakingTier {
        uint256 apr;          // Annual Percentage Rate for this tier (in percentage points, e.g., 1000 = 10%)
        uint256 lockUpDays;   // Lock-up period in days
    }

    // Mapping of tier ID to StakingTier struct
    mapping(uint256 => StakingTier) public stakingTiers;

    // Struct to store staking information for each user
    struct Stake {
        uint256 tierId;       // ID of the staking tier used
        uint256 amount;      // Amount of tokens staked
        uint256 startTime;   // Timestamp when the staking began
        uint256 endTime;     // Timestamp when the lock-up period ends
        uint256 rewardClaimed; // Amount of rewards already claimed
    }

    // Mapping of user address to array of their stakes
    mapping(address => Stake[]) public userStakes;

    // Events
    event Staked(address indexed user, uint256 tierId, uint256 amount);
    event Unstaked(address indexed user, uint256 stakeIndex, uint256 amount, uint256 reward);
    event RewardClaimed(address indexed user, uint256 stakeIndex, uint256 reward);
    event TierCreated(uint256 tierId, uint256 apr, uint256 lockUpDays);
    event TierUpdated(uint256 tierId, uint256 apr, uint256 lockUpDays);


    /**
     * @param _stakingTokenAddress The address of the ERC20 token to be used for staking.
     */
    constructor(address _stakingTokenAddress) {
        stakingToken = IERC20(_stakingTokenAddress);
    }

    /**
     * @dev Creates a new staking tier. Only callable by the owner.
     * @param _tierId The unique ID for the new tier.
     * @param _apr The annual percentage rate (APR) for the tier (e.g., 1000 for 10%).
     * @param _lockUpDays The lock-up period in days for the tier.
     */
    function createStakingTier(uint256 _tierId, uint256 _apr, uint256 _lockUpDays) external onlyOwner {
        require(stakingTiers[_tierId].lockUpDays == 0, "Tier already exists");
        stakingTiers[_tierId] = StakingTier({
            apr: _apr,
            lockUpDays: _lockUpDays
        });
        emit TierCreated(_tierId, _apr, _lockUpDays);
    }

    /**
     * @dev Updates an existing staking tier. Only callable by the owner.
     * @param _tierId The ID of the tier to update.
     * @param _apr The new annual percentage rate (APR) for the tier (e.g., 1000 for 10%).
     * @param _lockUpDays The new lock-up period in days for the tier.
     */
    function updateStakingTier(uint256 _tierId, uint256 _apr, uint256 _lockUpDays) external onlyOwner {
        require(stakingTiers[_tierId].lockUpDays > 0, "Tier does not exist");
        stakingTiers[_tierId] = StakingTier({
            apr: _apr,
            lockUpDays: _lockUpDays
        });
        emit TierUpdated(_tierId, _apr, _lockUpDays);
    }


    /**
     * @dev Allows a user to stake ERC20 tokens.
     * @param _tierId The ID of the staking tier to use.
     * @param _amount The amount of tokens to stake.
     */
    function stake(uint256 _tierId, uint256 _amount) external nonReentrant {
        require(_amount > 0, "Amount must be greater than zero");
        require(stakingTiers[_tierId].lockUpDays > 0, "Invalid tier ID");

        // Transfer tokens from user to contract
        require(stakingToken.transferFrom(msg.sender, address(this), _amount), "Transfer failed");

        uint256 lockUpSeconds = stakingTiers[_tierId].lockUpDays * 1 days; // Convert days to seconds
        uint256 endTime = block.timestamp + lockUpSeconds;

        // Create a new stake
        userStakes[msg.sender].push(Stake({
            tierId: _tierId,
            amount: _amount,
            startTime: block.timestamp,
            endTime: endTime,
            rewardClaimed: 0
        }));

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

    /**
     * @dev Allows a user to unstake tokens from a specific stake.
     * @param _stakeIndex The index of the stake to unstake.
     */
    function unstake(uint256 _stakeIndex) external nonReentrant {
        Stake storage stake = userStakes[msg.sender][_stakeIndex];

        require(stake.amount > 0, "Invalid stake index or stake already unstaked");
        require(block.timestamp >= stake.endTime, "Lock-up period not over");

        uint256 reward = calculateReward(msg.sender, _stakeIndex);

        // Transfer staked tokens back to the user
        uint256 amount = stake.amount;
        stake.amount = 0; // Mark stake as unstaked to prevent double withdrawal.
        require(stakingToken.transfer(msg.sender, amount), "Transfer failed");

        // Transfer reward to user
        if(reward > 0){
            require(stakingToken.transfer(msg.sender, reward), "Reward transfer failed");
        }

        totalStaked -= amount;
        emit Unstaked(msg.sender, _stakeIndex, amount, reward);
    }

    /**
     * @dev Allows a user to claim accumulated rewards for a specific stake *without* unstaking.
     * @param _stakeIndex The index of the stake for which to claim rewards.
     */
    function claimReward(uint256 _stakeIndex) external nonReentrant {
        Stake storage stake = userStakes[msg.sender][_stakeIndex];
        require(stake.amount > 0, "Invalid stake index or stake already unstaked");

        uint256 reward = calculateReward(msg.sender, _stakeIndex);
        require(reward > 0, "No rewards available to claim");

        // Update the rewardClaimed amount to avoid double claiming.  Crucially important!
        stake.rewardClaimed += reward;

        // Transfer reward to user
        require(stakingToken.transfer(msg.sender, reward), "Reward transfer failed");

        emit RewardClaimed(msg.sender, _stakeIndex, reward);
    }

    /**
     * @dev Calculates the reward earned for a specific stake.
     * @param _user The address of the user.
     * @param _stakeIndex The index of the stake.
     */
    function calculateReward(address _user, uint256 _stakeIndex) public view returns (uint256) {
        Stake storage stake = userStakes[_user][_stakeIndex];
        uint256 apr = stakingTiers[stake.tierId].apr;
        uint256 duration = block.timestamp - stake.startTime;

        // Annual reward = (stakedAmount * APR) / 10000 (because APR is in percentage points)
        // Reward = (Annual reward * duration) / 365 days
        uint256 annualReward = (stake.amount * apr) / 10000;
        uint256 reward = (annualReward * duration) / (365 days);

        // Return the reward, minus the amount already claimed. This is important for the claimReward function.
        return reward - stake.rewardClaimed;
    }

    /**
     * @dev Returns the staking information for a given user.
     * @param _user The address of the user.
     */
    function getUserStakes(address _user) external view returns (Stake[] memory) {
        return userStakes[_user];
    }

    /**
     * @dev Allows the owner to withdraw any remaining tokens from the contract
     * @param _tokenAddress The address of the token to withdraw
     * @param _amount The amount of tokens to withdraw
     */
    function withdrawTokens(address _tokenAddress, uint256 _amount) external onlyOwner {
        IERC20 token = IERC20(_tokenAddress);
        require(token.transfer(owner(), _amount), "Withdrawal failed");
    }
}
```

**Explanation:**

1.  **`pragma solidity ^0.8.0;`**:  Specifies the Solidity compiler version to be used.
2.  **`import` statements**: Imports necessary OpenZeppelin contracts:
    *   `IERC20`: Interface for interacting with ERC20 tokens.
    *   `Ownable`: Provides ownership management functionality.
    *   `ReentrancyGuard`: Prevents reentrancy attacks.

3.  **`contract MultiLayerStaking is Ownable, ReentrancyGuard { ... }`**: Defines the main staking contract, inheriting from `Ownable` and `ReentrancyGuard`.
    *   `Ownable` allows only the contract owner to perform certain actions.
    *   `ReentrancyGuard` prevents malicious contracts from recursively calling functions, which could lead to vulnerabilities.

4.  **State Variables:**
    *   `stakingToken`:  An `IERC20` interface representing the ERC20 token that users will stake. Its address is set in the constructor.
    *   `totalStaked`: Keeps track of the total amount of tokens staked in the contract.
    *   `StakingTier` struct:  Defines the structure for a staking tier, including:
        *   `apr`: Annual Percentage Rate (expressed as a number; e.g., 1000 represents 10%).
        *   `lockUpDays`: The number of days the staked tokens are locked.
    *   `stakingTiers`: A mapping from a tier ID (`uint256`) to a `StakingTier` struct, allowing you to define different staking tiers with varying APRs and lock-up periods.
    *   `Stake` struct: Represents a single staking instance for a user:
        *   `tierId`: The ID of the staking tier used.
        *   `amount`: The amount of tokens staked.
        *   `startTime`:  The timestamp when the staking began.
        *   `endTime`: The timestamp when the lock-up period ends.
        *   `rewardClaimed`: Keeps track of rewards already claimed to prevent double claiming.  Critically important for security.
    *   `userStakes`: A mapping from a user's address to an array of `Stake` structs, allowing a user to have multiple stakes.

5.  **Events:**
    *   `Staked`: Emitted when a user stakes tokens.
    *   `Unstaked`: Emitted when a user unstakes tokens.
    *   `RewardClaimed`: Emitted when a user claims rewards.
    *   `TierCreated`: Emitted when a new staking tier is created.
    *   `TierUpdated`: Emitted when a staking tier is updated.

6.  **`constructor(address _stakingTokenAddress)`**: Sets the address of the `stakingToken` when the contract is deployed.

7.  **`createStakingTier(uint256 _tierId, uint256 _apr, uint256 _lockUpDays)`**:  Allows the contract owner to create new staking tiers. It enforces that a tier ID cannot be re-used.
8.  **`updateStakingTier(uint256 _tierId, uint256 _apr, uint256 _lockUpDays)`**:  Allows the owner to update existing staking tiers. It requires that the tier exists.

9.  **`stake(uint256 _tierId, uint256 _amount)`**:
    *   Allows a user to stake ERC20 tokens.
    *   It requires the staking amount to be greater than zero and the tier ID to be valid.
    *   It transfers the specified amount of tokens from the user to the contract using `stakingToken.transferFrom()`.  **Important:** The user must have approved the contract to spend their tokens before calling this function.
    *   Calculates the `endTime` based on the selected tier's `lockUpDays`.
    *   Creates a new `Stake` struct and adds it to the `userStakes` array.
    *   Updates `totalStaked`.
    *   Emits the `Staked` event.

10. **`unstake(uint256 _stakeIndex)`**:
    *   Allows a user to unstake their tokens.
    *   It requires the `_stakeIndex` to be valid and the lock-up period to be over.
    *   Calculates the reward using `calculateReward()`.
    *   Transfers the staked tokens back to the user using `stakingToken.transfer()`.
    *   Transfers the calculated reward to the user.
    *   Marks the stake as unstaked by setting `stake.amount = 0`. This is essential to prevent double withdrawals.
    *   Updates `totalStaked`.
    *   Emits the `Unstaked` event.

11. **`claimReward(uint256 _stakeIndex)`**:
    *   Allows a user to claim their accumulated rewards without unstaking their principal.
    *   It requires a valid stake index and that there are rewards available.
    *   It updates `stake.rewardClaimed` **before** transferring the reward. This is crucial to prevent reentrancy attacks and double claiming.
    *   Transfers the reward to the user.
    *   Emits the `RewardClaimed` event.

12. **`calculateReward(address _user, uint256 _stakeIndex)`**:
    *   Calculates the reward earned for a specific stake.
    *   Calculates the annual reward based on the staked amount and the APR.
    *   Calculates the actual reward based on the duration of the stake (from `startTime` to the current block timestamp).
    *   **Crucially important:** It subtracts `stake.rewardClaimed` to ensure that the user can only claim the reward once.

13. **`getUserStakes(address _user)`**:  Allows anyone to view a user's staking information.

14. **`withdrawTokens(address _tokenAddress, uint256 _amount)`**: Allows the owner to withdraw any tokens from the contract.  This is useful for withdrawing any tokens that were accidentally sent to the contract or for withdrawing accumulated rewards.

**Important Considerations and Security:**

*   **Approval:**  Before calling the `stake` function, users must approve the contract to spend their ERC20 tokens. This is done using the `approve` function on the ERC20 token contract.
*   **Reentrancy:** The `nonReentrant` modifier from OpenZeppelin's `ReentrancyGuard` is used to prevent reentrancy attacks.  The `claimReward` function is particularly vulnerable if this isn't handled correctly. The update to `stake.rewardClaimed` *before* transferring tokens is the correct way to mitigate this.
*   **Integer Overflow/Underflow:** Solidity 0.8.0 and later have built-in overflow/underflow protection.
*   **Denial of Service (DoS):**  Carefully consider how external calls and loops might be exploited to cause a DoS.  For example, a large number of stakes could make iteration over `userStakes` expensive.
*   **Front-Running:**  Consider potential front-running vulnerabilities, especially when modifying reward parameters or creating new tiers.
*   **Access Control:** The `Ownable` contract ensures that only the owner can perform administrative functions.
*   **Reward Calculation:** Ensure the reward calculation is accurate and resistant to manipulation. Pay close attention to units (e.g., percentage points vs. percentages) and potential rounding errors.  Consider using a library like OpenZeppelin's `SafeMath` for calculations.

**How to Use (Conceptual JavaScript/Web3 Example):**

```javascript
// Assuming you have web3 and your contract ABI loaded

const contractAddress = "YOUR_CONTRACT_ADDRESS";
const stakingTokenAddress = "YOUR_TOKEN_ADDRESS";
const contract = new web3.eth.Contract(contractABI, contractAddress);
const stakingToken = new web3.eth.Contract(erc20ABI, stakingTokenAddress); // Assuming you have the ERC20 ABI

// Example: Creating a new staking tier (owner only)
async function createTier(tierId, apr, lockUpDays) {
  const accounts = await web3.eth.getAccounts();
  const owner = accounts[0]; // Assuming owner is the first account

  await contract.methods.createStakingTier(tierId, apr, lockUpDays)
    .send({ from: owner, gas: 200000 }) // Adjust gas limit as needed
    .then(console.log);
}

// Example: Staking tokens
async function stakeTokens(tierId, amount) {
  const accounts = await web3.eth.getAccounts();
  const user = accounts[0];

  // 1. Approve the contract to spend the user's tokens
  const amountToApprove = web3.utils.toWei(amount.toString(), 'ether'); // Convert to wei
  await stakingToken.methods.approve(contractAddress, amountToApprove)
    .send({ from: user, gas: 50000 })
    .then(() => console.log("Approval successful!"));

  // 2. Stake the tokens
  await contract.methods.stake(tierId, web3.utils.toWei(amount.toString(), 'ether')) // Stake in Wei
    .send({ from: user, gas: 200000 })
    .then(console.log);
}


// Example: Unstaking tokens
async function unstakeTokens(stakeIndex) {
  const accounts = await web3.eth.getAccounts();
  const user = accounts[0];

  await contract.methods.unstake(stakeIndex)
    .send({ from: user, gas: 200000 })
    .then(console.log);
}

// Example: Claiming rewards
async function claimRewards(stakeIndex) {
  const accounts = await web3.eth.getAccounts();
  const user = accounts[0];

  await contract.methods.claimReward(stakeIndex)
    .send({ from: user, gas: 200000 })
    .then(console.log);
}


// Example: Getting user stakes
async function getUserStakes() {
    const accounts = await web3.eth.getAccounts();
    const user = accounts[0];

    const stakes = await contract.methods.getUserStakes(user).call();
    console.log("User stakes:", stakes);
}

// Call the functions (replace with actual values)
// createTier(1, 500, 30); // Example: Tier ID 1, 5% APR, 30-day lock-up
// stakeTokens(1, 10);    // Example: Stake 10 tokens in tier 1
// unstakeTokens(0);      // Example: Unstake the first stake
// claimRewards(0);       // Claim rewards for the first stake
// getUserStakes();
```

**Important Notes about the JavaScript example:**

*   **`web3.utils.toWei()`**:  The JavaScript code uses `web3.utils.toWei()` to convert the amount from Ether (or whatever denomination your token uses) to Wei (the smallest unit).  Make sure to use the correct denomination (e.g., 'ether', 'gwei').  The Solidity contract works with the smallest unit of the ERC20 token.
*   **`gas`**:  Adjust the `gas` limit as needed for each transaction.  Too low a gas limit will cause the transaction to fail.
*   **`approve`**:  The user *must* approve the contract to spend their tokens before calling `stake()`.
*   **Error Handling:** The JavaScript code lacks robust error handling.  You should add `try...catch` blocks to handle potential errors during transaction execution.
*   **Asynchronous Operations:**  Web3 calls are asynchronous, so you need to use `async/await` or `.then()` to handle the results.
*   **Contract Abstraction:**  The `contractABI` and `erc20ABI` variables should hold the ABI (Application Binary Interface) of your staking contract and the ERC20 token contract, respectively.  You'll typically get this from the Solidity compiler output.
*   **Security:** This is a simplified example and should not be used in production without thorough security auditing.

This comprehensive example provides a good starting point for building a multi-layer staking protocol. Remember to prioritize security and thoroughly test your code before deploying it to a live environment.  Also, consider adding more advanced features such as dynamic reward rates, governance, and more sophisticated unlocking schedules.
👁️ Viewed: 9

Comments