Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Staking LP Tokens

Hello! Welcome back to your course on building a blockchain application.

In our last lesson, we discussed the "why" of incentivizing liquidity. You learned that liquidity mining—rewarding users who provide liquidity—is the standard way to overcome the risk of impermanent loss and attract capital to your token. We broke down the two-step process: a user gets LP tokens from Uniswap, then stakes those tokens in a separate contract to earn rewards.

Today, we transition from "why" to "how." Your goal for this lesson is to implement the staking contract that accepts and rewards users for staking their LP tokens. We will explore the elegant and gas-efficient algorithm that powers most staking contracts in DeFi and translate it into a working Solidity smart contract.

Let's begin with a high-level view of what this contract does.

Staking Smart Contracts Flow Diagram
This diagram shows the core user interactions with a staking contract. A user's wallet calls `stake()` to deposit tokens, `withdraw()` to retrieve them, and `getReward()` to claim earned rewards.

1. The Core Challenge: Fair and Efficient Reward Distribution

Imagine you want to distribute 100 of your project's tokens (MYT) every day to liquidity providers. The problem is complex:

  • Users can stake and withdraw at any time.
  • The total amount staked changes constantly.
  • A user's share of the rewards depends on both their stake size and how long they've staked.

A naive approach might be to loop through all stakers every block and distribute rewards, but this is computationally infeasible and would cost an astronomical amount in gas fees. DeFi developers solved this with a brilliant accounting trick.

The math behind this solution can be a bit dense, but the core idea is to track a single, cumulative value: rewardPerToken.

How to Implement a Stake and Reward Contract in Solidity

The article 'How to Implement a Stake and Reward Contract in Solidity' provides a great, simplified explanation of the math behind this algorithm. It's an excellent starting point before we dive into the code.

Please read the section titled 'The Math'. Focus on understanding the purpose of the currentRewardsPerToken and currentUserRewards formulas. Don't worry about the implementation details just yet; focus on the concept.

To summarize, instead of calculating rewards for every user at every moment, the contract maintains two key accumulators:

  1. rewardPerTokenStored: A global variable representing the total rewards accrued per single staked token since the beginning of the program.
  2. userRewardPerTokenPaid: A per-user variable that snapshots the value of rewardPerTokenStored the last time that user's rewards were updated.

A user's current earned rewards can then be calculated with a simple formula:

rewards = userBalance * (rewardPerTokenStored - userRewardPerTokenPaid)

This calculation is only performed when a user interacts with the contract (e.g., to stake more, withdraw, or claim), making it incredibly gas-efficient.

2. The Staking Algorithm in Detail

Now that you have the conceptual overview, let's watch a video that breaks down the algorithm in more detail and presents it as pseudocode. Your background in backend development will be helpful here, as this is fundamentally an algorithmic problem.

Staking Rewards - Math, Examples and Algorithm | DeFi

The video 'Staking Rewards - Math, Examples and Algorithm' by Smart Contract Programmer provides the detailed mathematical foundation for our contract. It culminates in a clear algorithm that we will then implement in Solidity.

Watch the section 'A More Gas-Efficient Equation' (02:01 - 05:44) to see the transition from the inefficient to the efficient formula. Then, watch the final section 'The Algorithm' (37:34 - 43:05), which presents the complete logic as pseudocode. This algorithm is the blueprint for our contract.

The key takeaway from the pseudocode is that every time a user's balance is about to change (via staking or withdrawing), we must first:

  1. Update the global rewardPerToken.
  2. Calculate and credit any rewards the user has earned up to that exact moment.
  3. Update the user's personal userRewardPerTokenPaid snapshot to the new global value.

Only after these accounting steps are complete do we update the user's staked balance.

3. Implementing the LP Token Staking Contract

With the algorithm clearly defined, we can now translate it into Solidity. In our case, the "staking token" will be the Uniswap V2 LP token for your MYT/ETH pair, and the "reward token" will be your MYT token itself.

We will follow a comprehensive video tutorial that builds this contract from scratch. As you follow along, notice how the Solidity code directly implements the pseudocode and mathematical concepts we've just discussed.

Staking Rewards | Solidity 0.8

The video 'Staking Rewards | Solidity 0.8' is a direct, code-along implementation of the algorithm. It will guide you through creating the state variables, functions, and modifiers needed for a complete and secure staking contract.

Watch the video from the beginning up to 23:47. I recommend having your code editor open to follow along. I've broken down the key concepts below to help you focus on what's important in each part.

Let's break down the implementation into its core components.

Part 1: State Variables and Constructor (Video time 00:00 - 03:04)

The contract begins by defining its state. These variables are the contract's memory and are direct translations of the concepts from our algorithm:

  • stakeToken & rewardToken: IERC20 interfaces for the LP token and your project's token.
  • rewardRate: How many reward tokens are distributed per second.
  • lastUpdateTime / updatedAt: The timestamp of the last reward calculation.
  • rewardPerTokenStored: The global reward accumulator.
  • userRewardPerTokenPaid: The per-user snapshot of the global accumulator.
  • rewards: A mapping to track each user's earned (but not yet claimed) rewards.
  • totalSupply: The total amount of LP tokens currently staked in the contract.
  • balances / balanceOf: Each user's individual staked LP token balance.

The constructor initializes the immutable token addresses.


Part 2: Core Reward Logic: rewardPerToken() and earned() (Video time 12:54 - 18:38)

  • rewardPerToken(): This internal function calculates the up-to-the-second value of the global reward accumulator. It computes how many rewards have been generated since the lastUpdateTime and distributes them proportionally across the totalSupply of staked tokens.
  • earned(account): This view function calculates the pending rewards for a specific user. It uses the formula we discussed earlier: balance * (current_reward_per_token - user_paid_reward_per_token) + previously_earned_rewards. This allows a user (or a dApp frontend) to see their earnings without a gas-costly transaction.

Part 3: The updateReward Modifier (Video time 18:25 - 21:30)

This is the heart of the contract's efficiency and security. A modifier in Solidity is reusable code that can be attached to functions. The updateReward(account) modifier encapsulates the critical accounting steps:

  1. It calls rewardPerToken() to update rewardPerTokenStored to the current time.
  2. It calls earned(account) to calculate the user's pending rewards and adds them to the rewards mapping.
  3. It updates userRewardPerTokenPaid[account] to the new rewardPerTokenStored value.

By attaching this modifier to our stake and withdraw functions, we ensure that a user's rewards are always calculated and snapshotted before their stake balance changes.


Part 4: User and Owner Functions (Video time 06:09 - 13:06 and 21:30 - 23:47)

  • stake(uint amount): The user calls this to deposit LP tokens. It uses the updateReward modifier, then transfers the tokens and updates the user's balance and the contract's totalSupply.
  • withdraw(uint amount): The user calls this to retrieve their staked LP tokens. It also uses updateReward before updating balances and transferring tokens back to the user.
  • getReward(): The user calls this to claim their accumulated rewards. It triggers updateReward, then transfers the amount stored in the rewards mapping to the user.
  • notifyRewardAmount(uint reward, uint duration): This is an owner-only function. The project owner calls this to fund the contract with reward tokens and define the rewardRate (reward / duration).
Test your understanding!

A user has 100 LP tokens staked in your contract. The rewardRate is 1 token/sec. The total staked supply (totalSupply) is 1000 tokens. The user decides to stake an additional 50 LP tokens.

Why is it crucial for the updateReward modifier to run before the user's balance is updated from 100 to 150?

Show answer

The updateReward modifier must run first to correctly calculate the rewards the user earned while they had a balance of 100 tokens. If the balance were updated to 150 first, the reward calculation would incorrectly use this higher balance for the time period when the user only had 100 tokens staked, giving them more rewards than they are entitled to. The pattern is always: 1. Calculate rewards based on old balance, 2. Update balance.

Conclusion

Congratulations! You've just walked through the implementation of one of the most fundamental and powerful smart contracts in DeFi. This contract is the engine that will power your token's liquidity incentive program.

Key Takeaways:

  • Staking rewards are calculated using a gas-efficient algorithm that avoids loops by tracking cumulative reward values.
  • The key state variables are rewardPerTokenStored (global) and userRewardPerTokenPaid (per-user).
  • The updateReward modifier is a critical component that ensures reward calculations are performed correctly before any change in a user's staked balance.
  • The contract has distinct functions for users (stake, withdraw, getReward) and the owner (notifyRewardAmount).

Preview of the Next Lesson:
You have now written a complex contract with time-dependent logic. But how can you be certain it works as expected under all conditions? What happens if a user stakes, another user stakes, time passes, and then the first user withdraws? Manually testing this is difficult and error-prone.

In the next lesson, we will begin our module on automated testing. You will learn how to use Hardhat's time manipulation features to test time-dependent reward accrual, ensuring your staking contract is robust, fair, and secure.

Can't find a good explanation? Sign up and we'll make it for you

Sign up