DEV Community

tobi akinrinade
tobi akinrinade

Posted on • Updated on

Introduction to Yield Farming and Implementation of a staking contract

This is my second time of learning about yield farming, i thought to write an article about what i have learnt so far and also implement a staking contract. This article teaches you what to know about yield farming and how to implement a staking contract using SushiSwap's MasterChef contract implementation.
What is yield farming?
Simply put Yield farming lets you lock up funds, providing rewards in the process. Yield farming allows crypto currency holders to lock their assest(tokens) , to which they get a reward in turn.
Yield farming can be related to the modern day banking system where a customer collects a loan and pays back with interest. in yield farming tokens(assets) are locked up or lents in protocols and smart contracts in order to get a return.
Yield farming is carried out in ERC20 tokens with the reward being in a form of ERC20 token

The first step to yield farming is adding the funds to a liquidity pool which are smart contracts with funds.
This pools powers a marketplace where users can exchange, borrow or lend tokens. Once you have added your funds to a pool, you become a liquidity provider. In reward for locking up your token you will be rewarded with fees generated from underlying DeFi Platforms.

One of the main benefits of yield farming to put bluntly is sweet profit. you can invest early in a project, it shoots up in value and sell your profits or choose to reinvest(Please note; this is not a trading advice as the market is highly volatile and DeFi is a riskier environment to put your money).

Is yield farming sustainable?
In my opinion , these projects raise huge amounts in a short period of time and and are then forgotten about, some have been described as scam; especially flash farming projects.
In the space of 12 months, DeFi has become a $15 billion industry — spawning governance tokens that are now worth even more than Bitcoin. But the rapid explosion of protocols has brought considerable growing pains… and concerns that the sector is not on a sustainable footing. When interest rates in conventional savings accounts stand at a fraction of a percent, while yield farming generates triple-digit returns, it’s inevitable that questions will emerge about whether this is a bubble that’s fit to burst.
As Ethereum co-founder Vitalik Buterin recently pointed out on a podcast with Ryan Sean Adams, such sky-high interest rates are “just a temporary promotion that was created by printing a bunch of compound tokens, and you just can’t keep printing compound tokens forever.”
Its analysts warned that the current yield farming trend in DeFi is not sustainable — and went on to predict that only a small handful of protocols would survive in the long-term.
Invest at your own risks tends to be the general consensus from experts.

Yield farming strategies
Yield Farming strategies are set of steps that aim on generating a high yield on the capitals. These steps usually involve at least one of the following elements;

  • Lending & Borrowing

  • Supplying capital to liquidity pools

  • Staking LP tokens

Lending & Borrowing
A simple way to put it, farmers supply coins(ERC20 tokens) such as USDC, USDT to lending platforms and start getting a return on their capital. liquidity mining and leverage can super charge that. An example is a farmer getting rewarded COMP tokens for lending and borrowing on compound finance . They can also borrow coins with their collateral to buy even more coins and earn reward tokens , these coins of course comes with a risk of potential liquidation.

Supplying Capital to liquidity pools

Farmers can supply coins to one of the liquidity pools in protocols like UNISWAP,UNUS or CURVE and get rewarded with fees that are charged from swapping different tokens. Liquidity mining can also supercharge this. By suppling coins to certain liquidity pools , farmers are rewarded with extra token

Staking LP tokens

Famers are allowed to stake liquidity provider or LP tokens that represent their participation in a liquidity pool. For example , synthetic ,ren and curve got into a partnership where users can provide WBTC, SBTC and renBTC to the curve BTC liquidity pool and receive curve LP tokens as a reward. These tokens can be staked on synthetics minter where farmers can further be rewarded in CRV, BAL, SNX and REN tokens.

I implemented a simple staking contract that takes an upgradable token as a stake asset and issues reward in a different token using SushiSwap's MasterChef contract implementation.
see full code here
the staking contract allows the owner to set an ERC20 tokens as the liquidity pool token ,users or farmers in this case can stake the ERC 20 token set and get rewards in beartokens.

I will explain the key functions and how the reward is calculated for each farmer

 // Constructor
    constructor(address _rewardTokenAddress, uint256 _rewardTokensPerBlock) {
        bearToken = BearToken(_rewardTokenAddress);
        rewardTokensPerBlock = _rewardTokensPerBlock;
    }
Enter fullscreen mode Exit fullscreen mode

At deployment of the contract, the contract owner sets the reward token (bearToken) and the reward per block meaning how much should be rewarded to the user per block.
this reward is calculated per block after the user joins a pool
Creating a pool

    function createPool(IERC20 _stakeToken) external onlyOwner {
        Pool memory pool;
        pool.stakeToken = _stakeToken;
        pools.push(pool);
        uint256 poolId = pools.length - 1;
        emit PoolCreated(poolId);
    }

Enter fullscreen mode Exit fullscreen mode

the contract owner creates a pool by setting the staketoken(an ERC20 token) , this updates the staking pools array.

    function deposit(uint256 _poolId, uint256 _amount) external {
        require(_amount > 0, "Deposit amount can't be zero");
        Pool storage pool = pools[_poolId];
        PoolStaker storage staker = poolStakers[_poolId][msg.sender];

        // Update pool stakers
        harvestRewards(_poolId);

        // Update current staker
        staker.amount = staker.amount + _amount;
        staker.rewardDebt =
            (staker.amount * pool.accumulatedRewardsPerShare) /
            REWARDS_PRECISION;

        // Update pool
        pool.tokensStaked = pool.tokensStaked + _amount;

        // Deposit tokens
        emit Deposit(msg.sender, _poolId, _amount);
        pool.stakeToken.safeTransferFrom(
            address(msg.sender),
            address(this),
            _amount
        );
    }
Enter fullscreen mode Exit fullscreen mode

The deposit function allows the user (farmer) to deposit LP tokens into the contract for bearToken allocation. on each deposit the reward across the block is calculated . The farmers rewards is calculated in the updateStakersRewards function which will be explained below.
the current stakers amount is updated and the reward debt is recalculated.
The staker's reward debt are all rewards accumulated before the staker joined the pool. For example, a pool is created at block 0 and different users start staking and depositing , if user A joins the pool at block 3 , and decides to withdraw or unstake at block 10, the users reward debt will be the total rewards accumulated from block 0 to 10 minius the total accumulated rewards from block 0 to 3 . That equals the users reward debt. This maintains the balance in the pool and ensure even distribution of rewards in the pool.

    /**
     * @dev Harvest user rewards from a given pool id
     */
    function harvestRewards(uint256 _poolId) public {
        updatePoolRewards(_poolId);
        Pool storage pool = pools[_poolId];
        PoolStaker storage staker = poolStakers[_poolId][msg.sender];
        uint256 rewardsToHarvest = ((staker.amount *
            pool.accumulatedRewardsPerShare) / REWARDS_PRECISION) -
            staker.rewardDebt;
        if (rewardsToHarvest == 0) {
            staker.rewardDebt =
                (staker.amount * pool.accumulatedRewardsPerShare) /
                REWARDS_PRECISION;
            return;
        }
        staker.rewards = 0;
        staker.rewardDebt =
            (staker.amount * pool.accumulatedRewardsPerShare) /
            REWARDS_PRECISION;
        emit HarvestRewards(msg.sender, _poolId, rewardsToHarvest);
        bearToken.mint(msg.sender, rewardsToHarvest);
    }

Enter fullscreen mode Exit fullscreen mode

This function enables harvesting the users reward from a given poolid. The users reward id calculated per block and summed up . The total accumulated rewards per each block is multiplied by the users total stake , then the users reward debt is subtracted from it to get the users reward. The users reward is set to 0 each time there is an harvest to prevent the users reward from adding previous harvested reward after harvest. The staker's/user's reward debt is then updated and the reward is minted to the staker's address.

  /**
     * @dev Update pool's accumulatedRewardsPerShare and lastRewardedBlock
     */
    function updatePoolRewards(uint256 _poolId) private {
        Pool storage pool = pools[_poolId];
        if (pool.tokensStaked == 0) {
            pool.lastRewardedBlock = block.number;
            return;
        }
        uint256 blocksSinceLastReward = block.number - pool.lastRewardedBlock;
        uint256 rewards = blocksSinceLastReward * rewardTokensPerBlock;
        pool.accumulatedRewardsPerShare =
            pool.accumulatedRewardsPerShare +
            ((rewards * REWARDS_PRECISION) / pool.tokensStaked);
        pool.lastRewardedBlock = block.number;
    }
Enter fullscreen mode Exit fullscreen mode

The pool is updated per block and each time a user makes a change to the pool such as depositing, withdrawal, staking, and unstaking.
This has to be done anytime each of these functions is called because the total reward per block is calculated based on a block's current state. the states involves, the blocks passed, staker A tokens , staker B tokens(all stakers tokens), total tokens, last rewarded block and pool total accumulated rewards pershare.
The total accumulatedReward per share is the total of all rewards accumulated by each staker on from starting block to the current block.
The pools current accumulatedReward per share is the current accumated reward per share( which is 0 from block 0 because no user or reward has entered the pool) plus rewards(number of blocks since last reward was calculated mutliplied by reward per block) multiplied by the total of the pool token staked.

   /**
     * @dev Withdraw all tokens from an existing pool
     */
    function withdraw(uint256 _poolId) external {
        Pool storage pool = pools[_poolId];
        PoolStaker storage staker = poolStakers[_poolId][msg.sender];
        uint256 amount = staker.amount;
        require(amount > 0, "Withdraw amount can't be zero");

        // Pay rewards
        harvestRewards(_poolId);

        // Update staker
        staker.amount = 0;
        staker.rewardDebt =
            (staker.amount * pool.accumulatedRewardsPerShare) /
            REWARDS_PRECISION;

        // Update pool
        pool.tokensStaked = pool.tokensStaked - amount;

        // Withdraw tokens
        emit Withdraw(msg.sender, _poolId, amount);
        pool.stakeToken.safeTransfer(address(msg.sender), amount);
    }

Enter fullscreen mode Exit fullscreen mode

A staker is able to withdraw all the tokens from an existing pool given the poolId. When this action is performed , the pool is updated and the harvest reward function as well as the updated pool function is performed again. This updates the pool and keeps the pool in check for balanced rewards.
The stakers amount is subtracted from the total of all token in the particular pool and the reward debt is also updated for that paticular staker. The staker then receives the total amount in the staker's account, transferred by the contract.

    function enterStaking(uint256 _amount) public {
        Pool storage pool = pools[0];
        PoolStaker storage staker = poolStakers[0][msg.sender];
        updatePoolRewards(0);
        if (staker.amount > 0) {
            uint256 pending = ((staker.amount *
                pool.accumulatedRewardsPerShare) / REWARDS_PRECISION) -
                staker.rewardDebt;
            if (pending > 0) {
                bearToken.transfer(msg.sender, pending);
            }
        }
        if (_amount > 0) {
            pool.stakeToken.safeTransferFrom(
                address(msg.sender),
                address(this),
                _amount
            );
            staker.amount = staker.amount + _amount;
        }

        staker.rewardDebt =
            (staker.amount * pool.accumulatedRewardsPerShare) /
            REWARDS_PRECISION;
        bearToken.mint(msg.sender, _amount);

        emit Deposit(msg.sender, 0, _amount);
    }

Enter fullscreen mode Exit fullscreen mode

This functions allows the staker /farmer to enter a particular pool with a particular amount of the stake token. The pending reward for the farmer is calculated based on the users current stake and transferred to the stakers wallet address (bear tokens).
The reward debt for the staker is calculated and updated in the pool.
The bearToken (reward token ) is minted based off the users stake amount.

    /**
     * @dev allows users to unstake reward token into the contract
     */
    function leaveStaking(uint256 _amount) public {
        Pool storage pool = pools[0];
        PoolStaker storage staker = poolStakers[0][msg.sender];
        updatePoolRewards(0);
        require(staker.amount >= _amount, "withdraw: not good");
        uint256 pending = ((staker.amount * pool.accumulatedRewardsPerShare) /
            REWARDS_PRECISION) - staker.rewardDebt;
        if (pending > 0) {
            bearToken.transfer(msg.sender, pending);
        }
        if (_amount > 0) {
            staker.amount = staker.amount - _amount;
            pool.stakeToken.safeTransfer(address(msg.sender), _amount);
        }

        staker.rewardDebt =
            (staker.amount * pool.accumulatedRewardsPerShare) /
            REWARDS_PRECISION;

        bearToken.burn(msg.sender, _amount);
        emit Withdraw(msg.sender, 0, _amount);
    }
Enter fullscreen mode Exit fullscreen mode

This function allows a staker to leave the stake at any point off desire . Once this function is performed the pool is then updated and rewards calculated. the staker/ farmer also gets the pending rewards in bear tokens and the reward debt is calculated to maintain balance in the pool.
You might notice the REWARD_PRECISION variable; since solidity doesn't handle float numbers, and amounts or accumulated share can be in decimals, we multiply them by a big number like 1e12 (value of reward precision) when calculating it and then divide it by the same number when using it.

Conclusion
I hope this sheds more light on yield farming and if as a dev you ever need to implement a staking contract, my implementation helps. In my research i have come to discover that most staking contracts copy the implementation of pancake swapSushiSwap's MasterChef contract with little tweaks ,
understanding this concept makes it easier to re-implement the contract to suit your specific use case.
Practicing and manipulating the rewards and accumulated share myself made me understand the concept of how staking works and how rewards are distributed within a pool, i would recommend that to anyone trying to understand the concept.
I hopes this explains more of DeFi in depth to anyone searching.

Top comments (0)