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 Reward Calculation Logic

Hello! Welcome back to our journey of building a complete staking application.

In the last lesson, we laid the critical groundwork by defining the data structures needed for a scalable reward system. You learned why we can't just loop through stakers and defined the key variables for our "lazy" reward pattern: rewardPerTokenStored (the global accumulator) and userRewardPerTokenPaid (the per-user checkpoint or "reward debt").

Today, we move from what to how. The goal of this lesson is to develop the core logic to calculate rewards earned by stakers over time. We will translate the theory from our previous lesson into the actual Solidity functions that bring this efficient system to life. You'll implement the two most important pieces of logic in our contract: the functions to update the global reward accumulator and to calculate a specific user's earnings.

1. From Inefficient Loops to an Efficient Formula

As a quick recap, the naive way to calculate rewards for a user would be to sum up their share of rewards for every second they've been staking:

This is a loop, which is prohibitively expensive in Solidity. The elegant solution, which we prepared for in the last lesson, uses our accumulator and checkpoint variables in a simple, gas-efficient formula:

This single calculation achieves the same result as the loop, but with constant (O(1)) complexity.

To understand the mathematics and the resulting algorithm more formally, the following video provides an excellent deep dive.

Staking Rewards - Math, Examples and Algorithm | DeFi

The video 'Staking Rewards - Math, Examples and Algorithm' by Smart Contract Programmer breaks down the core equations and presents a clear algorithm that we will implement. This will connect the high-level theory to a concrete, developer-focused plan.

Watch two key parts of this video: The Core Equations (02:01 - 05:44): Focus on the second, more efficient equation presented. This is the mathematical representation of the formula we are implementing. Don't worry about memorizing the derivation; focus on understanding the components. The Final Algorithm (37:34 - 43:05): This section translates the math into clear pseudocode. As a developer, you'll find this step bridges the gap between theory and implementation perfectly. The extended example from 18:08 to 37:34 is an excellent, detailed walkthrough that you can review later if you want to see the numbers change step-by-step.

2. Implementing rewardPerToken(): The Global Accumulator

The first function we'll build, rewardPerToken(), is responsible for calculating the up-to-the-second value of our global accumulator, rewardPerTokenStored. It doesn't store this value but calculates what it should be at the moment it's called.

The logic is as follows:

  1. Return rewardPerTokenStored immediately if there are no tokens staked (totalSupply == 0) to avoid a division-by-zero error.
  2. Otherwise, calculate the new rewards generated per token since the last update:
    new_rewards_per_token = (time_elapsed * reward_rate) / totalSupply
  3. Add this to the last saved value:
    current_reward_per_token = rewardPerTokenStored + new_rewards_per_token

Now, let's see how this is implemented in Solidity, including some important practical considerations.

Staking Rewards | Solidity 0.8

The video 'Staking Rewards | Solidity 0.8' provides a clean implementation of the rewardPerToken function. Pay close attention to how it handles edge cases and precision.

Watch from 15:01 to 18:38. Focus on these key implementation details: The check for totalSupply == 0. The use of a helper function lastTimeRewardApplicable() to ensure rewards aren't calculated past the official reward period. The multiplication by 1e18 before division. This is the critical precision trick.

A Note on Precision: The 1e18 Trick

As you saw in the video, the code multiplies by 1e18 before dividing. Why? Solidity only works with integers and will truncate any decimal result.

Consider reward_rate = 100, time_elapsed = 1, and totalSupply = 200.
The result of (100 * 1) / 200 would be 0.5. In Solidity, this becomes 0, and the rewards are lost forever.

By scaling the numerator up, we preserve the precision: (100 * 1 * 1e18) / 200 = 5e17. This value can be stored, and we'll remember to scale it back down later.

Scalable Solidity Staking: O(1) Reward Distribution

For a clear textual explanation of this scaling trick, refer back to the 'Scalable Solidity Staking' article.

Read the subsection titled 'A Quick Note on Precision (The 1e18 Trick)'. This reinforces what you saw in the video.

3. Implementing earned(): A User's Reward Calculation

Next, we'll write the earned() function. This function takes a user's address and calculates their total claimable rewards using our "magic formula".

The logic is:

  1. Get the current global rewardPerToken() value.
  2. Calculate newly_earned = balanceOf[user] * (rewardPerToken() - userRewardPerTokenPaid[user]).
  3. Because of the 1e18 scaling, we must divide the result by 1e18.
  4. Add these newly earned rewards to any previously calculated rewards stored in the rewards mapping for that user.

Let's watch the implementation.

Staking Rewards | Solidity 0.8

The next segment of the 'Staking Rewards | Solidity 0.8' video shows how to code the earned function.

Watch from 12:54 to 15:01. Notice how it directly implements the formula, calling the rewardPerToken() function we just discussed and dividing by 1e18 to get the final amount.

4. Putting It All Together: The updateReward Modifier

We have the calculation functions, but they are only useful if they are called at the right time. We need to update a user's rewards before their staked balance changes (i.e., before a stake or withdraw action completes).

In Solidity, a modifier is the perfect tool for this. It's a piece of code that can be attached to a function to run before and/or after the function's main body. This is analogous to middleware in backend frameworks or decorators in languages like Python and TypeScript.

Our updateReward modifier will do the following for a given user:

  1. Update rewardPerTokenStored to the current value.
  2. Update lastUpdateTime.
  3. Calculate the user's total earned rewards by calling the earned() function.
  4. Store this new total in rewards[user].
  5. Crucially, set the user's new checkpoint: userRewardPerTokenPaid[user] = rewardPerTokenStored. This ensures they don't get paid for the same rewards twice.

Staking Rewards | Solidity 0.8

Let's see how this is built. The video shows how to wrap all our logic into a reusable updateReward modifier.

Watch from 18:25 to 21:49. Observe how the modifier first updates the global state (rewardPerTokenStored) and then updates the specific user's state (rewards[account] and userRewardPerTokenPaid[account]).

5. The Updated StakingRewards Contract

Here is our contract with the new functions and the modifier. We will apply this modifier to our stake and withdraw functions in a future lesson, but for now, the logic is defined and ready.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";

contract StakingRewards is Ownable {
    // --- State Variables ---

    // Tokens
    IERC20 public immutable stakingToken;
    IERC20 public immutable rewardsToken;

    // Staking Data
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;

    // Reward Data
    uint256 public rewardRate;
    uint256 public lastUpdateTime;
    uint256 public rewardPerTokenStored;
    uint256 public duration;
    uint256 public finishAt;


    mapping(address => uint256) public userRewardPerTokenPaid;
    mapping(address => uint256) public rewards;
    
    // --- Events ---
    event RewardAdded(uint256 reward);
    event Staked(address indexed user, uint256 amount);
    event Withdrawn(address indexed user, uint256 amount);
    event RewardPaid(address indexed user, uint256 reward);


    // --- Modifier ---

    modifier updateReward(address _account) {
        rewardPerTokenStored = rewardPerToken();
        lastUpdateTime = lastTimeRewardApplicable();
        if (_account != address(0)) {
            rewards[_account] = earned(_account);
            userRewardPerTokenPaid[_account] = rewardPerTokenStored;
        }
        _;
    }

    // --- Constructor ---

    constructor(address _stakingToken, address _rewardsToken) {
        stakingToken = IERC20(_stakingToken);
        rewardsToken = IERC20(_rewardsToken);
    }
    
    // --- Core Reward Logic ---

    function lastTimeRewardApplicable() public view returns (uint256) {
        return Math.min(block.timestamp, finishAt);
    }

    function rewardPerToken() public view returns (uint256) {
        if (totalSupply == 0) {
            return rewardPerTokenStored;
        }
        return
            rewardPerTokenStored +
            (((lastTimeRewardApplicable() - lastUpdateTime) * rewardRate * 1e18) / totalSupply);
    }

    function earned(address _account) public view returns (uint256) {
        return
            ((balanceOf[_account] *
                (rewardPerToken() - userRewardPerTokenPaid[_account])) / 1e18) +
            rewards[_account];
    }
    
    // --- External Functions (incomplete stubs for now) ---
    function setRewardDuration(uint _duration) external onlyOwner {
        // To be implemented
    }

    function notifyRewardAmount(uint256 _reward) external onlyOwner updateReward(address(0)) {
        // To be implemented
    }
    
    function stake(uint256 _amount) external {
        // To be implemented
    }

    function withdraw(uint256 _amount) external {
        // To be implemented
    }

    function getReward() external {
        // To be implemented
    }

}
Test your understanding!

Imagine rewardPerTokenStored is currently 50e18. A user has a balanceOf 100 tokens and their userRewardPerTokenPaid is 20e18.

When the earned() function is called for this user, what will be the value of newly_earned_rewards before adding any previously stored rewards?

newly_earned = (balanceOf[user] * (rewardPerToken() - userRewardPerTokenPaid[user])) / 1e18

Show your calculation.

Show answer

newly_earned = (100 * (50e18 - 20e18)) / 1e18
newly_earned = (100 * 30e18) / 1e18
newly_earned = 3000e18 / 1e18
newly_earned = 3000

The user has earned 3000 units of the reward token since their last interaction.

Conclusion

Congratulations! You have now implemented the most complex and most important part of our staking contract. You've successfully translated the scalable O(1) reward distribution theory into working Solidity code.

Key Takeaways:

  • The rewardPerToken() function calculates the current value of the global reward accumulator, handling precision with 1e18 scaling and preventing division-by-zero errors.
  • The earned() function uses the core formula balance * (current_accumulator - user_checkpoint) to calculate a user's specific rewards.
  • The updateReward modifier provides a reusable and robust way to ensure rewards are always up-to-date before any state changes like staking or withdrawing occur.

Preview of the Next Lesson:
With the calculation logic in place, the next logical step is to allow users to collect their earnings. In the next lesson, you will implement a claim function for users to withdraw their accumulated rewards. We will also apply our new updateReward modifier to the stake() and withdraw() functions to make the entire system fully operational.

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

Sign up