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

Token Staking Architecture Design

Hello! Welcome to the eighth module of our course.

In the last module, we successfully built and thoroughly tested a MyTokenSale contract. You now have a secure and reliable way to distribute your MyToken to investors. But once the sale is over, what's next? To create a successful token project, you need to give your token utility—a reason for people to hold it and participate in your ecosystem.

One of the most powerful and popular ways to create utility in DeFi is through staking. This lesson kicks off a new, exciting phase: building a mechanism to reward users for their long-term commitment to your token.

Today, your goal is to design the smart contract architecture for a token staking mechanism. Given your backend development experience, you know that good architecture is the foundation of any robust system. In the world of smart contracts, a solid design is paramount for security, efficiency, and scalability. We'll focus on the "what" and "why" of the design before we dive into the implementation in subsequent lessons.

1. What is Staking?

At its core, staking is a simple concept: users lock up their tokens in a smart contract to earn rewards. This incentivizes holding the token, reduces selling pressure on the market, and can be used to grant stakers other privileges, like governance rights.

The flow is straightforward:

  1. A user stakes their tokens by sending them to a staking contract.
  2. The contract holds these tokens. While staked, the user earns rewards over time.
  3. The user can claim their earned rewards.
  4. The user can unstake their tokens to withdraw them back to their wallet.

The following diagram provides a clear visual of this interaction model.

Staking Smart Contracts Flow Diagram
This diagram shows the fundamental interactions with a staking contract. A user's wallet calls `stake()` to deposit tokens, `claim()` to receive rewards, and `unstake()` to withdraw their original tokens.

For a brief overview of the key components and considerations in a staking contract, let's turn to a quick read.

Guide to Developing a Staking & Unstaking Smart Contract

The following article, "Guide to Developing a Staking & Unstaking Smart Contract," provides a concise introduction to the concept and outlines the key components we need to consider in our design.

Please read the introduction (sections 1 and 1.1) and the 'Key Components of a Staking Smart Contract' (section 1.3). Focus on understanding the roles of the staking mechanism, reward distribution, and the unstaking process.

2. Core Architectural Components

As you just read, a staking contract has several key components. Let's design the architecture for our own StakingRewards contract by defining its state, functions, and data structures.

State Variables & Data Structures

What information must our contract store to function correctly?

  • Token Addresses: The contract needs to know which token is used for staking and which is given as a reward. For our project, we will use our MyToken for both staking and rewards.
    • stakingToken: The address of the ERC-20 token users will stake.
    • rewardsToken: The address of the ERC-20 token paid out as rewards.
  • User-Specific Data: We need to track each user's stake. A mapping is the perfect data structure for this, mapping a user's address to their uint balance.
    • balanceOf[user]: The amount of tokens a specific user has staked.
  • Global Data: The contract needs to track the total state of the system.
    • totalSupply: The total amount of tokens staked by all users combined.
  • Reward Calculation Data: This is the most critical part of our design. We need variables to manage the distribution of rewards.
    • rewardRate: The rate at which rewards are generated for the entire pool (e.g., tokens per second).
    • rewards[user]: The amount of rewards a user has earned but not yet claimed.

The article "Guide to Developing a Staking & Unstaking Smart Contract" also provides an excellent breakdown of these variables and introduces the idea of using structs to organize user data.

Guide to Developing a Staking & Unstaking Smart Contract

Let's revisit the same guide to look at a concrete architectural design.

Read the section '3.1. Defining Contract Variables and Data Structures'. Pay attention to how mappings like stakedAmount and rewards are used, and how a UserInfo struct can organize related data.

Core Functions

Based on the state we need to track, what actions or functions should our contract expose?

  • stake(uint amount): Allows a user to deposit amount of stakingToken.
  • withdraw(uint amount): Allows a user to withdraw amount of their staked stakingToken. (Often called unstake).
  • getReward(): Allows a user to claim their earned rewardsToken. (Often called claim).
  • earned(address account): A view function to check the current accumulated rewards for a given account.

Additionally, we'll need owner-only functions to manage the rewards program, such as funding the contract with reward tokens and setting the duration of the reward period.

3. The Reward Calculation Challenge

How do we calculate rewards for potentially thousands of stakers in a gas-efficient way?

A naive approach might be to loop through all stakers periodically and update their rewards. As a backend developer, you can immediately see the problem: this is an O(N) operation that becomes prohibitively expensive as the number of stakers (N) grows. It simply doesn't scale on a blockchain.

A much better architecture calculates rewards on-demand. A user's rewards are only calculated and updated when they interact with the contract (stake, withdraw, or claim). The key is to calculate the rewards accrued since their last interaction.

A simple formula for this might be:
rewards_earned = (time_elapsed) * (user_staked_balance) * (reward_rate_per_token)

However, this can still be inefficient if the totalStaked amount changes frequently, as this would affect the reward_rate_per_token.

An Advanced, Gas-Efficient Model

The standard, battle-tested approach in DeFi (popularized by Synthetix) uses a clever accounting trick. Instead of tracking each user's reward individually over time, we track a global value: rewardPerTokenStored.

  • rewardPerTokenStored: This variable represents the cumulative rewards distributed per one token staked since the beginning of the program. It's updated only when the total staked supply changes or a new reward period is started.
  • userRewardPerTokenPaid: A mapping that stores the value of rewardPerTokenStored for each user at the time of their last update.
  • rewards: A mapping that stores any rewards a user has accrued but not yet claimed.

With this, a user's newly earned rewards can be calculated with a simple formula whenever they interact:
earned = (balanceOf[user] * (rewardPerTokenStored - userRewardPerTokenPaid[user])) + rewards[user]

This is a highly efficient O(1) operation. The following video explains the math behind this model. Don't worry about understanding every single mathematical step right now; focus on the high-level concept of the two different equations and why the second, optimized one is better for Solidity.

Staking Rewards - Math, Examples and Algorithm | DeFi

The video "Staking Rewards - Math, Examples and Algorithm" from Smart Contract Programmer provides an excellent explanation of the math behind this efficient reward calculation model.

Watch from 02:01 to 05:22. The video presents two equations. The first is the intuitive but gas-intensive summation. The second is the optimized formula that our contract architecture will be based on. Grasping the difference is key.

Test your understanding!

Why is the second, more complex-looking formula presented in the video more gas-efficient for a smart contract than the first one?

Show answer

The first formula involves a summation (or a loop) over a period of time, calculating the reward share for each second. Running loops in Solidity can be very expensive, and the gas cost would grow with the duration of the stake, potentially hitting the block gas limit. The second formula is an algebraic simplification that avoids loops entirely. It calculates the total reward earned in a single O(1) operation by using pre-computed values (rewardPerToken), making it vastly more efficient and scalable.

4. The StakingRewards Contract Blueprint

Now, let's put all these design decisions together into a blueprint for our smart contract. The following video walks through the state variables and function signatures for a staking contract that uses this advanced reward calculation model. This will serve as the architectural foundation for the code we'll write in the next lessons.

Staking Rewards | Solidity 0.8

This video, "Staking Rewards | Solidity 0.8," also from Smart Contract Programmer, outlines the contract structure we will be using.

Watch from the beginning to 06:09. Pay close attention to the state variables being declared (e.g., stakingToken, rewardsToken, rewardPerTokenStored, userRewardPerTokenPaid) and the names of the core functions (stake, withdraw, getReward, earned). This is the architecture we will implement.

Based on the video and our discussion, our StakingRewards contract architecture will include:

  • Interfaces: IERC20 for our staking and reward tokens.
  • State Variables:
    • Immutable stakingToken and rewardsToken addresses.
    • owner address for access control.
    • Reward period variables: duration, finishAt, updatedAt.
    • Reward calculation variables: rewardRate, rewardPerTokenStored, userRewardPerTokenPaid, and rewards.
    • Staking balance variables: totalSupply and balanceOf.
  • Core Functions:
    • stake(uint amount)
    • withdraw(uint amount)
    • getReward()
    • earned(address account)
  • Owner-only Functions:
    • setRewardDuration(uint duration)
    • notifyRewardAmount(uint reward)

Conclusion

Great job! You have now designed the complete architecture for a sophisticated, gas-efficient staking contract. This is a significant step that bridges the gap between a simple token and a token with real DeFi utility.

Key Takeaways:

  • A staking contract's architecture must define the tokens involved, how user balances are tracked, and the core functions (stake, withdraw, claim).
  • The primary design challenge is creating a gas-efficient reward calculation mechanism that can scale to many users.
  • The industry-standard approach avoids loops and uses a global rewardPerToken accumulator to calculate rewards on-demand in an O(1) operation.
  • Our contract blueprint includes state variables to track time, reward rates, user balances, and the crucial accounting variables for the efficient reward model.

Preview of the Next Lesson:
With our architecture designed, it's time to start building. In the next lesson, you will implement the functions to allow users to stake (deposit) and unstake (withdraw) their ERC-20 tokens, bringing the first part of our design to life in Solidity.

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

Sign up