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

Implementing a Reward Claim Function

Hello! Welcome back to the course.

In our last lesson, we constructed the core mathematical engine of our staking contract. You implemented the earned() and rewardPerToken() functions, and you created the powerful updateReward modifier. This logic allows us to calculate, in a highly efficient way, exactly how many reward tokens a user has earned at any given moment.

Today, we'll build the feature that makes this all worthwhile for the user: the ability to actually withdraw those rewards. The goal of this lesson is to implement a claim function for users to withdraw their accumulated rewards. This function will act as the "payout" button for your stakers, transferring their earned tokens from the contract to their personal wallets.

This is a critical user-facing function, and its implementation involves a precise sequence of operations to ensure it's both correct and secure.

Let's look at where this new claim() (or getReward()) function fits into the overall system.

Staking Smart Contracts Interaction Flow
This diagram shows the main interactions a user has with a staking contract. Our focus today is on the `claim()` action, which moves Reward Tokens from the contract back to the user's wallet.

1. The Anatomy of a Reward Claim Function

A robust getReward function must perform several actions in a specific order:

  1. Update the User's Rewards: First, it must calculate any rewards the user has earned since their last interaction. Fortunately, we already built the updateReward modifier for this exact purpose.
  2. Retrieve the Total: Once updated, it needs to get the total claimable amount from the rewards mapping.
  3. Prevent Double-Spending: It's crucial to reset the user's pending reward balance to zero before sending the tokens. This prevents them from calling the function again to claim the same rewards.
  4. Transfer the Tokens: With the checks and state changes in place, the function can now safely transfer the reward tokens to the user.
  5. Log the Action: Finally, it should emit an event to create a public, on-chain record of the reward payment.

Let's see this logic implemented in practice. The following video demonstrates how to build a function that accomplishes these steps. Note that the video calls the function claim, but we will name ours getReward to be consistent with the contract structure we've been building.

Discrete Staking Rewards | Solidity 0.8

The video 'Discrete Staking Rewards' by Smart Contract Programmer provides a clear, step-by-step implementation of a reward claim function. It directly follows the scalable pattern we have been implementing.

Watch the segment from 08:59 to 10:20. Pay close attention to the sequence of operations: The call to updateRewards at the beginning. The check to ensure the reward amount is greater than zero. The line that resets the user's earned amount to zero. The final transfer call.

2. Implementing getReward in Our Contract

Now, let's integrate this logic into our StakingRewards.sol contract. We already have a function stub and a RewardPaid event defined from our initial setup. It's time to fill them in.

The implementation will look like this:

  1. Attach the updateReward(msg.sender) modifier to the getReward function. This single line handles the complex reward calculation by running our logic from the previous lesson before the function body executes.
  2. Inside the function, retrieve the reward amount from the rewards[msg.sender] mapping.
  3. Check if reward > 0.
  4. If it is, set rewards[msg.sender] = 0;.
  5. Transfer the reward amount of rewardsToken to msg.sender.
  6. Emit the RewardPaid event.

The article below provides a clear, text-based walkthrough of building a claim function. While it uses a test-driven development approach, the resulting code for emitting an event and transferring tokens is highly relevant.

Solidity Tutorial: Creating an ERC20 Staking Contract

The article 'Solidity Tutorial: Creating an ERC20 Staking Contract' by Cyrille D., published on Medium, shows how to build out the claim function, including emitting an event. This complements the video by providing a code-focused perspective.

Read the sections titled 'claim / should change token balances' and 'claim / events'. Note the use of token.safeTransfer (a best practice from OpenZeppelin) and the emit Claim line, which corresponds to our emit RewardPaid.

By combining these concepts, we can complete our getReward function.

// In your StakingRewards.sol contract

// ... (previous code)

event RewardPaid(address indexed user, uint256 reward);

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

// ... (constructor, reward logic functions) ...

/**
 * @notice A user claims their earned rewards
 */
function getReward() external updateReward(msg.sender) {
    uint256 reward = rewards[msg.sender];
    if (reward > 0) {
        rewards[msg.sender] = 0;
        rewardsToken.transfer(msg.sender, reward);
        emit RewardPaid(msg.sender, reward);
    }
}

// ... (rest of the contract)

Notice how clean the function body is. All the heavy lifting of calculation is handled by the updateReward modifier, making the getReward function's logic focused solely on the transfer itself.

Test your understanding!

In our getReward function, we reset the user's reward balance (rewards[msg.sender] = 0;) before we call rewardsToken.transfer(...).

Why is this order of operations important for security? What kind of attack does this help prevent? (Hint: Think about a malicious contract calling our function).

Show answer

This order follows a critical security principle in smart contract development called the Checks-Effects-Interactions pattern.

  1. Checks: We check if reward > 0.
  2. Effects: We update our contract's internal state (rewards[msg.sender] = 0;).
  3. Interactions: We interact with an external contract/address (rewardsToken.transfer(...)).

By updating our state before the external call, we prevent reentrancy attacks. If we transferred the tokens first, a malicious contract could use the transfer function's execution flow to immediately call back into our getReward function before we set their rewards to zero. They could repeat this process, draining all the rewards from the contract. Our current ordering makes this attack impossible. We'll cover this pattern in more detail in the security module.

Conclusion

Excellent work! You have now implemented a complete and secure function for users to claim their staking rewards. This brings together the calculation logic from the previous lesson with the necessary state changes and token transfer mechanics.

Key Takeaways:

  • The getReward function's primary role is to transfer earned tokens to the user.
  • The updateReward modifier is essential for ensuring reward calculations are up-to-date before a claim is processed.
  • Following the Checks-Effects-Interactions pattern (resetting state before transferring tokens) is crucial for preventing reentrancy attacks.
  • Emitting events like RewardPaid provides a transparent, on-chain log of important contract activities.

Preview of the Next Lesson:
Our staking contract is nearly complete! The final core pieces are the stake() and withdraw() functions that allow users to deposit and retrieve their principal staking tokens. In the next lesson, we will implement these functions and apply our updateReward modifier to them, making the entire reward system fully operational from start to finish.

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

Sign up