In this tutorial we will create a fixed rate staking contract that pays out 1 token for every 1 token staked per year. The contract will be built on an OpenZeppelin ERC20 token library with additional functionality for staking.
The user will stake their tokens which will lock them in the smart contract at what time rewards pending will start to accrue. When the user claims or unstakes their tokens they will receive the rewards to their wallet.
The way we are going to do this is to create two mappings from address => uint256. One will be for the amount staked so we know how much each user has staked, the second will be for the timestamp when they started staking.
A timestamp is the number of seconds that has passed since January 1st 1970 UTC. At time of writing it’s currently 1659416883 and this is available in Solidity through block.timestamp.
So let’s first create a basic ERC20 token and define those mappings.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract FixedStaking is ERC20 {
mapping(address => uint256) public staked;
mapping(address => uint256) private stakedFromTS;
constructor() ERC20("Fixed Staking", "FIX") {
_mint(msg.sender,1000000000000000000);
}
}
Now we need to add additional functions for the staking. Let’s first create an external function that allows a user to stake a set amount of their tokens.
function stake(uint256 amount) external {
require(amount > 0, "amount is <= 0");
require(balanceOf(msg.sender) >= amount, "balance is <= amount");
_transfer(msg.sender, address(this), amount);
if (staked[msg.sender] > 0) {
claim();
}
stakedFromTS[msg.sender] = block.timestamp;
staked[msg.sender] += amount;
}
We have two require statements at the start to do sanity checks on the amount value as it is user defined. We then use the internal _transfer token function to move the funds from the transaction sender to the token contract address which will store them while staking.
We then claim() any tokens outstanding. This is important as a user may already be staking tokens and we need to reset the rewards.
Finally we set the two mappings we defined to the timestamp and add the amount.
Now let’s create a function to unstake the rewards.
function unstake(uint256 amount) external {
require(amount > 0, "amount is <= 0");
require(staked[msg.sender] >= amount, "amount is > staked");
claim();
staked[msg.sender] -= amount;
_transfer(address(this), msg.sender, amount);
}
Again we have a couple of sanity checks at the start and then go on to claim any staking rewards. Finally we reduce the amount staked by the user input and transfer those tokens back out of the contract to the user.
Finally we need to create the claim function which can be run internally within the above functions or externally if the user wants to call it directly to claim his rewards.
function claim() public {
require(staked[msg.sender] > 0, "staked is <= 0");
uint256 secondsStaked = block.timestamp - stakedFromTS[msg.sender];
uint256 rewards = staked[msg.sender] * secondsStaked / 3.154e7; // 1:1 per year
_mint(msg.sender,rewards);
stakedFromTS[msg.sender] = block.timestamp;
}
We calculate the secondsStaked by subtracting the start time from the current timestamp. We then calculate the rewards by multiplying the amount staked by the seconds staked and dividing by the number of seconds in a year (3.154e7 = seven decimals = 31,540,000).
Next we user the internal _mint function like we did in the constructor argument to mint some new tokens for rewards. We could swap this out for a different mechanism perhaps distributing a 3rd party token or something like that.
We end by updating and resetting the stakedFromTS with the current timestamp.
Here is the full source code for the fixed rate staking token:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract FixedStaking is ERC20 {
mapping(address => uint256) public staked;
mapping(address => uint256) private stakedFromTS;
constructor() ERC20("Fixed Staking", "FIX") {
_mint(msg.sender,1000000000000000000);
}
function stake(uint256 amount) external {
require(amount > 0, "amount is <= 0");
require(balanceOf(msg.sender) >= amount, "balance is <= amount");
_transfer(msg.sender, address(this), amount);
if (staked[msg.sender] > 0) {
claim();
}
stakedFromTS[msg.sender] = block.timestamp;
staked[msg.sender] += amount;
}
function unstake(uint256 amount) external {
require(amount > 0, "amount is <= 0");
require(staked[msg.sender] >= amount, "amount is > staked");
claim();
staked[msg.sender] -= amount;
_transfer(address(this), msg.sender, amount);
}
function claim() public {
require(staked[msg.sender] > 0, "staked is <= 0");
uint256 secondsStaked = block.timestamp - stakedFromTS[msg.sender];
uint256 rewards = staked[msg.sender] * secondsStaked / 3.154e7;
_mint(msg.sender,rewards);
stakedFromTS[msg.sender] = block.timestamp;
}
}
This fixed rate staking contract could be taken further by adding a function to stakeAll() so that claimed rewards and total balance is staked together to prevent a user needing to perhaps do two transactions to restake claimed rewards.
The code is for demonstration purposes only, untested and not suitable for financial transactions.