In the cryptocurrency world this is understood as giving users some kind of right or reward for as long as they don’t transfer some tokens in their possession.
With a few lines of solidity code you can implement a staking mechanism, one of the most powerful incentive schemes in token economics.
Someone is said to have a stake in a venture when they contribute some assets in exchange of exercising some level of control, influence, or participation in its activities.
A staking mechanism usually encourages token holding against token trading, which in turn is expected to drive up the token valuation.
To build this staking mechanism we will need:
- - A staking token.
- - Data structures to keep track of stakes, stakeholders and rewards.
- - Methods to create and remove stakes.
- - A rewards system.
Ready?
Let’s get on with it.
A staking token can be created as an ERC20 token. I’m going to need SafeMath and Ownable later on, so let’s import and use those as well.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
/**
* @title Staking Token (STK)
* @author Ayo Solomon Nigeria
* @notice Implements a basic ERC20 staking token with incentive distribution.
*/
contract StakingToken is ERC20, Ownable {
using SafeMath for uint256;
/**
* @notice The constructor for the Staking Token.
* @param _owner The address to receive all tokens on construction.
* @param _supply The amount of tokens to mint on construction.
*/
constructor(address _owner, uint256 _supply) ERC20("StakingToken", "STK") {
_mint(_owner, _supply);
}
Looks great for a start, so let's see what's going on here
Stakeholders
In this implementation we are going to keep track of the stakeholders to facilitate a robust distribution of incentives later on. In theory it would be possible to not keep track of them as a normal ERC20 token would do, but in practice it is difficult to ensure that stakeholders don’t game the distribution system if you don’t track them.
For implementation we will just use a dynamic array of stakeholder addresses.
/**
* @notice We usually require to know who are all the stakeholders.
*/
address[] internal stakeholders;
The methods implemented allow just to add a stakeholder, remove a stakeholder, and verify whether an address belongs to a stakeholder. Other more efficient implementations are surely possible but I like this one for readability.
Stakes
A stake at its simplest form will need to record the stake size and the stake holder. A really simple implementation of this could be just a mapping from the address of the stakeholder to the stake size.
/**
* @notice The stakes for each stakeholder.
*/
mapping(address => uint256) internal stakes;
To make the code more readable I’m going to follow the function names from ERC20 and create equivalents to get the data from the stakes mapping.
/**
* @notice A method to check if an address is a stakeholder.
* @param _address The address to verify.
* @return bool, uint256 Whether the address is a stakeholder,
* and if so its position in the stakeholders array.
*/
function isStakeholder(address _address) public view returns (bool, uint256) {
for (uint256 s = 0; s < stakeholders.length; s += 1) {
if (_address == stakeholders[s]) return (true, s);
}
return (false, 0);
}
/**
* @notice A method to add a stakeholder.
* @param _stakeholder The stakeholder to add.
*/
function addStakeholder(address _stakeholder) public {
(bool _isStakeholder, ) = isStakeholder(_stakeholder);
if (!_isStakeholder) stakeholders.push(_stakeholder);
}
/**
* @notice A method to remove a stakeholder.
* @param _stakeholder The stakeholder to remove.
*/
function removeStakeholder(address _stakeholder) public {
(bool _isStakeholder, uint256 s) = isStakeholder(_stakeholder);
if (_isStakeholder) {
stakeholders[s] = stakeholders[stakeholders.length - 1];
stakeholders.pop();
}
}
/**
* @notice A method to retrieve the stake for a stakeholder.
* @param _stakeholder The stakeholder to retrieve the stake for.
* @return uint256 The amount of wei staked.
*/
function stakeOf(address _stakeholder) public view returns (uint256) {
return stakes[_stakeholder];
}
/**
* @notice A method to the aggregated stakes from all stakeholders.
* @return uint256 The aggregated stakes from all stakeholders.
*/
function totalStakes() public view returns (uint256) {
uint256 _totalStakes = 0;
for (uint256 s = 0; s < stakeholders.length; s += 1) {
_totalStakes = _totalStakes.add(stakes[stakeholders[s]]);
}
return _totalStakes;
}
We have giving STK holders the capability to create and remove stakes. We will burn the tokens as they are staked to stop users from transferring them until the stake is removed.
Please note that on stake creation _burn will revert if the user tries to stake more tokens than he owns, and on stake removal the update of the stakes mapping will revert if there is an attempt to remove more tokens that were staked.
Finally, we use addStakeholder and removeStakeholder to have a record of who has stakes, to be used later in the rewards system.
/**
* @notice A method for a stakeholder to create a stake.
* @param _stake The size of the stake to be created.
*/
function createStake(uint256 _stake) public {
_burn(msg.sender, _stake);
if (stakes[msg.sender] == 0) addStakeholder(msg.sender);
stakes[msg.sender] = stakes[msg.sender].add(_stake);
}
/**
* @notice A method for a stakeholder to remove a stake.
* @param _stake The size of the stake to be removed.
*/
function removeStake(uint256 _stake) public {
stakes[msg.sender] = stakes[msg.sender].sub(_stake);
if (stakes[msg.sender] == 0) removeStakeholder(msg.sender);
_mint(msg.sender, _stake);
}
Rewards mechanisms can have many different implementations and be quite heavy to run. For this contract we will implement a very simple version where the stakeholders periodically receive a reward in STK tokens equivalent to a 1% of their individual stakes.
In more sophisticated contracts the distribution of rewards would be automatically triggered when certain conditions are met, but in this case we will let the owner trigger it manually. Following best practice we will also keep track of the rewards and implement a method to withdraw them.
As before, to make the code readable we have followed the naming conventions from the ERC20.sol contract, first the data structure and data management methods:
/**
* @notice The accumulated rewards for each stakeholder.
*/
mapping(address => uint256) internal rewards;
/**
* @notice A method to allow a stakeholder to check his rewards.
* @param _stakeholder The stakeholder to check rewards for.
*/
function rewardOf(address _stakeholder)
public
view
returns(uint256)
{
return rewards[_stakeholder];
}
/**
* @notice A method to the aggregated rewards from all stakeholders.
* @return uint256 The aggregated rewards from all stakeholders.
*/
function totalRewards()
public
view
returns(uint256)
{
uint256 _totalRewards = 0;
for (uint256 s = 0; s < stakeholders.length; s += 1){
_totalRewards = _totalRewards.add(rewards[stakeholders[s]]);
}
return _totalRewards;
}
Follow the methods to calculate, distribute and withdraw rewards:
/**
* @notice A simple method that calculates the rewards for each stakeholder.
* @param _stakeholder The stakeholder to calculate rewards for.
*/
function calculateReward(address _stakeholder)
public
view
returns(uint256)
{
return stakes[_stakeholder] / 100;
}
/**
* @notice A method to distribute rewards to all stakeholders.
*/
function distributeRewards()
public
onlyOwner
{
for (uint256 s = 0; s < stakeholders.length; s += 1){
address stakeholder = stakeholders[s];
uint256 reward = calculateReward(stakeholder);
rewards[stakeholder] = rewards[stakeholder].add(reward);
}
}
/**
* @notice A method to allow a stakeholder to withdraw his rewards.
*/
function withdrawReward()
public
{
uint256 reward = rewards[msg.sender];
rewards[msg.sender] = 0;
_mint(msg.sender, reward);
}
Awesome 🥳 🎊,
You just got this running, hope you learned something, if you did like or leave a comment 🧡
Conclusion
A staking and rewards mechanism is a powerful incentive tool that only needs to be as complex as we want to make it. The methods provided in the ERC20 standard and SafeMath allows us to code it in about 200 lines of sparse code.
Full code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
/**
* @title Staking Token (STK)
* @author Ayo Solomon Nigeria
* @notice Implements a basic ERC20 staking token with incentive distribution.
*/
contract StakingToken is ERC20, Ownable {
using SafeMath for uint256;
/**
* @notice The constructor for the Staking Token.
* @param _owner The address to receive all tokens on construction.
* @param _supply The amount of tokens to mint on construction.
*/
constructor(address _owner, uint256 _supply) ERC20("StakingToken", "STK") {
_mint(_owner, _supply);
}
/**
* @notice We usually require to know who are all the stakeholders.
*/
address[] internal stakeholders;
/**
* @notice The accumulated rewards for each stakeholder.
*/
mapping(address => uint256) internal rewards;
/**
* @notice The stakes for each stakeholder.
*/
mapping(address => uint256) internal stakes;
/**
* @notice A method to check if an address is a stakeholder.
* @param _address The address to verify.
* @return bool, uint256 Whether the address is a stakeholder,
* and if so its position in the stakeholders array.
*/
function isStakeholder(address _address) public view returns (bool, uint256) {
for (uint256 s = 0; s < stakeholders.length; s += 1) {
if (_address == stakeholders[s]) return (true, s);
}
return (false, 0);
}
/**
* @notice A method to add a stakeholder.
* @param _stakeholder The stakeholder to add.
*/
function addStakeholder(address _stakeholder) public {
(bool _isStakeholder, ) = isStakeholder(_stakeholder);
if (!_isStakeholder) stakeholders.push(_stakeholder);
}
/**
* @notice A method to remove a stakeholder.
* @param _stakeholder The stakeholder to remove.
*/
function removeStakeholder(address _stakeholder) public {
(bool _isStakeholder, uint256 s) = isStakeholder(_stakeholder);
if (_isStakeholder) {
stakeholders[s] = stakeholders[stakeholders.length - 1];
stakeholders.pop();
}
}
/**
* @notice A method to retrieve the stake for a stakeholder.
* @param _stakeholder The stakeholder to retrieve the stake for.
* @return uint256 The amount of wei staked.
*/
function stakeOf(address _stakeholder) public view returns (uint256) {
return stakes[_stakeholder];
}
/**
* @notice A method to the aggregated stakes from all stakeholders.
* @return uint256 The aggregated stakes from all stakeholders.
*/
function totalStakes() public view returns (uint256) {
uint256 _totalStakes = 0;
for (uint256 s = 0; s < stakeholders.length; s += 1) {
_totalStakes = _totalStakes.add(stakes[stakeholders[s]]);
}
return _totalStakes;
}
/**
* @notice A method for a stakeholder to create a stake.
* @param _stake The size of the stake to be created.
*/
function createStake(uint256 _stake) public {
_burn(msg.sender, _stake);
if (stakes[msg.sender] == 0) addStakeholder(msg.sender);
stakes[msg.sender] = stakes[msg.sender].add(_stake);
}
/**
* @notice A method for a stakeholder to remove a stake.
* @param _stake The size of the stake to be removed.
*/
function removeStake(uint256 _stake) public {
stakes[msg.sender] = stakes[msg.sender].sub(_stake);
if (stakes[msg.sender] == 0) removeStakeholder(msg.sender);
_mint(msg.sender, _stake);
}
/**
* @notice A method to allow a stakeholder to check his rewards.
* @param _stakeholder The stakeholder to check rewards for.
*/
function rewardOf(address _stakeholder)
public
view
returns(uint256)
{
return rewards[_stakeholder];
}
/**
* @notice A method to the aggregated rewards from all stakeholders.
* @return uint256 The aggregated rewards from all stakeholders.
*/
function totalRewards()
public
view
returns(uint256)
{
uint256 _totalRewards = 0;
for (uint256 s = 0; s < stakeholders.length; s += 1){
_totalRewards = _totalRewards.add(rewards[stakeholders[s]]);
}
return _totalRewards;
}
/**
* @notice A simple method that calculates the rewards for each stakeholder.
* @param _stakeholder The stakeholder to calculate rewards for.
*/
function calculateReward(address _stakeholder)
public
view
returns(uint256)
{
return stakes[_stakeholder] / 100;
}
/**
* @notice A method to distribute rewards to all stakeholders.
*/
function distributeRewards()
public
onlyOwner
{
for (uint256 s = 0; s < stakeholders.length; s += 1){
address stakeholder = stakeholders[s];
uint256 reward = calculateReward(stakeholder);
rewards[stakeholder] = rewards[stakeholder].add(reward);
}
}
/**
* @notice A method to allow a stakeholder to withdraw his rewards.
*/
function withdrawReward()
public
{
uint256 reward = rewards[msg.sender];
rewards[msg.sender] = 0;
_mint(msg.sender, reward);
}
}
Top comments (1)
Staking is actually a great way to earn rewards! On one hand, you are contributing to the security of blockchain by this, and on the other, you are getting returns on it, similar to saving or a fixed account in a bank.
A few months back, I have started Tron v2 staking via AnCrypto wallet, and recently, the wallet has initiated zero-cost Tron transactions, which is fetching a good amount of traction.