James Bachini

Solidity Tutorial | Fixed Rate Staking Contract

Fixed Rate Staking Solidity Contract

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.

James On YouTube

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.

Fixed Rate Staking using block.timestamp

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.


Get The Blockchain Sector Newsletter, binge the YouTube channel and connect with me on Twitter

The Blockchain Sector newsletter goes out a few times a month when there is breaking news or interesting developments to discuss. All the content I produce is free, if you’d like to help please share this content on social media.

Thank you.

James Bachini

Disclaimer: Not a financial advisor, not financial advice. The content I create is to document my journey and for educational and entertainment purposes only. It is not under any circumstances investment advice. I am not an investment or trading professional and am learning myself while still making plenty of mistakes along the way. Any code published is experimental and not production ready to be used for financial transactions. Do your own research and do not play with funds you do not want to lose.