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
This article is the second part of a four part series.
Building a Decentralized Application With BEP-20 In Solidity — This article will help you understand the basics of Solidity
[Creating a Inheritable Staking contract in Solidity] — Second article in which we cover more advanced Solidity items and implement Staking and Rewarding
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
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.
Before we start summerizing, let’s also update Stake to hold data about how big of a reward is claimable.
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.
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.
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.
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.
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.
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
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.
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.
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