Staking crypto is the process of locking up or depositing crypto holdings into a DeFi protocol or smart contract to earn interest.
Today we will be building a smart contract where we could stake the ether and earn interest on it. It also penalises if a user tries to withdraw the funds before the unlocking date.If a user tries to do that, he has to give up the interest he has earned.
By the end of this article, you will get to learn the following things:
Learn how to write a staking smart contract
Deploy it to a private testnet
Use the faucet to get the native tokens
Test our staking smart contract by doing some transactions from the explorer
Ask your friends to join you in your private testnet and test the contract with you by using the Faucet
Advance time, using a postman call in our private testnet
Test the withdrawal of the of the Staked tokens
So let us start building! đ§âđ»
1.1: Use the following commands on your CLI to initialize your project.
mkdir staking-with-buildbear && cd staking-with-buildbear
1.2: Run the command npx hardhat on your CLI and choose Create a Javascript Object; choose yes for all the options.
Once you have successfully executed the above and if you were to open this repo in VS Code, you should have a directory structure, similar to the following:

2.1: Create a file named Staking.sol in the contracts folder.
2.2: We will start with defining the Solidity Version (this will determine the Solidity Compiler, which we will also use in our hardhat.confing.js file).
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;contract Staking {
2.3: Define the variables that we will be using in our contract:
address public owner;// amt of ethers staked by an address
struct Position {
uint positionId;
address walletAddress;
uint createdDate;
uint unlockDate;
uint percentInterest;
uint weiStaked;
uint weiInterest;
bool open;
}Position position;
uint public currentPositionId;
mapping (uint => Position) public positions;
mapping (address => uint[]) public positionIdsByAddress;
mapping (uint => uint) public tiers;
uint[] public lockPeriods; // 30 days / 90 days / 180 days
2.3.1: Critical Variables
Struct Position stores the amount of Ether staked by a specific address at a period of time with additional details. While these details are self-explanatory, you are free to ping us if you need any information regarding any of them.
Mapping of integers to positions, where each position can be queried by the id of the position
lockPeriods that will store the different lock periods of the stakes.
2.4: constructor for the smart contract
Use the code shown below for the constructor
constructor () payable {
owner = msg.sender;
currentPositionId = 0;
tiers[30] = 700; // 30 days -> 7%
tiers[90] = 1000; // 90 days -> 10%
tiers[180] = 1200; // 180 days -> 12% lockPeriods.push(30);
lockPeriods.push(90);
lockPeriods.push(180);
}
We have made the constructor payable, that will allow the deployer of the contract to send some ethers to it when itâs being deployed.
In the constructor we will set the msg.sender as the owner
the currentPositionId will be initialised to 0
then we have defined the
tiers30-days tier at 7% annual percentage yield
90-days tier at 10% annual percentage yield
and 180-days tier at 12% annual percentage yield
lastly, we will have the
lockPeriod, which will be 30, 90, and 180 days
2.5: Logical functions of our contract:
// num of days staked for
function stakeEther(uint numDays) external payable {
require(tiers[numDays] > 0, "Mapping not found");
positions[currentPositionId] = Position(
currentPositionId,
msg.sender,
block.timestamp,
block.timestamp + (numDays * 15180),
tiers[numDays],
msg.value,
calculateInterest(tiers[numDays], numDays, msg.value),
true
); positionIdsByAddress[msg.sender].push(currentPositionId);
currentPositionId += 1;
}function calculateInterest(uint basisPoints, uint numDays, uint weiAmount) private pure returns (uint) {
return basisPoints * weiAmount / 10000;
}function modifyLockPeriods(uint numDays, uint basisPoints) external {
require(owner == msg.sender, "Only owner may modify staking periods");
tiers[numDays] = basisPoints;
lockPeriods.push(numDays);
}function getLockPeriods() external view returns(uint[] memory) {
return lockPeriods;
}function getInterestRate(uint numDays) external view returns (uint) {
return tiers[numDays];
}function getPositionById(uint positionId) external view returns (Position memory) {
return positions[positionId];
}function getPositionIdsForAddress(address walletAddress) external view returns (uint[] memory) {
return positionIdsByAddress[walletAddress];
}function closePosition(uint positionId) external {
require(positions[positionId].walletAddress == msg.sender, "Only position creator can modify the position");
require(positions[positionId].open == true, "Position is closed");
positions[positionId].open = false; if(block.timestamp > positions[positionId].unlockDate) {
uint amount = positions[positionId].weiStaked + positions[positionId].weiInterest;
payable(msg.sender).call{value: amount}("");
} else {
payable(msg.sender).call{value: positions[positionId].weiStaked}("");
}
}
Let us discuss some the critical functions:
stakeEther takes an integer which will be the number of days that the Ether (being sent to the smart contract while calling this function) is being staked for.
calculateInterest function will take an integer of the number of basis points and this will provide the interest earned by the user with the help of the number of days the position is staked and the tier. As you will notice, this is a
privatefunction. A private function is one function that can only be accessible only inside the contract that defines the function and does not let any other external source access it.modifyLockPeriods will allow the deployer (and deployer only) to change lockPeriods of the contract. If this function was to be called by any other address it will fail as it is a protected function and can only be called by the owner.
getPositionById and getPositionByAddresses to fetch the position and the staked user
changeUnlockDate will be used to change the unlock date of the staked position.
Your smart contract should look similar to this

3.1: Creating a Private Testnet on BuildBear đ»ââïž *(why BuildBear, you ask? Have a look over here: **Where Localhost Fails and *Win Web3 Hackathons, using BuildBear Testnetâs analytics)
3.1.1: Visit the BuildBear App. Once you login with your Github account, you will see a page similar to the image added below

Here we have to create a simple node for our staking application so we will click on the create an endpoint and we will be redirected to the node configuration page. But for this article, we will be using the default configuration for creating our node.
So we will simply click on the create button as shown below.

Congratulations! You have created your private testnet node!
Your page should be updated to something similar to the following:

Click on the RPC URL (either copy of click to view) to get the RPC to your private testnet.
3.2: In order to perform transactions, we need funds from faucet. Donât worry! We donât have to find random faucets to get test ether.
Click on the Open Faucet option and add your choose your account.

After that, click on the Add to Metamask option in the right corner of the faucet page.
Your metamask would be provided with 1,000 BB ETH immediately.

3.3: Update thehardhat.config.js to the following:

In the networks, we have created a network called buildbear and added our RPC URL there and then added the private key of the metamask account (please do not expose your private key to anyone, ever).
Now our config is ready! Letâs write our smart contract.
4.1: Use the following script to deploy your staking contract:
const { ethers } = require("hardhat");
const hre = require("hardhat");async function main() {
const Staking = await hre.ethers.getContractFactory("Staking");
const staking = await Staking.deploy({value: ethers.utils.parseEther('10')}); await staking.deployed(); console.log(
"Staking contract deployed to:", staking.address
);
}main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Open your CLI and run the following command:
npx hardhat run scripts/deploy.js --network buildbear
Now if you go back to the BuildBear Explorer (link available from the BuildBear App, once you have created your private testnet). You will see a transaction executed.
Open the transaction hash, and you will get all the details of your transaction.

If youâll click on the advance button, you will see a trace that says unknownContract created.
This represents our Staking contract deployed.
But unknownContractsounds quite confusing! đ„. So letâs get to resolving this too.
4.2: Go back to your Dashboard, and click on the advanced button.

And submit your artifacts folder over here.
Now, again open your transaction and check the advanced option.
You will see that this time instead of getting unknownContract, we received the Staking contract

Isnât that great? đ
We will be using BuildBear Explorer to test out our contract.
5.1: In order to execute the transactions, visit your contract address on the explorer. Take the contract address and put it on the search option.
Your contract would look like

Click on the Contract option adjacent to Transactions
Here we have 3 options:
Code to submit your contractâs ABI
Read Contract
Write Contract
Since we have already submitted the ABI using the dashboard, weâll be redirected to the Read Contract option.
Your Read Contract option will have all the functions that you have written in your contract.

5.2: Let us stake our ethers on write contract.
Make sure you connect to your Web3 account.
Under the write contract option, you will see the stakeEthers function.
We need to pass in the values for numDays and amount. Thus, here we will pass 30 as the value for numDays and 2 ether for the amount.

Then click on the write button to call the stakeEthers function. This will trigger Metamask with 2 BB ETH has the value and the function call to the smart contract.
Once done, you will receive a transaction hash below the query button, telling whether your transaction was successful or not.

Click on the transaction hash and youâll receive all the information related to it.

We interacted with the contract 0x73eccD6288e117cAcA738BDAD4FEC51312166C1A and added 2 ETH in the contract.
Trace of the transaction will show your activity

In the read contract, function currentPositionId is equal to 1 because we just now staked our ethers.

This way we can run all the functions of our contract and run test cases for the contract.
5.3: Getting your interest out
Staking interest will only be earned once the lockPeriod is achieved.
We can test it out with the help of Postman request.
What we will do is, we will take the RPC URL of the node and do a post request on Postman.
Pass the body with the following json body
{
"jsonrpc": "2.0",
"id": 1,
"method": "evm_increaseTime",
"params": ["0x28DE80"]
}
This will increase the block timestamp of the node by 31 Days. Therefore, we will be able to close our stake and earn interest on them.
Thus, now if youâll run a POST request, you will receive a result similar to this

This means that the nodeâs timestamp has been increased by 31 days successfully.
Now to test our stake collection, we will go back to the explorer and open our contract and check the closePosition .
Now enter the positionId below in the params and click on write button.

You can see our transaction has been successfully executed.
To understand what we did just now, let us open the transaction hash we received below the write button and then open the advanced tab.

You will receive a trace similar to this.
Let us understand what just happened here:
When we called the stakeEther, we provided that the staked ether should be unlocked after 30 days
But it is not the most feasible way to unlock the ethers, thatâs why we used Postman here
We did a post request using the RPC URL and performed the evm_increaseTime method on it and in the params we provided 31 Days in hex so that we do not have to wait for so long
Once we have done the post request, the block timestamp of our node will be increased by exactly 31 Days. Therefore now it is possible to unlock our stake.
If we look at the trace shown above,
When we called the closePosition function, it interacted with our contract
The contract then sent the funds to the user account (in the example above the address
0xd51..is my wallet address to which the funds were sent to. TheUnknownContractAndFunctionover here is the manner in which the sending of funds is shown.
And now we just learned how staking crypto is done! đ
This is how BuildBear allows you to deploy your contract and can interact with it.
BONUS
To move one step ahead, you can ask your friends to stake their ethers on your staking contract and perform multiple functionalities. I have created my private testnet and the staking contract is available over here.
You can get the Faucet for the testnet over here.

PRO TIP: While creating my private testnet, I have disabled the option to advance time and hence, you will NOT be able to advance the block time of my private testnet. Tadaa!!!!
To learn more about BuildBear, read here docs
Get the above Github code from here
If you appreciate what we are doing, please follow us on Twitter and Join the Telegram group if you havenât done yet.
Author:
Pari Tomar (Twitter || LinkedIn), always open to feedback and learning.
BTW, if you would know anyone who would be keen to working with BuildBear. Please have a look over here!!!
