Building a MultiTierStaking Smart Contract with Solidity on Celo

Building a MultiTierStaking Smart Contract with Solidity on Celo https://celo.academy/uploads/default/optimized/2X/7/72009e7c8fe1a13990db5eb00afdb847660885c6_2_1024x576.png
none 0.0 0

Introduction

Smart contracts are the backbone of decentralized applications in the blockchain world, allowing the creation of trustless systems that can be audited and executed without the use of intermediaries. The MultiTierStaking smart contract is one such example, a decentralized staking solution that allows users to earn rewards by staking their cryptocurrency. The MultiTierStaking smart contract, with a minimum stake of 100 ether and a maximum duration of 365 days, provides a simple and secure way to earn rewards while participating in the network. In this article, we’ll look at how this innovative smart contract works and how it might affect the blockchain ecosystem.

Prerequisite

Before we get into the specifics of the MultiTierStaking smart contract, it’s important to understand what staking means in the world of blockchain and cryptocurrencies. Staking is the practice of keeping a certain amount of a cryptocurrency in a wallet or account to help secure the network and validate transactions. Users are frequently rewarded with additional tokens or coins in exchange for staking. This method is commonly used in proof-of-stake (PoS) blockchain networks, where validators are selected based on the amount of cryptocurrency staked. With this context in mind, let’s look at the MultiTierStaking contract and how it allows users to stake and earn rewards in a safe and efficient manner.

Requirements

Smart Contract

  • Now let’s begin writing our smart contract on Remix. The complete contract code should look like this:

// SPDX-License-Identifier: MIT
pragma solidity >=0.5.0 <0.9.0;
contract MultiTierStaking {
    uint256 public constant minimumStake = 100 ether;
    uint256 public constant maximumStakeDuration = 365 days;
    uint256 public constant minimumStakeTime = 7 days;
    mapping(address => uint256) public balances;
    mapping(address => uint256) public timeStaked;
    mapping(address => uint256) public totalRewards;
    uint256 public rewardRate;
    event Staked(address indexed staker, uint256 amount);
    event Unstaked(address indexed staker, uint256 amount);
    event RewardsClaimed(address indexed staker, uint256 amount);
    function stake() public payable {
        require(msg.value >= minimumStake, "Staking amount must be at least 100 ether");
        balances[msg.sender] += msg.value;
        timeStaked[msg.sender] = block.timestamp;
        emit Staked(msg.sender, msg.value);
    }
    function extendStakeDuration() public {
        uint256 _timeStaked = timeStaked[msg.sender];
        uint256 timeElapsed = block.timestamp - _timeStaked;
        require(timeElapsed < maximumStakeDuration, "You cannot extend your stake duration any further");
        uint256 remainingTime = maximumStakeDuration - timeElapsed;
        require(remainingTime >= minimumStakeTime, "You must wait at least 7 days before extending your stake duration again");
        uint256 extensionReward = rewardRate * remainingTime;
        balances[msg.sender] += extensionReward;
        timeStaked[msg.sender] = block.timestamp;
    }
    function splitStake(uint256[] memory amounts) public {
        uint256 totalAmount = 0;
        for (uint256 i = 0; i < amounts.length; i++) {
            require(amounts[i] >= minimumStake, "Staking amount must be at least 100 ether");
            totalAmount += amounts[i];
        }
        require(totalAmount == balances[msg.sender], "Invalid stake amounts");
        balances[msg.sender] = 0;
        timeStaked[msg.sender] = 0;
        for (uint256 i = 0; i < amounts.length; i++) {
            balances[msg.sender] += amounts[i];
            timeStaked[msg.sender] = block.timestamp;
            emit Staked(msg.sender, amounts[i]);
        }
    }
    function unstake() public {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "You do not have any staked balance to unstake");
        balances[msg.sender] = 0;
        timeStaked[msg.sender] = 0;
        uint256 reward = rewardRate * (block.timestamp - timeStaked[msg.sender]);
        totalRewards[msg.sender] += reward;
        (bool success,) = msg.sender.call{value: amount + reward}("");
        require(success, "Failed to send staked balance and rewards to user");
        emit Unstaked(msg.sender, amount);
    }
    function claimRewards() public {
        uint256 reward = totalRewards[msg.sender];
        require(reward > 0, "You do not have any unclaimed rewards");
        totalRewards[msg.sender] = 0;
        (bool success,) = msg.sender.call{value: reward}("");
        require(success, "Failed to send rewards to user");
        emit RewardsClaimed(msg.sender, reward);
    }
    function setRewardRate(uint256 _rewardRate) public {
        rewardRate = _rewardRate;
    }
}

Code Breakdown

// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.5.0 <0.9.0;
  • First, we declared our license and the Solidity version.
contract MultiTierStaking {
    uint256 public constant minimumStake = 100 ether;
    uint256 public constant maximumStakeDuration = 365 days;
    uint256 public constant minimumStakeTime = 7 days;
    mapping(address => uint256) public balances;
    mapping(address => uint256) public timeStaked;
    mapping(address => uint256) public totalRewards;
    uint256 public rewardRate;
    event Staked(address indexed staker, uint256 amount);
    event Unstaked(address indexed staker, uint256 amount);
    event RewardsClaimed(address indexed staker, uint256 amount);
  • This is the start of the MuliTierStaking contract, which implements a staking system with rewards.

  • The contract contains three constants minimumStake, maximumStakeDuration, and minimumStakeTime. These values, which are set at 100 ether, 365 days, and 7 days, are used throughout the contract to define the minimum stake amount, maximum stake duration, and minimum time between stake extensions.

  • The contract also includes three public mappings:balances, timeStaked, and totalRewards. These mappings keep track of stakers and their stakes, such as the amount staked, the time the stake was made or extended, and the total rewards earned.

  • The rewardRate variable is also defined as a public uint256 variable that determines the rate at which stakers earn rewards.

  • Finally, the contract contains three events: Staked, Unstaked, and RewardsClaimed. These events are triggered when a staker places a new stake, unstakes their tokens, or claims their rewards. These events can be used by external applications to monitor contract activity.

 function stake() public payable {
        require(msg.value >= minimumStake, "Staking amount must be at least 100 ether");
        balances[msg.sender] += msg.value;
        timeStaked[msg.sender] = block.timestamp;
        emit Staked(msg.sender, msg.value);
    }

A user can use this function to stake a certain amount of ether. It first ensures that the amount staked is greater than or equal to the’minimumStake’ value of 100 ether. If the check is successful, the’msg.value’ is added to the ‘balances’ mapping with the user’s address as the key, and the current block timestamp is recorded in the ‘timeStaked’ mapping with the user’s address as the key. Finally, it fires a ‘Staked’ event, which takes as arguments the user’s address and the amount of ether staked.

 function extendStakeDuration() public {
        uint256 _timeStaked = timeStaked[msg.sender];
        uint256 timeElapsed = block.timestamp - _timeStaked;
        require(timeElapsed < maximumStakeDuration, "You cannot extend your stake duration any further");
        uint256 remainingTime = maximumStakeDuration - timeElapsed;
        require(remainingTime >= minimumStakeTime, "You must wait at least 7 days before extending your stake duration again");
        uint256 extensionReward = rewardRate * remainingTime;
        balances[msg.sender] += extensionReward;
        timeStaked[msg.sender] = block.timestamp;
    }

The extendStakeDuration() function is used to add more time to the current stake duration to extend the duration of an existing stake. This is how it works:

  • First, the function uses the ‘timeStaked’ mapping to determine when the initial stake was made.

  • The time elapsed since the stake is then calculated by subtracting the initial stake time from the current block timestamp.

  • It determines whether the amount of time elapsed is less than the maximumStakeDuration constant, which is set to 365 days. If the time elapsed exceeds or equals maximumStakeDuration, the function throws an error message and terminates execution.

  • The function then calculates the remaining stake time by subtracting the elapsed time from the maximum stake duration.

  • It determines whether the remaining time is greater than or equal to the minimumStakeTime constant, which is set to seven days. If the remaining time is less than the minimumStakeTime, the function returns an error and terminates execution.

  • If the remaining time is greater than or equal to the minimumStakeTime, the function computes the reward for extending the stake duration by multiplying the rewardRate by the remaining time.

  • It then adds the extension reward to the staker’s account balance in the ‘balances’ mapping.

  • Finally, the function updates the stake time in the ‘timeStaked’ mapping to the current block timestamp for the staker.

 function splitStake(uint256[] memory amounts) public {
        uint256 totalAmount = 0;
        for (uint256 i = 0; i < amounts.length; i++) {
            require(amounts[i] >= minimumStake, "Staking amount must be at least 100 ether");
            totalAmount += amounts[i];
        }
        require(totalAmount == balances[msg.sender], "Invalid stake amounts");
        balances[msg.sender] = 0;
        timeStaked[msg.sender] = 0;
        for (uint256 i = 0; i < amounts.length; i++) {
            balances[msg.sender] += amounts[i];
            timeStaked[msg.sender] = block.timestamp;
            emit Staked(msg.sender, amounts[i]);
        }
    }
  • The splitStake function enables the staker to divide their current stake into multiple smaller stakes. The function accepts an array ‘amounts’ as input, which contains the stake amounts for each new stake.

  • The function first determines whether each value in the array is greater than or equal to the minimum stake of 100 ether before calculating the total amount to be staked. It then verifies that the total amount corresponds to the staker’s current balance.

  • If the checks are successful, the function returns the staker’s balance and stake time to zero. It then loops through the array, sets the staker’s new balance and stake time, and emits a ‘Staked’ event for each new stake.

 function unstake() public {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "You do not have any staked balance to unstake");
        balances[msg.sender] = 0;
        timeStaked[msg.sender] = 0;
        uint256 reward = rewardRate * (block.timestamp - timeStaked[msg.sender]);
        totalRewards[msg.sender] += reward;
        (bool success,) = msg.sender.call{value: amount + reward}("");
        require(success, "Failed to send staked balance and rewards to user");
        emit Unstaked(msg.sender, amount);
    }

The unstake() function enables users to withdraw their funds and claim their rewards. This procedure includes the following steps:

  • Determine whether the user has any staked balance. If not, an error message will be displayed.

  • Retrieve the staked amount and reset the user’s balance and timeStaked mapping to zero.

  • Calculate the user’s entitlement to the reward based on the rewardRate and the time duration for which the funds were staked.

  • Add the reward to the totalRewards mapping of the user.

  • Using the ‘call’ function, transfer the staked amount and the reward to the user’s address.

  • Emit an event to indicate that the user’s funds have been unstaked.

It’s worth noting that the call function is used to send funds to the user’s address. This is due to the transfer function’s gas limit, which can be exceeded if the user’s fallback function contains too much code or runs out of gas. The call function allows for more gas usage flexibility and can handle more complex fallback functions.

function claimRewards() public {
        uint256 reward = totalRewards[msg.sender];
        require(reward > 0, "You do not have any unclaimed rewards");
        totalRewards[msg.sender] = 0;
        (bool success,) = msg.sender.call{value: reward}("");
        require(success, "Failed to send rewards to user");
        emit RewardsClaimed(msg.sender, reward);
    }

Users can use the claimRewards() function to claim any unclaimed rewards they have earned from staking. It first determines whether the user has any unclaimed rewards, and if not, it reverses the transaction. If the user has unclaimed rewards, it reduces the user’s total rewards to zero to mark them as claimed, and then uses the ‘call’ function to send the rewards to the user’s address. If the ‘call’ function fails, the transaction will be rolled back.

The emit RewardsClaimed(msg.sender, reward) line sends an event to other parties informing them that the user has claimed their rewards and the amount claimed.

 function setRewardRate(uint256 _rewardRate) public {
        rewardRate = _rewardRate;
    }
}

This function determines the staking contract’s reward rate. It accepts a single parameter of type uint256, _rewardRate, which represents the number of rewards earned per second for each staked ether. The function is open to the public, which means that anyone can call it and change the reward rate for all stakeholders.

Deployment

  • To deploy our smart contract successfully, we need the Celo extension wallet, which can be downloaded from here

  • Next, we need to fund our newly created wallet, which can be done using the Celo Alfajores faucet here

  • You can now fund your wallet and deploy your contract using the Celo plugin in Remix.

Conclusion

Finally, the MultiTierStaking smart contract is an effective solution for implementing a flexible and rewarding staking mechanism on the Ethereum blockchain. Its modular design and use of cutting-edge programming techniques provide a solid foundation for developing a variety of staking scenarios with customizable rewards and durations. With the growing popularity of staking as a way to generate passive income and support blockchain ecosystems, the MultiTierStaking contract is poised to become a must-have tool for both developers and investors. We can continue to unlock new possibilities for decentralized finance and build a more inclusive and transparent financial system for everyone by leveraging the power of smart contracts.

Next Steps

6 Likes

Approved for you to get started. You can manage the tutorial here by changing the category to Proposals > In Progress then Proposals > Review as you complete the tutorial. Thanks!

2 Likes

i will be reviewing this

2 Likes

Approved @Kunaldawar

3 Likes

Well detailed and insightful, welldone brother.

2 Likes

Excellent

3 Likes

Very insightful!:sunglasses:

3 Likes

The content of the article is commendable, and the writer has done an excellent job explaining the concept of the MultiTierStaking smart contract on Celo. The article provides a comprehensive introduction to staking and its significance in the blockchain ecosystem, setting the stage for understanding the contract

2 Likes