Back

Creating a Inheritable Staking contract in Solidity

Adding inheritable Staking and reward mechanism to a BEP-20 Smart contract in Solidity.

by Percy Bolmér, August 3, 2021

By Percy Bolmér
By Percy Bolmér

This article is the second part of a four part series.

  1. Building a Decentralized Application With BEP-20 In Solidity — This article will help you understand the basics of Solidity

  2. [Creating a Inheritable Staking contract in Solidity] — Second article in which we cover more advanced Solidity items and implement Staking and Rewarding

  3. Using a Smart Contract In An Web Application — Third article of the series in which we learn how to connect to the Blockchain via an Web application using MetaMask

  4. Deploying Smart Contracts to Binance Smart Chain With Truffle — Fourth and last article in which we learn how to deploy our Smart contract to the real networks

In the first part we went through how we can setup a development environment for developing a custom BEP20 token. In this article we will be adding Staking to the token.

If you don’t have the full code from article one, you can find it here.

What is Staking?

Staking is when you invest your tokens into the network, and get a reward for doing it. The reason why staking is encouraged is because a term called Proof-Of-Stake. See it as mining, but instead of running a GPU to calculate stuff, you mine by storing your tokens in the network. You can read a detailed explanation link.

In this article we will be adding Staking by inheriting yet another contract.

Introducing Staking to the Token

It’s about time we made our DevToken a bit cooler with some added functionality. Right now it’s only a simple token that we really cant do much with. Let’s add the ability to stake the token and receive rewards from it.

Let’s begin by creating a new smart contract that can be inherited by our DevToken. Create a file called contracts/Stakable.sol. We will start out by writing an empty contract and add items to it as we go. This way we can in depth cover that each part actually does.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

/**
* @notice Stakeable is a contract who is ment to be inherited by other contract that wants Staking capabilities
*/
contract Stakeable {


    /**
    * @notice Constructor,since this contract is not ment to be used without inheritance
    * push once to stakeholders for it to work proplerly
     */
    constructor() {
    }

}
Stakeable — Our empty template for a stakeable contract

Before moving on, inherit the Stakeable contract into DevToken and make sure we can compile.

Open contracts/DevToken.sol and make sure to update the contract declaration to inherit the Stakeable contract, we also need to import the Stakeable.sol.

import "./Ownable.sol";
import "./Stakable.sol";
/**
* @notice DevToken is a development token that we use to learn how to code solidity 
* and what BEP-20 interface requires
*/
contract DevToken is Ownable, Stakeable{
DevToken — Inheriting both Ownable and Stakable contract
truffle compile // Compile to make sure everything is correct

Hopefully everything compiled smoothly, otherwise fix the errors and then let’s start building our staking mechanism.

The first thing we need to do is to create all our needed structures. We will use a mapping to keep track of stake indexes for accounts. A mapping in solidity is kind of like a hash map in most languages. It can be used to map a unique address to a value. A great breakdown can be found from @Doug Crescenzi.

The mapping we create will actually only hold an Index reference to an Array. This array will hold all our stakeholders. Why we do it this way will be explained soon.

We will save Stakes with information about when the stake is made, the amount and who made it. Notice also that we have a Stake event that will be triggered whenever a stake is made. If you are unsure about events, go back to article one of this series.

/**
    * @notice Constructor since this contract is not ment to be used without inheritance
    * push once to stakeholders for it to work proplerly
     */
    constructor() {
        // This push is needed so we avoid index 0 causing bug of index-1
        stakeholders.push();
    }
    /**
     * @notice
     * A stake struct is used to represent the way we store stakes, 
     * A Stake will contain the users address, the amount staked and a timestamp, 
     * Since which is when the stake was made
     */
    struct Stake{
        address user;
        uint256 amount;
        uint256 since;
    }
    /**
    * @notice Stakeholder is a staker that has active stakes
     */
    struct Stakeholder{
        address user;
        Stake[] address_stakes;
        
    }

    /**
    * @notice 
    *   This is a array where we store all Stakes that are performed on the Contract
    *   The stakes for each address are stored at a certain index, the index can be found using the stakes mapping
    */
    Stakeholder[] internal stakeholders;
    /**
    * @notice 
    * stakes is used to keep track of the INDEX for the stakers in the stakes array
     */
    mapping(address => uint256) internal stakes;
    /**
    * @notice Staked event is triggered whenever a user stakes tokens, address is indexed to make it filterable
     */
     event Staked(address indexed user, uint256 amount, uint256 index, uint256 timestamp);
Stakeable — All the structs we need to begin staking

We will begin with a method to add new Stakeholders. This method will add a empty space in the Stakeholders array and assign the new Stakeholder values to it. It will then return the used index, this index is the Stakeholders personal index in which all his/hers stakes are stored. This index can be retrieved using the address of the staker. This is the reason why we use a map to store the index and then retrieve the stakeholder by that index. It will save us gas because looking up a user by Index in a array is a lot cheaper than Iterating the whole array.


    /**
    * @notice _addStakeholder takes care of adding a stakeholder to the stakeholders array
     */
    function _addStakeholder(address staker) internal returns (uint256){
        // Push a empty item to the Array to make space for our new stakeholder
        stakeholders.push();
        // Calculate the index of the last item in the array by Len-1
        uint256 userIndex = stakeholders.length - 1;
        // Assign the address to the new index
        stakeholders[userIndex].user = staker;
        // Add index to the stakeHolders
        stakes[staker] = userIndex;
        return userIndex; 
    }
Stakeable — Adding a stakeholder and mapping them

Now that we can add new stakeholders, let’s add a Stake function so we can begin testing it. The only new thing to learn here is the require keyword. Require is used to make conditional checks. If this condition is not fulfilled the call will be rejected with an error message. So let’s make sure the Stake is not 0.

    /**
    * @notice
    * _Stake is used to make a stake for an sender. It will remove the amount staked from the stakers account and place those tokens inside a stake container
    * StakeID 
    */
    function _stake(uint256 _amount) internal{
        // Simple check so that user does not stake 0 
        require(_amount > 0, "Cannot stake nothing");
        

        // Mappings in solidity creates all values, but empty, so we can just check the address
        uint256 index = stakes[msg.sender];
        // block.timestamp = timestamp of the current block in seconds since the epoch
        uint256 timestamp = block.timestamp;
        // See if the staker already has a staked index or if its the first time
        if(index == 0){
            // This stakeholder stakes for the first time
            // We need to add him to the stakeHolders and also map it into the Index of the stakes
            // The index returned will be the index of the stakeholder in the stakeholders array
            index = _addStakeholder(msg.sender);
        }

        // Use the index to push a new Stake
        // push a newly created Stake with the current block timestamp.
        stakeholders[index].address_stakes.push(Stake(msg.sender, _amount, timestamp));
        // Emit an event that the stake has occured
        emit Staked(msg.sender, _amount, index,timestamp);
    }
Stakeable — A method that adds new stakeholders and pushes their stakes

If you have been paying attention, you might have noticed a strange thing. If not, that’s fine, Ill say it.

Have it struck you that the stake method does not modify the balance of any accounts ? Have you noticed that the _stake method has a internal modifier? That means that this function wont be allowed to be called from outside the contract.

This is not a bug! This is because the contract is meant to be inherited into the DevToken contract. The stakeable contract wont know the internal workings of the contract inheriting it, so that is needed to be implemented in the parent contract. Let’s open up contracts/DevToken.sol and expose this function, and adding some burning and write some unit tests to make sure it works.

   /**
    * Add functionality like burn to the _stake afunction
    *
     */
    function stake(uint256 _amount) public {
      // Make sure staker actually is good for it
      require(_amount < _balances[msg.sender], "DevToken: Cannot stake more than you own");

        _stake(_amount);
                // Burn the amount of tokens on the sender
        _burn(msg.sender, _amount);
    }
DevToken — Stake that is public and exposes the internal _stake

DevTokens stake will add some requirements like the sender balance has to be bigger than the amount staking. This can not be done in the Stakeable contract as it does not know the balance of the Staker.

We will also stake the amount, and then burn the same amount from the Stakers account.

Let’s test this to make sure it works.

I’ve created a new file named tests/Stakeable.js which will contain the tests related to the Stakeable contract.

Now, this is where events also get exciting. We can actually listen for events in javascript (which can be helpful if you want to make a website for your token). We will leverage events to perform our tests. We will trigger stakes and then look at the events fired and make sure the data in them is correct. Remember that we only fire events on successful Stakes.

To help us in the unit tests, truffle has a helpful library in javascript called truffle-assertions. You should have installed it already if you followed step one, but if not try installing these

npm install chai
npm install truffle-assertions

Truffle assertion allows us to take the response from executing a function on the blockchain and easily assert the events emitted by that call.

truffleAssert.eventEmitted(ReturnedTransaction,"EventName",(ev) => {
Your custom callback logic with assertion, ev is your event
},
"The error message to trigger if assertion faileed");

The ev item in the example above contains the information we sent in the Event and can be asserted. Let’s give it a go and try a simple test first.

We will make a simple test that stakes and then asserts the Staked event and checks that the information is correct.

const DevToken = artifacts.require("DevToken");
const { assert } = require('chai');
const truffleAssert = require('truffle-assertions');
contract("DevToken", async accounts => {
    it("Staking 100x2", async () => {
        devToken = await DevToken.deployed();

        // Stake 100 is used to stake 100 tokens twice and see that stake is added correctly and money burned
        let owner = accounts[0];
        // Set owner, user and a stake_amount
        let stake_amount = 100;
        // Add som tokens on account 1 asweel
        await devToken.mint(accounts[1], 1000);
        // Get init balance of user
        balance = await devToken.balanceOf(owner)

        // Stake the amount, notice the FROM parameter which specifes what the msg.sender address will be

        stakeID = await devToken.stake(stake_amount, { from: owner });
        // Assert on the emittedevent using truffleassert
        // This will capture the event and inside the event callback we can use assert on the values returned
        truffleAssert.eventEmitted(
            stakeID,
            "Staked",
            (ev) => {
                // In here we can do our assertion on the ev variable (its the event and will contain the values we emitted)
                assert.equal(ev.amount, stake_amount, "Stake amount in event was not correct");
                assert.equal(ev.index, 1, "Stake index was not correct");
                return true;
            },
            "Stake event should have triggered");

    });

    it("cannot stake more than owning", async() => {

        // Stake too much on accounts[2]
        devToken = await DevToken.deployed();

        try{
            await devToken.stake(1000000000, { from: accounts[2]});
        }catch(error){
            assert.equal(error.reason, "DevToken: Cannot stake more than you own");
        }
    });
});
Stakable.js — Simple first test to make sure it works

Run the tests and make sure it works, correct any issues that occurs.

truffle test

Let’s also further increase the Staking100x2 test to actually stake twice.

     // Stake again on owner because we want hasStake test to assert summary
        stakeID = await devToken.stake(stake_amount, { from: owner });
        // Assert on the emittedevent using truffleassert
        // This will capture the event and inside the event callback we can use assert on the values returned
        truffleAssert.eventEmitted(
            stakeID,
            "Staked",
            (ev) => {
                // In here we can do our assertion on the ev variable (its the event and will contain the values we emitted)
                assert.equal(ev.amount, stake_amount, "Stake amount in event was not correct");
                assert.equal(ev.index, 1, "Stake index was not correct");
                return true;
            },
            "Stake event should have triggered");
Stakeable.js — Adding a second stake in the Staking100x2 test

We will also add a test to make sure that the Index of the stakeholder is assigned correctly, so a stake on Account 1 will be made and the index should now be 2.

    it("new stakeholder should have increased index", async () => {
        let stake_amount = 100;
        stakeID = await devToken.stake(stake_amount, { from: accounts[1] });
        // Assert on the emittedevent using truffleassert
        // This will capture the event and inside the event callback we can use assert on the values returned
        truffleAssert.eventEmitted(
            stakeID,
            "Staked",
            (ev) => {
                // In here we can do our assertion on the ev variable (its the event and will contain the values we emitted)
                assert.equal(ev.amount, stake_amount, "Stake amount in event was not correct");
                assert.equal(ev.index, 2, "Stake index was not correct");
                return true;
            },
            "Stake event should have triggered");
    })
Stakeable.js — New Stakeholder should get a new index

Viewing the Stakes in the blockchain

Before we move on, let’s try examining the Transactions using Ganache. Execute a stake in the truffle console and use Ganache to monitor the event that was emitted.

truffle migrate
truffle console
devToken = await DevToken.deployed(); // await contract to be deployed
let accounts = await web3.eth.getAccounts() // Grab all accounts
await devToken.stake(100, {from: accounts[0]}) // Stake 100 from owner

The output of the stake command will be the transaction and it can be good to view it to get a grip of what is going on. Also remember that there is a gas cost for each action due to the nature of Ethereum.

Open Ganache and go to the Events tab, you should be able to display all the events that has occured on the network since we migrated.

The staked event we created is being shown in Ganache
The staked event we created is being shown in Ganache
Details about our Staked event
Details about our Staked event

Reward stakeholders and allow withdrawals

Now that our contract allows Stakes, it’s time to start implementing some kind of reward system for making stakes.

Let’s add a function that allows us to withdraw staked tokens in contracts/DevToken.sol. This function has to verify that we don’t withdraw more than we staked, and it should return the staked tokens to the address of the owner.

I’ll name it withdrawStake and it will be a public function since we want to allow it to be called from outside the smart contract. I won’t explain the details here, since it’s basically a copy of Stake but reversed.

   /**
    * @notice withdrawStake is used to withdraw stakes from the account holder
     */
    function withdrawStake(uint256 amount, uint256 stake_index)  public {

      uint256 amount_to_mint = _withdrawStake(amount, stake_index);
      // Return staked tokens to user
      _mint(msg.sender, amount_to_mint);
    }
DevToken — withdrawStake will withdraw stake and mint new tokens

withdrawStake calls the internal method _withdrawStake , so let’s jump into contracts/Stakeable.sol and create it.

Proceed and implement a reward system that will increase the user’s reward based on the duration of the stake. Each stake the user has made will get rewarded differently due to different durations, I think that feels fair.

We will implement a reward system that will reward the user with 0.01% per hour staked. So let’s begin by creating a variable that contains our reward rate. Note that normally in programming I would multiply with 0.01, but since we don’t allow decimals, we need to instead use the reverted math option and dividing. The opposite of 0.01 would be dividing by 1000.

Users will be rewarded per hour the stake has persisted, and we will give them a 0.1% reward per hour. Let’s create a variable at the top of the file so we can control the reward rate.

    /**
     * @notice
      rewardPerHour is 1000 because it is used to represent 0.001, since we only use integer numbers
      This will give users 0.1% reward for each staked token / H
     */
    uint256 internal rewardPerHour = 1000;
Stakeable — Reward rate that is set to 0.1%

We will begin by creating a necessary function called calculateStakeReward. This function’s main purpose is to calculate the reward that the Stakeholder should receive for his staking efforts. calculateStakeReward will accept a stake and calculate the reward that it is entitled to based on the time the stake has been active. This function will be internal , that is, only allowed to be accessed from inside the contract. It will also have the view modifier since it does not affect the state in any way.

The algorithm will be as

  • Calculate the duration via block.timestamp — the time of the stake, this will return seconds
  • Divide by 1 hours (Solidity internal variable for 3600 seconds)
  • Multiply by the stake amount
  • Divide by rewardPerHour Rate
    /**
      * @notice
      * calculateStakeReward is used to calculate how much a user should be rewarded for their stakes
      * and the duration the stake has been active
     */
      function calculateStakeReward(Stake memory _current_stake) internal view returns(uint256){
          // First calculate how long the stake has been active
          // Use current seconds since epoch - the seconds since epoch the stake was made
          // The output will be duration in SECONDS ,
          // We will reward the user 0.1% per Hour So thats 0.1% per 3600 seconds
          // the alghoritm is  seconds = block.timestamp - stake seconds (block.timestap - _stake.since)
          // hours = Seconds / 3600 (seconds /3600) 3600 is an variable in Solidity names hours
          // we then multiply each token by the hours staked , then divide by the rewardPerHour rate 
          return (((block.timestamp - _current_stake.since) / 1 hours) * _current_stake.amount) / rewardPerHour;
      }
Stakeable.sol — Calculate rewards based on a Hourly rate

Now that we have to ability to calculate the reward for a stake, let’s implement the _withdrawStake. The function will look up a stakeholder, based on the sender, require that the withdrawal amount is less than the staked amount.

Any empty stakes will also be deleted. Delete is a way of clearing up resources on the blockchain and important, as you will actually refund gas for the storage released. Delete accepts an array with the index specified and nullifies all values in that given index. It does NOT remove the index, which is really important since we use the order of the array as ID. Deleting is important, not only to clean up garbage but also to refund some gas.

One thing to see here that is new for us is the memory keyword. This means we will store the data temporarily, read more about it here.

    /**
     * @notice
     * withdrawStake takes in an amount and a index of the stake and will remove tokens from that stake
     * Notice index of the stake is the users stake counter, starting at 0 for the first stake
     * Will return the amount to MINT onto the acount
     * Will also calculateStakeReward and reset timer
    */
     function _withdrawStake(uint256 amount, uint256 index) internal returns(uint256){
         // Grab user_index which is the index to use to grab the Stake[]
        uint256 user_index = stakes[msg.sender];
        Stake memory current_stake = stakeholders[user_index].address_stakes[index];
        require(current_stake.amount >= amount, "Staking: Cannot withdraw more than you have staked");

         // Calculate available Reward first before we start modifying data
         uint256 reward = calculateStakeReward(current_stake);
         // Remove by subtracting the money unstaked 
         current_stake.amount = current_stake.amount - amount;
         // If stake is empty, 0, then remove it from the array of stakes
         if(current_stake.amount == 0){
             delete stakeholders[user_index].address_stakes[index];
         }else {
             // If not empty then replace the value of it
             stakeholders[user_index].address_stakes[index].amount = current_stake.amount;
             // Reset timer of stake
            stakeholders[user_index].address_stakes[index].since = block.timestamp;    
         }

         return amount+reward;

     }
Stakeable — Withdrawing a stake resets the timer if not zeroed

We will now add a withdrawStake inside the DevToken that exposes this function, since it has a internal modifier. The stakeable contract doesn’t know what contract that will inherit it and there for the best it can do is return the reward amount, and the inheritor will be responsible to actually mint those tokens. This function should be callable from outside the blockchain and will there for have a public modifier.

    /**
    * @notice withdrawStake is used to withdraw stakes from the account holder
     */
    function withdrawStake(uint256 amount, uint256 stake_index)  public {

      uint256 amount_to_mint = _withdrawStake(amount, stake_index);
      // Return staked tokens to user
      _mint(msg.sender, amount_to_mint);
    }
DevToken — withdrawStake calls the internal function and mints tokens

We will test our new functions shortly, but to make it easier we will add a way to check if a Account has any Stakes. Let’s create a new struct which summarizes all stakes on an Account. The struct will go inside the Stakable contract.

     /**
     * @notice
     * StakingSummary is a struct that is used to contain all stakes performed by a certain account
     */ 
     struct StakingSummary{
         uint256 total_amount;
         Stake[] stakes;
     }
Stakable — StakingSummary holds data about an accounts stakes

Before we start summerizing, let’s also update Stake to hold data about how big of a reward is claimable.

    /**
     * @notice
     * A stake struct is used to represent the way we store stakes, 
     * A Stake will contain the users address, the amount staked and a timestamp, 
     * Since which is when the stake was made
     */
    struct Stake{
        address user;
        uint256 amount;
        uint256 since;
        // This claimable field is new and used to tell how big of a reward is currently available
        uint256 claimable;
    }
Stakeable — Added claimable field which shows much big reward is available

Since we modified the Stake struct, all new initializations of it has to be changed, luckily we only have on inside the _stake function. Simply add a input parameter to the new Stake, in our case claimable should start at 0.

      stakeholders[index].address_stakes.push(Stake(msg.sender, _amount, timestamp,0));
Stakable — _stake function initializes a Stake, adding 0 to the input

Now let’s create a function called hasStake. It will be a have two modifiers,

public since it’s allowed to be executed from outside the blockchain, and view since it does not modify values in the blockchain. The function will return a StakingSummary struct stored in memory. Memory is the Solidity storage type that is only available during the runtime of the function, and a cheap way of storing data.

Our function will take all the Stakes of the account and loop through all the stakes and calculate their current reward.


     /**
     * @notice
     * hasStake is used to check if a account has stakes and the total amount along with all the seperate stakes
     */
    function hasStake(address _staker) public view returns(StakingSummary memory){
        // totalStakeAmount is used to count total staked amount of the address
        uint256 totalStakeAmount; 
        // Keep a summary in memory since we need to calculate this
        StakingSummary memory summary = StakingSummary(0, stakeholders[stakes[_staker]].address_stakes);
        // Itterate all stakes and grab amount of stakes
        for (uint256 s = 0; s < summary.stakes.length; s += 1){
           uint256 availableReward = calculateStakeReward(summary.stakes[s]);
           summary.stakes[s].claimable = availableReward;
           totalStakeAmount = totalStakeAmount+summary.stakes[s].amount;
       }
       // Assign calculate amount to summary
       summary.total_amount = totalStakeAmount;
        return summary;
    }
Stakeable — Allowing Accounts to view their stakes

It’s time we tested the withdrawal and make sure that the calculation of the rewards is correct.

Fastfoward time during Tests in Truffle

Now that we have the functions in place and need to test them, we will notice a more tricky part. We don’t want the test to wait for an hour to notice if the reward was payed out correctly (or at least I don’t want too). We need someway of actually fast forwarding the time of the blockchain, thankfully we can do that using some.

We will borrow a trick from Andy Watt whom wrote an article about how to fast forward the ganache blockchain during truffle tests. To put it shortly, ganache accepts calls that allows us to change the time.

One of them is evm_increaseTime which increases the block time, HOWEVER time is not changed until the block is mined. So a call also has to be made to evm_mine.

Inside the test folder, create a folder called helpers. Andy then has a few functions that makes these actions very trivial during tests. Create a file called truffleTestHelpers.js and copy the following gist into it. Remember to read the file so that you can somewhat understand whats going on, (all though, at a first stage it might not be too important), and finally, be sure to read Andy’s article.

advanceTime = (time) => {
    return new Promise((resolve, reject) => {
      web3.currentProvider.send({
        jsonrpc: '2.0',
        method: 'evm_increaseTime',
        params: [time],
        id: new Date().getTime()
      }, (err, result) => {
        if (err) { return reject(err) }
        return resolve(result)
      })
    })
  }
  
  advanceBlock = () => {
    return new Promise((resolve, reject) => {
      web3.currentProvider.send({
        jsonrpc: '2.0',
        method: 'evm_mine',
        id: new Date().getTime()
      }, (err, result) => {
        if (err) { return reject(err) }
        const newBlockHash = web3.eth.getBlock('latest').hash
  
        return resolve(newBlockHash)
      })
    })
  }
  
  advanceTimeAndBlock = async (time) => {
    await advanceTime(time)
    await advanceBlock()
    return Promise.resolve(web3.eth.getBlock('latest'))
  }
  
  module.exports = {
    advanceTime,
    advanceBlock,
    advanceTimeAndBlock,
  }
A small helper function from Andy watt where we can fast forward the block time.

Begin by importing the helper functions in the Staking.js at the top of the file.

const helper = require("./helpers/truffleTestHelpers");

Unit test staking and withdrawal

We are finally ready to implement the unit tests to the stake functionality and withdrawal.

We going to be working inside Stakeable.js, but we will cover test by test since it’s quiet a big chunk of code.

The first test is a simple one, to just check that we cannot withdraw more than what was originally staked.

    it("cant withdraw bigger amount than current stake", async() => {
        devToken = await DevToken.deployed();

        let owner = accounts[0];

        // Try withdrawing 200 from first stake
        try {
            await devToken.withdrawStake(200, 0, {from:owner});
        }catch(error){
            assert.equal(error.reason, "Staking: Cannot withdraw more than you have staked", "Failed to notice a too big withdrawal from stake");
        }
    });
Stakable.js — Unit test to make sure withdrawal does not exceed original stake

The next one is a unit test to make sure withdrawal works. We will see if withdrawal really works as expected, we will withdraw 50 tokens from a Stake. We will also make sure the amount if updated using the new Summary that we created. Remember that the Stake is performed in a unit test before this test, It’s not the best solution, try implementing a new stake and use that.

  it("withdraw 50 from a stake", async() => {
        devToken = await DevToken.deployed();

        let owner = accounts[0];
        let withdraw_amount = 50;
        // Try withdrawing 50 from first stake
        await devToken.withdrawStake(withdraw_amount, 0, {from:owner});
        // Grab a new summary to see if the total amount has changed
        let summary = await devToken.hasStake(owner);

        assert.equal(summary.total_amount, 200-withdraw_amount, "The total staking amount should be 150");
        // Itterate all stakes and verify their amount aswell. 
        let stake_amount = summary.stakes[0].amount;

        assert.equal(stake_amount, 100-withdraw_amount, "Wrong Amount in first stake after withdrawal");
    });
Stakeable.js — Withdrawal of tokens from a stake.

Now let’s add a tests that withdraws 50 tokens from the stake again, (original stake amount was 100). This means that the stake should be removed since it’s now empty. To check that we will verify that the related Account in the stake is set to the null account, which in solidity is:

0x0000000000000000000000000000000000000000

    it("remove stake if empty", async() => {
        devToken = await DevToken.deployed();

        let owner = accounts[0];
        let withdraw_amount = 50;
        // Try withdrawing 50 from first stake AGAIN, this should empty the first stake
        await devToken.withdrawStake(withdraw_amount, 0, {from:owner});
        // Grab a new summary to see if the total amount has changed
        let summary = await devToken.hasStake(owner);
        console.log(summary);

        assert.equal(summary.stakes[0].user, "0x0000000000000000000000000000000000000000", "Failed to remove stake when it was empty");
    });
Stakeable.js — Removeal of stake should work

Now it’s time to make some more advanced tests. We will need to leverage the helper we created before to fast forward the time. This will be used to make sure the rewards are calculated correctly.

    it("calculate rewards", async() => {
        devToken = await DevToken.deployed();

        let owner = accounts[0];

        // Owner has 1 stake at this time, its the index 1 with 100 Tokens staked
        // So lets fast forward time by 20 Hours and see if we gain 2% reward
        const newBlock = await helper.advanceTimeAndBlock(3600*20);
        let summary = await devToken.hasStake(owner);

        
        let stake = summary.stakes[1];
        assert.equal(stake.claimable, 100*0.02, "Reward should be 2 after staking for twenty hours with 100")
        // Make a new Stake for 1000, fast forward 20 hours again, and make sure total stake reward is 24 (20+4)
        // Remember that the first 100 has been staked for 40 hours now, so its 4 in rewards.
        await devToken.stake(1000, {from: owner});
        await helper.advanceTimeAndBlock(3600*20);

        summary = await devToken.hasStake(owner);

        stake = summary.stakes[1];
        let newstake = summary.stakes[2];

        assert.equal(stake.claimable, (100*0.04), "Reward should be 4 after staking for 40 hours")
        assert.equal(newstake.claimable, (1000*0.02), "Reward should be 20 after staking 20 hours");
    });
Stakeable.js — Calculate rewards by fast forwarding time

The final tests will be used to make sure that the Stakeholder is rewarded correctly when withdrawing the amount, and that the Stake resets the Claimable counter after each withdrawal.

    it("reward stakes", async() => {
        devToken = await DevToken.deployed();
        // Use a fresh Account, Mint 1000 Tokens to it
        let staker = accounts[3];
        await devToken.mint(accounts[3],1000);
        let initial_balance = await devToken.balanceOf(staker);
        // Make a stake on 200, fast forward 20 hours, claim reward, amount should be Initial balanace +4
        await devToken.stake(200, {from: staker});
        await helper.advanceTimeAndBlock(3600*20);

        let stakeSummary = await devToken.hasStake(staker);
        let stake = stakeSummary.stakes[0];
        // Withdraw 100 from stake at index 0 
        await devToken.withdrawStake(100, 0, { from: staker});

        // Balance of account holder should be updated by 104 tokens
        let after_balance = await devToken.balanceOf(staker);

        let expected = 1000-200+100+Number(stake.claimable);
        assert.equal(after_balance.toNumber(), expected, "Failed to withdraw the stake correctly")
    
        // Claiming them again should not return any rewards since we reset timer
    
        try{
            await devToken.withdrawStake(100, 0 , {from:staker});
        }catch(error){
            assert.fail(error);
        }
        let second_balance = await devToken.balanceOf(staker);
        // we should have gained 100 this time.
        assert.equal(second_balance.toNumber(), after_balance.toNumber()+100, "Failed to reset timer second withdrawal reward")
    });
Stakeable.js — Verify that rewards are handed out properly

Make sure to trigger the tests so that you know they are working.

truffle test

Conclusion of second article

This concludes the second article. There are two more articles in this series, you can find the full code I have in the stakeable branch of my repository.

We have covered the following

  • What staking is
  • How to implement a simple staking
  • Rewarding Stakes
  • Writing unit tests for our code

The third article will be about using the smart contract in a web application.

I hope you enjoyed it, feel free to reach out if there are any questions.

If you enjoyed my writing, please support future articles by buying me an Coffee

Sign up for my Awesome newsletter