Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome back to our course on building your own cryptocurrency token.
In our last lesson, you successfully implemented the stake() and withdraw() functions, creating the entry and exit points for your staking contract. Users can now deposit and retrieve their tokens. However, the core incentive for staking—earning rewards—is still missing.
This lesson addresses a critical prerequisite for that incentive mechanism. Our goal is to create the data structures to track each user's staked amount and reward debt. Simply looping through all stakers to distribute rewards is not feasible on a blockchain due to prohibitive gas costs. We need a more sophisticated, gas-efficient system. Today, you'll learn about the elegant data structures that make this possible.
1. The Scalability Problem: Why Loops Don't Work
Imagine your token becomes popular, and thousands of users stake their tokens. If your contract had to iterate through a list of all stakers every time rewards were calculated, the gas cost for that single transaction would be enormous. The transaction might even fail if it exceeds the block gas limit. This approach is not scalable and would render the contract unusable.
This is a fundamental challenge in smart contract development that is different from traditional backend systems where iterating over a large dataset is common. On the blockchain, every computation costs money (gas), so we must be extremely efficient.
2. The Solution: The "Lazy" Reward Pattern
The solution is a design pattern often called the "lazy" reward pattern or an O(1) reward distribution system. Instead of the contract proactively sending rewards to everyone, it calculates a user's rewards only when they interact with the contract (e.g., stake more, withdraw, or claim).
Scalable Solidity Staking: O(1) Reward Distribution
The article "Scalable Solidity Staking" provides an excellent analogy to understand this concept. Read the introduction to grasp the core idea of the 'lazy' reward pattern.
Read the section titled 'The Lazy Reward Pattern: Calculate Only When Needed'. The bakery analogy is a perfect way to visualize why this approach is so efficient.
To implement this pattern, we need several key pieces of information stored in our contract's state.
3. Core Data Structures for Reward Tracking
Let's define the state variables required for our StakingRewards contract. We will need variables to track the overall reward distribution and separate mappings to track each user's individual state.
A fantastic, concise walkthrough of these state variables is provided in the "Staking Rewards | Solidity 0.8" video.
Staking Rewards | Solidity 0.8
Watch the beginning of this video from Smart Contract Programmer to see a professional implementation of the necessary state variables for a staking rewards contract.
Watch from 00:13 to 03:04. Pay close attention to each variable's name and purpose. The video covers both global variables (like rewardRate) and user-specific mappings (like balanceOf and rewards).
Based on the video and best practices, let's categorize and define our data structures.
Contract-Wide (Global) State Variables
These variables track the overall state of the reward pool.
IERC20 public rewardsToken: The token that will be given as a reward.uint256 public rewardRate: The rate at which rewards are distributed per second.uint256 public lastUpdateTime: The timestamp of the last time rewards were updated.uint256 public rewardPerTokenStored: This is a crucial variable. It's a global accumulator that tracks the cumulative rewards earned per single token staked since the contract began. We'll explore this in depth shortly.
User-Specific State Variables (Mappings)
We use mappings to efficiently store and retrieve data for each user. A mapping in Solidity is like a hash map or dictionary in other languages, providing O(1) lookup time.
mapping(address => uint256) public balanceOf: We already have this from our previous lesson. It tracks how many tokens each user has staked.mapping(address => uint256) public userRewardPerTokenPaid: This is the concept of "reward debt". For each user, this stores the value ofrewardPerTokenStoredat the time of their last interaction. It acts as a personal checkpoint.mapping(address => uint256) public rewards: This tracks the amount of rewards a user has earned but not yet claimed.

Our StakingRewards.sol contract now looks like this with the new state variables.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.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;
mapping(address => uint256) public userRewardPerTokenPaid;
mapping(address => uint256) public rewards;
constructor(address _stakingToken, address _rewardsToken) {
stakingToken = IERC20(_stakingToken);
rewardsToken = IERC20(_rewardsToken);
}
// ... stake and withdraw functions from previous lesson ...
// ... reward logic functions to be added ...
}
4. Understanding Reward Per Token and Reward Debt
The most complex but powerful part of this system is the interplay between rewardPerTokenStored and userRewardPerTokenPaid.
The article "The staking algorithm of SushiSwap MasterChef" provides the best explanation of this mechanism.
The staking algorithm of SushiSwap MasterChef and ...
To understand how we can avoid loops, we need to grasp the concept of a global accumulator and individual reward debt. This article explains it perfectly.
Read the sections 'Rather than tracking reward accruals...', 'How we track reward accruals...', and 'What if someone hasn’t been staking from the beginning?'. Focus on how the 'Accumulated reward per token' is calculated and the purpose of the 'reward debt' variable.
Let's summarize the key ideas from the article:
-
Global Accumulator (
rewardPerTokenStored): The contract maintains a single number that represents the total rewards one token would have earned if it were staked from the very beginning. This number only ever increases. It's calculated roughly as:new rewards / total tokens staked.
Every time a user interacts, we update this global number first. -
Reward Debt (
userRewardPerTokenPaid): When a user stakes, we record the current value of the global accumulator (rewardPerTokenStored) and save it as their personaluserRewardPerTokenPaid. This acts as their starting point. It's called "debt" because it represents the rewards they are not entitled to—all the rewards distributed before they staked. -
The Calculation: A user's newly earned rewards can then be calculated with a simple formula:
earned = balanceOf[user] * (rewardPerTokenStored - userRewardPerTokenPaid[user])This finds the difference in the global accumulator between now and the user's last interaction, and multiplies it by their staked balance. This single line of math replaces an entire loop, making the calculation incredibly efficient.
Test your understanding!
Alice stakes 100 tokens when rewardPerTokenStored is 50. Later, Bob stakes 200 tokens when rewardPerTokenStored has grown to 80.
- What is the initial value of
userRewardPerTokenPaidfor Alice? - What is the initial value of
userRewardPerTokenPaidfor Bob? - If Alice checks her rewards when
rewardPerTokenStoredis 95, how many new rewards has she earned? (Show the formula).
Show answer
userRewardPerTokenPaid[Alice]is set to 50. This is her "checkpoint" or "debt."userRewardPerTokenPaid[Bob]is set to 80. He is not entitled to the rewards generated between the accumulator value of 0 and 80.earned = balanceOf[Alice] * (rewardPerTokenStored - userRewardPerTokenPaid[Alice])
earned = 100 * (95 - 50)
earned = 100 * 45 = 4500(before considering token decimals).
Conclusion
In this lesson, you have put the crucial data structures in place for a scalable, gas-efficient staking reward system. By avoiding loops and instead using a global accumulator combined with a personal "reward debt" for each user, you've adopted a pattern used by major DeFi protocols.
Key Takeaways:
- Iterating over arrays of users in Solidity is not scalable due to gas costs.
- The "Lazy Reward Pattern" calculates rewards on-demand when a user interacts with the contract.
- The key data structures are:
rewardPerTokenStored: A global accumulator for rewards earned per single staked token.userRewardPerTokenPaid: A per-user snapshot of the global accumulator, acting as a "reward debt" or checkpoint.rewards: A per-user mapping to store claimable rewards.
- The formula
balance * (global_accumulator - user_checkpoint)is the core of this efficient calculation.
Preview of the Next Lesson:
With our data structures defined, we are now ready to implement the logic that uses them. In the next lesson, you will develop the core logic to calculate rewards earned by stakers over time, by creating the rewardPerToken() and earned() functions that bring this entire system to life.